Java NIO介绍
1 NIO 3个核心的组件
- Channel(通道),包括服务端ServerSocketChannel 及SocketChannel
- Buffer(缓冲区)
- Selector(选择器)
2 实例代码
2.1 服务器端
package nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
public class BasicServerSocketChannelDemo {
public static void main(String[] args) throws IOException {
//创建selector,管理多个channel
Selector selector = Selector.open();
//创建ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//设置为非阻塞模式,只有非阻塞模式才能使用selector
serverSocketChannel.configureBlocking(false);
//注册,并设置关注channel 上的事件(客户端连接)
//第二个参数也可以通过selectionKey.interestOps(SelectionKey.OP_ACCEPT) 设置,
// register方法还有第三个参数-attachment,将来可以通过selectionKey.attachment()获得,担任也可以通过selectionKey.attach(att)设置
SelectionKey selectionKeyx = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//绑定网络端口
serverSocketChannel.bind(new InetSocketAddress(9900));
while (true){
//select 方法, 没有事件发生,线程阻塞,有事件,线程才会恢复运行
//select 在事件未处理时,它不会阻塞, 事件发生后要么处理,要么取消,不能置之不理
int select = selector.select();
//处理事件, selectedKeys 内部包含了所有发生的事件
Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
while (selectionKeyIterator.hasNext()){
SelectionKey selectionKey = selectionKeyIterator.next();
//要从 selectedKeys 集合中删除,否则会一直存在于selectedKeys集合中。
selectionKeyIterator.remove();
//按事件类型分别处理
//如果是客户端连接
if(selectionKey.isAcceptable()){
//也可以直接调用最开始创建的serverSocketChannel,或者通过以下获得serverSocketChannel
//ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
//接受客户端的连接
SocketChannel socketChannel = serverSocketChannel.accept();
//设置为非阻塞模式
socketChannel.configureBlocking(false);
//将socketChannel 注册到selector上,并关注此socketChannel上的可读事件
socketChannel.register(selector,SelectionKey.OP_READ);
System.out.println("客户端接入"+socketChannel.getRemoteAddress());
}else if(selectionKey.isReadable()){
//可读事件,收到数据
try {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer= ByteBuffer.allocate(1024);
int read = socketChannel.read(byteBuffer);
//客户端正常断开
if(read==-1){
//取消 SelectionKey对应的 SocketChannel 在selector 的注册
selectionKey.cancel();
System.out.println("连接断开");
}else {
byteBuffer.flip();
System.out.println("收到数据:"+ StandardCharsets.UTF_8.decode( byteBuffer).toString());
}
} catch (IOException e) {
//客户端异常断开,如杀掉进程 socketChannel.read方法会触发IOException
selectionKey.cancel();
System.out.println("连接异常断开");
}
}else if(selectionKey.isWritable()){
//可写事件,正常情况不能订阅可写事件,否则会一直触发,
//只有一种情况会需要,当调用 socketChannel.write(byteBuffer)后,byteBuffer.hasRemaining()==true,表示未写完数据时,需要稍后等触发可写事件后继续写入(缓存满了就回出现这种情况,重现的方法 是发送方不断地发送数据 直至byteBuffer.hasRemaining()==true,同时接收方再接受方法上打断点)
//在写完数据后 需要取消订阅可写事件
System.out.println("触发Writable 事件");
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer= ByteBuffer.wrap("abc".getBytes(StandardCharsets.UTF_8));
int write = socketChannel.write(byteBuffer);
//
if(!byteBuffer.hasRemaining()){
selectionKey.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
}
2.2客户端
package nio;
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.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
public class BasicSocketChannelDemo {
public static void main(String[] args) throws IOException {
//创建selector
Selector selector = Selector.open();
//SocketChannel
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞模式,只有非阻塞模式才能使用selector
socketChannel.configureBlocking(false);
//注册,并设置关注channel 上的事件(客户端连接)
socketChannel.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 9900));
while (true){
//select 方法, 没有事件发生,线程阻塞,有事件,线程才会恢复运行
//select 在事件未处理时,它不会阻塞, 事件发生后要么处理,要么取消,不能置之不理
int select = selector.select();
//处理事件, selectedKeys 内部包含了所有发生的事件
Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
while (selectionKeyIterator.hasNext()){
SelectionKey selectionKey = selectionKeyIterator.next();
//要从 selectedKeys 集合中删除,否则会一直存在于selectedKeys集合中。
selectionKeyIterator.remove();
//按事件类型分别处理
//如果是与服务端建立连接时间
if(selectionKey.isConnectable()){
socketChannel.finishConnect();
System.out.println("与服务端连接成功");
}else if(selectionKey.isReadable()){
try {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = socketChannel.read(byteBuffer);
//正常断开
if(read==-1){
//取消 SelectionKey对应的 SocketChannel 在selector 的注册
selectionKey.cancel();
System.out.println("连接断开");
}else {
byteBuffer.flip();
System.out.println("收到数据:"+ StandardCharsets.UTF_8.decode( byteBuffer).toString());
}
} catch (IOException e) {
//异常断开,如杀掉进程 socketChannel.read方法会触发IOException
selectionKey.cancel();
System.out.println("连接异常断开");
}
}else if(selectionKey.isWritable()){
//可写事件,正常情况不能订阅可写事件,否则会一直触发,
//只有一种情况会需要,当调用 socketChannel.write(byteBuffer)后,byteBuffer.hasRemaining()==true,表示未写完数据时,需要稍后等触发可写事件后继续写入(缓存满了就回出现这种情况,重现的方法 是发送方不断地发送数据 直至byteBuffer.hasRemaining()==true,同时接收方再接受方法上打断点)
//在写完数据后 需要取消订阅可写事件
ByteBuffer byteBuffer= ByteBuffer.wrap("abc".getBytes(StandardCharsets.UTF_8));
int write = socketChannel.write(byteBuffer);
//
if(!byteBuffer.hasRemaining()){
selectionKey.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
}
=
2.3 概念解析
2.3.1 SelectionKey 事件类型
- connect - 客户端连接服务端成功时触发
- accept - 服务器端成功接受连接时触发
- read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
- write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况
可写事件,正常情况不能订阅可写事件,否则会一直触发,只有一种情况会需要,当调用 socketChannel.write(byteBuffer)后,byteBuffer.hasRemaining()==true,表示未写完数据时,需要稍后等触发可写事件后继续写入(缓存满了就回出现这种情况)
调试重现的方法:发送方不断地发送数据 直至byteBuffer.hasRemaining()==true,同时接收方再接受方法上打断点)
在写完数据后 需要取消订阅可写事件
2.3.2 监听 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 发生时(jdk linux 版本的bug)
- 调用 selector.wakeup()
- 调用 selector.close()
- selector 所在线程 interrupt
2.3.3 处理事件
- 为何要 selectionKeyIterator.remove()
因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如
第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey
第二次触发了 sckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常 - selectionKey.cancel();
cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件
2.3.4 消息分割的方案
因为TCP的数据没有边界的,故需要接收方和发送方约定消息分割的协议。常见的方案:
- 固定消息长度,数据包大小一样,服务器按预定长度读取,缺点是浪费带宽
- 另按分隔符拆分,缺点是效率低
- TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量
Http 1.1 是 TLV 格式
Http 2.0 是 LTV 格式