Android | TCP的C(Java|Android)/S(Java)通信实战经典聊天室案例(文末附本案例代码实现概述、观察者模式实现小结)

简介: Android | TCP的C(Java|Android)/S(Java)通信实战经典聊天室案例(文末附本案例代码实现概述、观察者模式实现小结)

案例GitHub地址


创建TCP服务端

  • 在sample模块下,

新建一个名为tcp的package,
创建TcpServer:

  • **指定服务端端口号(ip 默认为本机ip)

启动循环读取消息队列的子线程,
死循环,不断等待客户端请求连接,
一旦连接上,
直接新建一个子线程(丢给ClientTask)去处理这个socket,
于是主线程又可以回到accept() 阻塞,等待下一个连接请求;
同时,将连接上的socket 对应的线程类,注册为消息队列的观察者,
让线程类担任观察者,负责接收被观察者的通知信息并做socket 通信。**

/**
 * <pre>
 *     author : 李蔚蓬(简书_凌川江雪)
 *     time   : 2019/10/30 16:57
 *     desc   :指定服务端端口号(ip 默认为本机ip)
 *             启动循环读取消息队列的子线程,
 *             死循环,不断等待客户端请求连接,
 *             一旦连接上,直接新建一个子线程(丢给ClientTask)去处理这个socket,
 *             于是主线程又可以回到accept() 阻塞,等待下一个连接请求;
 *             同时,将连接上的socket 对应的线程类,注册为消息队列的观察者,
 *             让线程类担任观察者,负责接收被观察者的通知信息并做socket 通信
 * </pre>
 */
public class TcpServer {

    public void start() {

        ServerSocket serverSocket = null;
        try {
            serverSocket = new ServerSocket(9090);
            MsgPool.getInstance().start();//启动读消息的子线程

            while (true) {
//            /*
//            阻塞的方法!!!  等待(客户端的) TCP 连接请求
//            客户端有 TCP 请求并连接上了 ServerSocket,.
//            那 accept() 就会返回一个 同一连接上 对应 客户一端socket 的 服务一端socket
//             */
                Socket socket = serverSocket.accept();

                //客户端连接之后,打印相关信息
//            System.out.println("ip: " + socket.getInetAddress().getHostAddress() +
//                    ", port = " + socket.getPort() + " is online...");
                System.out.println("ip = " + "***.***.***.***" +
                        ", port = " + socket.getPort() + " is online...");

//            /*
//                连接上了之后不能直接拿IO流去读写,
//                因为getInputStream() 和 getOutputStream() 都是阻塞的!!!!
//                如果直接拿IO 流,不做其他处理,
//                那么Server端的处理流程是这样的:
//                accept()-- getInputStream()处理第一个客户端 -- 处理完毕,accept()-- getInputStream()处理第二个客户端....
//                所以必须开启子线程去读写客户端,才能做成聊天室
//
//                针对每一个连接上来的客户端去单独起一个线程,跟客户端进行通信
//
//                过程:客户端连上之后,打印其信息,
//                然后直接新建一个子线程(丢给ClientTask)去处理这个socket,
//                于是主线程又可以回到accept() 阻塞,等待下一个连接请求
//             */
                ClientTask clientTask = new ClientTask(socket);
                MsgPool.getInstance().addMsgComingListener(clientTask);
                clientTask.start();


            }


        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new TcpServer().start();
    }
}
  • 针对每一个连接上来的客户端去单独起一个线程,跟客户端进行通信,

准备一个线程类,名为ClientTask,
针对每一个连接上来的客户端去单独起一个线程,跟客户端进行通信,
这里便是线程类;
run()中死循环不断读取本类实例对应的客户端发来的信息,
或者发送给对应的连接对面客户端(服务端)要发送的信息;
实现MsgPool.MsgComingListener, 成为消息队列的观察者!!!

/**
 * <pre>
 *     author : 李蔚蓬(简书_凌川江雪)
 *     time   : 2019/10/30 17:23
 *     desc   :针对每一个连接上来的客户端去单独起一个线程,跟客户端进行通信,
 *             这里便是线程类;
 *             run()中死循环不断读取客户端发来的信息,发送给客户端(服务端)要发送的信息;
 *             实现MsgPool.MsgComingListener, 成为消息队列的观察者!!!
 * </pre>
 */
public class ClientTask extends Thread implements MsgPool.MsgComingListener {

