NIO 实现聊天系统之点对点通讯

13,965 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第22天,点击查看活动详情

没有比人更高的山,没有比心更宽的海,人是世界的主宰

NIO 实现聊天系统之点对点通讯

背景

    我们之前利用 NIO 的主要的API实现了群聊天系统,实现了一个客户端进行消息发送的时候其他的客户端都能接收到消息,但是作为一个聊天系统怎么可能不存在点对点的通讯呢,比如张三和李四的私聊消息,不应该让其他的客户端读到才对,那么我们应该怎么实现点对点的通讯呢,接下来我们就进行点对点通讯的开发实现

实现方案

    我们这里的方案比较简单,因为没有客户端所以只能进行模仿,你是要单发的话,那就要指定是单聊,如果是群发的话需要指定群聊,之后在按照固定格式:目标客户端地址-消息内容进行消息发送,不过也是通过服务端进行消息处理,进行内容的转发

服务端实现

  1. 先创建一个选择器Selector
  2. 创建一个ServerSocketChannel
  3. 设置ServerSocketChannel 监听的地址
  4. 将ServerSocketChannel 设置成非阻塞的
  5. 将ServerSocketChannel 注册到选择器上面
  6. 循环监听和处理客户端通道的数据信息
  7. 如果是客户端发送消息的话,那么服务端进行消息转发
public class ChatServer {
    public static void main(String[] args) throws Exception {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8080));
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // channel 通道存储缓存
        Map<String, SocketChannel> channelMap = new ConcurrentHashMap<>();
        while (true) {
            int selected = selector.select();
            if (selected > 0) {
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    if (key.isAcceptable()) {
                        SocketChannel channel = serverSocketChannel.accept();
                        channel.configureBlocking(false);
                        channel.register(selector, SelectionKey.OP_READ);
                        System.out.println("客户端 : " 
                                   + channel.getRemoteAddress() + "已上线 ......");
                        // 将上线的客户端进行channel缓存起来
                        // channel.getRemoteAddress().toString() 是key操作
                        channelMap.put(channel.getRemoteAddress().toString(), channel);
                    }
                    if (key.isReadable()) {
                        SocketChannel selfChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        int read = selfChannel.read(buffer);
                        if (read > 0) {
                            String msg = new String(buffer.array(), CharsetUtil.UTF_8);
                            Iterator<SelectionKey> keyIterator = selector.keys()
                                                            .iterator();
                            while (keyIterator.hasNext()) {		
                                SelectionKey selectionKey = keyIterator.next();
                                buffer.flip(); // 重点
                                if (msg.contains("-")) { //解析单聊
                                    String[] channelRes = msg.split("-");
                                    String channelKey = channelRes[0];
                                    String msgInfo = channelRes[1];
                                    // 根据Key获取channel通道
                                    SocketChannel socketChannel = channelMap
                                        .get(channelKey);
                                    // 根据解析出来的目标通道进行消息指定发送
                                    if (selectionKey.channel() == socketChannel) {
                                        socketChannel
                                            .write(ByteBuffer.wrap(msgInfo.getBytes()));
                                    }
                                } else {// 群聊
                                    if (selectionKey.channel() instanceof SocketChannel 
                                        && selectionKey.channel() != selfChannel) {
                                        ((SocketChannel)selectionKey.channel()).
                                            write(ByteBuffer.wrap(msg.getBytes()));
                                    }
                                }
                            }
                        }
                    }
                    iterator.remove();
                }
            }
        }
    }
}

客户端实现

  1. 先创建一个选择器Selector
  2. 创建一个SocketChannel
  3. 设置SocketChannel 连接的地址
  4. 将SocketChannel 设置成非阻塞的
  5. 将SocketChannel 注册到选择器上面
  6. 循环监听和处理客户端通道的数据信息
  7. 接收客户端发过来的消息
public class ChatClient {
    public static void main(String[] args) throws Exception {
        // 客户端初始化流程
        Selector selector = Selector.open();
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
        // 开启一个线程进行监听服务端发来的消息
        new Thread(() -> {
            while (true) {
                try {
                    // 如果有就绪事件,那么进行处理
                    int selected = selector.select();
                    if (selected > 0) {
                        Iterator<SelectionKey> iterator = selector
                            .selectedKeys().iterator();
                        while (iterator.hasNext()) {
                            SelectionKey selectionKey = iterator.next();
                            // 已读就绪事件监听
                            if (selectionKey.isReadable()) {
                                SocketChannel channel = (SocketChannel) selectionKey
                                    .channel();
                                ByteBuffer buffer = ByteBuffer.allocate(1024);
                                int read = channel.read(buffer);
                                if (read > 0) {
                                    System.out.println(new String(buffer.array()));
                                }
                            }
                            iterator.remove();
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        // 客户端发送指令功能模块
        Scanner scanner = new Scanner(System.in);
        System.out.println("你要给谁发消息呢?是单聊还是群聊?");

        while (scanner.hasNextLine()) {
            System.out.println("如果是单聊那么输入:1");
            System.out.println("如果是群聊那么输入:2");
            String line = scanner.nextLine();
            if (line.equals("1")) {
                System.out.println("单聊,你要发给谁呢,请输入特定客户标签");
                System.out.println("发送格式:channel远程地址-消息内容");
                line = scanner.nextLine();
                socketChannel.write(ByteBuffer.wrap(line.getBytes()));
            } else if (line.equals("2")) {
                System.out.println("群聊发送消息");
                line = scanner.nextLine();
                socketChannel.write(ByteBuffer.wrap(line.getBytes()));
            } else {
                System.out.println("输入错误,请重新输入");
                System.out.println("如果是单聊那么输入:1");
                System.out.println("如果是群聊那么输入:2");
                break;
            }
        }
    }
}

结果展示

服务端启动

image.png

多客户端启动

image.png
image.png
image.png
    此时服务端缓存的 channelMap 缓存中的信息详情是

/127.0.0.1:54967=
    java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:54967]
/127.0.0.1:54933=
    java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:54933]
/127.0.0.1:54884=
    java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:54884]

    channel.getRemoteAddress() 是 channelMap缓存中的key,value是 java.nio.channels.SocketChannel,所以我们就可以通过这个key进行通道获取,之后通过通道进行消息发送,比如 /127.0.0.1:54933 客户端,发送消息给 /127.0.0.1:54884 客户端,发送格式:客户端地址ID信息-消息内容
image.png
    客户端 /127.0.0.1:54884 收到来自 /127.0.0.1:54933 客户端 发来的消息
image.png
    另一个客户端 /127.0.0.1:54967 并没有收到消息
image.png

小结

    到此我们就利用NIO实现了群聊和点对点通讯的单聊,实现了简单的聊天工具,也是为了更加深刻的掌握NIO组件,我们后续会继续深耕Netty这一个大名鼎鼎的网络通讯工具,它比NIO 要好用的多,实现了框架和业务功能代码完全分离