课程《一站式学习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编程模型
- 在Selector上注册服务器Channel,监听ACCEPT事件;
- 当Client1连接进服务器,ACCEPT事件触发,调用handles对该事件进行处理:向Selector上注册Client1的Channel的READ事件;
- 当Client1向服务器发送数据,READ事件触发,调用handles对该事件进行处理:转发消息;
- 当Client2连接进服务器,再次触发ACCEPT事件,同样调用handles进行相同处理,以此类推。
几个需要注意的点:
- 与BIO不同,NIO编程模型中,accept操作与读写处理操作是在同一个线程中进行的。
- 虽然使用Selector可实现非阻塞式调用,但Selector的
select()
方法是阻塞式的:如果当前没有Selector监听事件出现,则该方法阻塞(返回值为出现事件的数量)。 - 同时可有多个事件被触发,调用Selector的
selectedKeys()
方法可以获得可操作Channel的SelectionKey集合。
二、基于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();
}
}
}
}