    private Socket mSocket;
    private InputStream mIs;
    private OutputStream mOs;

    public ClientTask(Socket socket) {

        try {
            mSocket = socket;
            mIs = socket.getInputStream();
            mOs = socket.getOutputStream();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void run() {
        BufferedReader br = new BufferedReader(new InputStreamReader(mIs));

        String line = null;
        /*
            读取并输出客户端信息。
            如果没有客户端发送信息,readLine() 便会阻塞在原地
         */
        try {
            while ((line = br.readLine()) != null) {
                System.out.println("read " + mSocket.getPort() + " = " + line);
                //把信息发送加入到消息队列,
                // 借助消息队列的被观察者通知方法,
                // 将消息转发至其他Socket(所有socket都在创建ClientTask的时候,
                // 备注成为MsgPool 的观察者)
                MsgPool.getInstance().sendMsg(mSocket.getPort() + ": " + line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    //作为消息队列的观察者对应的更新方法,
    // 消息队列中最新的消息会推送通知到这里的msg参数,
    // 这里拿到最新的推送消息后,写进输出流,
    // 推到TCP 连接的客户一端的 socket
    @Override
    public void onMsgComing(String msg) {
        try {
            mOs.write(msg.getBytes());
            mOs.write("\n".getBytes());
            mOs.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • **准备一个消息队列,

每一个Client发送过来的消息,
都会被加入到队列当中去,
队列中默认有一个子线程,
专门从队列中,死循环,不断去取数据(取出队列的队头),
取到数据就做相关处理,比如分发给其他的socket;**

/**
 * <pre>
 *     author : 李蔚蓬(简书_凌川江雪)
 *     time   : 2019/10/30 17:45
 *     desc   :每一个Client发送过来的消息,
 *             都会被加入到队列当中去,
 *             队列中默认有一个子线程,
 *             专门从队列中,死循环,不断去取数据,
 *             取到数据就做相关处理,比如分发给其他的socket;
 * </pre>
 */
public class MsgPool {

    private static MsgPool mInstance = new MsgPool();

    /*
        这里默认消息是String类型,
        或者可以自行封装一个Model 类,存储更详细的信息

        block n.块; 街区;障碍物,阻碍
        顾名思义,这是一个阻塞的队列,当有消息过来时,就把消息发送给这个队列,
        这边会起一个线程专门从队列里面去取消息,
        如果队列中没有消息,就会阻塞在原地
     */
    private LinkedBlockingQueue<String> mQueue = new LinkedBlockingQueue<>();

    public static MsgPool getInstance() {
        return mInstance;
    }

    private MsgPool() {
    }

    //这是一个阻塞的队列,
    // 当有消息过来时,即客户端接收到消息时,
    // 就把消息发送(添加)到这个队列中
    //现在所有的客户端都可以发送消息到这个队列中
    public void sendMsg(String msg) {
        try {
            mQueue.put(msg);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    //要一早就调用本方法,
    // 启动这个读取消息的线程,在后台不断运行
    public void start() {
        //开启一个线程去读队列的数据
        new Thread() {
            @Override
            public void run() {
                //无限循环读取信息
                while (true) {
                    try {
                        //取出并移除队头;没有消息时,take()是阻塞的
                        String msg = mQueue.take();
                        notifyMsgComing(msg);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }

    //被观察者方法,遍历所有已注册的观察者,一次性通知更新
    private void notifyMsgComing(String msg) {
        for (MsgComingListener listener : mListeners) {
            listener.onMsgComing(msg);
        }
    }

    //观察者接口
    public interface MsgComingListener {
        void onMsgComing(String msg);//更新方法
    }

    //被观察者,存放观察者
    private List<MsgComingListener> mListeners = new ArrayList<>();

    //被观察者方法,添加观察者到列表
    public void addMsgComingListener(MsgComingListener listener) {
        mListeners.add(listener);
    }
}

**所有的客户端都可发送消息到队列中,
然后所有的客户端都在等待
消息队列的消息新增(mQueue.put())这个时刻,
消息队列一新增消息,
即一接收到某个客户端发送过来消息(mQueue.put()),
则消息都会一次性转发给所有客户端,
所以这里涉及到一个观察者设计模式,
消息队列(MsgPool)或消息(Msg)是被观察者,
所有客户端处理线程(ClientTask)都是观察者**

观察者模式实现小结:

**观察者接口准备更新(数据或UI的)方法;
被观察者接口准备三个抽象方法;

观察者实现类具体实现更新逻辑,可以有参数,参数为更新需要的数据;

被观察者实现类准备一个观察者List以及实现三个方法:
1.观察者注册方法:
参数为某观察者,功能是把观察者参数加到观察者List中;
2.注销观察者方法:
参数为某观察者,功能是把观察者参数从观察者List中移除;
3.通知观察者方法:无参数或者把需要通知的数据作为参数,
功能是遍历所有已注册的观察者,
即遍历 注册添加到 观察者List中的观察者,逐个调用List中所有观察者的更新方法;即一次性更新所有已注册的观察者!

使用时,
实例化一个被观察者和若干个观察者,
将所有观察者注册到被观察者处,
调用被观察者的通知方法,一次性更新所有已注册的观察者!**

创建TCP客户端

  • 创建两个Package,整理一下项目架构,创建一个TcpClient:
/**
 * <pre>
 *     author : 李蔚蓬(简书_凌川江雪)
 *     time   : 2019/10/31 15:36
 *     desc   :
 * </pre>
 */
public class TcpClient {

    private Scanner mScanner;

    public TcpClient() {
        mScanner = new Scanner(System.in);
        mScanner.useDelimiter("\n");
    }

    /**
     * 配置socket
     * 准备IO 流,
     * 主线程写,子线程读
     *
     */
    public void start() {
        try {
            Socket socket = new Socket("***", 9090);
            InputStream is = socket.getInputStream();
            OutputStream os = socket.getOutputStream();

            final BufferedReader br = new BufferedReader(new InputStreamReader(is));
            BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os));

            /*
                实现:
                通过 reader,
                在任何时候 能够读到 Server端 发来的数据
                通过 writer,
                在任何时候 能够向 Server端 去写数据
             */
            //在等待客户端 发送消息过来的话,这里是需要阻塞的,
            // 阻塞的时候又没有办法向客户端发送数据,所以读写独立的话,肯定是要起线程的

            //起一个线程,专门用于
            // 读Server 端 发来的数据,数据一过来就读然后输出,
            // 输出服务端发送的数据
            new Thread() {
                @Override
                public void run() {

                    try {
                        String line = null;
                        while ((line = br.readLine()) != null) {
                            System.out.println(line);
                        }
                    } catch (IOException e) {
                    }
                }
            }.start();

            //给Server端 发送数据
            while (true) {
                //next() 是阻塞的,不断地读控制面板,有数据就会通过bufferWriter,
                // 即outputStream 写给Server
                String msg = mScanner.next();
                bw.write(msg);
                bw.newLine();
                bw.flush();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        new TcpClient().start();
    }
}
  • 反复测试:

移植客户端到Android移动端

  • 复制TcpClient到biz包下迭代,名为TcpClientBiz:
/**
 * <pre>
 *     author : 李蔚蓬(简书_凌川江雪)
 *     time   : 2019/10/31 15:36
 *     desc   : 定义接口,完成客户端的收发逻辑
 * </pre>
 */
public class TcpClientBiz {

    private Socket mSocket;
    private InputStream mIs;
    private OutputStream mOs;

    /**
     * Looper.getMainLooper(),将主线程中的 Looper 扔进去了,
     * 也就是说 handleMessage 会运行在主线程中,
     * !!!!!!!!!!
     * 这样可以在主线程中更新 UI 而不用把 Handler 定义在主线程中。
     * !!!!!!!!!!
     */
    private Handler mUiHandler = new Handler(Looper.getMainLooper());

//    /*
//        注意,因为UdpClient 的send 和 receive 是绑定的,
//        所以其 返回信息的处理接口 是作为 发送信息方法 的参数的,由此产生绑定逻辑
//
//        但是这里 TcpClient 就不是send 和 receive 一一绑定了,
//        其没有数量的对应关系,只是一个持续的 任意数据包数量的 全双工的连接,
//        无需Udp 的绑定逻辑, Listener 由此不使用跟send 方法绑定的逻辑,
//        使用单独set 的逻辑表达方式
//     */

    public interface onMsgComingListener {
        void onMsgComing(String msg);
        void onError(Exception ex);
        void popToast();
    }

    private onMsgComingListener mListener;

    public void setOnMsgComingListener(onMsgComingListener listener) {
        mListener = listener;
    }

    //------------------------------------------------------------------------

    public TcpClientBiz() {

//        //socket 的new 到 IO 流的获取 这几行代码是已经做了网络操作的,
//        // 所以必须开一个子线程去进行,!!!!
//        // 毕竟 TcpClientBiz() 在调用的时候肯定是在UI 线程进行的
//
//        /*
//            另外需要注意一点!!!
//            下面的socket 和 IO 流初始化是在子线程中进行的,
//            所以我们不知道什么时候会完成初始化,
//            因此在使用的时候是需要进行一个UI 交互提醒的,
//            比如loading 动画,启动页面时使用loading动画,初始化完成之后再取消loading 动画,
//
//         */
        new Thread() {
            @Override
            public void run() {
                try {
                    mSocket = new Socket("172.18.1.59", 9090);//连接到 Server端
                    mIs = mSocket.getInputStream();
                    mOs = mSocket.getOutputStream();

                    mUiHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            mListener.popToast();
                        }
                    });

                    //读到消息则 借用回调 回到MainActivity 进行UI 更新
                    readServerMsg();

                } catch (final IOException e) {

                    mUiHandler.post(new Runnable() {
                        @Override
                        public void run() {

                            if (mListener != null) {
                                mListener.onError(e);
                            }
                        }
                    });
                }
            }
        }.start();


    }

    /**
     * 一旦本类被实例化,马上启动
     * 不断阻塞等待Server端 信息
     * readLine() 没有消息时阻塞,
     * 一有消息,马上发给接口处理逻辑
     *
     * @throws IOException
     */
    private void readServerMsg() throws IOException {

        final BufferedReader br = new BufferedReader(new InputStreamReader(mIs));

        String line = null;

        while ((line = br.readLine()) != null) {

            final String finalLine = line;

            /*
                !!!!!!!!!!!!!!!!
                基于主线程MainLooper 以及 回调机制
                在 业务类内部 调用 外部实现的处理逻辑方法
                !!!!!!!!!!
             */
            mUiHandler.post(new Runnable() {
                @Override
                public void run() {

                    //读到消息则 借用回调 回到MainActivity 进行UI 更新
                    if (mListener != null) {
                        mListener.onMsgComing(finalLine);
                    }
                }
            });

        }
    }

    /**
     * 把参数msg 写入BufferWriter(O流),发送给Server端,
     * 一般这个msg 消息 是EditText 中的内容,
     *
     * 调用时机:一般是EditText 右边的按钮被点击的时候
     *
     * 调用时,封装输出流,
     * 把参数msg 写入BufferWriter(O流),发送给Server端,
     *
     * 在要发送消息给Server 的时候调用
     * 发送的消息会在Server 端的 ClientTask 类中
     * 的run() 中的while ((line = br.readLine()) != null) 处被读取到,
     * 并通过 MsgPool.getInstance().sendMsg() 被添加到消息队列中
     *
     * @param msg  要发送的信息
     */
    public void sendMsg(final String msg) {

        //开一个线程去做输出,完成任务之后线程就自动回收
        new Thread(){
            @Override
            public void run() {
                try {
                    //一有消息过来,就封装输出流,写入并 发送信息到 Server端
                    BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(mOs));
                    bw.write(msg);
                    bw.newLine();
                    bw.flush();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();
    }

    public void onDestroy() {
        //!!!!
        // 独立地try...catch...的原因:
        // !!!!
        // 如果把三个close 都放在同一个try 块里面
        // 那假如第一个close 出现了异常,
        // 后面两个close 即使没异常,
        // 也处理不了了,这显然是不符合条件的
        // !!!!!

        try {
            if (mIs != null) {
                mIs.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            if (mOs != null) {
                mOs.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            if (mSocket != null) {
                mSocket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • rename一下MainActivity为UdpActivity:

复制UdpActivity一份,原地粘贴,命名为TcpActivity:

public class TcpActivity extends AppCompatActivity {

    private EditText mEtMsg;
    private Button mBtnSend;
    private TextView mTvContent;

    private TcpClientBiz mTcpClientBiz = new TcpClientBiz();


    public Context getTcpActivityContext() {
        return getApplicationContext();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initViews();

        mTcpClientBiz.setOnMsgComingListener(new TcpClientBiz.onMsgComingListener() {
            @Override
            public void onMsgComing(String msg) {
                appendMsgToContent("Server:" + msg);
            }

            @Override
            public void onError(Exception ex) {
                ex.printStackTrace();
            }

            @Override
            public void popToast() {
                Toast.makeText(TcpActivity.this, "初始化完成!!!!可以开始发送信息了!!!", Toast.LENGTH_SHORT).show();
            }
        });
    }

    private void initViews() {
        mEtMsg = findViewById(R.id.id_et_msg);
        mBtnSend = findViewById(R.id.id_btn_send);
        mTvContent = findViewById(R.id.id_tv_content);

        mBtnSend.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String msg = mEtMsg.getText().toString();
                if (TextUtils.isEmpty(msg)) {
                    return;
                }

                //发送后清除编辑框文本
                mEtMsg.setText("");

                //msg 负责发送数据,onMsgReturnedListener() 则负责处理对应的返回的信息
                mTcpClientBiz.sendMsg(msg);
            }
        });
    }

    private void appendMsgToContent(String msg) {
        mTvContent.append(msg + "\n");
    }

    /*
        回收资源
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mTcpClientBiz.onDestroy();
    }
}

更改启动页面:

  • 反复测试(一个模拟机和两台真机的聊天哈哈哈):
  • 最终Server端聊天记录:
服务端诸类代码实现概述(TcpServer、ClientTask、MsgPool)
  • TcpServer:

死循环,阻塞,等待客户端请求连接,while (true) & .accept();
一旦连接上,获取对应的socket对象并
把它丢给ClientTask的构造方法,new ClientTask(socket)
直接新建一个子线程,去处理这个socket(.start()),
将连接上的socket 对应的线程类,注册到消息队列类中的队列中,
成为消息队列的观察者;MsgPool.getInstance().addMsgComingListener(clientTask)
启动消息队列读读队列的线程,
MsgPool.getInstance().start();

  • ClientTask:

public class ClientTask extends Thread implements MsgPool.MsgComingListener
让线程类作为消息队列的观察者,
负责接收被观察者的通知信息并做socket 通信;
类中:

- 1/3 构造方法:

接收TcpServer对过来的socket对象,
用之初始化其IO流;

- 2/3 **`run()`:`<读取Client的 I流,加入 MsgPool.mQueue>`

封装输入流,
读取客户端发送过来的信息并输出:
while ((line = br.readLine()) != null){...}
System.out.println(...);
把信息发送加入到消息队列:MsgPool.getInstance().sendMsg(...);
如果没有客户端发送信息,
readLine() 便会阻塞(注意这里会阻塞!所以要放在子线程!)在原地**

 - 3/3 **`onMsgComing(String msg)`:`<取出 MsgPool.mQueue,写入Client的 O流>`

作为消息队列的观察者对应的更新方法,
消息队列中最新的消息会推送通知到这里的msg参数,
(消息队列类有一个子线程死循环阻塞读取队头,
String msg = mQueue.take();
notifyMsgComing(msg);
notifyMsgComing中遍历所有已注册的观察者,
遍历时调用观察者的onMsgComing(msg)
正是本方法!!!)
本方法中拿到最新的推送消息后,
写进输出流,
发送给对应的 TCP 连接的客户一端socket**

  • class MsgPool消息列表类

    • **实现单例模式

private static MsgPool mInstance = new MsgPool();
`public static MsgPool getInstance() {

    return mInstance;
}`

`private MsgPool() {

}`**

- **准备消息列表底层数据结构:

private LinkedBlockingQueue<String> mQueue = new LinkedBlockingQueue<>();**

- **sendMsg(String msg):

当有消息过来时,即客户端接收到消息时,
就把消息发送(添加)到消息队列中:mQueue.put(msg);
ClientTaskrun()调用本方法!!!;**

-  **start()

启动读取消息的子线程,在后台不断运行,
死循环 阻塞 读取队头,
一有消息取出就通知所有已注册的观察者,
String msg = mQueue.take();
notifyMsgComing(msg);
在TcpServer中一开始配置好服务ip和端口就调用了;**

- **实现被观察者通知方法:`notifyMsgComing(String msg)`

实现被观察者方法,添加观察者到列表:
public void addMsgComingListener(MsgComingListener listener)
观察者接口MsgComingListener
被观察者列表private List<MsgComingListener> mListeners = new ArrayList<>();**

客户端诸类代码实现概述(TcpClientBiz、TcpActivity)
  • TcpClientBiz:

连接Server端,
后台子线程不断接收Server端发送过来的信息,
前台封装、提供向Server端发送信息的方法

- 准备一个绑定了`mainLooper`的`Handler`
- **定义`<回调机制>`

回调接口及其抽象方法;
声明 全局 回调接口变量;
回调接口置入函数;
`setOnMsgComingListener(onMsgComingListener listener) {

    mListener = listener;
}`**

- **构造方法:**
     - **开启子线程!!!,

配置连接到Server端的socket; mSocket = new Socket("***.**.*.**", 9090);**

     - **通过socket获得IO流;

(以上,socket,IO流都初始化给全局变量)**

     - **使用全局 回调接口变量,

抽象调用业务方法;(Toast提醒、Error处理之类)**

     - **调用`readServerMsg()`!!!;**

- **`readServerMsg()`

一旦本类被实例化,就会被启动!!!

开启一个子线程,
拿着全局变量I流,封装成BufferReader,
死循环 阻塞等待 读取Server端信息
while ((line = br.readLine()) != null)
一旦有信息,
借助Handler.post(),
使用全局 回调接口变量抽象调用接口方法onMsgComing()
通过回调机制交给Activity层处理;**

- `sendMsg(final String msg)`

开启一个子线程,
拿着全局变量O流,封装成BufferWriter,
把参数msg 写入BufferWriter(O流),发送给Server端;
调用时机:在要发送消息给Server 的时候调用,
一般是EditText 右边的按钮被点击的时候

- onDestroy():

回收socket、IO流

  • **TcpActivity

主要是各种组件的配置,
注意几点即可:**

- **需要实例化一个全局TcpClientBiz实例

然后用匿名内部类实现回调接口及其方法,
再set 给TcpClientBiz实例;**

- **点击按钮时把EditText的内容发送给Server端;

msg = mEtMsg.getText().toString();
mTcpClientBiz.sendMsg(msg);**

- **onDestroy()中调用`mTcpClientBiz.onDestroy();`回收资源**

**所有的客户端都可发送消息到队列中,
然后所有的客户端都在等待
消息队列的消息新增(mQueue.put())这个时刻,
消息队列一新增消息,
即一接收到某个客户端发送过来消息(mQueue.put()),
则消息都会一次性转发给所有客户端,
所以这里涉及到一个观察者设计模式,
消息队列(MsgPool)或消息(Msg)是被观察者,
所有客户端处理线程(ClientTask)都是观察者**

观察者模式实现小结:

**观察者接口准备更新(数据或UI的)方法;
被观察者接口准备三个抽象方法;

观察者实现类具体实现更新逻辑,可以有参数,参数为更新需要的数据;

被观察者实现类准备一个观察者List以及实现三个方法:
1.观察者注册方法:
参数为某观察者,功能是把观察者参数加到观察者List中;
2.注销观察者方法:
参数为某观察者,功能是把观察者参数从观察者List中移除;
3.通知观察者方法:无参数或者把需要通知的数据作为参数,
功能是遍历所有已注册的观察者,
即遍历 注册添加到 观察者List中的观察者,逐个调用List中所有观察者的更新方法;即一次性更新所有已注册的观察者!

使用时,
实例化一个被观察者和若干个观察者,
将所有观察者注册到被观察者处,
调用被观察者的通知方法,一次性更新所有已注册的观察者!**

相关文章
|
6天前
|
JavaScript NoSQL Java
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
147 96
接替此文【下篇-服务端+后台管理】优雅草蜻蜓z系统JAVA版暗影版为例-【蜻蜓z系列通用】-2025年全新项目整合搭建方式-这是独立吃透代码以后首次改变-独立PC版本vue版搭建教程-优雅草卓伊凡
|
1月前
|
SQL Java 数据库连接
如何在 Java 代码中使用 JSqlParser 解析复杂的 SQL 语句?
大家好,我是 V 哥。JSqlParser 是一个用于解析 SQL 语句的 Java 库,可将 SQL 解析为 Java 对象树,支持多种 SQL 类型(如 `SELECT`、`INSERT` 等)。它适用于 SQL 分析、修改、生成和验证等场景。通过 Maven 或 Gradle 安装后,可以方便地在 Java 代码中使用。
244 11
|
1月前
|
JSON Java 数据挖掘
利用 Java 代码获取淘宝关键字 API 接口
在数字化商业时代,精准把握市场动态与消费者需求是企业成功的关键。淘宝作为中国最大的电商平台之一,其海量数据中蕴含丰富的商业洞察。本文介绍如何通过Java代码高效、合规地获取淘宝关键字API接口数据,帮助商家优化产品布局、制定营销策略。主要内容包括: 1. **淘宝关键字API的价值**:洞察用户需求、优化产品标题与详情、制定营销策略。 2. **获取API接口的步骤**:注册账号、申请权限、搭建Java开发环境、编写调用代码、解析响应数据。 3. **注意事项**:遵守法律法规与平台规则,处理API调用限制。 通过这些步骤,商家可以在激烈的市场竞争中脱颖而出。
|
2月前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
75 3
|
2月前
|
前端开发 Java 测试技术
java日常开发中如何写出优雅的好维护的代码
代码可读性太差,实际是给团队后续开发中埋坑,优化在平时,没有那个团队会说我专门给你一个月来优化之前的代码,所以在日常开发中就要多注意可读性问题,不要写出几天之后自己都看不懂的代码。
78 2
|
2月前
|
Java 编译器 数据库
Java 中的注解(Annotations):代码中的 “元数据” 魔法
Java注解是代码中的“元数据”标签,不直接参与业务逻辑,但在编译或运行时提供重要信息。本文介绍了注解的基础语法、内置注解的应用场景,以及如何自定义注解和结合AOP技术实现方法执行日志记录,展示了注解在提升代码质量、简化开发流程和增强程序功能方面的强大作用。
139 5
|
2月前
|
安全 Java API
Java中的Lambda表达式:简化代码的现代魔法
在Java 8的发布中,Lambda表达式的引入无疑是一场编程范式的革命。它不仅让代码变得更加简洁,还使得函数式编程在Java中成为可能。本文将深入探讨Lambda表达式如何改变我们编写和维护Java代码的方式,以及它是如何提升我们编码效率的。
|
3天前
|
JavaScript 搜索推荐 Android开发
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
22 8
【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
|
15天前
|
前端开发 Java Shell
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
115 20
【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
|
12天前
|
Dart 前端开发 Android开发
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
36 4
【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex

热门文章

最新文章

  • 1
    如何修复 Android 和 Windows 不支持视频编解码器的问题?
  • 2
    【08】flutter完成屏幕适配-重建Android,增加GetX路由,屏幕适配,基础导航栏-多版本SDK以及gradle造成的关于fvm的使用(flutter version manage)-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 3
    当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
  • 4
    【04】flutter补打包流程的签名过程-APP安卓调试配置-结构化项目目录-完善注册相关页面-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程
  • 5
    APP-国内主流安卓商店-应用市场-鸿蒙商店上架之必备前提·全国公安安全信息评估报告如何申请-需要安全评估报告的资料是哪些-优雅草卓伊凡全程操作
  • 6
    【09】flutter首页进行了完善-采用android studio 进行真机调试开发-增加了直播间列表和短视频人物列表-增加了用户中心-卓伊凡换人优雅草Alex-开发完整的社交APP-前端客户端开发+数据联调|以优雅草商业项目为例做开发-flutter开发-全流程-商业应用级实战开发-优雅草Alex
  • 7
    Android经典面试题之Kotlin中Lambda表达式和匿名函数的区别
  • 8
    【02】仿站技术之python技术,看完学会再也不用去购买收费工具了-本次找了小影-感觉页面很好看-本次是爬取vue需要用到Puppeteer库用node.js扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-优雅草卓伊凡
  • 9
    【01】仿站技术之python技术,看完学会再也不用去购买收费工具了-用python扒一个app下载落地页-包括安卓android下载(简单)-ios苹果plist下载(稍微麻烦一丢丢)-客户的麻将软件需要下载落地页并且要做搜索引擎推广-本文用python语言快速开发爬取落地页下载-优雅草卓伊凡
  • 10
    如何配置 Java 环境变量:设置 JAVA_HOME 和 PATH