通过选择器 Selector 开发高性能聊天室

本文介绍了如何使用Java NIO实现一个聊天室功能,服务端通过选择器处理多个客户端连接,实现非阻塞I/O,提高效率。客户端连接到服务端后,可以发送消息,服务端接收到消息后广播给所有在线客户端。通过这种方式,实现了多用户实时聊天功能。
摘要由CSDN通过智能技术生成

一 点睛

选择器(Selector)的核心作用:可以在一个选择器上注册多个通道,并且可以通过选择器切换使用这些通道。

如果不使用选择器,服务端的 I/O 代码,会给每个客户端创建一个新线程,也就是用 N 个线程去处理 N 个客户端请求。因此,如果有 1 万个客户请求,就会创建 1 万个线程,这显然是不合理地。而 NIO 处理这种问题的思路是,用一个线程处理全部请求,并通过 Selector 切换处理不同的请求通道。

二 实战

1 需求

使用 NIO 实现一个聊天室功能。要求如下:服务端启动后可以接收多个客户端连接,每个客户端都可以向服务端发送消息;服务端接受到消息后,会在控制台打印此客户端的信息,并且将此消息发给全部的客户端。

2 服务端

服务端只创建一个处理请求的线程

package nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * @className: ChatServer
 * @description: 服务器
 * @date: 2022/5/23
 * @author: cakin
 */
public class ChatServer {
    /*
        clientsMap:保存所有的客户端
        key:客户端的名字
        value:客户端连接服务端的Channel
     */
    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) {
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.configureBlocking(false);
            ServerSocket serverSocket = serverSocketChannel.socket();
            // 将聊天服务绑定到 7777、8888和9999 三个端口上
            serverSocket.bind(new InetSocketAddress(port));
            System.out.println("服务端启动成功,端口" + port);
            // 在服务端的选择器上,注册一个通道,并标识该通道所感兴趣的事件是:接收客户端连接(接收就绪)
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        }

        while (true) {
            // 一直阻塞,直到选择器上存在已经就绪的通道(包含感兴趣的事件)
            selector.select();
            // selectionKeys包含了所有通道与选择器之间的关系(接收连接、读、写)
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
            // 如果selector中有多个就绪通道(接收就绪、读就绪、写就绪等),则遍历这些通道
            while (keyIterator.hasNext()) {
                SelectionKey selectedKey = keyIterator.next();
                String receive = null;
                // 与客户端交互的通道
                SocketChannel clientChannel;
                try {
                    // 接收就绪(已经可以接收客户端的连接了)
                    if (selectedKey.isAcceptable()) {
                        ServerSocketChannel server = (ServerSocketChannel) selectedKey.channel();
                        clientChannel = server.accept();
                        // 切换到非阻塞模式
                        clientChannel.configureBlocking(false);
                        // 再在服务端的选择器上,注册第二个通道,并标识该通道所感兴趣的事件是:接收客户端发来的消息(读就绪)
                        clientChannel.register(selector, SelectionKey.OP_READ);
                        // 用“key四位随机数”的形式模拟客户端的key值
                        String key = "key" + (int) (Math.random() * 9000 + 1000);
                        // 将该建立完毕连接的 通道 保存到clientsMap中
                        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);
                            // 如果终止客户端,则 read() 会抛出 IOException 异常,可以依次判断是否有客户端退出。
                        } catch (IOException e) {
                            // 获取退出连接的 client 对应的 key
                            String clientKey = getClientKey(clientChannel);
                            System.out.println("客户端" + clientKey + "退出聊天室");
                            clientsMap.remove(clientKey);
                            clientChannel.close();
                            selectedKey.cancel();
                            continue;
                        }
                        if (result > 0) {
                            readBuffer.flip();
                            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();
                        // 获取发送消息从client对应的key
                        String sendKey = getClientKey(clientChannel);
                        // 将接收到的消息,拼接成“发送消息的客户端Key:消息”的形式,再广播给所有client
                        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 (Exception e) {
                    e.printStackTrace();
                }
            }
            selectionKeys.clear();
        }
    }

    public static String getClientKey(SocketChannel clientChannel) {
        String sendKey = null;
        //很多client在发下消息,通过for找到是哪个client在发消息,找到该client的key
        for (Map.Entry<String, SocketChannel> entry : clientsMap.entrySet()) {
            if (clientChannel == entry.getValue()) {
                //找到发送消息的client所对应的key
                sendKey = entry.getKey();
                break;
            }
        }
        return sendKey;
    }
}

