基于原生NIO实现聊天室

109 阅读3分钟

服务端代码

public class ChatServer {
    // 保存所有客户端信息
    private static Map<String, SocketChannel> clientsMap = new HashMap<>();
​
    public static void main(String[] args) throws IOException {
        int[] ports = new int[]{7777,8888,9999};
        Selector selector = Selector.open(); // 打开选择器
​
        for(int port : ports) {
            // 创建非阻塞的socket通道和socket对象
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            ServerSocket serverSocket = serverSocketChannel.socket();
​
            // socket绑定到三个端口上
            serverSocket.bind(new InetSocketAddress(port));
            System.out.println("服务端启动成功,端口:" + port);
​
            // 在服务端的选择器上注册一个通道 感兴趣的事件是接收客户端连接
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        }
        while(true){
            // 在这里一直阻塞 直至选择器上存在已经就绪的通道
            selector.select();
            // 包含了所有通道与选择器之间的关系(接收连接、读、写)
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 轮询selector的所有就绪通道
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            while(keyIterator.hasNext()){
                SelectionKey selectedKey = keyIterator.next();
                String receive = null; // 记录接收信息
                // 创建一个与客户端交互的通道
                SocketChannel clientChannel;
                try {
                    // 接收就绪 进入连接
                    if(selectedKey.isAcceptable()){
                        // 创建服务端socket通道
                        ServerSocketChannel server = (ServerSocketChannel) selectedKey.channel();
                        // 建立连接
                        clientChannel = server.accept();
                        // 切换到非阻塞模式
                        clientChannel.configureBlocking(false);
​
                        // 在选择器上注册第二个通道 感兴趣的事件是接收客户端发来的消息(读就绪)
                        clientChannel.register(selector, SelectionKey.OP_READ);
                        // 随机模拟客户端的key值
                        String key = "游客" + (int)(Math.random()*9000 + 1000);
                        clientsMap.put(key, clientChannel);
                    }
                    // 读就绪
                    else if(selectedKey.isReadable()){
                        clientChannel = (SocketChannel) selectedKey.channel();
                        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                        int result = -1;
                        try {
                            // 读取客户端消息 放到readBuffer中
                            result = clientChannel.read(readBuffer);
                        } catch (IOException e) { // 通常是客户端退出导致的异常
                            // 从多个连接的客户端中找到退出的客户端
                            String clientKey = getClientKey(clientChannel);
                            System.out.println("客户端" + clientKey + "退出了聊天室");
                            clientsMap.remove(clientKey);
                            clientChannel.close();
                            selectedKey.cancel();
​
                            continue;
                        }
                        if(result > 0){ // 在readBuffer中读到了数据
                            readBuffer.flip(); // 将position归位
                            // 正确解码消息 并打印
                            Charset charset = Charset.forName("utf-8");
                            receive = String.valueOf(charset.decode(readBuffer).array());
                            System.out.println(clientChannel + ":" + receive);
                            // 处理客户端第一次发来的连接测试信息
                            if("connecting".equals(receive)){
                                receive = "新客户端加入聊天!";
                            }
                            // 将读取到的客户消息保存在attachment中 用于后续向所有客户端转发此消息
                            selectedKey.attach(receive);
                            // 将通道感兴趣事件标识为:向客户端发送消息(写就绪)
                            selectedKey.interestOps(SelectionKey.OP_WRITE);
                        }
                    }
                    //  写就绪
                    else if(selectedKey.isWritable()){
                        clientChannel = (SocketChannel) selectedKey.channel();
                        // 获取消息的发送者的key
                        String sendKey = getClientKey(clientChannel);
                        // 格式化形式将内容广播给所有客户端
                        for(Map.Entry<String, SocketChannel> entry : clientsMap.entrySet()){
                            SocketChannel eachClient = entry.getValue();
                            ByteBuffer broadcastMsg = ByteBuffer.allocate(1024);
                            broadcastMsg.put((sendKey + ":" + selectedKey.attachment()).getBytes());
                            // 转化为写模式
                            broadcastMsg.flip();
                            eachClient.write(broadcastMsg);
                        }
                        selectedKey.interestOps(SelectionKey.OP_READ);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            selectionKeys.clear();
        }
    }
    // 根据客户端的socket通道找到对应的key
    public static String getClientKey(SocketChannel clientChannel){
        String sendKey = null;
        for(Map.Entry<String, SocketChannel> entry : clientsMap.entrySet()){
            if(clientChannel == entry.getValue()){
                sendKey = entry.getKey();
                break;
            }
        }
        return sendKey;
    }
}

客户端代码

public class ChatClient {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            // 创建选择器设置其感兴趣的事件为向服务端发送连接(连接就绪)
            Selector selector = Selector.open();
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
​
            int[] ports = {7777, 8888, 9999};
            int port = ports[(int)(Math.random() * 3)]; // 随机用服务端的一个端口
            socketChannel.connect(new InetSocketAddress("127.0.0.1", port));
            while(true){
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
                // 不断轮询
                while(keyIterator.hasNext()){
                    SelectionKey selectedKey = keyIterator.next();
                    if(selectedKey.isConnectable()){ // 连接就绪
                        ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
                        // 创建一个用于和服务端交互的channel
                        SocketChannel client = (SocketChannel) selectedKey.channel();
                        // 如果状态为正在连接中
                        if(client.isConnectionPending()){
                            boolean isConnected = client.finishConnect();
                            if(isConnected){
                                System.out.println("连接成功!访问的端口是:" + port);
                                // 向服务端发送一条测试消息
                                sendBuffer.put("connecting".getBytes());
                                sendBuffer.flip();
                                // 往client通道中写入测试消息
                                client.write(sendBuffer);
                            }
                            // 只用一个线程来处理客户端的发送信息请求
                            new Thread(()->{
                                while(true){
                                    try {
                                        // 先清空原来缓冲区中的内容
                                        sendBuffer.clear();
                                        // 接收用户输入的消息 并发送给服务端
                                        InputStreamReader reader = new InputStreamReader(System.in);
                                        BufferedReader bReader = new BufferedReader(reader);
                                        String message = bReader.readLine();
​
                                        sendBuffer.put(message.getBytes());
                                        sendBuffer.flip();
                                        client.write(sendBuffer);
                                    } catch (IOException e) {
                                        e.printStackTrace();
                                    }
                                }
                            }).start();
                            // 在选择器注册一个对读取服务端消息(读就绪)感兴趣的通道
                            client.register(selector, SelectionKey.OP_READ);
                        }
                    }
                    // 客户端读取服务端的反馈消息
                    else if(selectedKey.isReadable()){
                        SocketChannel client = (SocketChannel) selectedKey.channel();
                        ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                        // 将服务端的消息放入readBuffer中
                        int len = client.read(readBuffer);
                        if(len > 0){
                            String receive = new String(readBuffer.array(), 0, len);
                            System.out.println(receive);
                        }
                    }
                }
                selectionKeys.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

运行结果

服务端启动成功,端口:7777
服务端启动成功,端口:8888
服务端启动成功,端口:9999
java.nio.channels.SocketChannel[connected local=/127.0.0.1:8888 remote=/127.0.0.1:54492]:connecting
java.nio.channels.SocketChannel[connected local=/127.0.0.1:8888 remote=/127.0.0.1:54492]:大家好
连接成功!访问的端口是:8888
游客9615:新客户端加入聊天!
大家好
游客9615:大家好