【Java】基于NIO的多人聊天室详解编程语言

前言

学习了NIO的基本原理及使用方法之后,开始尝试写一个NIO实现的聊天室,练习一下代码流程。

服务器端

服务器端主要负责接受各客户端的连接,接收客户端发来的信息,并且将其广播给所有已连接客户端。

/** 
 * Created by makersy on 2019 
 */ 
 
/** 
 * NIO服务器端 
 */ 
public class NioServer {
    
 
    /** 
     * 启动 
     */ 
    public void start() throws IOException {
    
 
        // 1. 创建一个Selector 
        Selector selector = Selector.open(); 
 
        // 2. 通过ServerSocketChannel创建channel通道 
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); 
 
        // 3. 为channel通道绑定监听端口 
        serverSocketChannel.bind(new InetSocketAddress(8000)); 
 
        // 4. 设置channel为非阻塞模式 
        serverSocketChannel.configureBlocking(false); 
 
        // 5. 将channel注册到selector上,监听连接事件 
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);  //ACCEPT : 接收客户端连接 
        System.out.println("服务器启动成功"); 
 
        // 6. 循环等待新接入的连接 
 
        for (; ; ) {
     // 也可while(true) c语言常用:for(;;)  
            //获取可用channel数量 
            int readyChannels = selector.select();  //select() 是一个阻塞方法 
 
            //防止空轮询 
            if (readyChannels == 0) continue; 
 
            //获取可用channel的集合 
            Set<SelectionKey> selectionKeys = selector.selectedKeys(); 
 
            Iterator iterator = selectionKeys.iterator(); 
 
            while (iterator.hasNext()) {
    
                //selectionKey实例 
                SelectionKey selectionKey = (SelectionKey) iterator.next(); 
 
                //移除Set中的当前selectionKey 
                iterator.remove(); 
 
                // 7. 根据就绪状态,调用对应方法处理业务逻辑 
                //如果是 接入事件 
                if (selectionKey.isAcceptable()) {
    
                    acceptHandler(serverSocketChannel, selector); 
                } 
 
                //如果是 可读事件 
                if (selectionKey.isReadable()) {
    
                    readHandler(selectionKey, selector); 
                } 
 
            } 
        } 
 
    } 
 
    /** 
     * 接入事件处理器 
     */ 
    private void acceptHandler(ServerSocketChannel serverSocketChannel, 
                               Selector selector) throws IOException {
    
        //如果是接入事件,创建socketChannel 
        SocketChannel socketChannel = serverSocketChannel.accept(); 
 
        //将socketChannel设置为非阻塞工作模式 
        socketChannel.configureBlocking(false); 
 
        //将socketChannel注册到selector上,监听 可读事件 
        socketChannel.register(selector, SelectionKey.OP_READ); 
 
        //回复客户端提示信息 
        socketChannel.write(Charset.forName("UTF-8") 
                .encode("你与聊天室其他人都不是朋友关系,请注意隐私安全")); 
 
    } 
 
    /** 
     * 可读事件处理器,读取客户端发送来的信息,并广播给其他客户端 
     */ 
    private void readHandler(SelectionKey selectionKey, Selector selector) throws IOException {
    
 
        //要从 selectionKey 中获取到已经就绪的channel 
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); 
 
        //创建一个buffer 
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024); 
 
        //使用buffer循环读取请求信息 
        String request = ""; 
        while (socketChannel.read(byteBuffer) > 0) {
    
            //切换buffer为读模式 
            byteBuffer.flip(); 
 
            //读取buffer中的内容 
            request += Charset.forName("UTF-8").decode(byteBuffer); 
 
        } 
        //将 socketChannel 再次注册到selector上,监听 可读事件 
        socketChannel.register(selector, SelectionKey.OP_READ); 
 
        //将客户端发送的请求信息广播给其他客户端 
        if (request.length() > 0) {
    
            //广播给其他客户端 
            broadCast(selector, socketChannel, request); 
        } 
    } 
 
    //广播给其他客户端 
    private void broadCast(Selector selector, SocketChannel sourceChannel, String request) {
    
        //获取到所有已接入的客户端channel 
        Set<SelectionKey> selectionKeys = selector.keys(); 
 
        //循环向所有channel广播信息 
        selectionKeys.forEach(selectionKey -> {
    
 
            Channel targetChannel = selectionKey.channel(); 
 
            // 发送消息,剔除发消息的客户端 
            if (targetChannel instanceof SocketChannel && targetChannel != sourceChannel) {
    
                try {
    
                    //将信息发送到targetChannel客户端 
                    ((SocketChannel) targetChannel).write(Charset.forName("UTF-8").encode(request)); 
                } catch (IOException e) {
    
                    e.printStackTrace(); 
                } 
            } 
 
        }); 
 
    } 
 
    public static void main(String[] args) {
    
        try {
    
            new NioServer().start(); 
        } catch (IOException e) {
    
            e.printStackTrace(); 
        } 
    } 
} 
 