3 客户端

package nio;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * @className: ChatClient
 * @description: 客户端
 * @date: 2022/5/23
 * @author: cakin
 */
public class ChatClient {
    public static void main(String[] args) {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            // 切换到非阻塞模式
            socketChannel.configureBlocking(false);
            Selector selector = Selector.open();
            // 在客户端的选择器上,注册一个通道,并标识该通道所感兴趣的事件是:向服务端发送连接(连接就绪)。对应于服务端的OP_ACCEPT事件
            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();
                // selectionKeys 包含了所有通道与选择器之间的关系(请求连接、读、写)
                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.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 (Exception 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();
        }
    }
}

三 测试

依次启动服务端和2个客户端,并发送消息

1 服务端打印

服务端启动成功,端口7777

服务端启动成功,端口8888

服务端启动成功,端口9999

java.nio.channels.SocketChannel[connected local=/127.0.0.1:7777 remote=/127.0.0.1:52781]:connecting

java.nio.channels.SocketChannel[connected local=/127.0.0.1:7777 remote=/127.0.0.1:52892]:connecting

java.nio.channels.SocketChannel[connected local=/127.0.0.1:7777 remote=/127.0.0.1:52892]:你好    

java.nio.channels.SocketChannel[connected local=/127.0.0.1:7777 remote=/127.0.0.1:52781]:你来自哪里?            

2 第1个客户端打印

连接成功!访问的端口是:7777

key9664:新客户端加入聊天!

key4978:新客户端加入聊天!

key4978:你好    

你来自哪里?

key9664:你来自哪里?            

3 第2个客户端打印

连接成功!访问的端口是:7777

key4978:新客户端加入聊天!

你好

key4978:你好    

key9664:你来自哪里?            

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
项目简介: 采用I/O复用技术select实现socket通信,采用多线程负责每个客户操作处理,完成Linux下的多客户聊天室! OS:Ubuntu 15.04 IDE:vim gcc make DB:Sqlite 3 Time:2015-12-09 ~ 2012-12-21 项目功能架构: 1. 采用client/server结构; 2. 给出客户操作主界面(注册、登录、帮助和退出)、登录后主界面(查看在线列表、私聊、群聊、查看聊天记录、退出); 3. 多客户可同时连接服务器进行自己操作; ##服务器端## 1. server.c:服务器端主程序代码文件; 2. config.h:服务器端配置文件(包含需要的头文件、常量、数据结构及函数声明); 3. config.c:服务器端公共函数的实现文件; 4. list.c:链表实现文件,用于维护在线用户链表的添加、更新、删除操作; 5. register.c:服务器端实现用户注册; 6. login.c:服务器端实现用户登录; 7. chat.c:服务器端实现用户的聊天互动操作; 8. Makefile:服务器端make文件,控制台执行make命令可直接生成可执行文件server ##客户端## 1. client.c:客户端主程序代码文件; 2. config.h:客户端配置文件(包含需要的头文件、常量、数据结构及函数声明); 3. config.c:客户端公共函数的实现文件; 4. register.c:客户端实现用户注册; 5. login.c:客户端实现用户登录; 6. chat.c:客户端实现用户的聊天互动操作; 7. Makefile:客户端make文件,控制台执行make命令可直接生成可执行文件client;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值