JAVA IO 从零实践 (11.NIO通信 实现一个群聊系统)

本文介绍了一个使用JavaNIO实现的群聊系统,服务端通过选择器监听客户端连接和读取消息,并将接收到的消息转发给其他所有在线客户端,实现了非阻塞的群聊功能。客户端则包含发送和接收消息的逻辑,其中接收消息部分由单独线程处理。
摘要由CSDN通过智能技术生成

在上一篇中我们使用同步非阻塞的思想 集合NIO中的管道 选择器 还有缓冲区实现了多个客户端与一个服务端的通信。这次我们来一个更复杂的应用 多人群聊系统:

  • 编写一个NIO群聊系统,实现客户端和客户端的通信(非阻塞)
  • 服务端:可以检测用户上线 离线 并实现转发功能
  • 客户端:可以无阻塞的发丝哦那个消息给其他客户端用户们,同时接受其他客户端用户发来的消息

听着是不是很有意思??

那我们开始把!


首先我们梳理一下 这个群里系统的 核心设计点:
在上一篇中我们实现 客户端和服务端通信的时候, 客户端首先创建了一个管道 然后绑定给 selector 选择器 发送数据。
服务端这边 监听,首先selector 轮询 先判断接受请求 再处理读取数据的请求然后收到数据了。

而现在 情况发送了一点小小的改变:

  • 有多个客户端(之前也支持多客户端)意味着有多个管道来访问
  • 服务端原来是收到消息 现在它收到消息之后要把这个消息发给其他的客户端,这样就有了群聊效果
  • 客户端原来只是发消息 现在它还要兼顾收群聊系统里面其他客户端发来的消息

ok我们一步一步来:

服务端

public class Server {

    //定义一些选择器 通道 端口

    private Selector selector;

    private ServerSocketChannel serverSocketChannel;

    private static final int PORT = 9999;

