03 单线程模式下的Java网路编程以及NIO的使用
阻塞模式(单线程只能一个连接)------> 非阻塞模式(单线程支持多个连接,总是运行) -----> 多路复用即selector监控多个channel(单线程支持多个连接,没有事件阻塞,有事件发生则处理事件)
一、阻塞模式下网络数据传输简单示例
1-1 step1:服务端程序启动
服务端程序
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
@Slf4j
public class Server1 {
/*这段代码是服务端处理客户端请求的程序*/
public static void main(String[] args) throws IOException {
// 0.Byte buffer
ByteBuffer buffer = ByteBuffer.allocate(16);
// 1.创建服务器的连接通道,服务器通过这个通道获取数据
ServerSocketChannel ssc = ServerSocketChannel.open();
// 2.服务器绑定8080端口(监听端口)
ssc.bind(new InetSocketAddress(8080));
// 3.创建连接集合保存处理客户端与服务的连接
List<SocketChannel> channels = new ArrayList<>();
while(true){
// 4. accept建立与客户端连接,SocketChannel用来与客户端连接
log.debug("connecting...");
SocketChannel sc = ssc.accept(); // 服务的accept默认是阻塞方法,会让线程暂停,连接建立后会继续运行
log.debug("connected... {}",sc);
channels.add(sc);
for(SocketChannel channel: channels){
// 5.接受客户端发送的数据
log.debug("before read ... {}",channel);
channel.read(buffer); // read方法也是阻塞的方法,当客户端没有数据发送的时候,read方法会阻塞。
buffer.flip();
printBytebuffer(buffer);
buffer.clear();
log.debug("after read ... {}",channel);
}
}
}
static void printBytebuffer(ByteBuffer tmp){ // 注意:传入的bytebuffer必须时写模式
System.out.println(StandardCharsets.UTF_8.decode(tmp).toString());
}
}
执行结果
14:54:23.111 [main] DEBUG Server.Server1 - connecting...
注意(accept处阻塞):上面服务端程序工作在阻塞模式下,在没有连接的到来的时候,服务端调用accept方法会阻塞线程让出CPU,直到有客户端连接到来才会继续执行。
1-2 step2:客户端1请求连接,但是没有发送数据(断点1)
客户端程序
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
public class Client1 {
// 客户端代码:
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost",8080));
System.out.println("waiting...!"); // 断点1
sc.write(StandardCharsets.UTF_8.encode("hello!"));
System.out.println("Sending the first data!"); // 断点2
sc.write(StandardCharsets.UTF_8.encode("hello again!"));
System.out.println("Sending the second data!"); // 断点3
}
}
客户端程序运行到断点1后,服务端的执行结果
- 可以看到连接的客户端程序的端口号为11954
14:54:23.111 [main] DEBUG Server.Server1 - connecting...
14:58:50.126 [main] DEBUG Server.Server1 - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:11954]
14:58:50.129 [main] DEBUG Server.Server1 - before read ... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:11954]
注意(read处阻塞):当有socket连接时,服务端程序从accept出开始执行,执行到read方法处,由于客户端没有发送数据再次将服务端程序在read处阻塞等待数据的到来。
1-3 step3:客户端1建立连接后第一次发送数据(断点2)
客户端程序运行到断点2后,服务端的执行结果
- 可以看到服务端能够接受到客户端发送的数据,打印完数据后,客户端又去等待下一次连接。
14:54:23.111 [main] DEBUG Server.Server1 - connecting...
14:58:50.126 [main] DEBUG Server.Server1 - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:11954]
14:58:50.129 [main] DEBUG Server.Server1 - before read ... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:11954]
hello!
15:07:45.140 [main] DEBUG Server.Server1 - after read ... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:11954]
15:07:45.140 [main] DEBUG Server.Server1 - connecting...
注意:服务端在收到数据后,继续执行read之后的语句。
1-4 step4:客户端1建立连接后第一次发送数据(断点3)
执行结果:
- 可以发现现有的服务端程序在单线程环境下无法同时处理read事件以及accept事件。
14:54:23.111 [main] DEBUG Server.Server1 - connecting...
14:58:50.126 [main] DEBUG Server.Server1 - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:11954]
14:58:50.129 [main] DEBUG Server.Server1 - before read ... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:11954]
hello!
15:07:45.140 [main] DEBUG Server.Server1 - after read ... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:11954]
15:07:45.140 [main] DEBUG Server.Server1 - connecting...
1-5 step5:客户端2建立连接但不发送数据,保持客户端1连接
服务端执行结果:在处理完客户端2的accpet事件后,服务端才处理了客户端1的第二次发送的数据,然后再去等待客户2的数据发送。
显然,单线程环境下的工作在阻塞模式下的服务端程序由于无法同时处理read和accept事件造成数据接受/连接建立的不及时。
- 一个可行的策略是为每次连接都分配一个线程工作在阻塞模式下
新的客户端连接到来-->新线程处理--->线程阻塞并循环直到处理完所有读写事件---->客户端断开连接---->回收线程
14:54:23.111 [main] DEBUG Server.Server1 - connecting...
14:58:50.126 [main] DEBUG Server.Server1 - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:11954]
14:58:50.129 [main] DEBUG Server.Server1 - before read ... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:11954]
hello!
15:07:45.140 [main] DEBUG Server.Server1 - after read ... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:11954]
15:07:45.140 [main] DEBUG Server.Server1 - connecting...
15:18:10.029 [main] DEBUG Server.Server1 - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:4177]
15:18:10.029 [main] DEBUG Server.Server1 - before read ... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:11954] // 客户端1第二次发送的数据在客户端2建立连接后才得到处理
hello again!
15:18:10.029 [main] DEBUG Server.Server1 - after read ... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:11954]
15:18:10.030 [main] DEBUG Server.Server1 - before read ... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:4177]
1-6 阻塞模式下网络数据传输总结
1-6-1 特点
-
阻塞模式下,相关方法都会导致线程暂停
- ServerSocketChannel.accept 会在没有连接建立时让线程暂停
- SocketChannel.read 会在没有数据可读时让线程暂停
- 阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置
-
单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
-
但多线程下,有新的问题,体现在以下方面
- 32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低
- 服务端的accept方法(建立连接)与buffer的read方法(读取数据)都是阻塞的。
-
采用阻塞的方式去建立连接:ServerSocketChannel.accept 会在没有连接建立时让线程暂停,直到有客户端连接到来
ServerSocketChannel工作在阻塞模式
- 采用阻塞的方式从读写通道获取数据:SocketChannel.read 会在没有数据可读时让线程暂停,如果没有数据到来,也会一直阻塞
SocketChannel工作在阻塞模式
1-6-2 缺点
单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
多线程下,阻塞模式下网络传输的缺点(具有早期服务处理网络连接的确定):
- 单个线程无法同时进行连接的建立与数据的读取,必须为每个连接分配线程资源,只适合连接少的情况,连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低
2)单个线程与连接终身绑定,存在单个连接大部分时间是没有数据传送的,线程资源没有得到充分的利用。
3)采用线程池技术来减少线程数和线程上下文切换,但治标不治本。
- 存在:很多连接建立,但长时间 inactive,此时会阻塞线程池中所有线程,因此不适合长连接,只适合短连接。
二、非阻塞模式下网络数据传输的示例
2-1 非阻塞模式下服务端示例
对阻塞模式下服务端代码的改进
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class Server2 {
/*非阻塞模式下:服务端处理客户端请求的程序
* 单个线程可以处理进行多个连接的建立与数据的读取。
* */
public static void main(String[] args) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(16);
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); //配置的连接的获取为非阻塞模式
ssc.bind(new InetSocketAddress(8080));
List<SocketChannel> channels = new ArrayList<>();
while(true){
SocketChannel sc = ssc.accept(); // 非阻塞模式,如果没有连接则会返回null
if(sc != null){
sc.configureBlocking(false); //配置数据的读取为非阻塞模式
log.debug("connected... {}",sc);
channels.add(sc);
}
for(SocketChannel channel: channels){
// 5.接受客户端发送的数据
int read = channel.read(buffer); // 非阻塞模式下,如果没有数据可读则返回0
if(read > 0){
buffer.flip();
printBytebuffer(buffer);
buffer.clear();
log.debug("after read ... {}",channel);
}
}
}
}
static void printBytebuffer(ByteBuffer tmp){ // 注意:传入的bytebuffer必须时写模式
System.out.println(StandardCharsets.UTF_8.decode(tmp).toString());
}
}
建立2个客户端连接并发送数据的结果:
- 可以看到工作在非阻塞模式的服务端可以处理多个连接的请求以及数据的传输。
16:15:26.834 [main] DEBUG Server.Server2 - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:14336]
16:15:35.941 [main] DEBUG Server.Server2 - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:14345]
hello!
16:16:10.572 [main] DEBUG Server.Server2 - after read ... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:14345]
hello again!
16:16:13.576 [main] DEBUG Server.Server2 - after read ... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:14345]
Exception in thread "main" java.io.IOException: 远程主机强迫关闭了一个现有的连接。
at sun.nio.ch.SocketDispatcher.read0(Native Method)
at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:43)
at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223)
at sun.nio.ch.IOUtil.read(IOUtil.java:197)
at sun.nio.ch.SocketChannelImpl.read(SocketChannelImpl.java:380)
at Server.Server2.main(Server2.java:32)
2-2 非阻塞模式下总结
2-2-1 特点
非阻塞模式下,连接的建立与数据的读取不会让线程暂停
-
1)在 ServerSocketChannel.accept 在没有连接建立时,会返回 null,继续运行
-
2)SocketChannel.read 在没有数据可读时,会返回 0,但线程不必阻塞,可以去执行其它 SocketChannel 的 read 或是去执行 ServerSocketChannel.accept (单线程能够处理多个连接)
-
3)写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去2
2-2-2 缺点
非阻塞模式缺点(CPU资源利用率不高,我们希望有活干活,没活让出CPU):
- 非阻塞模式下,即使没有连接建立和可读数据,线程仍然在不断运行,浪费CPU资源(特别是对于单核CPU)
- 数据复制过程中,线程实际还是阻塞的(AIO 即异步IO改进的地方) 三、采用selector的网络数据传输的示例
三、利用selector(多路复用)进行网络数据传输
3-1 多路复用(selector)概述(重要)
selector:能够对多个channel进行管理
多路复用:单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用
- 多路复用仅针对网络 IO、普通文件 IO 没法利用多路复用
- 不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证
特性1:有可连接事件时才去连接
特性2:有可读事件才去读取
特性3:有可写事件才去写入:限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件
channel中发生事件的类型:
事件类型 | 说明 |
---|---|
accept | 客户端发送连接请求时,服务端channel会有accept事件发生 |
connect | 服务端与客户端连接建立完成会触发 |
read | 可读事件 |
write | 可写事件 |
- 如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证
- 有可连接事件时才去连接
- 有可读事件才去读取
- 有可写事件才去写入
- 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件
3-2 利用selector监听accept事件发生(特性1)
服务器代码:selector是管理者,而各种类型的channel是被管理,实际编程时,通过selecor提供的key对相关channel进行操作。
import lombok.extern.slf4j.Slf4j;
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.nio.charset.StandardCharsets;
import java.util.Iterator;
@Slf4j
public class Server3 {
public static void main(String[] args) throws IOException {
// selector:选择器
// 1.定义selector去管理多个channel
Selector selector = Selector.open();
ByteBuffer buffer = ByteBuffer.allocate(16);
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 配置为非阻塞模式
// 2.建立selector与channel的联系(将channel注册到selector中)
// SelectionKey就是将来事件发生后,通过它可以知道发生的事件来源于哪个channel
SelectionKey sscKey = ssc.register(selector,0,null); // 0表示不关联任何事件
sscKey.interestOps(SelectionKey.OP_ACCEPT); // key只关联accept事件即这里的定义的sscKey只关注accept事件
log.debug("register key:{}",sscKey);
ssc.bind(new InetSocketAddress(8080));
while(true){
// 03: select方法
// 2种情况:1)没有事件发生,线程阻塞。 2)有事件发生线程会继续运行。
selector.select();
// 04:处理事件,selectedKeys包含了所有发生的事件
// 集合的遍历的过程中如果要删除,则必须使用迭代器
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
log.debug("key: {}",key);
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept(); // 获取连接建立后的读写通道
log.debug("{}",sc);
}
}
}
static void printBytebuffer(ByteBuffer tmp){ // 注意:传入的bytebuffer必须时写模式
System.out.println(StandardCharsets.UTF_8.decode(tmp).toString());
}
}
让3个客户端连接服务端的执行结果
- 可以看到三个客户端的连接操作都是通过相同的key获取的,都是
register key:sun.nio.ch.SelectionKeyImpl@5eb5c224
21:27:58.105 [main] DEBUG Server.Server3 - register key:sun.nio.ch.SelectionKeyImpl@5eb5c224
21:28:12.239 [main] DEBUG Server.Server3 - key: sun.nio.ch.SelectionKeyImpl@5eb5c224
21:28:12.240 [main] DEBUG Server.Server3 - java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:3584]
21:28:41.676 [main] DEBUG Server.Server3 - key: sun.nio.ch.SelectionKeyImpl@5eb5c224
21:28:41.677 [main] DEBUG Server.Server3 - java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:3595]
21:28:47.979 [main] DEBUG Server.Server3 - key: sun.nio.ch.SelectionKeyImpl@5eb5c224
21:28:47.979 [main] DEBUG Server.Server3 - java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:3604]
class名称 | 说明 | 备注 |
---|---|---|
SelectionKey | A token representing the registration of a SelectableChannel with a Selector . |
|
Selector | A multiplexor of SelectableChannel objects. |
|
ServerSocketChannel | A selectable channel for stream-oriented listening sockets. | 监听套接字的通道 |
SocketChannel | A selectable channel for stream-oriented connecting sockets. | 连接套接字的通道 |
SelectableChannel | A channel that can be multiplexed via a Selector . |
注意:selector使用的基本方法(就连接而言)
1)定义selector用于管理channel,定义被管理的ServerSocketChannel
2)调用ServerSocketChannel的register的方法在管理者selector那边进行登记同时关联事件类型并获得key。
此后通过这个key获取被管理的channnel。
3)selector.select()会将有事件的key给弄出来,然后遍历,逐个处理事件。
上图是SocketChannel关系图。
- 可以看到SocketChannel是SelectableChannel的子类
上图是selector与ServerSocketChannel的关系图。
- 二者都实现了Closeable接口
上图是Selector关系图。
3-3 selector取消事件的处理
代码示例
import lombok.extern.slf4j.Slf4j;
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.nio.charset.StandardCharsets;
import java.util.Iterator;
@Slf4j
public class Server4 {
public static void main(String[] args) throws IOException {
// selector:选择器
// 1.定义selector去管理多个channel
Selector selector = Selector.open();
ByteBuffer buffer = ByteBuffer.allocate(16);
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 配置为非阻塞模式
// 2.建立selector与channel的联系(将channel注册到selector中)
// SelectionKey就是将来事件发生后,通过它可以知道发生的事件来源于哪个channel
SelectionKey sscKey = ssc.register(selector,0,null); // 0表示不关联任何事件
sscKey.interestOps(SelectionKey.OP_ACCEPT); // key只关联accept事件即这里的定义的sscKey只关注accept事件
log.debug("register key:{}",sscKey);
ssc.bind(new InetSocketAddress(8080));
while(true){
// 03: select方法
// 2种情况:1)没有事件发生,线程阻塞。 2)有事件发生线程会继续运行。
selector.select();
// 04:处理事件,selectedKeys包含了所有发生的事件
// 集合的遍历的过程中如果要删除,则必须使用迭代器
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
log.debug("key:{}",key);
// key.cancel();
}
}
}
static void printBytebuffer(ByteBuffer tmp){ // 注意:传入的bytebuffer必须时写模式
System.out.println(StandardCharsets.UTF_8.decode(tmp).toString());
}
}
当不对事件进行处理: 下次访问事件集合的时候这个事件依旧存在。
22:15:42.598 [main] DEBUG Server.Server4 - key:sun.nio.ch.SelectionKeyImpl@5eb5c224
22:15:42.598 [main] DEBUG Server.Server4 - key:sun.nio.ch.SelectionKeyImpl@5eb5c224
22:15:42.598 [main] DEBUG Server.Server4 - key:sun.nio.ch.SelectionKeyImpl@5eb5c224
22:15:42.598 [main] DEBUG Server.Server4 - key:sun.nio.ch.SelectionKeyImpl@5eb5c224
...
总结:对于事件要么处理,要么使用cancel方法不处理,什么都不做会导致事件一直在集合中。
3-4 selector处理连接/读写事件并关闭连接(特性2)
实现代码
import lombok.extern.slf4j.Slf4j;
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.nio.charset.StandardCharsets;
import java.util.Iterator;
@Slf4j
public class Server4 {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ByteBuffer buffer = ByteBuffer.allocate(16);
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 配置为非阻塞模式
SelectionKey sscKey = ssc.register(selector,0,null); // 0表示不关联任何事件
sscKey.interestOps(SelectionKey.OP_ACCEPT); // key只关联accept事件即这里的定义的sscKey只关注accept事件
log.debug("Register Server Socket Channel key with accept event: {}",sscKey);
ssc.bind(new InetSocketAddress(8080));
while(true){
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
/*要点1:获取key后从集合中移除这个key,避免重复处理造成NPE问题(集合的遍历的过程中如果要删除,则必须使用迭代器)*/
iter.remove();
// 区分事件类型
if(key.isAcceptable()){
log.debug("Accept Event happen! Server Socket Channel key: {}",key);
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
/*获取连接建立后的读写通道,用于读写的SocketChannel必须在建立连接后才能获得*/
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
sc.register(selector,0,null);
SelectionKey scKey = sc.register(selector,0,null);
log.debug("Register Socket Channel key with Read event: {}",scKey);
scKey.interestOps(SelectionKey.OP_READ); // 绑定socketChannel的可读事件
}else if(key.isReadable()){
log.debug("Read event happen ! Socket Channel key{}",key);
try{
SocketChannel channel = (SocketChannel) key.channel(); // 拿到触发事件的channel
ByteBuffer tmp_buffer = ByteBuffer.allocate(16);
/*客户端异常断开也会产生读事件,也会产生读事件
* 并返回-1.
* */
int read = channel.read(tmp_buffer);
/*要点2:检测到客户端正常断开*/
if(read == -1){
log.debug("Client normal close event happen ! Socket Channel key{}",key);
key.cancel();
}else{
tmp_buffer.flip();
printBytebuffer(tmp_buffer);
}
}catch (IOException e){
e.printStackTrace();
log.debug("Client abnormal close event happen ! Socket Channel key{}",key);
/*要点2:客户端非正常断开连接,服务端抛出异常并被捕获,捕获后需要将关联的key从事件集合中删除*/
key.cancel();
}
}
}
}
}
static void printBytebuffer(ByteBuffer tmp){ // 注意:传入的bytebuffer必须时写模式
System.out.println(StandardCharsets.UTF_8.decode(tmp).toString());
}
}
3-4-1 要点1:发生事件的key需要手动从发生事件的key的集合中删除。
- 没有( iter.remove())删除处理过的key会发生NPE问题
select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如
- 第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey
- 第二次触发了 sckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常
16:53:49.510 [main] DEBUG Server.Server4 - Register Server Socket Channel key with accept event: sun.nio.ch.SelectionKeyImpl@5eb5c224
16:53:58.535 [main] DEBUG Server.Server4 - Accept Event happen! Server Socket Channel key: sun.nio.ch.SelectionKeyImpl@5eb5c224
16:53:58.535 [main] DEBUG Server.Server4 - Register Socket Channel key with Read event: sun.nio.ch.SelectionKeyImpl@7dc5e7b4
16:53:58.538 [main] DEBUG Server.Server4 - Accept Event happen! Server Socket Channel key: sun.nio.ch.SelectionKeyImpl@5eb5c224
Exception in thread "main" java.lang.NullPointerException
at Server.Server4.main(Server4.java:46)
3-4-2 要点2:客户端连接断开处理(异常断开/主动断开)
关键:利用cancel 方法取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件
- 对客户端异常断开的处理,会造成整个服务器程序宕机(需要捕获异常)
- 对客户端正常断开的处理
20:27:52.677 [main] DEBUG Server.Server4 - Register Server Socket Channel key with accept event: sun.nio.ch.SelectionKeyImpl@5eb5c224
20:27:59.217 [main] DEBUG Server.Server4 - Accept Event happen! Server Socket Channel key: sun.nio.ch.SelectionKeyImpl@5eb5c224
20:27:59.218 [main] DEBUG Server.Server4 - Register Socket Channel key with Read event: sun.nio.ch.SelectionKeyImpl@7dc5e7b4
20:27:59.219 [main] DEBUG Server.Server4 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
hello!
20:27:59.220 [main] DEBUG Server.Server4 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
hello again!
20:27:59.220 [main] DEBUG Server.Server4 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
20:27:59.220 [main] DEBUG Server.Server4 - Client normal close event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
3-5 selector处理消息边界
3-5-1 消息边界带来的问题
代码示例
服务端程序:设置buffer的大小为3个字节
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
@Slf4j
public class Server1 {
/*这段代码是服务端处理客户端请求的程序*/
public static void main(String[] args) throws IOException {
// 0.Byte buffer
ByteBuffer buffer = ByteBuffer.allocate(16);
// 1.创建服务器的连接通道,服务器通过这个通道获取数据
ServerSocketChannel ssc = ServerSocketChannel.open();
// 2.服务器绑定8080端口(监听端口)
ssc.bind(new InetSocketAddress(8080));
// 3.创建连接集合保存处理客户端与服务的连接
List<SocketChannel> channels = new ArrayList<>();
while(true){
// 4. accept建立与客户端连接,SocketChannel用来与客户端连接
log.debug("connecting...");
SocketChannel sc = ssc.accept(); // 服务的accept默认是阻塞方法,会让线程暂停,连接建立后会继续运行
log.debug("connected... {}",sc);
channels.add(sc);
for(SocketChannel channel: channels){
// 5.接受客户端发送的数据
log.debug("before read ... {}",channel);
channel.read(buffer); // read方法也是阻塞的方法,当客户端没有数据发送的时候,read方法会阻塞。
buffer.flip();
printBytebuffer(buffer);
buffer.clear();
log.debug("after read ... {}",channel);
}
}
}
static void printBytebuffer(ByteBuffer tmp){ // 注意:传入的bytebuffer必须时写模式
System.out.println(StandardCharsets.UTF_8.decode(tmp).toString());
}
}
客户端程序:发送2次数据,每次发送6个字节数据(UTF8编码每个汉字为3个字节)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
public class Client1 {
// 客户端代码:
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost",8080));
System.out.println("waiting...!");
sc.write(StandardCharsets.UTF_8.encode("中国")); // 6个字节
System.out.println("Sending the first data!");
sc.write(StandardCharsets.UTF_8.encode("万岁")); // 6个字节
System.out.println("Sending the second data!");
sc.close(); // 客户端正常断开
}
}
执行结果:可以看到2次数据12字节的数据,服务端由于buffer大小的限制,分三次读取,造成数据解决不准确。
20:37:26.877 [main] DEBUG Server.Server4 - Register Server Socket Channel key with accept event: sun.nio.ch.SelectionKeyImpl@5eb5c224
20:37:31.327 [main] DEBUG Server.Server4 - Accept Event happen! Server Socket Channel key: sun.nio.ch.SelectionKeyImpl@5eb5c224
20:37:31.328 [main] DEBUG Server.Server4 - Register Socket Channel key with Read event: sun.nio.ch.SelectionKeyImpl@7dc5e7b4
20:37:31.329 [main] DEBUG Server.Server4 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
中�
20:37:31.330 [main] DEBUG Server.Server4 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
���
20:37:31.330 [main] DEBUG Server.Server4 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
�岁
20:37:31.330 [main] DEBUG Server.Server4 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
20:37:31.330 [main] DEBUG Server.Server4 - Client normal close event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
3-5-2 消息边界问题需要考虑的情况以及常用解决策略
边界问题的三种情况:
由于实际网络传输过程中消息的长度是不确定的(bytebuffer与消息的长度不是很匹配)
-
情况1:实际消息的长度大于bytebuffer容量(采用扩容机制)
-
情况2:半包现象
-
情况3:黏包现象
消息边界处理的一些常用策略(主要针对上面的情况2和情况3):
1)服务端与客户端约定固定消息长度,数据包大小一样,服务器按预定长度读取
- 浪费带宽
2)服务端与客户端约定消息的分隔符
- 消息读取需要寻找分隔符,效率低
3)约定消息传输的格式:TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer
- buffer 需要提前分配,如果内容过大,则影响 server 吞吐量
注意:Http 1.1 是 TLV 格式,Http 2.0 是 LTV 格式
3-5-3 利用分隔符作为消息边界(单次消息长度大于buffer引发的问题)
服务端测试代码:设置buffer的大小为8并使用\n作用消息的边界。
import lombok.extern.slf4j.Slf4j;
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.nio.charset.StandardCharsets;
import java.util.Iterator;
@Slf4j
public class Server5 {
// 基本思想:按照应用层分割符读取读取数据,如果buffer中最终数据不是以\n结尾,
// 保留这个数据直到倒数第一个\n不进行读取,将其compact后,再一个数据包数据读入之后再处理。
private static void split(ByteBuffer source) {
source.flip(); // 转换为读模式
int oldLimit = source.limit(); //原始的buffer大小
for (int i = 0; i < oldLimit; i++) {
if (source.get(i) == '\n') {
//分配bytebuffer接受传输过来的数据记录
// 当前记录大小 = 当前位置+1-position
ByteBuffer target = ByteBuffer.allocate(i + 1 - source.position());
// 0 ~ limit
source.limit(i + 1);
target.put(source); // 从source 读,向 target 写
target.flip();
printBytebuffer(target);
source.limit(oldLimit);
}
}
source.compact(); // 对buffer中没有读完的数据进行处理,移动到开头,转换为写模式
}
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ByteBuffer buffer = ByteBuffer.allocate(16);
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 配置为非阻塞模式
SelectionKey sscKey = ssc.register(selector,0,null); // 0表示不关联任何事件
sscKey.interestOps(SelectionKey.OP_ACCEPT); // key只关联accept事件即这里的定义的sscKey只关注accept事件
log.debug("Register Server Socket Channel key with accept event: {}",sscKey);
ssc.bind(new InetSocketAddress(8080));
while(true){
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
/*要点1:获取key后从集合中移除这个key,避免重复处理造成NPE问题(集合的遍历的过程中如果要删除,则必须使用迭代器)*/
iter.remove();
// 区分事件类型
if(key.isAcceptable()){
log.debug("Accept Event happen! Server Socket Channel key: {}",key);
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
/*获取连接建立后的读写通道,用于读写的SocketChannel必须在建立连接后才能获得*/
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
sc.register(selector,0,null);
SelectionKey scKey = sc.register(selector,0,null);
log.debug("Register Socket Channel key with Read event: {}",scKey);
scKey.interestOps(SelectionKey.OP_READ); // 绑定socketChannel的可读事件
}else if(key.isReadable()){
log.debug("Read event happen ! Socket Channel key{}",key);
try{
SocketChannel channel = (SocketChannel) key.channel(); // 拿到触发事件的channel
ByteBuffer tmp_buffer = ByteBuffer.allocate(8);
/*客户端异常断开也会产生读事件,也会产生读事件
* 并返回-1.
* */
int read = channel.read(tmp_buffer);
if(read == -1){ // 检测到客户端正常断开
log.debug("Client normal close event happen ! Socket Channel key{}",key);
key.cancel();
}else{
// tmp_buffer.flip();
// printBytebuffer(tmp_buffer);
split(tmp_buffer);
}
}catch (IOException e){
e.printStackTrace();
log.debug("Client abnormal close event happen ! Socket Channel key{}",key);
/*客户端非正常断开连接,服务端抛出异常并被捕获,捕获后需要将关联的key从事件集合中删除*/
key.cancel();
}
}
}
}
}
static void printBytebuffer(ByteBuffer tmp){ // 注意:传入的bytebuffer必须时写模式
System.out.println(StandardCharsets.UTF_8.decode(tmp).toString());
}
}
客户端测试代码:发送13个字节并利用\n结尾
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
public class Client2 {
// 客户端代码:
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost",8080));
System.out.println("waiting...!");
sc.write(StandardCharsets.UTF_8.encode("1234567899999\n")); // 13个字节
System.out.println("Sending the first data!");
// sc.write(StandardCharsets.UTF_8.encode("万岁")); // 6个字节
// System.out.println("Sending the second data!");
sc.close(); // 客户端正常断开
}
}
执行结果:由于单个消息的大小超过buffer的最大容量,导致13字节的消息丢失了8个字节。
- 实际上进行了2次读取,第一次读取由于没有分割符并且bytebuffer定义为局部变量所以丢失了。
21:46:06.406 [main] DEBUG Server.Server5 - Register Server Socket Channel key with accept event: sun.nio.ch.SelectionKeyImpl@5eb5c224
21:46:09.626 [main] DEBUG Server.Server5 - Accept Event happen! Server Socket Channel key: sun.nio.ch.SelectionKeyImpl@5eb5c224
21:46:09.627 [main] DEBUG Server.Server5 - Register Socket Channel key with Read event: sun.nio.ch.SelectionKeyImpl@7dc5e7b4
21:46:09.628 [main] DEBUG Server.Server5 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
21:46:09.628 [main] DEBUG Server.Server5 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
99999
21:46:09.629 [main] DEBUG Server.Server5 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
21:46:09.629 [main] DEBUG Server.Server5 - Client normal close event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
3-5-4 利用扩容机制解决消息过大问题
3-5-3问题
1)bytebuffer是局部变量
解决策略:将buffer与key进行私有化绑定,利用attachment(附件)这个参数进行绑定。
2)单次消息大小大于buffer容量
解决策略:进行buffer的扩容
代码实现
import lombok.extern.slf4j.Slf4j;
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.nio.charset.StandardCharsets;
import java.util.Iterator;
@Slf4j
public class Server5 {
// 基本思想:按照应用层分割符读取读取数据,如果buffer中最终数据不是以\n结尾,
// 保留这个数据直到倒数第一个\n不进行读取,将其compact后,再一个数据包数据读入之后再处理。
private static void split(ByteBuffer source) {
source.flip(); // 转换为读模式
int oldLimit = source.limit(); //原始的buffer大小
for (int i = 0; i < oldLimit; i++) {
if (source.get(i) == '\n') {
//分配bytebuffer接受传输过来的数据记录
// 当前记录大小 = 当前位置+1-position
ByteBuffer target = ByteBuffer.allocate(i + 1 - source.position());
// 0 ~ limit
source.limit(i + 1);
target.put(source); // 从source 读,向 target 写
target.flip();
printBytebuffer(target);
source.limit(oldLimit);
}
}
source.compact(); // 对buffer中没有读完的数据进行处理,移动到开头,转换为写模式
}
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); // 配置为非阻塞模式
ByteBuffer buffer = ByteBuffer.allocate(16);
SelectionKey sscKey = ssc.register(selector,0,buffer); // 0表示不关联任何事件
sscKey.interestOps(SelectionKey.OP_ACCEPT); // key只关联accept事件即这里的定义的sscKey只关注accept事件
log.debug("Register Server Socket Channel key with accept event: {}",sscKey);
ssc.bind(new InetSocketAddress(8080));
while(true){
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
iter.remove();
if(key.isAcceptable()){
log.debug("Accept Event happen! Server Socket Channel key: {}",key);
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
sc.configureBlocking(false);
sc.register(selector,0,null);
ByteBuffer buffer1 = ByteBuffer.allocate(8);
SelectionKey scKey = sc.register(selector,0,buffer1); // 利用附件绑定事件
log.debug("Register Socket Channel key with Read event: {}",scKey);
scKey.interestOps(SelectionKey.OP_READ); // 绑定socketChannel的可读事件
}else if(key.isReadable()){
log.debug("Read event happen ! Socket Channel key{}",key);
try{
SocketChannel channel = (SocketChannel) key.channel(); // 拿到触发事件的channel
// 利用附件(attachment)即将buffer与key进行绑定,方便对buffer进行动态调整
ByteBuffer curBuffer = (ByteBuffer)key.attachment();
int read = channel.read(curBuffer);
if(read == -1){ // 检测到客户端正常断开
log.debug("Client normal close event happen ! Socket Channel key{}",key);
key.cancel();
}else{
split(curBuffer);
// 扩容机制的实现:通过判断buffer有没有剩余容量判断是否需要扩容!
if(curBuffer.position() == curBuffer.limit()){
ByteBuffer newBuffer = ByteBuffer.allocate(curBuffer.capacity()*2);
curBuffer.flip();
newBuffer.put(curBuffer);
key.attach(newBuffer);
log.debug("Message size more than buffer size,new size",newBuffer.capacity());
}
}
}catch (IOException e){
e.printStackTrace();
log.debug("Client abnormal close event happen ! Socket Channel key{}",key);
key.cancel();
}
}
}
}
}
static void printBytebuffer(ByteBuffer tmp){ // 注意:传入的bytebuffer必须时写模式
System.out.println(StandardCharsets.UTF_8.decode(tmp).toString());
}
}
客户端代码:发送24个字节,服务端的buffer只有8
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
public class Client2 {
// 客户端代码:
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost",8080));
System.out.println("waiting...!");
sc.write(StandardCharsets.UTF_8.encode("123456781234567812345678\n"));
System.out.println("Sending the first data!");
sc.close(); // 客户端正常断开
}
}
执行结果
22:27:30.471 [main] DEBUG Server.Server5 - Register Server Socket Channel key with accept event: sun.nio.ch.SelectionKeyImpl@5eb5c224
22:27:36.749 [main] DEBUG Server.Server5 - Accept Event happen! Server Socket Channel key: sun.nio.ch.SelectionKeyImpl@5eb5c224
22:27:36.749 [main] DEBUG Server.Server5 - Register Socket Channel key with Read event: sun.nio.ch.SelectionKeyImpl@7dc5e7b4
22:27:36.751 [main] DEBUG Server.Server5 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
22:27:36.751 [main] DEBUG Server.Server5 - Message size more than buffer size,new size 16
22:27:36.751 [main] DEBUG Server.Server5 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
22:27:36.751 [main] DEBUG Server.Server5 - Message size more than buffer size,new size 32
22:27:36.751 [main] DEBUG Server.Server5 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
123456781234567812345678
22:27:36.752 [main] DEBUG Server.Server5 - Read event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
22:27:36.752 [main] DEBUG Server.Server5 - Client normal close event happen ! Socket Channel keysun.nio.ch.SelectionKeyImpl@7dc5e7b4
总结:可以看到由于单条信息的大小为24个字节,服务端进行了2次扩容后,才将信息完整得获取。
扩容的简单实现方法:
注意:netty不仅实现扩容,还实现了容量的缩小
1)检测当前buffer中是否有余量,没有余量说明需要扩容。
2)分配大小是之前2倍的新的buffer,将之前的buffer的数据拷贝到新的buffer中。
3)将新的buffer与key绑定。
3-6 buffer设置需要考虑的问题
私有性问题:每个 channel 都需要记录可能被切分的消息,所以ByteBuffer 不能被多个 channel 共同使用,需要为每个 channel 维护一个独立的 ByteBuffer。
大小的设置问题:
- ByteBuffer 不能太大,比如一个 ByteBuffer 1Mb 的话,要支持百万连接就要 1Tb 内存,因此需要设计大小可变的 ByteBuffer
- 思路1:首先分配一个较小的 buffer,例如 4k,如果发现数据不够,再分配 8k 的 buffer,将 4k buffer 内容拷贝至 8k buffer,
- 优点是消息连续容易处理,
- 缺点是数据拷贝耗费性能,参考实现 http://tutorials.jenkov.com/java-performance/resizable-array.html
- 思路2:是用多个数组组成 buffer,一个数组不够,把多出来的内容写入新的数组
- 缺点:消息存储不连续解析复杂
- 优点:避免拷贝引起的性能损耗
- 思路1:首先分配一个较小的 buffer,例如 4k,如果发现数据不够,再分配 8k 的 buffer,将 4k buffer 内容拷贝至 8k buffer,
3-7 selector服务端发送数据(可写事件处理,特性3)
服务端代码
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.nio.charset.Charset;
import java.util.Iterator;
public class Server6 {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
// 在selector中注册key
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
while(true){
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
iter.remove();
if(key.isAcceptable()){
/*server socket Channel 只有一种类型的key,可以直接调用accept方法,可以不同key去获取关联的channel*/
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
/*向客户端发送数据*/
StringBuilder sb = new StringBuilder();
for(int i = 0;i < 30000000;++i){
sb.append("a");
}
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
while(buffer.hasRemaining()){ // 注意:这种数据的写入在大规模数据量下会造成忙等。
/*返回值代表实际写入的数据量*/
int write = sc.write(buffer);
System.out.println(write);
}
}
}
}
}
}
客户端代码
package Server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
public class Client6 {
// 客户端代码:
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("localhost",8080));
int count = 0;
while(true){
ByteBuffer buffer = ByteBuffer.allocate(1024*1024);
count += sc.read(buffer);
System.out.println(count);
buffer.clear();
}
}
}
服务端执行结果
1310710
5898195
0
0
3014633
12451745
0
131071
6815692
0
0
0
0
0
0
0
0
0
377954
客户端执行结果
131071
......
29490975
29622046
29753117
29884188
30000000
存在的问题:当服务端写入的数据特别大的时候,即便我们想要进行数据一次性发送,实际写入的过程中也是多次写入,此时这种写入的模式与非阻塞的思想相违背。
3-7-1 利用可写事件确保大规模数据写入的非阻塞
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.nio.charset.Charset;
import java.util.Iterator;
public class Server6 {
public static void main(String[] args) throws IOException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
// 在selector中注册key
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
while(true){
selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
iter.remove();
if(key.isAcceptable()) {
/*server socket Channel 只有一种类型的key,可以直接调用accept方法,可以不同key去获取关联的channel*/
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
SelectionKey scKey = sc.register(selector, 0, null);
scKey.interestOps(SelectionKey.OP_READ);
/* 2.向客户端发送数据,write 表示实际写了多少字节*/
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 8000000; ++i)
sb.append("a");
ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());
int write = sc.write(buffer);
System.out.println("第一次写入字节:" + write);
// 3.判断buffer是否写完,没有写完则关联写事件
if (buffer.hasRemaining()) {
scKey.interestOps(SelectionKey.OP_WRITE | scKey.interestOps());
scKey.attach(buffer);
}
}else if(key.isWritable()){
ByteBuffer buffer = (ByteBuffer) key.attachment();
SocketChannel sc = (SocketChannel) key.channel();
int write = sc.write(buffer);
System.out.println("通过关联的可写事件写入的字节:" + write);
// 4.判断buffer是否写完,写完则移除写入buffer并取消写事件。
if(!buffer.hasRemaining()){
key.attach(null);
key.interestOps(key.interestOps()-SelectionKey.OP_WRITE);
}
}
}
}
}
}
服务端执行结果
第一次写入字节:1179639
通过关联的可写事件写入的字节:5898195
通过关联的可写事件写入的字节:922166
总结:数据的写入通过关联可写事件从而确保大规模数据写入时候,不会陷入忙等状态。
四、 Java网络编程小结
4-1 网络IO发展的路径
阻塞IO -----> 非阻塞IO -------> 多路复用(通过selector配合读个channel)
4-2 Selector总结
好处
- 一个线程配合 selector 就可以监控多个 channel 的事件,事件发生线程才去处理。避免非阻塞模式下所做无用功
- 让这个线程能够被充分利用
- 节约了线程的数量
- 减少了线程上下文切换
创建
Selector selector = Selector.open();
绑定 Channel 事件
也称之为注册事件,绑定的事件 selector 才会关心
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, 绑定事件);
- channel 必须工作在非阻塞模式
- FileChannel 没有非阻塞模式,因此不能配合 selector 一起使用
- 绑定的事件类型可以有
- connect - 客户端连接成功时触发
- accept - 服务器端成功接受连接时触发
- read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
- write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况
监听 Channel 事件
可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件
方法1,阻塞直到绑定事件发生
int count = selector.select();
方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)
int count = selector.select(long timeout);
方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件
int count = selector.selectNow();
💡 select 何时不阻塞
- 事件发生时
- 客户端发起连接请求,会触发 accept 事件
- 客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于 buffer 缓冲区,会触发多次读取事件
- channel 可写,会触发 write 事件
- 在 linux 下 nio bug 发生时(NIO在linux平台有bug,Netty框架采用了一定的方式进行了处理)
- 调用 selector.wakeup()
- 调用 selector.close()
- selector 所在线程 interrupt
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?