「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。
JAVA NIO
始于Java1.4 ,提供了新的Java IO 操作非阻塞API 。用意是替代Java IO 和 Java Networking 相关的API 。
NIO中有三个核心组件
Buffer 缓冲区
- 缓冲区本质上是一个可以写入数据的内存块(类似数组),然后可以再次读取。此内存块包含在NIO Buffer 对象中,该对象提供了一组方法,可以更轻松地使用内存块。
相比较直接对数组的操作,Buffer API更加容易操作和管理。
使用Buffer进行数据写入与读取,需要进行如下四个步骤
- 将数据写入缓冲区
- 调用buffer.flip() ,转换为读取模式
- 缓冲区读取数据调用buffer.clear() 或buffer.compact()
- 清除缓冲区
Buffer工作原理
Buffer 三个重要属性
-
capacity容量:作为一个内存块,Buffer具有一定的固定大小,也成为“容量”。
-
position位置:写入模式时代表写数据的位置。读取模式时代表读取数据的位置。
-
limit限制:写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量。
ByteBuffer内存类型
ByteBuffer为性能关键型代码提供了直接内存(direct堆外)和非直接内存(heap堆,底层是数组的实现方式)两种实现。堆外内存获取的方式:
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(noBytes);
好处:
- 进行网络IO或者文件IO时比heapBuffer少一次拷贝。(file/socket ---OS memory -----jvm heap )GC(垃圾回收机制)会移动对象内存,在写file或socket的过程中,JVM的实现中,会先把数据复制到堆外,再进行写入 。
- GC范围之外,降低GC压力,但实现了自动管理。DirectByteBuffer中有一个Cleaner对象(PhantomReference),Cleaner被GC前会执行clean方法,触发DirectByteBuffer中定义的Deallocator
建议:
-
性能确实可观的时候才去使用;分配给大型、长寿命;(网络传输、文件读写场景)
-
通过虚拟机参数MaxDirectMemorySize限制大小,防止耗尽整个机器的内存
public class BufferDemo { public static void main(String[] args) { // 构建一个byte字节缓冲区,容量是4 ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4); // 默认写入模式,查看三个重要的指标 System.out.println(String.format("初始化:capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); // 写入3字节的数据 byteBuffer.put((byte) 1) ; byteBuffer.put((byte) 2) ; byteBuffer.put((byte) 3) ; // 再看数据 System.out.println(String.format("写入3字节后,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); // 转换为读取模式(不调用flip方法,也是可以读取数据的,但是position记录读取的位置不对) System.out.println("#######开始读取"); byteBuffer.flip(); byte a = byteBuffer.get(); System.out.println("a= "+a); byte b = byteBuffer.get(); System.out.println("b= "+b); //读取两个之后查看一下容量。 System.out.println(String.format("读取两个之后查看一下容量。,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); // 继续写入3字节,此时读模式下,limit=3,position=2.继续写入只能覆盖写入一条数据 // clear()方法清除整个缓冲区。compact()方法仅清除已阅读的数据。转为写入模式 byteBuffer.compact(); byteBuffer.put((byte)3); byteBuffer.put((byte)4); byteBuffer.put((byte)5); System.out.println(String.format("清除之后再查看一下容量。,capacity容量:%s, position位置:%s, limit限制:%s", byteBuffer.capacity(), byteBuffer.position(), byteBuffer.limit())); // rewind() 重置position为0 // mark() 标记position的位置 // reset() 重置position为上次mark()标记的位置 } }
Channel通道
SocketChannel
SocketChannel用于建立TCP网络连接,类似java.net.Socket。有两种创建socketChannel形式:
-
客户端主动发起和服务器的连接
-
服务端获取的新连接
// 客户端主动发起连接的方式 SocketChannel socketChannel = SocketChannel.open(); //设置为非阻塞模式 socketChannel.configureBlocking(false); socketChannel.connect(new InetSocketAddress("127.0.0.1",80)); //发送请求数据-向通道写入数据 channel.write(byteBuffer); //读取服务端返回-读取缓冲区的数据 socketChannel.read(byteBuffer); //关闭连接 socketChannel.close();
write写:在非阻塞模式下,write() 在尚未写入任何内容时就可能返回了。需要在循环中调用write() 。
read读:在非阻塞模式下,read() 方法可能直接返回而根本不读取任何数据,根据返回的int值判断读取了多少字节。
ServerSocketChannel
ServerSocketChannel可以监听新建的TCP连接通道,类似ServerSocket 。
//核心代码
//创建网络服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(8080));
while (true) {
//获取新tcp连接通道
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
//tcp 请求 读取/响应
}
}
serverSocketChannel.accept(): 如果该通道处于非阻塞模式,那么如果没有挂起的连接,该方法将立即返回null。必须检查返回的SocketChannel是否为null 。
public class NIOClient {
public static void main(String[] args) throws IOException {
// 客户端主动发起连接的方式
SocketChannel socketChannel = SocketChannel.open();
//设置为非阻塞模式
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1",8080));
while (!socketChannel.finishConnect()) {
// 没连接上,则一直等待
Thread.yield();
}
System.out.println("请输入:");
Scanner scanner = new Scanner(System.in);
String msg = scanner.nextLine();
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
while (buffer.hasRemaining()){
//发送请求数据-向通道写入数据
socketChannel.write(buffer);
}
//收到服务端响应
ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
//读取服务端返回-读取缓冲区的数据
while (socketChannel.isOpen()&&socketChannel.read(requestBuffer)!=-1){
// 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
if(requestBuffer.position()>0) break ;
}
requestBuffer.flip();
byte[] content = new byte[requestBuffer.limit()];
requestBuffer.get(content);
System.out.println("服务返回的连接内容 : " + new String(content));
//关闭连接
scanner.close();
socketChannel.close();
}
}
public class NIOServer {
//直接基于非阻塞的写法
public static void main(String[] args) throws Exception {
//创建网络服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//设置为非阻塞模式
serverSocketChannel.configureBlocking(false);
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(8080));
System.out.println("启动成功");
while (true) {
//获取新tcp连接通道
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false); // 默认是阻塞的,一定要设置为非阻塞
if (socketChannel != null) {
//tcp 请求 读取/响应
System.out.println("收到新连接 : "+socketChannel.getRemoteAddress());
try {
ByteBuffer requestByteBuffer = ByteBuffer.allocate(1024);
while (socketChannel.isOpen()&& socketChannel.read(requestByteBuffer)!=-1 ){
if (requestByteBuffer.position() > 0) break; //有接收数据,让程序继续运行
}
if (requestByteBuffer.position() == 0) continue;
//开启读模式
requestByteBuffer.flip();
byte[] content = new byte[requestByteBuffer.limit()];
requestByteBuffer.get(content);
System.out.println("收到数据 : "+new String(content));
//响应200
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Length: 11\r\n\r\n" +
"Hello World";
ByteBuffer buffer = ByteBuffer.wrap(response.getBytes()) ;
while (buffer.hasRemaining()){
socketChannel.write(buffer);
}
} catch (IOException e){
e.printStackTrace();
}
}
}
}
}
为什么会只收到一个连接显示呢,明明用的是非阻塞NIO的写法 ?
//因为这段代码的写法导致,程序被阻塞了,所以看起来像BIO 。这两个客户端只发送了连接,并没有发送内容。
while (socketChannel.isOpen()&& socketChannel.read(requestByteBuffer)!=-1 ){
if (requestByteBuffer.position() > 0) break; //有接收数据,让程序继续运行
}
那么如何解决上面阻塞的问题呢,是要像BIO一样用多线程的方式吗?不建议,因为NIO本身是非阻塞的API设计,在设计上和BIO有很大的不同,应该想办法改进这段问题代码 。
public class NIOServer1 {
/**
* 已经建立连接的集合
*/
private static ArrayList<SocketChannel> channels = new ArrayList<>();
//直接基于非阻塞的写法
public static void main(String[] args) throws Exception {
//创建网络服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
//绑定端口
serverSocketChannel.bind(new InetSocketAddress(8080));
System.out.println("启动成功");
while (true) {
//获取新tcp连接通道
SocketChannel socketChannel = serverSocketChannel.accept();
if (socketChannel != null) {
//tcp 请求 读取/响应
System.out.println("收到新连接 : "+socketChannel.getRemoteAddress());
//设置为非阻塞模式
socketChannel.configureBlocking(false);
channels.add(socketChannel);
}else {
// 没有新连接的情况下,就去处理现有连接的数据,处理完的就删除掉
Iterator<SocketChannel> iterator = channels.iterator();
while (iterator.hasNext()) {
try{
SocketChannel ch = iterator.next();
ByteBuffer requestByteBuffer = ByteBuffer.allocate(1024);
while (ch.isOpen()&& ch.read(requestByteBuffer)!=-1 ){
// 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
if (requestByteBuffer.position() > 0) break;
}
if (requestByteBuffer.position() == 0) continue;
//开启读模式
requestByteBuffer.flip();
byte[] content = new byte[requestByteBuffer.limit()];
requestByteBuffer.get(content);
System.out.println("收到数据 : "+new String(content));
//响应200
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Length: 11\r\n\r\n" +
"Hello World";
ByteBuffer buffer = ByteBuffer.wrap(response.getBytes()) ;
while (buffer.hasRemaining()){
ch.write(buffer);
}
}catch (IOException e) {
e.printStackTrace();
iterator.remove();
}
}
}
}
}
}
_这种低效,浪费CPU的循环检查,会是NIO服务端的正确开发方式吗 ?_在NIO中提供了selector选择器方式避免循环检查。
Selector 选择器
Selector是一个Java NIO 组件 ,可以检查一个或多个NIO通道,并确定哪些通道已准备好进行读取或写入。实现单个线程可以管理多个通道,从而管理多个网络连接。
一个线程使用Selector监听多个channel的不同事件:
四个事件分别对应SelectionKey四个常量。
- Connect连接(SelectionKey.OP_CONNECT)
- Accept准备就绪(OP_ACCEPT)
- Read读取(OP_READ)
- Write写入(OP_WRITE)
Selector 选择器
实现一个线程处理多个通道的核心概念理解:事件驱动机制。
非阻塞的网络通道下,开发者通过Selector 注册对于通道感兴趣的事件类型,线程通过监听事件来触发相应的代码执行。(拓展:更底层是操作系统的多路复用机制)
//核心代码
Selector selector = Selector.open();
//创建网络服务端
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
//注册感兴趣的事件
channel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
//由accept轮询,变成了事件通知的方式
int readyChannels = selector.select();//select 收到新的事件,方法才会返回
if( readyChannels == 0) continue;
Set<SelectionKey> selectionkey = selector.selectedKeys();
Iterator<SelectionKey> keyIterator= selectionkey.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
//判断不同的事件类型,执行对应的逻辑处理
//key.isAcceptable();/key.isConnectable();/key.isReadable();/key.isWritable();
keyIterator.remove();
if( key.isAcceptable()){
ServerSocketChannel server= (ServerSocketChannel)key.attachment();
SocketChannel clientSocketChannel = server.accept();
clientSocketChannel.configureBlocking(false);
// 通道注册
clientSocketChannel.register(selector,SelectionKey.OP_READ,clientSocketChannel);
System.out.println("收到新连接 : " + clientSocketChannel.getRemoteAddress());
}
if( key.isReadable()){
SocketChannel channel = (SocketChannel)key.attachment();
}
}
}
// 客户端和上边一样没有改动的地方
/**
* 结合Selector实现的非阻塞服务端(放弃对channel的轮询,借助消息通知机制)
*/
public class NIOServerV2 {
public static void main(String[] args) throws Exception {
// 1. 创建网络服务端ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false); // 设置为非阻塞模式
// 2. 构建一个Selector选择器,并且将channel注册上去
Selector selector = Selector.open();
SelectionKey selectionKey = serverSocketChannel.register(selector, 0, serverSocketChannel);// 将serverSocketChannel注册到selector
selectionKey.interestOps(SelectionKey.OP_ACCEPT); // 对serverSocketChannel上面的accept事件感兴趣(serverSocketChannel只能支持accept操作)
// 3. 绑定端口
serverSocketChannel.socket().bind(new InetSocketAddress(8080));
System.out.println("启动成功");
while (true) {
// 不再轮询通道,改用下面轮询事件的方式.select方法有阻塞效果,直到有事件通知才会有返回
selector.select();
// 获取事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();
// 遍历查询结果e
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
// 被封装的查询结果
SelectionKey key = iter.next();
iter.remove();
// 关注 Read 和 Accept两个事件
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.attachment();
// 将拿到的客户端连接通道,注册到selector上面
SocketChannel clientSocketChannel = server.accept(); // mainReactor 轮询accept
clientSocketChannel.configureBlocking(false);
clientSocketChannel.register(selector, SelectionKey.OP_READ, clientSocketChannel);
System.out.println("收到新连接 : " + clientSocketChannel.getRemoteAddress());
}
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel) key.attachment();
try {
ByteBuffer requestBuffer = ByteBuffer.allocate(1024);
while (socketChannel.isOpen() && socketChannel.read(requestBuffer) != -1) {
// 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)
if (requestBuffer.position() > 0) break;
}
if(requestBuffer.position() == 0) continue; // 如果没数据了, 则不继续后面的处理
requestBuffer.flip();
byte[] content = new byte[requestBuffer.limit()];
requestBuffer.get(content);
System.out.println(new String(content));
System.out.println("收到数据,来自:" + socketChannel.getRemoteAddress());
// TODO 业务操作 数据库 接口调用等等
// 响应结果 200
String response = "HTTP/1.1 200 OK\r\n" +
"Content-Length: 11\r\n\r\n" +
"Hello World";
ByteBuffer buffer = ByteBuffer.wrap(response.getBytes());
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
} catch (IOException e) {
// e.printStackTrace();
key.cancel(); // 取消事件订阅
}
}
}
selector.selectNow();
}
// 问题: 此处一个selector监听所有事件,一个线程处理所有请求事件. 会成为瓶颈! 要有多线程的运用
}
}
NIO对比BIO
如果你的程序需要支撑大量的连接,使用NIO是最好的方式。
Tomcat8中,已经完全去除BIO相关的网络处理代码,默认采用NIO进行网络处理。
NIO与多线程结合的改进方案
推荐阅读Doug Lea的著名文章《Scalable IO in Java》gee.cs.oswego.edu/dl/cpjslide…
code 基于reactor的思想对代码进行改进。待定。
小结
NIO为开发者提供了功能丰富及强大的IO处理API ,但是在应用于网络应用开发的过程中,直接使用JDk提供的API,比较繁琐。而且要想将性能进行提升,光有NIO还不够,还需要将多线程技术与之结合起来。
因为网络编程本身的复杂性,以及JDK API开发的使用难度较高,所以在开源社区中,涌出来很多对JDK NIO 进行封装、增强后的网络编程框架,例如:Netty 、Mina 等。