客户端启动类

向服务器端发送数据。同时新开线程接收服务器端发来的数据。

/** 
 * Created by makersy on 2019 
 */ 
 
/* 
 NIO客户端,封装启动代码 
 */ 
public class NioClient {
    
 
    /** 
     * 启动方法 
     */ 
    public void start(String nickname) throws IOException {
    
 
        //连接服务器端 
        SocketChannel socketChannel = SocketChannel.open( 
                new InetSocketAddress("127.0.0.1", 8000)); 
 
        System.out.println("客户端 " + nickname + " 启动成功!"); 
 
        //接收服务器端响应 
        Selector selector = Selector.open(); 
        socketChannel.configureBlocking(false);  //非阻塞 
        socketChannel.register(selector, SelectionKey.OP_READ);  //注册为接收模式 
        //需要新开一个线程,专门负责接收服务端的响应数据。因为当前线程需要负责发送给服务器端数据 
        new Thread(new NioClientHandler(selector)).start();  //新开线程 
 
        //向服务器端发送数据 
        Scanner scanner = new Scanner(System.in); 
        while (scanner.hasNextLine()) {
    
            String request = scanner.nextLine(); 
            if (request != null && request.length() > 0) {
    
                socketChannel.write(Charset.forName("UTF-8").encode(nickname + " : " + request)); 
            } 
        } 
 
    } 
} 
 

客户端接收服务器端数据类

负责接收服务端发来的数据。

/** 
 * Created by makersy on 2019 
 */ 
 
/** 
 * 客户端线程类,专门接收服务器端响应信息 
 */ 
 
public class NioClientHandler implements Runnable {
    
 
    private Selector selector; 
 
    public NioClientHandler(Selector selector) {
    
        this.selector = selector; 
    } 
 
    @Override 
    public void run() {
    
        try {
    
            for (; ; ) {
    
                int readyChannels = selector.select(); 
 
                if (readyChannels == 0) continue; 
 
                Set<SelectionKey> selectionKeys = selector.selectedKeys(); 
 
                Iterator iterator = selectionKeys.iterator(); 
 
                while (iterator.hasNext()) {
    
                    SelectionKey selectionKey = (SelectionKey) iterator.next(); 
 
                    iterator.remove(); 
 
                    if (selectionKey.isReadable()) {
    
                        readHandler(selectionKey, selector); 
                    } 
 
                } 
            } 
        } catch (IOException e) {
    
            e.printStackTrace(); 
        } 
    } 
 
    /** 
     * 可读事件处理器 
     */ 
    private void readHandler(SelectionKey selectionKey, Selector selector) throws IOException {
    
 
        //要从 selectionKey 中获取到已经就绪的channel 
        SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); 
 
        //创建一个buffer 
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024); 
 
        //使用buffer循环读取服务器端请求信息 
        String response = ""; 
        while (socketChannel.read(byteBuffer) > 0) {
    
 
            //切换buffer为读模式 
            byteBuffer.flip(); 
 
            //读取buffer中的内容 
            response += Charset.forName("UTF-8").decode(byteBuffer); 
 
        } 
        //将 socketChannel 再次注册到selector上,监听 可读事件 
        socketChannel.register(selector, SelectionKey.OP_READ); 
 
        //将服务器端响应信息打印到本地 
        if (response.length() > 0) {
    
            System.out.println(response); 
        } 
    } 
} 

实例:A客户端、B客户端

到这里服务器端和客户端响应启动代码就已经完成了,如果我们需要连接到服务器端进入群聊,只要新开一个线程,命名即可。

假设我新建一个AClient,代码如下:

/** 
 * Created by makersy on 2019 
 */ 
 
public class AClient {
    
    public static void main(String[] args) throws IOException {
    
        new NioClient().start("AClient"); 
    } 
} 

再次创建新线程流程同上面一样。

效果演示

A客户端

mark

B客户端

mark

最后

不得不说,NIO的程序还是挺难写的,而且还会出现许多的问题。像我写的这么简单的代码就肯定有很多bug存在,更别说复杂的业务代码了。这也应该就是大多数人不使用JDK原生NIO进行网络编程的原因了。

原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/tech/pnotes/19361.html

(0)
上一篇 2021年7月19日 22:08
下一篇 2021年7月19日 22:08

相关推荐

发表回复

登录后才能评论