zl程序教程

您现在的位置是:首页 >  移动开发

当前栏目

Android NIO 系列教程(八) --NIO简易聊天室

Androidnio教程 -- 系列 简易 聊天室
2023-09-27 14:28:04 时间

Android NIO 系列教程(一) NIO概述
Android NIO 系列教程(二) – Channel
Android NIO 系列教程(三) – Buffer
Android NIO 系列教程(四) – Selector
Android NIO 系列教程(五) – FileChannel
Android NIO 系列教程(六) – SocketChannel
Android NIO 系列教程(七) – ServerSocketChannel
Android NIO 系列教程(八) --NIO简易聊天室
从上面几章,我们已经知道了 NIO 的 SocketChannel ,ServerSocketChannel,Selector 等知识,这章我们来做个总结,实现一个简易的聊天室。
今天要实现的效果如下:
NIO聊天室
首先,先构建服务器,从上面几章的理解,我们总结出以下步骤

  1. 创建 selector
  2. 创建ServerSocketChannel
  3. 创建非阻塞模式
  4. 绑定要监听的端口
  5. 向selector注册 channel
  6. 通过 selectionKeys 拿到想要监听的事件

所以服务端的代码如下:
首先是初始化:

 //1.创建 selector
 Selector selector = Selector.open();
 //2.创建 ServerSocketChannel
 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
 //3.绑定端口
 serverSocketChannel.bind(new InetSocketAddress(Constants.PORT));
 //4.设置为非阻塞模式
 serverSocketChannel.configureBlocking(false);
 //5.将channel注册到selector中
 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
 System.out.println("服务端启动成功,开始监听...");

接着监听感兴趣的事件,比如客户端接入和客户端的数据:

 // 7.通过selectedKeys() 拿到 selectedKeys 集合
    Set<SelectionKey> selectionKeys = selector.selectedKeys();
     Iterator<SelectionKey> iterator = selectionKeys.iterator();
     while (iterator.hasNext()){
         //拿到 selectedKeys 实例
         SelectionKey selectionKey = iterator.next();
         //移除 selectedKeys 实例
         iterator.remove();
         /**
          * 如果是接入事件
          */
         if (selectionKey.isAcceptable()){
             handleAccept(serverSocketChannel,selector);
         }

         /**
          * 如果是可读事件
          */
         if (selectionKey.isReadable()){
             handleRead(selectionKey,selector);
         }
     }

如果是 accept 事件,我们处理的比较简单,就打印个提示即可:

    /**
     * 处理接受事件
     * @param serverSocketChannel
     * @param selector
     * @throws IOException
     */
    private static void handleAccept(ServerSocketChannel serverSocketChannel,Selector selector) throws IOException {
        //拿到 SocketChannel 客户端
        SocketChannel socketChannel = serverSocketChannel.accept();
        System.out.println("新客户端连接:"+socketChannel.getRemoteAddress().toString());
        //设置 socketchannel 为非阻塞模式
        socketChannel.configureBlocking(false);
        //客户端注册读事件,这样我们才能接收到客户端的信息
        socketChannel.register(selector,SelectionKey.OP_READ);
        //发送 conected 提示服务端已经接收到
      //  ByteBuffer buf = Charset.forName("utf-8").encode(Constants.CLIENT_CONNECTED);
       // socketChannel.write(buf);

    }

注意到对 socketChannel 注册成 read 模式,这样,客户端的数据我们才能接收到。
接着是监听:

    /**
     * 处理客户端读事件,并广播出去
     * @param selectionKey
     * @param selector
     * @throws IOException
     */
    private static void handleRead(SelectionKey selectionKey,Selector selector) throws IOException {
        //拿到已经就绪的 channel
        SocketChannel channel = (SocketChannel) selectionKey.channel();
        if (channel != null) {
            //读取channel的数据
            ByteBuffer buf = ByteBuffer.allocate(512);
            StringBuilder sb = new StringBuilder();
            int readByte = channel.read(buf);
            while (readByte > 0) {
                //切换为读模式
                buf.flip();
                String msg = String.valueOf(Charset.forName("utf-8").decode(buf));
                sb.append(msg);
                readByte = channel.read(buf);
            }
            buf.clear();
            //将 channel 继续注册为可读事件
            channel.register(selector, SelectionKey.OP_READ);
            if (sb.length() > 0) {
                System.out.println(channel.getRemoteAddress().toString()+" : " + sb.toString());
                 //返回数据
                //String responeMsg = sb.length();
                //channel.write(Charset.forName("utf-8").encode(responeMsg));
                //广播
                broadcastMsg(selector,channel,sb.toString());
            }

        }

    }

