目录
用Java里NIO的原生API搭建一个在线群聊系统。
在线群聊的要求:
-
编写一个NIO群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
-
能够实现多人群聊(多客户端)
-
服务器端:可以监测用户上线,离线,并实现消息转发功能
-
客户端:通过Channel可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(由服务器转发得到)
实现流程:
1 server:把serverSocketChannel注册到serverSelector
(监听serverSocketChannel是否有可accept连接的事件发生,有就会在serverSelector中把serverSocketChannel的key记录下来)
client:把socketChannel注册到clientSelector
(监听socketChannel是否有可read的事件发生,有就会在clientSelector中把socketChannel的key记录下来)
2 client:open/connect(address) 连接到server ---- 触发----> server:serverSelector 监听到serverSocketChannel的可accept事件
3 server:serverSocketChannel.accept()可以拿到连接到服务器的客户端的socketChannel,接着把这个channel注册到serverSelector
(监听这个注册上去的socketChannel是否有可read事件发生,有就会在serverSelector中把该socketChannel的key记录下来)
4 client:socketChannel.write()往socketChannel中写入数据 ----触发----> server:serverSelector监听到socketChannel的可read事件
5 server:由isReadable的key可以拿到发送数据的客户端的socketChannel,再从这个通道中把写入的数据在server端读出来
6 server:由serverSelector.keys()可以拿到所有注册在serverSelector(连接到服务器的)的客户端socketChannel,把刚才读到的数据逐一地写到各个除发送数据的那个外的其他客户端的通道中 ----触发----> client:clientSelector监听到socketChannel的可read事件
7 client:socketChannel.read()客户端用自己的socketChannel把从服务器转发过来的消息读取出来
-
容易混淆的点说明:
(1)上面 流程3 :触发acceptable事件时被选择器标记的是serverSocketChannel,而不是注册过来的客户端的socketChannel。我之前想当然认为在这里key.channel()和serverSocketChannel.accept()拿到的是同一个。
(2)上面 流程7 :这里有的教程是用客户端选择器clientSelect监听到的key反向得到socketChannel然后再读,这个也是可以的,但是有点舍近求远了,而且很容易造成误导,此外在客户端本地的selector上本来就只注册了自己的socketChannel。
-
实例代码
server端:
主要是5个主要部分
①初始化服务器:创建和注册serverSocketChannel、创建ServerSelector、绑定地址
②ServerSelector监听事件:acceptable和readable事件
③读从客户端发送的消息
④将读到的消息转发给其他客户端
⑤启动服务器
public class GroupChatServer {
//定义属性
private Selector ServerSelector;
private ServerSocketChannel listenChannel;
private static final int PORT = 6667;
//构造器
//初始化工作
public GroupChatServer() {
try {
//得到选择器
ServerSelector = Selector.open();
//ServerSocketChannel
listenChannel = ServerSocketChannel.open();
//绑定端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
//设置非阻塞模式
listenChannel.configureBlocking(false);
//将该 listenChannel 注册到 selector
listenChannel.register(ServerSelector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void listen() {
try {
//循环处理
while (true) {
int count = ServerSelector.select();
if (count > 0) { //有事件处理
// 遍历得到 selectionKey 集合
Iterator<SelectionKey> iterator = ServerSelector.selectedKeys().iterator();
while (iterator.hasNext()) {
//取出 selectionkey
SelectionKey key = iterator.next();
//监听到 accept
if (key.isAcceptable()) {
SocketChannel sc = listenChannel.accept();
sc.configureBlocking(false);
//将该 sc 注册到 seletor
sc.register(ServerSelector, SelectionKey.OP_READ);
//提示
System.out.println(sc.getRemoteAddress() + " 上线 ");
}
if (key.isReadable()) {//通道发送read事件,即通道是可读的状态
// 处理读(专门写方法..)
readData(key);
}
//当前的 key 删除,防止重复处理
iterator.remove();
}
} else {
System.out.println("等待....");
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//发生异常处理....
}
}
//读取客户端消息
public void readData(SelectionKey key) {
SocketChannel channel = null;
try {
//得到 channel
channel = (SocketChannel) key.channel();
//创建 buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
//根据 count 的值做处理
if (count > 0) {
//把缓存区的数据转成字符串
String msg = new String(buffer.array());
//输出该消息
System.out.println("来自客户端的消息:" + msg.trim());
//向其它的客户端转发消息(去掉自己),专门写一个方法来处理
sendInfoToOtherClients(msg, channel);
}
} catch (IOException e) {
try {
System.out.println(channel.getRemoteAddress() + "离线了..");
//取消注册
key.cancel();
//关闭通道
channel.close();
} catch (IOException e2) {
e2.printStackTrace();
}
}
}
//转发消息给其它客户(通道)
private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {
System.out.println("服务器转发消息中...");
//遍历所有注册到 selector 上的 SocketChannel,并排除 self
for (SelectionKey key : ServerSelector.keys()) {//注意区分selectedKeys()和keys()
//通过 key 取出对应的 SocketChannel
Channel targetChannel = key.channel();
//排除自己
if (targetChannel instanceof SocketChannel && targetChannel != self) {
//转型
SocketChannel dest = (SocketChannel) targetChannel;
//将 msg 存储到 buffer
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//将 buffer 的数据写入通道
dest.write(buffer);
}
}
}
public static void main(String[] args) {
//创建服务器对象
GroupChatServer groupChatServer = new GroupChatServer();
groupChatServer.listen();
}
}
client端:
主要是5个主要部分
①初始化客户端:创建和注册socketChannel、创建clientSelector、连接到服务器
②clientSelector监听事件:readable事件
③发送消息到客户端
④读取服务器转发的消息
⑤启动客户端:需要新启一个线程,这样可以分别执行监听和发消息的工作
public class GroupChatClient {
//定义相关的属性
private final String HOST = "127.0.0.1";//服务器的ip
private final int PORT = 6667;//服务器端口
private Selector selector;
private SocketChannel socketChannel;
private String username;
//构造器,完成初始化工作
public GroupChatClient() throws IOException {
selector = Selector.open();
//连接服务器
socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));
//设置非阻塞
socketChannel.configureBlocking(false);
//将 channel 注册到selector
socketChannel.register(selector, SelectionKey.OP_READ);
//得到 username
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username + " is ok...");
}
//向服务器发送消息
public void sendInfo(String info) {
info = username + " 说:" + info;
try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
//读取从服务器端回复的消息
public void readInfo() {
try {
int readChannels = selector.select();
if (readChannels > 0) {//有可以用的通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
//得到相关的通道
// SocketChannel sc = (SocketChannel) key.channel();// 数据就在自己的socket通道中,不需要再通过Key反向获取,而且本地selector里也只注册了自己的Channel
//得到一个 Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取
socketChannel.read(buffer);
//把读到的缓冲区的数据转成字符串
String msg = new String(buffer.array());
System.out.println(msg.trim());
}
}
iterator.remove(); //删除当前的 selectionKey,防止重复操作
} else {
//System.out.println("没有可以用的通道...");
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
//启动客户端
GroupChatClient chatClient = new GroupChatClient();
//启动一个线程,每隔 3 秒,读取从服务器发送数据
new Thread(() -> {
while (true) {
chatClient.readInfo();
try {
Thread.currentThread().sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//发送数据给服务器端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String s = scanner.nextLine();
chatClient.sendInfo(s);
}
}
}
-
提示:
1 客户端也需要创建Selector来监听是否有消息传来的事件,这样如果没有消息,线程可以做别的事,不会阻塞。客户端的Selector和服务器端的Selector不是一个,所以我这里用了server和client作区别,有些视频教程里面没有说明这点,看的时候会比较迷惑。
2 多人群聊就是客户端多开
我用的是2023.2的新版IDEA,设置程序多开的话是点这里运行配置
run configurations --> modify options --> allow module instances
老版的话,运行程序并行运行
run configurations --> allow parallel run