如何使用NIO非阻塞线程模型实现一个群聊系统?
一、实现的功能?
服务端:可以监测用户的上线、离线,并且实现消息的转发功能
客户端:通过channel可以无阻塞的发送消息给其他的用户,同时可以接收其他用户发送的消息(由服务器转发得到)
二、具体代码
服务端代码逻辑
- 服务器启动并监听6667端口
- 服务端接收客户端的消息,并实现转发(处理上线和离线)
package com.zzq.nio.groupchat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
/**
* @author: HP
* @date: 2020/12/23
* @description:
*/
public class GroupChatServer {
private Selector selector;//选择器
private ServerSocketChannel serverSocketChannel;//监听客户端事件
private static final int PORT=6667;//端口
/**
* 初始化对象信息
*/
public GroupChatServer(){
try {
//获取选择器
selector = Selector.open();
//serverSocketChannel
serverSocketChannel = ServerSocketChannel.open();
//绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
//设置非阻塞模式
serverSocketChannel.configureBlocking(false);
//将serverSocketChannel注册到selector,事件为accept
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 监听
*/
public void listen(){
try{
while(true){
//获取发生事件的个数
int count = selector.select();
//有事件发生
if(count>0){
//获取selector和channel通道的注册信息集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
//遍历集合
while(iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
//判断是什么事件
if(selectionKey.isAcceptable()){//连接事件
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
//将socketChannel注册到selector,监听读事件
socketChannel.register(selector,SelectionKey.OP_READ);
//提示信息
System.out.println(socketChannel.getRemoteAddress()+"上线了");
}
//通道发生读事件,从通道中读取数据
if(selectionKey.isReadable()){
//读取客户端数据
readData(selectionKey);
}
//移除
iterator.remove();
}
}else {
//System.out.println("等待事件发生。。。");
}
}
}catch (Exception e){
e.printStackTrace();
}
}
/**
* 读取客户端数据:要把channel里面的客户端数据读取到selector的buffer缓冲区中,
* 然后selector将buffer中的数据转发给其他的客户端(除去读取的客户端)。
* @param selectionKey
*/
private void readData(SelectionKey selectionKey){
SocketChannel socketChannel=null;
try{
//获取channel
socketChannel =(SocketChannel) selectionKey.channel();
//创建一个buffer(注意:该buffer是位于selector和buffer之间)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将channel里面的数据读取到buffer
int count = socketChannel.read(byteBuffer);
if(count>0){
//把buffer里面的数据转换成字符串
String msg = new String(byteBuffer.array());
//输出客户端信息
System.out.println("客户端:"+msg);
//将信息广播道其他的客户端(除掉自己)
sendInfoToOtherClients(msg,socketChannel);
}
}catch (Exception ex){
try {
System.out.println(socketChannel.getRemoteAddress()+"离线了》》》");
//取消注册
selectionKey.cancel();
//关闭通道
socketChannel.close();
} catch (Exception e) {
e.printStackTrace();
}
ex.printStackTrace();
}
}
/**
* 转发消息给其他的客户端
* @param msg
* @param self
*/
private void sendInfoToOtherClients(String msg,SocketChannel self) throws Exception{
System.out.println("服务器转发信息中。。。");
//遍历所有注册到selector上的SocketChannel
for (SelectionKey key:selector.keys()) {
//取出对应的SocketChannel
SelectableChannel channel = key.channel();
//排除自己
if(channel instanceof SocketChannel && channel!=self){
//转型
SocketChannel dest = (SocketChannel)channel;
//将msg,存储到buffer
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
//将buffer的数据写入到通道
dest.write(buffer);
}
}
}
public static void main(String[] args) {
GroupChatServer server = new GroupChatServer();
server.listen();
}
}
客户端代码逻辑
- 连接服务器
- 发送消息
- 接收服务器的消息
package com.zzq.nio.groupchat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
/**
* @author: HP
* @date: 2020/12/24
* @description:
*/
public class GroupChatClient {
private final String HOST="127.0.0.1";//服务器地址
private final int PORT = 6667;//服务器端口
private Selector selector;
private SocketChannel socketChannel;
private String username;
//初始化
public GroupChatClient(){
try {
selector = Selector.open();
socketChannel = SocketChannel.open(new InetSocketAddress(HOST,PORT)); //绑定服务器地址和端口号
socketChannel.configureBlocking(false);//设置非阻塞
socketChannel.register(selector, SelectionKey.OP_READ);//将channel注册到selector,监听事件为read事件
username = socketChannel.getLocalAddress().toString().substring(1);//获取用户名
System.out.println(username);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 向服务器发送消息
* @param msg
*/
public void sendInfo(String msg){
//包装一下数据
msg = username+"说:"+msg;
//将客户端buffer中数据发送到channel中
try {
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端读取从服务器发送的消息
*/
public void readInfo(){
try {
//获取发生事件数
int count = selector.select();
if(count>0){
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
if(key.isReadable()){
//得到通道
SocketChannel channel = (SocketChannel)key.channel();
//创建一个缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//将数据写到缓冲区
channel.read(buffer);
String msg = new String(buffer.array());
System.out.println(msg.trim());
}
iterator.remove();
}
}else {
System.out.println("没有可用的通道。。。");
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
//启动客户端
final GroupChatClient client = new GroupChatClient();
new Thread(){
//运行方法,每隔3秒从服务端读取数据
public void run() {
while(true){
client.readInfo();
try {
Thread.currentThread().sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
//发送数据给服务端
Scanner sc = new Scanner(System.in);
while(sc.hasNextLine()){
String msg = sc.nextLine();
client.sendInfo(msg);
}
}
}
三、运行截图