    public Server(){
        //接受选择器
        try {
            //接受选择器
            selector =Selector.open();

            serverSocketChannel = ServerSocketChannel.open();

            serverSocketChannel.bind(new InetSocketAddress(PORT));

            serverSocketChannel.configureBlocking(false);

            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

定义好需要的东西 然后初始化 这些都是前几篇里面相同的内容

    public static void main(String[] args) {
        Server server = new Server();
        server.listen();
    }

    private void listen() {
        try{
           while (selector.select()>0){
               //获取所有准备好的事件
               Iterator<SelectionKey> it = selector.selectedKeys().iterator();

               while (it.hasNext()) {
                   SelectionKey selectionKey =it.next();
                   //判断一下这个事件是什么
                   if(selectionKey.isAcceptable()){
                       SocketChannel channel = serverSocketChannel.accept();
                       channel.configureBlocking(false);
                       channel.register(selector,SelectionKey.OP_READ);
                   }else if (selectionKey.isReadable())
                   {
                       //处理客户端的消息 把它转发给其他客户端

                       readClientData(selectionKey);
                   }
                   it.remove();
               }

           }

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

    }

在主方法中我们处于监听的状态,上面的监听代码和上一篇中几乎一模一样,
在上一篇中已经详细的描述了 这里就不多赘述了。

唯一改变的就是 readClientData(selectionKey);

之前通信的时候 这里服务端收到消息之后打出来就ok了 , 现在我们需要把它们转发到其他客户端的管道中去:


    /**
     * 接受当前客户端通道的信息,转发给其他客户端
     * @param selectionKey
     */
    private void readClientData(SelectionKey selectionKey) {
        SocketChannel clientChannel = null;

        try {
            clientChannel = (SocketChannel) selectionKey.channel();

            ByteBuffer buffer = ByteBuffer.allocate(1024);

            int count = clientChannel.read(buffer);

            if (count>0){
                buffer.flip();
                String msg = new String(buffer.array(),0,count);
                System.out.println("接收到客户端消息"+msg);
                sentToAllClient(msg,clientChannel);
            }
        } catch (Exception e) {
            try {
                System.out.println("有人离线了"+clientChannel.getRemoteAddress());
                selectionKey.cancel();
                serverSocketChannel.close();
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
        }

    }

我们从当前管道中 读取出消息 转换为字符串
然后把它发送给其他的客户端:

    private void sentToAllClient(String msg, SocketChannel clientChannel) throws IOException{

        System.out.println("--服务端转发当前客户端消息给其他所有在线注册的客户端--");

        System.out.println("当前处理线程为:"+Thread.currentThread().getName());

        for (SelectionKey key:selector.keys()){

            Channel channel = key.channel();

            //不要发数据发给自己: 判断遍历所有的通道时 当前通道是不是当前客户端的同一个通道
            if (channel instanceof SocketChannel && channel != clientChannel){
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                ((SocketChannel)channel).write(buffer);
            }
        }

    }

这个方法要注意 首先你想不想一个客户端发给群聊的所有客户端之后 要不要再发给它自己

这里我们选择不发自己:

所以我们要判断一下 这里获取到选择器里面所有的管道之后:

判断 第一它不是服务端管道(因为服务端也有一个channel)其次它不是自己当前的channel。

然后我们在for循环中 把这条消息 挨个发给所有的的channel。就实现了群聊的核心功能。

客户端

/**
 * 群聊系统客户端
 */

public class Client {
    private Selector selector;

    private SocketChannel socketChannel;

    private static final int PORT = 9999;

    public Client(){
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9999));
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("当前客户端就绪:");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) {
        Client client = new Client();

        //定义一个线程专门负责监听服务端发过来的读消息事件
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    client.readFromServer();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();

        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.println("主线程发消息-请说:");
            String msg = scanner.nextLine();
            client.sendToServer(msg);
        }

    }

    private void sendToServer(String msg) {
        try {
            socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    private void readFromServer() throws IOException {
        System.out.println("副线程收消息:");
        //监听其他客户端发来的消息
        while (selector.select()>0){
            Iterator<SelectionKey> it = selector.selectedKeys().iterator();

            while (it.hasNext()) {
                SelectionKey selectionKey =it.next();
                //判断一下这个事件是什么
             if (selectionKey.isReadable()){
                    SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = ByteBuffer.allocate(1024);
                    int len;
                    while ((len = clientChannel.read(buffer))>0){
                        buffer.flip();
                        System.out.println(new String(buffer.array(),0,len));
                        buffer.clear();
                    }
                }
                it.remove();
            }
        }
    }
}

客户端很简单 发消息的流程和上一篇中一模一样 照抄过来就好了

额外增加的部分:

就是我们要收其他客户端发来的消息,所以我们在客户端中要单开一个线程出来专门收消息。


完工 我们来测试一下:

启动服务器和三个客户端:
client1 发消息

当前客户端就绪:
副线程收消息:
主线程发消息-请说:
你好
主线程发消息-请说:
你总是问我爱你值不值得
主线程发消息-请说:
但是你应该明白
主线程发消息-请说:
爱 就是不问 值得不值得

client2 收消息

当前客户端就绪:
副线程收消息:
主线程发消息-请说:
你总是问我爱你值不值得
但是你应该明白
爱 就是不问 值得不值得

client1 收消息

当前客户端就绪:
副线程收消息:
主线程发消息-请说:
你总是问我爱你值不值得
但是你应该明白
爱 就是不问 值得不值得
接收到客户端消息你好
--服务端转发当前客户端消息给其他所有在线注册的客户端--
当前处理线程为:main
接收到客户端消息你总是问我爱你值不值得
--服务端转发当前客户端消息给其他所有在线注册的客户端--
当前处理线程为:main
接收到客户端消息但是你应该明白
--服务端转发当前客户端消息给其他所有在线注册的客户端--
当前处理线程为:main
接收到客户端消息爱 就是不问 值得不值得
--服务端转发当前客户端消息给其他所有在线注册的客户端--
当前处理线程为:main

Process finished with exit code 130

通过这样的实践能更好的体会NIO通信模式和多路复用机制。
往后我们会遇到很多应用比如redis tomcat nginx等等他们的底层都有多路复用的设计思想。
这个专栏就到此结束啦!
希望能给大家一点启发和帮助

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值