package edu.cheat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
public class ChatServer {
private static final int PORT = 8000;//设置服务器的端口号
private Selector selector;
//相当于监控器,检测注册在上面的链接是否有数据传过来
private ServerSocketChannel serverChannel;
//这是连接客户端与服务端的管道,当客户端连接时,就会新建一个这样的管道,并将其注册在selector上,这样selector就可以监控管道里是否会有东西进来了
private CharsetDecoder decoder;
//这个东西是可以将管道里的东西翻译出来的工具,它就相当于翻译官,将客户端传过来的字节数据转化为可以看懂的文字
private Map<SocketChannel, String> memberChannels;
//这个东西储存着你的名字与连接(相当于“电话簿”)
public ChatServer() throws IOException {
selector = Selector.open();//打开注册器
serverChannel = ServerSocketChannel.open();//打开连接通道
serverChannel.socket().bind(new InetSocketAddress(PORT));//绑定服务器所开的端口号
serverChannel.configureBlocking(false);//设置为非阻塞模式
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
//将改通道注册到selector注册器上。SelectionKey.OP_ACCEPT这个的意思是监听连接事件
decoder = StandardCharsets.UTF_8.newDecoder();
//这个是规定的解码时要以什么样的格式去解码
memberChannels = new HashMap<>();
}
public void run() throws IOException {
System.out.println("聊天室服务器已启动,监听端口:" + PORT);
while (true) {
selector.select();
//selector.select() 方法是 NIO 中 Selector 类的方法,它的作用是阻塞并等待直到有一个或多个通道在注册的事件上就绪。
//
Set<SelectionKey> keys = selector.selectedKeys();//在调用 selector.select() 方法后,通过 selector.selectedKeys() 可以获取到所有已经就绪的通道对应的 SelectionKey 对象。
//也就是说selector.select() 方法可以发现有新就绪的事件,而selectedKeys()会把所有的在selector上的所有准备就绪的事件都存储在这个集合里,selectedKeys里存的是准备就绪的连接
for (Iterator<SelectionKey> iterator = keys.iterator(); iterator.hasNext();) {//这里是要将selectedKeys里的所有准备就绪的链接取出来
//iterator.hasNext()是判断集合里是否存在下一个对象
// Iterator<SelectionKey> iterator = keys.iterator()这个的作用是获得一个集合的迭代器
SelectionKey key = iterator.next();//iterator.next()的作用是从keys集合里取出来一个对象
iterator.remove();//这里是已经将当前的对象拿出来了,那么此时迭代器所指向的集合里的对象就可以删除了,
// 因为在逻辑上这个被取出来的事件对象已经被处理了,而被处理过的对象就不能继续待在SelectionKeys集合里了
if (key.isAcceptable()) {//判断当前事件是请求接收事件还是请求读取事件
// key.isAcceptable()这个方法用来判断此事件是否是请求接收事件
// key.isReadable()这个方法用来判断此事件是否是读取事件
handleAccept(key);
} else if (key.isReadable()) {
handleRead(key);
}
}
}
}
private void handleAccept(SelectionKey key) throws IOException {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();//在这两句代码中,(ServerSocketChannel) 强转的作用是将
// key.channel() 返回的通道对象强制转换为 ServerSocketChannel 类型的对象,
// 以便调用 accept() 方法来接受客户端的连接。
client.configureBlocking(false);//将这个新建的client连接也设置为非阻塞模式
// 生成一个随机的用户名
String username = "User" + UUID.randomUUID().toString().substring(0, 8);
memberChannels.put(client, username);//将这个新连接赐予名字,并将其放进“电话簿”里
client.register(selector, SelectionKey.OP_READ);
//将新连接也放到注册器上,通过注册 SelectionKey.OP_READ,服务器就能够监听客户端的请求读取事件,
// 这样一来,服务器就可以通过 Selector 监听并处理来自客户端的数据
System.out.println("新客户端连接:" + client.socket().getRemoteSocketAddress());
broadcastMessage(username + " 加入了聊天室");
}
private void handleRead(SelectionKey key) throws IOException {//这是处理请求读取事件的方法
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
//缓冲区,相当于一个小货车,客户端从管道连接中传过来的数据就相当于货物,这个1024就规定着这个货车的大小,
// 数字越大,货车容量越大
StringBuilder message = new StringBuilder();//StringBuilder这是一个类,它的对象可以不断的存储字符信息
int bytesRead = client.read(buffer);//client.read(buffer)里的client是客户端与服务端之间的管道,
// read是读取方法(相当于工人,向buffer这个货车上装货物),而bytesRead就是指这次货物装了多少
while (bytesRead > 0) {//这里进入循环,因为在逻辑上,第一次的buffer(货车)里肯定是会有东西的,
// 所以bytesRead肯定会大于0
buffer.flip();//这一行代码将 buffer 切换为读模式,以便后续的解码操作可以从缓冲区的开头开始读取数据,
// (读取毕竟要从开头向末尾读,如果直接从末尾开始不就什么都读不到了嘛),因为这是在同一个循环里,
// buffer的默认模式不会将其内部读到末尾的指针给重新归位的
CharBuffer charBuffer = decoder.decode(buffer);
//这行代码使用解码器 decoder 对 buffer 中的数据进行解码,将其转换为字符形式的数据,存储到 charBuffer 中,
// 这就是翻译官在工作了
message.append(charBuffer);
//message就是StringBuilder类的对象,它存储信息。append方法就是往里写信息的
buffer.clear();
//清空货车
bytesRead = client.read(buffer);
//再读剩余未读的数据,如果没有数据(货物)了,那么此时bytesRead就等于-1.循环也会结束
}
String username = memberChannels.get(client);//通过“电话簿”找到相应连接的名字
String msg = message.toString().trim(); // 去除消息首尾空格
if (msg.equals("quit")) {// 如果消息为 "quit"
memberChannels.remove(client); // 从 Map ““电话簿””中移除客户端信息
key.cancel(); // 取消该 SelectionKey(就是关闭当前事件)
client.close(); // 关闭客户端连接
broadcastMessage(username + " 退出了聊天室");
} else {
broadcastMessage(username + ": " + msg);
}
}
private void broadcastMessage(String message) throws IOException {
ByteBuffer buffer = StandardCharsets.UTF_8.encode(message);
//这里将消息用utf-8的格式编码为字节信息,那么当另一端接收到时就用CharsetDecoder decoder 这个翻译官来翻译
for (SocketChannel client : memberChannels.keySet()) {//遍历每一个客户端
client.write(buffer); // 向每个客户端发送消息
buffer.rewind(); // 重置缓冲区的位置,准备下一次写入(上面也讲到过,这里是重置内部指针的位置了)
}
}
public static void main(String[] args) throws IOException {//启动服务器
ChatServer server = new ChatServer();
server.run();
}
}
这是服务端
package edu.cheat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
public class ChatClient {
private static final String SERVER_HOST = "127.0.0.1"; //服务器地址(127.0.0.1就是本地地址)
private static final int SERVER_PORT = 8000; // 端口
private SocketChannel socketChannel;//创建一个通道,一会连接客户端要用
private CharsetDecoder decoder;//翻译官😂
public ChatClient() throws IOException {
socketChannel = SocketChannel.open(new InetSocketAddress(SERVER_HOST, SERVER_PORT));
//用固定的方法绑定端口 SERVER_HOST SERVER_PORT是已经定义过的
decoder = StandardCharsets.UTF_8.newDecoder();//指定解码格式(指定翻译官的语言😂)
}
public void run() {
try {
Thread messageReader = new Thread(new MessageReader());//新建一条线程
messageReader.start();//开启此线程
Scanner scanner = new Scanner(System.in);//新建扫描器
while (true) {//进入循环,会一直等待用户的输入
String message = scanner.nextLine();//接收键盘输入
ByteBuffer buffer = StandardCharsets.UTF_8.encode(message);//将输入的信息用utf-8编码为字节信息(方便传输)
socketChannel.write(buffer);//往通道里写入(这里相当于向服务器发了一个请求读写事件)
}
} catch (IOException e) {
e.printStackTrace();
}
}
private class MessageReader implements Runnable {
//这个类实现了Runnable接口(这个接口是专门设置或者说是调控线程的),
// 那么这个类就可以去参与进对某一线程的创建里了
@Override
public void run() {//Runnable接口里唯一的一个方法,当线程启动时会先执行此方法
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
while (true) {//进入循环
socketChannel.read(buffer);
//读取通道中的信息,因为没有设置非阻塞队列,所以阻塞队列会一直等待信息的输入,
// 也就是客户端会一直监听着服务器是否有发送消息过来
buffer.flip();
CharBuffer charBuffer = decoder.decode(buffer);
System.out.println(charBuffer.toString());
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException {
ChatClient client = new ChatClient();//新建客户端对象
client.run();//启动run方法(启动客户端)
}
}
这是客户端
此外:
非阻塞模式(Non-blocking mode)是一种I/O模式,与传统的阻塞式I/O(Blocking I/O)相对应。
在阻塞式I/O中,当我们从通道中读取数据时,如果没有数据可读,那么程序将会一直阻塞,直到有数据可读为止。同样地,当我们向通道中写入数据时,如果通道已满,那么程序也会一直阻塞,直到有空余的空间为止。
而在非阻塞模式下,程序不会一直等待阻塞的I/O操作完成,而是立即返回并继续执行其他任务。当I/O操作完成时,程序会通过回调或轮询等方式获取结果。
使用非阻塞模式可以有效提高系统的并发性和响应速度,因为它允许程序同时处理多个I/O操作,而不必等待每个操作完成后再进行下一个操作。
在Java NIO中,可以将通道(Channel)设置为非阻塞模式,通过调用configureBlocking(false)
方法来实现。在非阻塞模式下,我们可以使用Selector等机制来监听通道上的事件,并根据事件类型进行相应的操作。