注释已经很清楚了,这里就不过多解释了。然后广播的代码为:

    /**
     * g
     * @param selector
     * @param targetChannel
     * @param msg
     * @throws IOException
     */
    private static void broadcastMsg(Selector selector,SocketChannel targetChannel,String msg) throws IOException {
        //拿到已连接的客户端个数
        Set<SelectionKey> keys = selector.keys();
        for (SelectionKey selectionKey : keys) {
            Channel channel =  selectionKey.channel();
            //不是自己本身,其他通道才需要拿到信息
            if (channel instanceof SocketChannel){
                if (targetChannel != null &&
                        channel == targetChannel ){
                    continue;
                }
                ((SocketChannel) channel).write(Charset.forName("utf-8").encode(msg));
            }
        }

    }

接着是客户端,我们也总结了以下步骤:

  1. 创建 selector
  2. 创建 SocektChannel
  3. 设置非阻塞模式
  4. 使用 connect 连接服务器
  5. 通过 read() 或 write() 读写数据

首先,首先初始化为:

        //1.创建 selector
        Selector selector = Selector.open();
        //2.创建SocketChannel
        SocketChannel socketChannel = SocketChannel.open();
        //3.设置为非阻塞模式
        socketChannel.configureBlocking(false);
        //4.连接服务器
        socketChannel.connect(new InetSocketAddress("localhost", Constants.PORT));
        //注册读事件,读取服户端信息
        socketChannel.register(selector, SelectionKey.OP_READ);

接着,读取终端的信息,并发送给服务端:

if (socketChannel.finishConnect()) {
            while (!isFinish) {
                String msg = br.readLine();
                if ("bye".equals(msg)) {
                    isFinish = true;
                    sendData(socketChannel,"客户端A退出了");
                    readerThread.exit();
                    socketChannel.close();
                    System.out.println("服务端已退出");
                    break;
                } else {
                    sendData(socketChannel,msg);
                }

            }
        }

其中 sendData 如下:

    /**
     * 给服务器发送数据
     * @param channel
     * @param msg
     * @throws IOException
     */
    private static void sendData(SocketChannel channel,String msg) throws IOException {
        if (channel.isConnected()){
            channel.write(Charset.forName("utf-8").encode(msg));
        }
    }

而读取服务端的信息,则另开一个线程去监听数据即可:

static class ReaderThread extends Thread{
        private Selector selector;
        private boolean isFinish = false;
        public ReaderThread(Selector selector) {
            this.selector = selector;
        }

        @Override
        public void run() {
            super.run();
            try {
                while (!isFinish){
                    //调用 select 方法,拿到 channel
                    int channels = selector.select();
                    if (channels == 0){
                        continue;
                    }
                    //如果能拿到,则通过 selectedKeys() 方法,拿到 selectedKeys 的集合
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()){
                        //拿到 selectedKeys 实例
                        SelectionKey selectionKey = iterator.next();
                        //如果是可读事件
                        if (selectionKey.isReadable()){
                            //拿到 就绪的 SocketChannel
                            SocketChannel channel = (SocketChannel) selectionKey.channel();
                            if (channel != null) {
                                //读取channel的数据
                                ByteBuffer buf = ByteBuffer.allocate(1024);
                                StringBuilder sb = new StringBuilder();
                                while (channel.read(buf) > 0) {
                                    //切换为读模式
                                    buf.flip();
                                    String msg = String.valueOf(Charset.forName("utf-8").decode(buf));
                                    sb.append(msg);
                                }
                                //将 channel 继续注册为可读事件
                                channel.register(selector, SelectionKey.OP_READ);
                                if (Constants.CLIENT_CONNECTED.equals(sb.toString())){
                                    sendData(channel,"我是客户端A");
                                }else {
                                    System.out.println(sb.toString());
                                }

                            }

                        }
                        iterator.remove();
                    }
                }
            } catch (IOException e) {
               // e.printStackTrace();
            }finally {
                exit();
            }
        }

        public void exit(){
            isFinish = true;
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

这样,我们一个简易的聊天室就完成了。

可能你会问到,这样并没有体现 NIO 的单个 selector 和 多个channel 的优势啊,还有其他等问题,这些,等我们后面再去优化。