NIO实现多人聊天室
时间:2022-07-22
本文章向大家介绍NIO实现多人聊天室,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
一、项目介绍
上周发布了《java的IO模型》一文,讲到了NIO,打铁要趁热,这周来个实战,用NIO实现一个简易的多人聊天室。聊天室,肯定是需要一个服务端和一个客户端的。就像QQ群一样,首先我们每个人都要安装QQ,这个就是客户端,服务端呢就是腾讯的QQ服务器,我们在客户端发送一条消息,服务端接收到了,然后再转发到别的客户端上,所以大家在这个QQ群的都能收到你发的消息。那接下来就开始是服务端和客户端的编码。
二、服务端编码
整个流程大致如下:
- 在构造方法里初始化一些参数。首先是得到选择器,然后打开ServerSocketChannel ,并监听6666端口;然后设置成非阻塞模式,注册到选择器上。
- 然后写一个监听的方法,看看选择器上有没有通道有事件需要处理的,如果有,我们就遍历选择器,针对拿到的不同事件进行不同的处理。如果是连接事件,即有客户端连接这个服务端,就打印出“xxx上线了”;如果是读取事件,表示客户端有人发消息到服务端了,那我们就将消息读取到通道中,最后转成字符串,打印在控制台,并且将该消息发送到其他客户端。
- 发送消息到其他客户端的逻辑也很简单,就是遍历所有注册到选择器上的通道(除了自己),然后将消息发送到这些通道中。
- 最后在main方法中创建服务端对象,开始监听即可。
以下是完整代码:
public class GroupChatServer {
// 定义需要用到属性
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT = 6666;
/**
* 构造器,进行初始化
*/
public GroupChatServer() {
try {
// 得到选择器
selector = Selector.open();
// 得到ServerSocketChannel
listenChannel = ServerSocketChannel.open();
// 绑定端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
// 设置非阻塞模式
listenChannel.configureBlocking(false);
// 将listenChannel注册到selector上
listenChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 监听
*/
public void listen() {
try {
while (true) {
int count = selector.select();
// count > 0 就表示有事件要处理
if (count > 0) {
// 拿到selectionKey的集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
// 取出selectionKey
SelectionKey key = iterator.next();
// 监听到不同的事件进行不同的处理
listenHandler(key);
// 删除当前key,防止重复处理
iterator.remove();
}
} else {
System.out.println("当前没事件要处理,等待中……");
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 针对监听到的不同事件做不同的处理
* @param key
* @throws Exception
*/
public void listenHandler(SelectionKey key) {
SocketChannel sc = null;
try {
// 如果是连接事件
if (key != null && key.isAcceptable()) {
// 拿到socketChannel并注册到selector上
sc = listenChannel.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
// 提示:xxx上线了
System.out.println(sc.getRemoteAddress().toString().substring(1) + "上线了");
}
// 如果是读取事件
if (key != null && key.isReadable()) {
// 拿到socketChannel
sc = (SocketChannel) key.channel();
sc.configureBlocking(false);
// 创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将客户端消息读到buffer中去
int count = sc.read(buffer);
if (count > 0) {
// 如果读取到了数据,就把缓冲区数据转成字符串输出
String msg = new String(buffer.array());
System.out.println("from client " + msg);
// 将该消息转发给其他客户端,
sendMsgToClient(msg, sc);
}
}
} catch (IOException e) {
try {
System.out.println(sc.getRemoteAddress().toString().substring(1) + "离线了");
// 取消注册
key.cancel();
// 关闭通道
sc.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
/**
* 发送消息到客户端,要排除当前客户端。
* 假如A、B、C三个客户端,当前消息是A发来的,那么只转发给B和C就可以。
* @param msg
* @param self
* @throws IOException
*/
public void sendMsgToClient(String msg, SocketChannel self) throws IOException {
System.out.println("服务器转发消息中");
// 遍历所有注册到selector上的SocketChannel,并排除自己
for (SelectionKey key : selector.keys()) {
Channel channel = key.channel();
if (channel instanceof SocketChannel && channel != self) {
SocketChannel targetChannel = (SocketChannel) channel;
// 将msg写到buffer中
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
// 将buffer数据写入通道
targetChannel.write(buffer);
}
}
}
public static void main(String[] args) {
GroupChatServer server = new GroupChatServer();
server.listen();
}
}
三、客户端编码
客户端的流程就简单很多了,整体流程大致如下:
- 首先也是在构造方法里进行一些初始化,通过SocketChannel连接服务端,设置非阻塞,注册到选择器上。
- 然后在写一个向服务端发送消息的方法,直接调用SocketChannel的write方法。
- 再写一个接收服务端消息的方法,遍历选择器,如果存在有事件发生的通道,就拿到该通道,然后将通道中的数据读取出来,打印在控制台。
- 最后是main方法,先创建客户端对象,然后new一个线程去调用接收消息的方法,然后在启用键盘录入,调用发送消息的方法,以便在控制台发送消息。
完整代码如下:
public class GroupChatClient {
// 定义相关属性
private final String HOST = "127.0.0.1";
private final int PORT = 6666;
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注册到selector上
socketChannel.register(selector, SelectionKey.OP_READ);
// 拿到username
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username + " is ok");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 向服务端发送消息
*
* @param msg
*/
public void sendMsgToServer(String msg) {
msg = username + ":" + msg;
try {
// 发送消息
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 接收服务端发来的消息
*/
public void acceptMsgFromServer() {
try {
int readChannel = selector.select();
if (readChannel > 0) { // 有可用的通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isReadable()) {
// 获取到通道
SocketChannel sc = (SocketChannel) key.channel();
// 创建buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 将通道中的数据读到buffer中
sc.read(buffer);
// 将buffer中的数据转成字符串
String msg = new String(buffer.array());
System.out.println(msg.trim());
// 移除当前的key,防止重复操作
iterator.remove();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 1. 启动客户端
GroupChatClient client = new GroupChatClient();
// 2. 启动线程,每隔3秒就读取一次服务端的消息
new Thread() {
public void run() {
while (true) {
client.acceptMsgFromServer();
try {TimeUnit.SECONDS.sleep(3); } catch (Exception e) { e.printStackTrace();}
}
}
}.start();
// 3. 发送数据到服务端
@SuppressWarnings("resource")
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String msg = scanner.nextLine();
client.sendMsgToServer(msg);
}
}
}
这就是用NIO非阻塞模式实现的聊天室,你可以启动多个客户端,一个客户端发送消息,服务端和别的客户端都能收到。
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法