Java网络编程——NIO三大组件BufferChannelSelector
Posted 胡玉洋
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java网络编程——NIO三大组件BufferChannelSelector相关的知识,希望对你有一定的参考价值。
Java NIO(Java New IO)也称为Java Non-Blocking IO,也就是非阻塞IO,说是非阻塞IO,其实NIO也支持阻塞IO模型(默认就是),相对于BIO来说,NIO最大的特点是支持IO多路复用模式,可以通过一个线程监控多个IO流(Socket)的状态,来同时管理多个客户端,极大提高了服务器的吞吐能力。
在NIO中有3个比较重要的组件:Buffer、Channel、Selector
Buffer
Buffer顾名思义,缓冲区,类似于List、Set、Map,实际上它就是一个容器对象,对数组进行了封装,用数组来缓存数据,还定义了一些操作数组的API,如 put()、get()、flip()、compact()、mark() 等。在NIO中,无论读还是写,数据都必须经过Buffer缓冲区,如下图:
随便创建一个Buffer,再put两个字节:
ByteBuffer byteBuffer = ByteBuffer.allocate(12);
byteBuffer.put((byte)'a');
byteBuffer.put((byte)'b');
发现这两个字节是被存到Buffer中一个叫hb的数组中了:
Buffer是所有缓存类的父类,对应实现有ByteBuffer、CharBuffer、IntBuffer、LongBuffer等跟ava基本数据类型对应的几个实现类:
一般最长用的就是ByteBuffer,创建 ByteBuffer 有两种方式:HeapByteBuffer 和 DirectByteBuffer:
(1)HeapByteBuffer:占用JVM堆内内存,不用考虑垃圾回收,属于用户空间,相对于DirectByteBuffer来说拷贝数据效率较低,会受到Full GC影响(Full GC后,可能需要移动数据位置)。创建HeapByteBuffer的方式为:
ByteBuffer heapBuffer = ByteBuffer.allocate(1024);
(2)DirectByteBuffer:占用堆外内存,读写效率高(读数据可以减少一次数据的复制),初次分配效率较低(需要调用系统函数),不受JVM GC的影响,但使用时要注意垃圾回收,使用不当可能造成内存泄漏。创建DirectByteBuffer的方式为:
ByteBuffer directBuffer = ByteBuffer.allocateDirect(1024);
为了可以更灵活地读/写数据,Buffer中有几个比较重要的属性:
● 容量(capacity):即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
● 位置(position):当前读/写到哪个位置,下一次读/写就会从下一个位置开始,每次读写缓冲区数据时都会改变(累加),为下次读写作准备
● 上限(limit):表示缓冲区的临时读/写上限,不能对缓冲区超过上限的位置进行读写操作,上限是可以修改的(flip函数)。读的时候读的是从position到limit之间的数据,写的时候也是从position位置开始写到limit位置。
● 标记(mark):调用mark函数可以记录当前position的值(mark = position),以后再调用reset()可以让position重新恢复到之前标记的位置(position = mark)
用个例子来看下这几个属性在读/写数据过程中的变化
public class BufferTest
public static void main(String[] args) throws IOException
// 1、初始化Buffer,先初始化一个长度为12的ByteBuffer,也就是创建了类型为byte一个长度为12的数组:
ByteBuffer byteBuffer = ByteBuffer.allocate(12);
System.out.println("【初始化ByteBuffer】 capacity: " + byteBuffer.capacity() + " position: " + byteBuffer.position() + " limit: " + byteBuffer.limit());
// 2、写数据,写的过程中,每写入一个字节,position自增1,当写入8个字节数据后,position=8
for (int i = 0; i < 8; i++)
byteBuffer.put((byte) i);
System.out.println("【ByteBuffer写完数据】 capacity: " + byteBuffer.capacity() + " position: " + byteBuffer.position() + " limit: " + byteBuffer.limit());
// 3、将Buffer由写模式转化为读模式
byteBuffer.flip();
System.out.println("【ByteBuffer调flip】 capacity: " + byteBuffer.capacity() + " position: " + byteBuffer.position() + " limit: " + byteBuffer.limit());
// 4、读数据,如果依次读取了6个字节,那现在position就指向下标为6的位置,limit不变
for (int i = 0; i < 6; i++)
if (i == 3)
byteBuffer.mark();
byte b = byteBuffer.get();
System.out.println("【ByteBuffer读完数据】 capacity: " + byteBuffer.capacity() + " position: " + byteBuffer.position() + " limit: " + byteBuffer.limit());
// 5、重置position
byteBuffer.reset();
System.out.println("【ByteBuffer调reset】 capacity: " + byteBuffer.capacity() + " position: " + byteBuffer.position() + " limit: " + byteBuffer.limit());
① 初始化Buffer,先初始化一个长度为12的ByteBuffer,也就是创建了类型为byte一个长度为12的数组:
② 写数据,写的过程中,每写入一个字节,position自增1,当写入8个字节数据后,position=8,如下图:
③ 写数据转读数据,现在Buffer中一共有8个字节的数据。因为对Buffer的读/写,都是从position位置到limit位置进行读/写的。如果现在想读取Buffer中的数据,需要执行一下Buffer的flip()函数,把limit置为8(position的值),position重新置为0,这时候position到limit之间的数据才是有效的(我们想要读取的)数据。所以通常将Buffer由写模式转化为读模式时需要执行flip()函数:
④ 读数据,如果依次读取了6个字节,那现在position就指向下标为6的位置,limit不变:
⑤ 重置position,前面在position=3的时候,调用byteBuffer.mark();标记了一下当时position的值(mark=3),当读取完6个字节后,position=6。这时调用一下byteBuffer.reset()可以把position重置为当时mark的值(position=mark),也就是3:
ByteBuffer中常用的的方法还有很多:
byteBuffer.put((byte) 'a'); //在position位置存入字符a对应的字节
byteBuffer.put(1, (byte) 5); //在1位置存入数字5对应的字节
byteBuffer.get();// 从position的位置读取一个字节的数据,读完后会导致position加1
byteBuffer.get(i);// 从position=i的位置读取一个字节的数据,读完后不会导致position加1
byteBuffer.reset(); // 重置position的值为mark
byteBuffer.position(5); // 重置position的值为5
byteBuffer.flip(); // 写完数据后,切换到读模式,把limit置为position的值,position置为0,
byteBuffer.clear(); // 清空ByteBuffer,position=0,limit=capacity
byteBuffer.compact(); // 读了一部分数据后,切换到写模式,会把未读的数据向前压缩,只留下有效数据(一般认为position~limit之间的数据为有效数据),比如原来pos=2,limit=8,capacity=12,执行compact()后,pos=6,limit=12,capacity=12
这里不再一一详细介绍。
Channel
Channel是对原 I/O 包中的流的模拟,到任何目的地(或来自任何地方)的所有数据都必须通过一个 Channel 对象,通道是双向的(一个Channel既可以读数据,也可以写数据),BIO中的InputStream/OutputStream是单向的(InputStream/OutputStream只能读/写数据)。
Channel 有文件通道和网络通道,文件通道的实现主要是FIleChannel,网络通道的实现主要有ServerSocketChannel(主要用于服务器接收客户端请求,类似于BIO中的ServerSocket)、SocketChannel(主要用户服务器和客户端直接的数据读写,类似于BIO中的Socket)、DatagramChannel(用于基于UDP协议的数据读写)。
FileChannel可以对文件进行读写,下面是个简单的例子:
public class FileChannelTest
public static void main(String[] args) throws IOException
FileChannel fileChannel = FileChannel.open(Paths.get("/Users/danny/data/file/file.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ);
// 通过FileChannel从文件中读数据
ByteBuffer readBuffer=ByteBuffer.allocate(10);
while (fileChannel.read(readBuffer) != -1)
while (readBuffer.hasRemaining())
byte b=readBuffer.get();
// 通过FileChannel向文件中写数据
ByteBuffer writeBuffer=ByteBuffer.allocate(10);
writeBuffer.put("Data".getBytes());
writeBuffer.flip();
while (writeBuffer.hasRemaining())
fileChannel.write(writeBuffer);
ServerSocketChannel 主要用于服务器接收客户端请求,SocketChannel 主要用户服务器和客户端直接的数据读写,跟BIO中ServerSocket和Socket通信差不多:
服务端:
public class ServerSocketChannelTest
public static void main(String[] args) throws IOException
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(true); // 设置阻塞模式为阻塞,默认就是true
serverSocketChannel.bind(new InetSocketAddress(8080));
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
SocketChannel socketChannel = serverSocketChannel.accept(); // 如果没有接收到新的客户端连接,这里会阻塞
System.out.println("收到到客户端连接");
int length = socketChannel.read(byteBuffer); // 如果读不到数据,这里会阻塞,无法处理其他Channel的读操作和连接请求
System.out.println("读取到客户端数据:" + new String(byteBuffer.array(), 0, length));
客户端:
public class SocketChannelTest
public static void main(String[] args) throws IOException
Socket socket = new Socket();
socket.connect(new InetSocketAddress("127.0.0.1", 8080));
System.out.println("连接服务端完成");
socket.getOutputStream().write(Constant.MESSAGE_128B.getBytes());
System.out.println("向服务端发送数据完成");
socket.close();
Selector
选择器Selector相当于管家,管理所有的IO事件,通过Selector可以使一个线程管理多个Channel(也就是多个网络连接),当一个或多个注册到Selector上的Channel发生可读/可写事件时,Selector能够感知到并返回这些事件。
一个Channel可以注册到多个不同的Selector上,多个Channel也可以注册到同一个Selector上。当某个Channel注册到Selector上时,会包装一个SelectionKey(包含一对一的Selector和Channel)放到该Selector中,这些后面看源码的时候再仔细画图分析。
根据理解画了一张Selector在整个服务端和客户端交互中的作用的图,大致如下:
Selector可以作为一个观察者,可以把已知的Channel(无论是服务端用来监听客户端连接的ServerSocketChannel,还是服务端和客户端用来读写数据的SocketChannel)及其感兴趣的事件(READ、WRITE、CONNECT、ACCEPT)包装成一个SelectionKey,注册到Selector上,Selector就会监听这些Channel注册的事件(监听的时候如果没有事件就绪,Selector所在线程会被阻塞),一旦有事件就绪,就会返回这些事件的列表,继而服务端线程可以依次处理这些事件。
NIO使用了Selector,IO模型就是属于IO多路复用(同步非阻塞),可以同事检测多个IO事件,即使某一个IO事件尚未就绪,可以处理其他就绪的IO事件。同步体现在在Selector监听IO事件(Selector.select()方法)时,如果没有就绪事件,就会等待,不能做其他事;非阻塞体现在当某一个IO事件尚未就绪时,可以处理其他就绪的IO事件,比如在上图中,如果客户端2一直不发送数据,服务端也可以正常处理其他客户端的请求,而在BIO中(单线程环境),如果某个客户端连接到了服务端而迟迟不写数据,那么服务器端就会一直等待而无法及时接收其他客户端的请求。正是因为Selector,才可以让NIO在单线程的环境就能处理多个网络连接,为高并发编程打下基础。
转载请注明出处——胡玉洋 《Java网络编程——NIO三大组件Buffer、Channel、Selector》
以上是关于Java网络编程——NIO三大组件BufferChannelSelector的主要内容,如果未能解决你的问题,请参考以下文章
Java网络编程——NIO三大组件BufferChannelSelector