【Java网络编程】基于BIO/NIO/AIO的多人聊天室(四):NIO聊天室

课程《一站式学习Java网络编程 全面理解BIO/NIO/AIO》的学习笔记(四):
NIO编程模型 & 基于NIO的多人聊天室实现

源码地址:https://github.com/NoxWang/web-program

【Java网络编程】基于BIO/NIO/AIO的多人聊天室(一):java IO与内核IO
【Java网络编程】基于BIO/NIO/AIO的多人聊天室(二):BIO聊天室
【Java网络编程】基于BIO/NIO/AIO的多人聊天室(三):NIO概述与实践
【Java网络编程】基于BIO/NIO/AIO的多人聊天室(五):AIO聊天室
【Java网络编程】基于BIO/NIO/AIO的多人聊天室(六):思维导图

一、NIO编程模型

  1. 在Selector上注册服务器Channel,监听ACCEPT事件;
  2. 当Client1连接进服务器,ACCEPT事件触发,调用handles对该事件进行处理:向Selector上注册Client1的Channel的READ事件;
  3. 当Client1向服务器发送数据,READ事件触发,调用handles对该事件进行处理:转发消息;
  4. 当Client2连接进服务器,再次触发ACCEPT事件,同样调用handles进行相同处理,以此类推。

几个需要注意的点:

  • 与BIO不同,NIO编程模型中,accept操作与读写处理操作是在同一个线程中进行的。
  • 虽然使用Selector可实现非阻塞式调用,但Selector的select()方法是阻塞式的:如果当前没有Selector监听事件出现,则该方法阻塞(返回值为出现事件的数量)。
  • 同时可有多个事件被触发,调用Selector的selectedKeys()方法可以获得可操作Channel的SelectionKey集合。
    NIO编程模型

二、基于NIO的多人聊天室实现

2.1 服务端

仅需要一个线程。Selector需要监听两种事件:服务端Channel的ACCEPT事件,客户端Channel的READ事件。两种事件的具体处理在handles()方法中实现。具体实现见代码注释。

package server;

import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
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.nio.charset.StandardCharsets;
import java.util.Set;

public class ChatServer {

    /** 默认监听端口 */
    private static final int DEFAULT_PORT = 8888;
    /** 用户自定义的监听端口 */
    private int port;

    /** 处理服务器端 IO 的通道 */
    private ServerSocketChannel server;
    /** 监听 channel 上发生的事件和 channel 状态的变化 */
    private Selector selector;

    /** 缓冲区大小 */
    private static final int BUFFER_SIZE = 1024;
    /** 用于从通道读取数据的 Buffer */
    private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER_SIZE);
    /** 用于向通道写数据的 Buffer */
    private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER_SIZE);

    /** 客户端退出命令 */
    private static final String QUIT = "\\quit";
    /** 指定编解码方式 */
    private Charset charset = StandardCharsets.UTF_8;

    public ChatServer() {
        this(DEFAULT_PORT);
    }

    public ChatServer(int port) {
        this.port = port;
    }

    /**
     * 服务端主逻辑
     */
    private void start() {
        try {
            // 创建一个新的通道,并设置为非阻塞式调用(open()方法产生的通道默认为阻塞式调用)
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            // 绑定监听端口
            server.socket().bind(new InetSocketAddress(port));

            // 创建Selector
            selector = Selector.open();
            // 在selector上注册serverChannel的accept事件
            server.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("启动服务器,监听端口:" + port + "...");

            while (true) {
                // select()方法为阻塞式调用,如果当前没有selector监听事件出现,则该方法阻塞(返回值为出现事件的数量)
                selector.select();
                // 获取所有被触发Channel的SelectionKey集合
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey key : selectionKeys) {
                    // 处理被触发的事件
                    handles(key);
                }
                selectionKeys.clear();
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 关闭selector:解除注册,同时关闭对应的通道
            close(selector);
        }
    }

    /**
     * 需要处理两个事件:ACCEPT & READ
     */
    private void handles(SelectionKey key) throws IOException {
        // ACCEPT事件 --- 和客户端建立了连接
        if (key.isAcceptable()) {
            ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
            // 获得连接进来的客户端的channel
            SocketChannel clientChannel = serverChannel.accept();
            // 转换为非阻塞式调用
            clientChannel.configureBlocking(false);

            // 注册该客户端channel的READ事件
            clientChannel.register(selector, SelectionKey.OP_READ);
            System.out.println(getClientName(clientChannel) + "已连接");
        }

        // READ事件 --- 客户端发送了消息
        else if (key.isReadable()) {
            SocketChannel clientChannel = (SocketChannel) key.channel();
            String fwdMsg = receive(clientChannel);
            if (fwdMsg.isEmpty() || readyToQuit(fwdMsg)) { // 客户端异常 or 客户端准备退出
                // 取消注册该通道上的该事件
                key.cancel();
                // 更改状态后,强制返回selector,令其重新检测
                selector.wakeup();
                System.out.println(getClientName(clientChannel) + "已断开");
            } else {
                System.out.println(getClientName(clientChannel) + ":" + fwdMsg);
                forwardMessage(clientChannel, fwdMsg);
            }
        }
    }

    /**
     * 读取客户端发来的消息
     * @param clientChannel 客户端 channel
     * @return 发来的消息
     * @throws IOException
     */
    private String receive(SocketChannel clientChannel) throws IOException {
        // 将rBuffer转为写模式(起到清空的作用)
        rBuffer.clear();
        // 从clientChannel中读取数据,写入rBuffer,直至channel中没有数据可读
        while ((clientChannel.read(rBuffer)) > 0);
        // 将rBuffer从写模式转换为读模式
        rBuffer.flip();
        // 使用utf8编码解码rBuffer,并转为字符串类型
        return String.valueOf(charset.decode(rBuffer));
    }

    /**
     * 转发消息给其他客户端
     * @param clientChannel 发来消息的客户端 channel
     * @param fwdMsg 需要转发的消息
     * @throws IOException
     */
    private void forwardMessage(SocketChannel clientChannel, String fwdMsg) throws IOException {
    	// keys()返回所有注册过的SelectionKey
        for (SelectionKey key : selector.keys()) {
            // key有效并且是客户端socket
            if (key.isValid() && key.channel() instanceof SocketChannel) {
                SocketChannel connectedClient = (SocketChannel) key.channel();
                if (!connectedClient.equals(clientChannel)) {
                    wBuffer.clear();
                    // 将需要转发的消息写进wBuffer,注意使用utf8编码
                    wBuffer.put(charset.encode(getClientName(clientChannel) + ":" + fwdMsg));
                    // 将wBuffer从写入模式转换为读取模式
                    wBuffer.flip();
                    while (wBuffer.hasRemaining()) {
                        connectedClient.write(wBuffer);
                    }
                }
            }
        }
    }

    private String getClientName(SocketChannel client) {
        return "客户端[" + client.socket().getPort() + "]";
    }

    private boolean readyToQuit(String msg) {
        return QUIT.equals(msg);
    }

    private void close (Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ChatServer chatServer = new ChatServer();
        chatServer.start();
    }
}

