美文网首页
NIO实现多人聊天室

NIO实现多人聊天室

作者: 贪挽懒月 | 来源:发表于2020-07-18 15:36 被阅读0次

    一、项目介绍

    上周发布了《java的IO模型》一文,讲到了NIO,打铁要趁热,这周来个实战,用NIO实现一个简易的多人聊天室。聊天室,肯定是需要一个服务端和一个客户端的。就像QQ群一样,首先我们每个人都要安装QQ,这个就是客户端,服务端呢就是腾讯的QQ服务器,我们在客户端发送一条消息,服务端接收到了,然后再转发到别的客户端上,所以大家在这个QQ群的都能收到你发的消息。那接下来就开始是服务端和客户端的编码。


    欢迎大家关注我的公众号 javawebkf,目前正在慢慢地将简书文章搬到公众号,以后简书和公众号文章将同步更新,且简书上的付费文章在公众号上将免费。


    二、服务端编码

    整个流程大致如下:

    • 在构造方法里初始化一些参数。首先是得到选择器,然后打开ServerSocketChannel ,并监听6666端口;然后设置成非阻塞模式,注册到选择器上。

    • 然后写一个监听的方法,看看选择器上有没有通道有事件需要处理的,如果有,我们就遍历选择器,针对拿到的不同事件进行不同的处理。如果是连接事件,即有客户端连接这个服务端,就打印出“xxx上线了”;如果是读取事件,表示客户端有人发消息到服务端了,那我们就将消息读取到通道中,最后转成字符串,打印在控制台,并且将该消息发送到其他客户端。

    • 发送消息到其他客户端的逻辑也很简单,就是遍历所有注册到选择器上的通道(除了自己),然后将消息发送到这些通道中。

    • 最后在main方法中创建服务端对象,开始监听即可。

    以下是完整代码:

    public class GroupChatServer {
        
        // 定义需要用到属性
        private Selector selector;
        private ServerSocketChannel listenChannel;
        private static final int PORT = 6666;
        
        /**
         * 构造器,进行初始化
         */
        public GroupChatServer() {
            try {
                // 得到选择器
                selector = Selector.open();
                // 得到ServerSocketChannel
                listenChannel = ServerSocketChannel.open();
                // 绑定端口
                listenChannel.socket().bind(new InetSocketAddress(PORT));
                // 设置非阻塞模式
                listenChannel.configureBlocking(false);
                // 将listenChannel注册到selector上
                listenChannel.register(selector, SelectionKey.OP_ACCEPT);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
        /**
         * 监听
         */
        public void listen() {
            try {
                while (true) {
                    int count = selector.select();
                    // count > 0 就表示有事件要处理
                    if (count > 0) {
                        // 拿到selectionKey的集合
                        Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                        while (iterator.hasNext()) {
                            // 取出selectionKey
                            SelectionKey key = iterator.next();
                            // 监听到不同的事件进行不同的处理
                            listenHandler(key);
                            // 删除当前key,防止重复处理
                            iterator.remove();
                        }
                    } else {
                        System.out.println("当前没事件要处理,等待中……");
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        
        
        /**
         * 针对监听到的不同事件做不同的处理
         * @param key
         * @throws Exception
         */
        public void listenHandler(SelectionKey key) {
            SocketChannel sc = null;
            try {
                // 如果是连接事件
                if (key != null && key.isAcceptable()) {
                    // 拿到socketChannel并注册到selector上
                    sc = listenChannel.accept();
                    sc.configureBlocking(false);
                    sc.register(selector, SelectionKey.OP_READ);
                    // 提示:xxx上线了
                    System.out.println(sc.getRemoteAddress().toString().substring(1) + "上线了");
                }
                // 如果是读取事件
                if (key != null && key.isReadable()) {
                    // 拿到socketChannel
                    sc = (SocketChannel) key.channel();
                    sc.configureBlocking(false);
                    // 创建buffer
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    // 将客户端消息读到buffer中去
                    int count = sc.read(buffer);
                    if (count > 0) {
                        // 如果读取到了数据,就把缓冲区数据转成字符串输出
                        String msg = new String(buffer.array());
                        System.out.println("from client " + msg);
                        // 将该消息转发给其他客户端,
                        sendMsgToClient(msg, sc);
                    }
                }
            } catch (IOException e) {
                try {
                    System.out.println(sc.getRemoteAddress().toString().substring(1) + "离线了");
                    // 取消注册
                    key.cancel();
                    // 关闭通道
                    sc.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
        }
        
        
        /**
         * 发送消息到客户端,要排除当前客户端。
         * 假如A、B、C三个客户端,当前消息是A发来的,那么只转发给B和C就可以。
         * @param msg
         * @param self
         * @throws IOException 
         */
        public void sendMsgToClient(String msg, SocketChannel self) throws IOException {
            System.out.println("服务器转发消息中");
            // 遍历所有注册到selector上的SocketChannel,并排除自己
            for (SelectionKey key : selector.keys()) {
                Channel channel = key.channel();
                if (channel instanceof SocketChannel && channel != self) {
                    SocketChannel targetChannel = (SocketChannel) channel;
                    // 将msg写到buffer中
                    ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                    // 将buffer数据写入通道
                    targetChannel.write(buffer);
                }
            }
        }
        
        
        public static void main(String[] args) {
            GroupChatServer server = new GroupChatServer();
            server.listen();
        }
    }
    

    三、客户端编码

    客户端的流程就简单很多了,整体流程大致如下:

    • 首先也是在构造方法里进行一些初始化,通过SocketChannel连接服务端,设置非阻塞,注册到选择器上。

    • 然后在写一个向服务端发送消息的方法,直接调用SocketChannel的write方法。

    • 再写一个接收服务端消息的方法,遍历选择器,如果存在有事件发生的通道,就拿到该通道,然后将通道中的数据读取出来,打印在控制台。

    • 最后是main方法,先创建客户端对象,然后new一个线程去调用接收消息的方法,然后在启用键盘录入,调用发送消息的方法,以便在控制台发送消息。

    完整代码如下:

    public class GroupChatClient {
    
        // 定义相关属性
        private final String HOST = "127.0.0.1";
        private final int PORT = 6666;
        private Selector selector;
        private SocketChannel socketChannel;
        private String username;
    
        /**
         * 构造器,进行初始化
         */
        public GroupChatClient() {
            try {
                selector = Selector.open();
                // 连接服务器
                socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
                // 设置非阻塞
                socketChannel.configureBlocking(false);
                // 将socketChannel注册到selector上
                socketChannel.register(selector, SelectionKey.OP_READ);
                // 拿到username
                username = socketChannel.getLocalAddress().toString().substring(1);
                System.out.println(username + " is ok");
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 向服务端发送消息
         * 
         * @param msg
         */
        public void sendMsgToServer(String msg) {
            msg = username + ":" + msg;
            try {
                // 发送消息
                socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 接收服务端发来的消息
         */
        public void acceptMsgFromServer() {
            try {
                int readChannel = selector.select();
                if (readChannel > 0) { // 有可用的通道
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        if (key.isReadable()) {
                            // 获取到通道
                            SocketChannel sc = (SocketChannel) key.channel();
                            // 创建buffer
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            // 将通道中的数据读到buffer中
                            sc.read(buffer);
                            // 将buffer中的数据转成字符串
                            String msg = new String(buffer.array());
                            System.out.println(msg.trim());
                            // 移除当前的key,防止重复操作
                            iterator.remove();
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) {
            // 1. 启动客户端
            GroupChatClient client = new GroupChatClient();
            // 2. 启动线程,每隔3秒就读取一次服务端的消息
            new Thread() {
                public void run() {
                    while (true) {
                        client.acceptMsgFromServer();
                        try {TimeUnit.SECONDS.sleep(3); } catch (Exception e) { e.printStackTrace();} 
                    }
                }
            }.start();
            // 3. 发送数据到服务端
            @SuppressWarnings("resource")
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String msg = scanner.nextLine();
                client.sendMsgToServer(msg);
            }
        }
    }
    

    这就是用NIO非阻塞模式实现的聊天室,你可以启动多个客户端,一个客户端发送消息,服务端和别的客户端都能收到。

    相关文章

      网友评论

          本文标题:NIO实现多人聊天室

          本文链接:https://www.haomeiwen.com/subject/stffkktx.html