案例要求:
- 编写一个 NIO 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
- 实现多人群聊
- 服务器端:可以监测用户上线,离线,并实现消息转发功能
- 客户端:通过 channel 可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发 得到)
- 目的:进一步理解 NIO 非阻塞网络编程机制
NIOChatServer.java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* 服务端思路:
* 1. 服务端启动并监听
* 2. 服务端接收客户端消息,并实现转发(并处理上线离线)
*
* SelectionKey,表示 Selector 和网络通道的注册关系, 共四种:
* int OP_ACCEPT:有新的网络连接可以 accept,值为 16
* int OP_CONNECT:代表连接已经建立,值为 8
* int OP_READ:代表读操作,值为 1
* int OP_WRITE:代表写操作,值为 4
*/
public class NIOChatServer {
private static final int PORT = 5758;
private Selector selector;
private ServerSocketChannel listenChannel;
/**
* 无参构造
*/
public NIOChatServer() {
initServer();
try {
listen();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 初始化Server
*/
public void initServer() {
try {
// 创建 Selector
selector = Selector.open();
// 创建 ServerSocketChannel
listenChannel = ServerSocketChannel.open();
// 设为非阻塞
listenChannel.configureBlocking(false);
// 绑定端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
// 注册到 Selector
listenChannel.register(this.selector, SelectionKey.OP_ACCEPT);
System.out.println("Server is listening ...");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 服务器监听
*/
public void listen() throws IOException {
while (true) {
// 未收到事件阻塞中
int select = selector.select(1000);
if (select == 0) continue;
// 遍历SelectionKey集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
handle(selectionKey);
iterator.remove(); // 手动移除,防止重复操作
}
}
}
/**
* 处理通道事件
*/
public void handle(SelectionKey selectionKey) {
// 连接事件
if (selectionKey.isAcceptable()) {
handleAccept(selectionKey);
}
// 读取事件
if (selectionKey.isReadable()) {
handleRead(selectionKey);
}
}
/**
* 处理 连接事件
*/
public void handleAccept(SelectionKey selectionKey) {
try {
// 获取连接的 SocketChannel
SocketChannel socketChannel = listenChannel.accept();
// 设为非阻塞
socketChannel.configureBlocking(false);
// 注册到 Selector
socketChannel.register(selector, SelectionKey.OP_READ);
// 提示
System.out.println(socketChannel.getRemoteAddress() + " 上线 ");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 处理 读事件
* @param selectionKey
*/
public void handleRead(SelectionKey selectionKey) {
// 获取到关联的 SocketChannel
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
// 创建Buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
try {
// 读数据
int read = socketChannel.read(byteBuffer);
if (read > 0) {
socketChannel.read(byteBuffer);
// 把读到的数据转成字符串
String msg = new String(byteBuffer.array()).trim();
msg = socketChannel.getRemoteAddress() + "说:" + msg;
System.out.println(msg);
// 消息转发给其他客户端
sendMsgToClients(socketChannel, msg);
} else { // 未读到数据,客户端退出
// 提示
System.out.println(socketChannel.getRemoteAddress() + " 离线 ");
// 取消注册
selectionKey.cancel();
// 关闭通道
socketChannel.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 转发消息到其他客户端(通道)
* @param socketChannel
* @param msg
*/
public void sendMsgToClients(SocketChannel socketChannel, String msg) {
// 遍历Selector上所有注册的Channel,排除自己
selector.keys().forEach(selectionKey -> {
// 通过SelectionKey获取SocketChannel
Channel channel = (Channel) selectionKey.channel();
// 因为Channel有可能是ServerSocketChannel或者SocketChannel
if (channel instanceof SocketChannel && channel != socketChannel) {
// 转为目标SocketChannel
SocketChannel destChannel = (SocketChannel) channel;
// 创建Buffer,并把消息填充进去
ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
try {
destChannel.write(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
public static void main(String[] args) {
new NIOChatServer();
}
}
NIOChatClient.java
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.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
/**
* 客户端思路:
* 1. 连接服务器
* 2. 发送消息
* 3. 接收服务器消息
*/
public class NIOChatClient {
/**
* 服务端ip与端口
*/
private static final String HOST = "127.0.0.1";
private static final int PORT = 5758;
private Selector selector;
private SocketChannel socketChannel;
private String username;
public NIOChatClient() {
initClient();
readMsgFromServer();
sendMsgToServer();
}
/**
* 初始化客户端
*/
public void initClient() {
try {
// 创建Selector
selector = Selector.open();
// 连接服务器
socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
// 非阻塞
socketChannel.configureBlocking(false);
// 注册到Selector 关系为 读
socketChannel.register(selector, SelectionKey.OP_READ);
// 给客户端分配一个用户名
username = socketChannel.getLocalAddress().toString().substring(1);
// 提示
System.out.println(username + "连接成功 ");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 向服务端发送消息
*/
public void sendMsgToServer() {
// 用户输入信息
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());
try {
socketChannel.write(byteBuffer);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 读取服务端消息 另起一个线程,每秒读取
*/
public void readMsgFromServer() {
new Thread(() -> {
try {
int select = selector.select(); // 没有事件阻塞中
if (select > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 读事件
if (selectionKey.isReadable()) {
// 获取对应的SocketChannel
SocketChannel channel = (SocketChannel) selectionKey.channel();
// 创建Buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 读数据到 Buffer
int read = channel.read(byteBuffer);
if (read > 0) {
// 提示
System.out.println(new String(byteBuffer.array()).trim());
}
}
}
}
Thread.currentThread().sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
public static void main(String[] args) {
new NIOChatClient();
}
}
运行结果
Server
client1
client2