2.2 客户端

ChatClient.java:客户端主线程,Selector监听客户端的CONNECT事件和READ事件

package client;

import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Set;

public class ChatClient {

    /** 服务器地址 */
    private String host;
    private static final String DEFAULT_SERVER_HOST = "127.0.0.1";

    /** 服务器端口 */
    private int port;
    private static final int DEFAULT_SERVER_PORT = 8888;

    /** 客户端 Channel */
    private SocketChannel client;
    /** 监听Channel的Selector */
    private Selector selector;

    /** 缓冲区大小 */
    private static final int BUFFER_SIZE = 1024;
    /** 用于从通道读取数据的 Buffer */
    private ByteBuffer rBuffer = ByteBuffer.allocate(BUFFER_SIZE);
    /** 用于向通道写数据的 Buffer */
    private ByteBuffer wBuffer = ByteBuffer.allocate(BUFFER_SIZE);

    /** 客户端退出命令 */
    private static final String QUIT = "\\quit";
    /** 指定编解码方式 */
    private Charset charset = StandardCharsets.UTF_8;

    public ChatClient() {
        this(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
    }

    public ChatClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public boolean readyToQuit(String msg) {
        return QUIT.equals(msg);
    }

    private void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 客户端主要逻辑
     */
    private void start() {
        try {
            // 创建Channel,并设置为非阻塞式调用
            client = SocketChannel.open();
            client.configureBlocking(false);

            // 创建Selector
            selector = Selector.open();
            // 注册 连接就绪CONNECT 事件
            client.register(selector, SelectionKey.OP_CONNECT);
            // 向服务器发送连接请求
            client.connect(new InetSocketAddress(host, port));

            while (true) {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey key : selectionKeys) {
                    handles(key);
                }
                selectionKeys.clear();
            }

        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClosedSelectorException e) {
            // 用户正常退出
        } finally {
            close(selector);
        }
    }

    /**
     * 处理 CONNECT (连接就绪)和 READ (服务器转发消息)事件
     */
    private void handles(SelectionKey key) throws IOException {
        if (key.isConnectable()) {  // 处理 CONNECT
            SocketChannel clientChannel = (SocketChannel) key.channel();
            if (clientChannel.isConnectionPending()) {  // 返回true:连接已就绪
                // 结束连接状态,完成连接
                clientChannel.finishConnect();
                new Thread(new UserInputHandler(this)).start();
            }
            // 注册READ事件,以接收服务端转发的消息
            clientChannel.register(selector, SelectionKey.OP_READ);

        } else if (key.isReadable()) {  // 处理READ
            SocketChannel clientChannel = (SocketChannel) key.channel();
            String msg = receive(clientChannel);
            if (msg.isEmpty()) {
                // 服务器异常
                close(selector);
            } else {
                System.out.println(msg);
            }
        }
    }

    /**
     * 向服务端发送信息
     * @param msg 用户输入的信息
     * @throws IOException
     */
    public void send(String msg) throws IOException {
        if (msg.isEmpty()) {
            return;
        }

        wBuffer.clear();
        wBuffer.put(charset.encode(msg));
        wBuffer.flip();
        while (wBuffer.hasRemaining()) {
            client.write(wBuffer);
        }

        if (readyToQuit(msg)) {
            close(selector);
        }
    }

    /**
     * 读取服务端转发来的消息
     * @param clientChannel 客户端channel
     * @return 收到的消息
     * @throws IOException
     */
    private String receive(SocketChannel clientChannel) throws IOException {
        rBuffer.clear();
        while (clientChannel.read(rBuffer) > 0);
        rBuffer.flip();
        return String.valueOf(charset.decode(rBuffer));
    }

    public static void main(String[] args) {
        ChatClient chatClient = new ChatClient();
        chatClient.start();
    }
}

UserInputHandler.java:处理用户输入

package client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class UserInputHandler implements Runnable {
    ChatClient client;

    public UserInputHandler(ChatClient client) {
        this.client = client;
    }
    @Override
    public void run() {
        BufferedReader consoleReader = new BufferedReader(
                new InputStreamReader(System.in)
        );
        while (true) {
            try {
                String input = consoleReader.readLine();
                client.send(input);

                if (client.readyToQuit(input)) {
                    break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值