持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第20天,点击查看活动详情
意志坚强的人能把世界放在手中像泥块一样任意揉捏 —— 歌德
NIO 群聊实现之服务端实现
案例背景
我们进行了NIO的相关组件学习,但是都是知道了组件的API以及作用,但是并不知道如何将组件放到一起进行使用,所以我们通过一个客户端和服务端通信的案例进行学习,并加深三大组件的使用,之前分享一了一篇文章是客户端与服务端的简单通讯,这次进行了客户端的群聊案例实现,服务端负责服务处理客户端连接事件的进入,以及客户端消息的转发操作
开发步骤
- 创建选择器
- 创建服务端 ServerSocketChannel
- 创建服务端的监听端口
- 设置服务端的 ServerSocketChannel 是非阻塞的
- 将服务端的 ServerSocke tChannel 注册到 Selector 选择器上面,并将事件设置成连接事件
- 监听就绪事件
- 根据相应的就绪事件进行逻辑处理【连接事件、可读事件、可写事件等等】
- 如果有客户端发来消息的时候,那么需要将客户端发来的消息发送给其他客户端
代码实现
下面代码进行了上面步骤的实现,进行了服务端的初始化操作,并且进行了客户端的连接事件监听,就绪事件的处理,比如客户端发来消息的时候那么就是客户端的连接通道就绪的时候
import io.netty.util.CharsetUtil;
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.util.Iterator;
public class ChatServer {
private Selector selector;
private ServerSocketChannel listenerChannel;
private static final int PORT = 8080;
public ChatServer() {
init();
}
/**
* 服务端初始化方法
*/
private void init() {
try {
selector = Selector.open();
listenerChannel = ServerSocketChannel.open();
// 绑定端口
listenerChannel.bind(new InetSocketAddress(PORT));
// 设置非阻塞模式
listenerChannel.configureBlocking(false);
// 将 listenerChannel 注册到 selector,事件为:OP_ACCEPT
listenerChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务端初始化完毕......");
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 处理客户端的监听事件
*/
public void listenEvent() {
try {
while (true) {
int selected = selector.select();
if (selected > 0) { // 如果返回值大于 0 说明有事件处理
// 遍历 SelectionKey 集合进行处理,获取就绪事件的集合
Iterator<SelectionKey> iterator =
selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 根据不同的就绪事件做不同的处理
if (key.isAcceptable()) {// 处理客户端连接事件
HandleAcceptEvent();
}
if (key.isReadable()) { // 处理通道的读事件
// 处理读事件读取客户端消息
handleReadEvent(key);
}
// 将当前的 selectionKey 删除,防止重复处理
iterator.remove();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 处理客户端的连接事件
* @throws IOException
*/
private void HandleAcceptEvent() throws IOException {
SocketChannel socketChannel = listenerChannel.accept();
socketChannel.configureBlocking(false);// 设置非阻塞
// 注册到 selector 选择器上面
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println(socketChannel.getRemoteAddress() + " 上线了。。。");
}
/**
* 处理客户端发来消息的读事件
* @param key
*/
private void handleReadEvent(SelectionKey key) {
SocketChannel channel = null;
try {
// 获取 SocketChannel 通道信息
channel = (SocketChannel) key.channel();
// 创建 Buffer 缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 通过将socketChannel 将数据读入到 buffer缓冲区
int selected = channel.read(buffer);
if (selected > 0) {
String msg = new String(buffer.array(), CharsetUtil.UTF_8);
// 向其它客户端转发消息,但是需要将自己排出出去
sendMsgToOtherClient(msg, channel);
}
} catch (IOException e) {// 如果捕获到异常就说明客户端下线了
try {
System.out.println(channel.getRemoteAddress() + "离线了。。。");
// 取消注册
key.cancel();
// 关闭通道
channel.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
}
/**
* 转发消息给其它客户端,但是需要将自己和ServerSocketChannel排除掉
* @param msg
* @param self
*/
private void sendMsgToOtherClient(String msg, SocketChannel self) {
// 遍历所有注册到 selector 上的 其他SocketChannel通道 并排除自己的通道
// 通过 selector.keys() 获取注册到selector上面的所有通道信息
for (SelectionKey key : selector.keys()) {
// 通过 key 取出对应的 SocketChannel,因为ServerSocketChannel
// 也注册到了selelcor上面
// 并且排除自己的通道
if (key.channel() instanceof SocketChannel && key.channel() != self) {
try { // 将buffer的数据写入通道,并转发给其他通道
((SocketChannel) key.channel())
.write(ByteBuffer.wrap(msg.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ChatServer chatServer = new ChatServer();
chatServer.listenEvent();
}
}
结果展示
服务端启动结果
小结
其实群聊服务和之前的客户端和服务端通信并没有太大的区别,主要的区别在于就是服务端需要将客户端发过来的消息进行转发,通知其他客户端此客户端发来的消息,下一篇我们需要进行客户端的实现