NIO网络编程
三大核心组件:buffer缓冲区,channel通道,selector选择器
Buffer缓冲区
缓冲区本质上是一个可以写入数据的内存块(类似数组),然后可以再次读取。此内存块包含在NIO Buffer对象中,该对象提供了一组方法,可以更轻松地使用内存块。
相比较直接对数组的操作,Buffer API更加容易操作和管理。
使用Buffer进行数据写入与读取,需要进行如下四个步骤:
1、将数据写入缓冲区
2、调用buffer.flip(),转换成读取模式
3、缓冲区读取数据
4、调用buffer.clear()或buffer.compact()清除缓冲区
Buffer工作原理
三个重要属性:
1、capacity容量:作为一个内存块,Buffer具有一定的固定大小,也成为“容量”。
2、position位置:写入模式时代表写数据的位置。读取模式时代表读取数据的位置。
3、limit限制:写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量。
//需要使用ByteBuffer创建
//ByteBuffer源码:堆内存申请缓冲区,是byte[]数组形式,本质上就是对数组的封装。
ByteBuffer byteBuffer = ByteBuffer.allocate(4);//创建了容量为4的ByteBuffer对象
byteBuffer.put((byte) 1);//写入数据
byteBuffer.flip();//读模式的转换,不转换读模式,会导致postion出问题,读取会出问题
byte a = byteBuffer.get();//读取数据
byteBuffer.compact();//仅清除已阅读的数据,转为写入模式
//byteBuffer.clear();清除整个缓冲区
ByteBuffer内存类型
ByteBuffer为性能关键型代码提供了直接内存(direct堆外)和非直接内存(heap堆)两种实现。使用上是没有区别,但是需要显示申请。堆外内存获取的方式:
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(noBytes);
好处:
1、进行网络IO或者文件IO时,比heapBuffer少一次拷贝。(file/socket ---- OS memory ---- jvm heap)
GC会移动对象内存,在写file或socket的过程中,JVM的实现中,会先把数据复制到堆外,再进行写入。
2、GC范围之外,降低GC压力,但实现了自动管理。DirectByteBuffer中有一个Cleaner对象(PhantomReference),Cleaner被GC前会执行clean方法,触发DirectByteBuffer中定义的Deallocator。
建议:
1、性能确实可观的时候才取使用;分配给大型、长寿命;(网络传输、文件读写场景)。
2、通过虚拟机参数MaxDirectMemorySize限制大小,防止耗尽整个机器的内存。
Channel通道
channel可以创建连接,和传输数据(BIO需要Socket+Stream)
SocketChannel
SocketChannel用于建立TCP网络连接,类似java.net.Socket。有两种创建socketChannel形式:
1、客户端主动发起和服务器的连接。
2、服务端获取的新链接。
//客户端主动发起连接的方式
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);//设置为非阻塞模式
socketChannel.connect(new InetSocketAddress("http://163.com", 80));
channel.write(byteBuffer);//发送请求数据--向通道写入数据
int bytesRead = socketCheannel.read(byteBuffer);//读取服务端返回-==读取缓冲区的数据
socketChannel.close();//关闭连接
write写:非阻塞模式,可能没有写就返回。write()在尚未写入任何内容时,就可能返回了。需要在循环中调用write()。
read读:非阻塞模式,可能没有读到数据。read()方法可能直接返回而根本不读取任何数据,根据返回的int值判断读取了多少字节。(-1:通道关闭,网络连接关闭。0:没有读到数据。>0:有数据。)
ServerSocketChannel
ServerSocketChannel可以监听新建的TCP连接通道,类似ServerSocket。
//创建网络服务端
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);//设置为非阻塞模式,需要手动设置,默认是阻塞模式
serverSocketChannel.socket().bind(new InetSocketAddress(8080));//绑定端口
//serverSocketChannel只是为了绑定端口
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();//获取新tcp连接通道
if(socketChannel != null){
//tcp请求 读取/响应
}
}
serverSocketChannel.accept():如果该通道处于非阻塞模式,那么如果没有挂起的连接,该方法将立即返回null。必须检查返回的socketChannel是否为null。
Selector选择器
Selector是一个Java NIO组件,可以检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。实现单个线程可以管理多个通道,从而管理多个网络连接。
一个线程使用Selector监听多个channel的不同事件:
四个事件分别对应SelectionKey四个常量:
1、Connet连接(SelectionKey.OP_CONNECT)
2、Accept准备就绪(OP_ACCRPT)
3、Read读取(OP_READ)
4、Write写入(OP_WRITE)
实现一个线程处理多个通道的核心概念:事件驱动机制
非阻塞的网络通道下,开发者通过Selector注册对于通道感兴趣的事件类型,线程通过监听事件来触发相应的代码执行。(拓展:更底层是操作系统的多路复用机制)
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectorKey key = channel.register(selector, SelectionKey.OP_READ);//注册感兴趣的事件
while(true){//由accept轮询,变成了事件通知的方式
int readyChannels = selector.select();//select收到新的事件,方法才会返回
if(readyChannels == 0) continue;
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
//判断不同的事件类型,执行对应的逻辑处理
//key.isAcceptable()
//key.isConnectable()
//key.isReadable()
//key.isWritable()
keyIterator.remove();
}
}