一、非阻塞 vs 阻塞
1、 阻塞
阻塞模式下,ServerSocketChannel.accept 会在没有连接建立时让线程暂停,SocketChannel.read 会在没有数据可读时让线程暂停。阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置。
(1)服务端
public class Server {
public static void main(String[] args) throws Exception{
ByteBuffer bf=ByteBuffer.allocate(16);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8888));
List<SocketChannel> socketChannels=new ArrayList<>();
while (true){
System.out.println("wait connect ...");
serverSocketChannel.accept();
System.out.println("connected ..."+socketChannel);
socketChannel.configureBlocking(false);
socketChannels.add(socketChannel);
for (SocketChannel channel : socketChannels) {
int read = channel.read(bf);
if(read>0){
bf.flip();
ByteBufferUtil.debugRead(bf);
}
}
}
}
}
2、非阻塞
非阻塞模式下,在 ServerSocketChannel.accept 在没有连接建立时,会返回 null,继续运行。
SocketChannel.read 在没有数据可读时,会返回 0,但线程不阻塞,可以去执行其它 SocketChannel 的 read 或是去执行 ServerSocketChannel.accept。
写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去
但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然在不断运行,白白浪费 cpu
(1)服务端
public class Server {
public static void main(String[] args) throws Exception{
ByteBuffer bf=ByteBuffer.allocate(16);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8888));
//非阻塞模式
serverSocketChannel.configureBlocking(false);
List<SocketChannel> socketChannels=new ArrayList<>();
while (true){
//非阻塞,线程还会继续运行,如果没有连接建立,返回是null
SocketChannel socketChannel = serverSocketChannel.accept();
if(socketChannel!=null){
System.out.println("connected ..."+socketChannel);
//非阻塞模式
socketChannel.configureBlocking(false);
socketChannels.add(socketChannel);
}
for (SocketChannel channel : socketChannels) {
//非阻塞,线程仍然会继续运行,如果没有读到数据,read 返回 0
int read = channel.read(bf);
if(read>0){
bf.flip();
ByteBufferUtil.debugRead(bf);
}
}
}
}
}
二、多路复用Selector
单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用
多路复用仅针对网络 IO、普通文件 IO 没法利用多路复用
Selector 能够保证,有可连接事件时才去连接,有可读事件才去读取,有可写事件才去写入
(1)服务端
public class Server {
public static void main(String[] args) throws Exception{
ByteBuffer bf=ByteBuffer.allocate(16);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8888));
Selector selector = Selector.open();
//非阻塞模式
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
selector.select();
//获取所有事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历所有事件,逐一处理
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey next = iterator.next();
//判断事件类型
if(next.isAcceptable()){
ServerSocketChannel channel = (ServerSocketChannel)next.channel();
SocketChannel socketChannel = channel.accept();
System.out.println("socketChannel is "+socketChannel);
}
//处理完毕,必须将事件移除
iterator.remove();
}
}
}
}
注意:
事件发生后必须要处理,要么处理,要么取消(cancel)。如果什么都不做,下次该事件仍会触发,这是因为 nio 底层使用的是水平触发。
cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件
1、处理read事件
public class Server {
public static void main(String[] args) throws Exception{
ByteBuffer bf=ByteBuffer.allocate(16);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8888));
Selector selector = Selector.open();
//非阻塞模式
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
selector.select();
//获取所有事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历所有事件,逐一处理
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()){
SelectionKey next = iterator.next();
//判断事件类型
if(next.isAcceptable()){
ServerSocketChannel channel = (ServerSocketChannel)next.channel();
SocketChannel socketChannel = channel.accept();
//设置socketChannel为非阻塞
socketChannel.configureBlocking(false);
//设置socketChannel为可读
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("socketChannel is "+socketChannel);
}else if(next.isReadable()){
SocketChannel socketChannel=(SocketChannel)next.channel();
ByteBuffer byteBuffer=ByteBuffer.allocate(128);
int read = socketChannel.read(byteBuffer);
if(read==-1){
next.cancel();
}else {
byteBuffer.flip();
ByteBufferUtil.debugRead(byteBuffer);
}
}
//处理完毕,必须将事件移除
iterator.remove();
}
}
}
}
注意
iter.remove()是必须的,因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要使用者编码删除。
例如
第一次触发了 ServerSocketChannel 上key的 accept 事件,没有移除 ServerSocketChannel-key
第二次触发了 SocketChannel-key 上的 read 事件,但这时 selectedKeys 中还有上次的 ServerSocketChannel-key ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常。
(1)处理消息的边界
处理消息边界的三种思路
1)固定消息长度,数据包大小一样,服务器按预定长度读取,缺点是浪费带宽
2)按分隔符拆分,缺点是效率低
3)TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量。
Http 1.1 是 TLV 格式,Http 2.0 是 LTV 格式
public class Server {
public static void main(String[] args) throws Exception {
ByteBuffer bf = ByteBuffer.allocate(16);
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8888));
//创建 selector, 管理多个 channel
Selector selector = Selector.open();
//非阻塞模式
serverSocketChannel.configureBlocking(false);
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
List<SocketChannel> socketChannels = new ArrayList<>();
while (true) {
selector.select();
//获取所有事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
//遍历所有事件,逐一处理
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey next = iterator.next();
//判断事件类型
if (next.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) next.channel();
SocketChannel socketChannel = channel.accept();
//设置socketChannel为非阻塞
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16); // attachment
//将一个 byteBuffer 作为附件关联到 selectionKey 上
//设置socketChannel为可读
socketChannel.register(selector, SelectionKey.OP_READ, buffer);
System.out.println("socketChannel is " + socketChannel);
} else if (next.isReadable()) {
SocketChannel socketChannel = (SocketChannel) next.channel();
//使用附件接受数据
ByteBuffer byteBuffer = (ByteBuffer)next.attachment();
int read = socketChannel.read(byteBuffer);
if (read == -1) {
next.cancel();
} else {
split(byteBuffer);
//需要扩容
if (byteBuffer.position() == byteBuffer.limit()) {
ByteBuffer newBuffer = ByteBuffer.allocate(byteBuffer.capacity() * 2);
byteBuffer.flip();
newBuffer.put(byteBuffer);
next.attach(newBuffer);
}
}
}
//处理完毕,必须将事件移除
iterator.remove();
}
}
}
private static void split(ByteBuffer source) {
source.flip();
for (int i = 0; i < source.limit(); i++) {
// 找到一条完整消息
if (source.get(i) == '\n') {
int length = i + 1 - source.position();
// 把这条完整消息存入新的 ByteBuffer
ByteBuffer target = ByteBuffer.allocate(length);
// 从 source 读,向 target 写
for (int j = 0; j < length; j++) {
target.put(source.get());
}
debugAll(target);
}
}
source.compact(); // 0123456789abcdef position 16 limit 16
}
}
(2)ByteBuffer 大小分配
每个 channel 都需要记录可能被切分的消息,因为 ByteBuffer 不能被多个 channel 共同使用,因此需要为每个 channel 维护一个独立的 ByteBuffer
ByteBuffer 不能太大,比如一个 ByteBuffer 1Mb 的话,要支持百万连接就要 1Tb 内存,因此需要设计大小可变的 ByteBuffer