NIO与BIO的区别
面向流与面向缓冲
上一篇文章中简单实现了BIO,用到了类似ObjectOutputStream和ObjectInputStream的类,这是因为Java BIO是面向流的。 这流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。
而Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
阻塞与非阻塞IO
Java BIO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
NIO三大核心组件
NIO有三大核心组件:Selector选择器、Channel管道、buffer缓冲区。
Selector
经常听到的多路复用在Java中指的就是它。
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器(Selectors),然后使用一个单独的线程来操作这个选择器,进而“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
应用程序将向Selector对象注册需要它关注的Channel,以及具体的某一个Channel会对哪些IO事件感兴趣。Selector中也会维护一个“已经注册的Channel”的容器。
Channel
通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操作系统)。那么既然是和操作系统进行内容的传递,那么说明应用程序可以通过通道读取数据,也可以通过通道向操作系统写数据,而且可以同时进行读写。
- 所有被Selector注册的通道,只能是继承了SelectableChannel类的子类。
- ScoketChannel:可以理解为TCP通道,简单理解就是TCP客户端。具体就是Socket套接字的监听通道,一个Socket套接字对应了一个客户端ip:port到服务器ip:port的通信连接。
- ServerSocketChannel:简单理解就是TCP得服务端,具体就是应用服务器程序的监听通道。只有通过这个通道,应用程序才能向操作系统注册支持“多路复用IO”的端口监听。同时支持UDP协议和TCP协议。
- FileChannel不能用于Selector,因为FileChannel是阻塞的。
通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。
buffer缓冲区
网络通讯中负责数据读写的区域,本质上是内存中的一块,我们可以将数据写入这块内存,之后从这块内存获取数据。
java.nio定义了很多Buffer,CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、ByteBuffer、MappedByteBuffer。后续用ByteBuffer进行代码演示。
JDK的NIO实战
原生JDK网络编程-NIO实战
先贴下代码,后面再细聊
代码结构为:
- NioServerHandler
- NioServer
- NioClientHandler
- NioClient
NioServerHandler:
public class NioServerHandler implements Runnable {
private Selector selector;
private ServerSocketChannel serverChannel;
private volatile boolean started;
public NioServerHandler(int port) {
try {
// 创建选择器
selector = Selector.open();
// 打开监听通道
serverChannel = ServerSocketChannel.open();
// 如果为true 则此通道将被置于阻塞模式;
// 如果为false 则此通道将被置于非阻塞模式
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(port));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 标记服务器已开启
started = true;
System.out.println("server start. port: " + port);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
public void stop() {
started = false;
}
@Override
public void run() {
// 循环遍历selector
while (started) {
try {
// 阻塞,只有当至少一个注册的事件发生的时候才会继续.
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Throwable t) {
t.printStackTrace();
}
}
// selector关闭后会自动释放里面管理的资源
if (selector != null) {
try {
selector.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
// 处理新接入的请求消息
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
System.out.println("connect success");
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
}
// 读消息
if (key.isReadable()) {
System.out.println("socket channel readable");
SocketChannel sc = (SocketChannel) key.channel();
// 创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
// 读取到字节,对字节进行编解码
if (readBytes > 0) {
// 将缓冲区当前的limit设置为position,position=0,
// 用于后续对缓冲区的读取操作
buffer.flip();
// 根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
// 将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String message = new String(bytes, StandardCharsets.UTF_8);
System.out.println("server receive message: " + message);
// 处理数据
String result = "server respond message: " + message;
// 发送应答消息
doWrite(sc, result);
}
//链路已经关闭,释放资源
else if (readBytes < 0) {
key.cancel();
sc.close();
}
}
}
}
private void doWrite(SocketChannel channel, String response)
throws IOException {
// 将消息编码为字节数组
byte[] bytes = response.getBytes();
// 根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
// 将字节数组复制到缓冲区
writeBuffer.put(bytes);
// flip操作
writeBuffer.flip();
// 发送缓冲区的字节数组
channel.write(writeBuffer);
}
}
NioServer:
public class NioServer {
private static NioServerHandler nioServerHandler;
public static void start() {
if (nioServerHandler != null) {
nioServerHandler.stop();
}
nioServerHandler = new NioServerHandler(10086);
new Thread(nioServerHandler, "Server").start();
}
public static void main(String[] args) {
start();
}
}
NioClientHandler:
public class NioClientHandler implements Runnable {
private String host;
private int port;
private volatile boolean started;
private Selector selector;
private SocketChannel socketChannel;
public NioClientHandler(String ip, int port) {
this.host = ip;
this.port = port;
try {
// 创建选择器
this.selector = Selector.open();
// 打开监听通道
socketChannel = SocketChannel.open();
// 如果为true 则此通道将被置于阻塞模式;
// 如果为false 则此通道将被置于非阻塞模式
socketChannel.configureBlocking(false);
started = true;
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
}
public void stop() {
started = false;
}
@Override
public void run() {
// 连接服务器
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
// 循环遍历selector
while (started) {
try {
// 阻塞方法,当至少一个注册的事件发生的时候就会继续
selector.select();
// 获取当前有哪些事件可以使用
Set<SelectionKey> keys = selector.selectedKeys();
// 转换为迭代器
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key;
while (it.hasNext()) {
key = it.next();
// 我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。
// 如果我们没有删除处理过的键,那么它仍然会在事件集合中以一个激活的键出现,这会导致我们尝试再次处理它。
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
}
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 具体的事件处理方法
*/
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
// 获得关心当前事件的channel
SocketChannel sc = (SocketChannel) key.channel();
// 处理连接就绪事件,但是三次握手未必就成功了,所以需要等待握手完成和判断握手是否成功
if (key.isConnectable()) {
// finishConnect的主要作用就是确认通道连接已建立,
// 方便后续IO操作(读写)不会因连接没建立而导致NotYetConnectedException异常。
if (sc.finishConnect()) {
// 连接既然已经建立,当然就需要注册读事件,写事件一般是不需要注册的。
socketChannel.register(selector, SelectionKey.OP_READ);
} else {
System.exit(-1);
}
}
// 处理读事件,也就是当前有数据可读
if (key.isReadable()) {
// 创建ByteBuffer,并开辟一个1k的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 将通道的数据读取到缓冲区,read方法返回读取到的字节数
int readBytes = sc.read(buffer);
if (readBytes > 0) {
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String result = new String(bytes, StandardCharsets.UTF_8);
System.out.println("client receive message: " + result);
}
// 链路已经关闭,释放资源
else if (readBytes < 0) {
key.cancel();
sc.close();
}
}
}
}
private void doConnect() throws IOException {
// 如果此通道处于非阻塞模式,则调用此方法将启动非阻塞连接操作。
// 如果连接马上建立成功,则此方法返回true。否则,此方法返回false。
// 因此我们必须关注连接就绪事件,并通过调用finishConnect方法完成连接操作。
if (socketChannel.connect(new InetSocketAddress(host, port))) {
// 连接成功,关注读事件
socketChannel.register(selector, SelectionKey.OP_READ);
} else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
}
// 写数据对外暴露的API
public void sendMsg(String msg) throws IOException {
doWrite(socketChannel, msg);
}
private void doWrite(SocketChannel sc, String request) throws IOException {
byte[] bytes = request.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
sc.write(writeBuffer);
}
}
NioClient:
public class NioClient {
private static NioClientHandler nioClientHandler;
public static void start() {
if (nioClientHandler != null) {
nioClientHandler.stop();
}
nioClientHandler = new NioClientHandler("127.0.0.1", 10086);
new Thread(nioClientHandler, "Client").start();
}
//向服务器发送消息
public static boolean sendMsg(String msg) throws Exception {
nioClientHandler.sendMsg(msg);
return true;
}
public static void main(String[] args) throws Exception {
start();
Scanner scanner = new Scanner(System.in);
while (NioClient.sendMsg(scanner.next())) {
}
}
}
Client端分别输入1、2、3,日志输出结果如下。
NioServer:
server start. port: 10086
connect success
socket channel readable
server receive message: 1
socket channel readable
server receive message: 2
socket channel readable
server receive message: 3
NioClient:
1
client receive message: server respond message: 1
2
client receive message: server respond message: 2
3
client receive message: server respond message: 3
然后代码中的几个细节如下:
1、Selector对象是通过调用静态工厂方法open()来实例化的,如下:
Selector Selector = Selector.open();
2、要实现Selector管理Channel,需要将channel注册到相应的Selector上,如下:
channel.configureBlocking(false);
SelectionKey key= channel.register(selector, SelectionKey,OP_READ);
通过调用通道的register()方法会将它注册到一个Selector上。与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出IllegalBlockingModeException异常,这也是不能将FileChannel与Selector一起使用的原因,因为前面说到FileChannel不能切换到非阻塞模式,而套接字通道都可以。另外通道一旦被注册,将不能再回到阻塞状态,此时若调用通道的configureBlocking(true)将抛出BlockingModeException异常。
3、在实际运行中,我们通过Selector.select()方法可以选择已经准备就绪的通道,这些通道包含了需要处理的事件。
下面是Selector几个重载的select()方法:
select():阻塞到至少有一个通道在你注册的事件上就绪了。
select(long timeout):和select()一样,但最长阻塞时间为timeout毫秒。
selectNow():非阻塞,立刻返回。
select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。
一旦调用select()方法,并且返回值不为0时,则可以通过调用Selector的selectedKeys()方法来访问已选择键集合。
Set selectedKeys = selector.selectedKeys();
这个时候,循环遍历selectedKeys集中的每个键,并检测各个键所对应的通道的就绪事件,再通过SelectionKey关联的Selector和Channel进行实际的业务处理。
注意每次迭代末尾的keyIterator.remove()调用。Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。否则的话,下次该通道变成就绪时,Selector会再次将其放入已选择键集中。
SelectionKey
什么是SelectionKey
SelectionKey key= channel.register(selector, SelectionKey,OP_READ);
SelectionKey是一个抽象类,表示selectableChannel在Selector中注册的标识.每个Channel向Selector注册时,都将会创建一个SelectionKey。SelectionKey将Channel与Selector建立了关系,并维护了channel事件。
可以通过cancel方法取消键,取消的键不会立即从selector中移除,而是添加到cancelledKeys中,在下一次select操作时移除它。所以在调用某个key时,需要使用key.isValid()进行校验。
SelectionKey类型和就绪条件
在向Selector对象注册感兴趣的事件时,JAVA NIO共定义了四种:OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT(定义在SelectionKey中),分别对应读、写、请求连接、接受连接等网络Socket操作。
| 操作类型 | 就绪条件及说明 |
|---|---|
| OP_READ | 当操作系统读缓冲区有数据可读时就绪。并非时刻都有数据可读,所以一般需要注册该操作,仅当有就绪时才发起读操作,有的放矢,避免浪费CPU。 |
| OP_WRITE | 当操作系统写缓冲区有空闲空间时就绪。一般情况下写缓冲区都有空闲空间,小块数据直接写入即可,没必要注册该操作类型,否则该条件不断就绪浪费CPU;但如果是写密集型的任务,比如文件下载等,缓冲区很可能满,注册该操作类型就很有必要,同时注意写完后取消注册。 |
| OP_CONNECT | 当SocketChannel.connect()请求连接成功后就绪。该操作只给客户端使用。 |
| OP_ACCEPT | 当接收到一个客户端连接请求时就绪。该操作只给服务器使用。 |
服务端和客户端分别需要处理的类型
ServerSocketChannel和SocketChannel可以注册自己感兴趣的操作类型,当对应操作类型的就绪条件满足时OS会通知channel,下表描述各种Channel允许注册的操作类型,Y表示允许注册,N表示不允许注册,其中服务器SocketChannel指由服务器ServerSocketChannel.accept()返回的对象。
| 角色 | 组件 | accpet事件 | connect事件 | write事件 | read事件 |
|---|---|---|---|---|---|
| client | SocketChanel | √ | √ | √ | |
| server | SoceketChannel | √ | √ | ||
| server | ServerSocketChannel | √ |
- 服务器启动ServerSocketChannel,关注OP_ACCEPT事件,
- 客户端启动SocketChannel,连接服务器,关注OP_CONNECT事件
- 服务器接受连接,启动一个服务器的SocketChannel,这个SocketChannel可以关注OP_READ、OP_WRITE事件,一般连接建立后会直接关注OP_READ事件
- 客户端这边的客户端SocketChannel发现连接建立后,可以关注OP_READ、OP_WRITE事件,一般是需要客户端需要发送数据了才关注OP_READ事件
- 连接建立后客户端与服务器端开始相互发送消息(读写),根据实际情况来关注OP_READ、OP_WRITE事件。
Buffer
Buffer用于和NIO通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。以写为例,应用程序都是将数据写入缓冲,再通过通道把缓冲的数据发送出去,读也是一样,数据总是先从通道读到缓冲,应用程序再读缓冲的数据。
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存(其实就是数组)。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
Buffer的几个属性
private int position = 0;
private int limit;
private int capacity;
capacity
作为一个内存块,capacity表示Buffer固定的大小值。你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
position
- 写模式:position表示当前能写的位置。初始的position值为0。当一个byte、long等数据写到Buffer后,position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
- 读模式:从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
limit
- 写模式:表示你最多能往Buffer里写多少数据,Buffer的capacity。
- 读模式:表示最多能读到多少数据。切换为读模式时,limit会被设置成写模式下的position值。换句话说,能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
直观点如下图:
Buffer的分配
上面提到Buffer的分配,分别是在堆上分配以及在直接内存上分配:
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
直接内存
HeapByteBuffer与DirectByteBuffer,在原理上,前者可以看出分配的buffer是在heap区域的,其实真正flush到远程的时候会先拷贝到直接内存,再做下一步操作;在NIO的框架下,很多框架会采用DirectByteBuffer来操作,这样分配的内存不再是在java堆上,经过性能测试,可以得到非常快速的网络交互,在大量的网络交互下,一般速度会比HeapByteBuffer要快速好几倍。
直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域,但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError 异常出现。
NIO可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
直接内存与堆内存比较
public static void main(String[] args) {
allocateCompare(); // 分配比较
operateCompare(); // 读写比较
}
public static void allocateCompare() {
// 操作次数 1000W
int time = 10000000;
long st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
// 非直接内存分配申请
ByteBuffer.allocate(2);
}
System.out.println("在进行" + time + "次分配操作时,堆内存分配耗时:" + (System.currentTimeMillis() - st) + "ms");
st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
// 直接内存分配申请
ByteBuffer.allocateDirect(2);
}
System.out.println("在进行" + time + "次分配操作时,直接内存 分配耗时:" + (System.currentTimeMillis() - st) + "ms");
}
public static void operateCompare() {
// 1亿次操作
int time = 100000000;
ByteBuffer buffer = ByteBuffer.allocate(2 * time);
long st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
buffer.putChar('a');
}
buffer.flip();
for (int i = 0; i < time; i++) {
buffer.getChar();
}
System.out.println("在进行" + time + "次读写操作时,堆内存读写耗时:" + (System.currentTimeMillis() - st) + "ms");
ByteBuffer buffer_d = ByteBuffer.allocateDirect(2 * time);
st = System.currentTimeMillis();
for (int i = 0; i < time; i++) {
buffer_d.putChar('a');
}
buffer_d.flip();
for (int i = 0; i < time; i++) {
buffer_d.getChar();
}
System.out.println("在进行" + time + "次读写操作时,直接内存读写耗时:" + (System.currentTimeMillis() - st) + "ms");
}
结果:
在进行10000000次分配操作时,堆内存分配耗时:74ms
在进行10000000次分配操作时,直接内存 分配耗时:5759ms
在进行100000000次读写操作时,堆内存读写耗时:91ms
在进行100000000次读写操作时,直接内存读写耗时:55ms
结论:
- 直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
- 直接内存IO读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显
当然,实际使用中也不会频繁分配。
Buffer的读写
Buffer方法总结
测试代码:
public static void main(String[] args) {
System.out.println("------Test get-------------");
ByteBuffer buffer = ByteBuffer.allocate(32);
buffer.put((byte) 'a') // 0
.put((byte) 'b') // 1
.put((byte) 'c') // 2
.put((byte) 'd') // 3
.put((byte) 'e') // 4
.put((byte) 'f'); // 5
System.out.println("before flip()" + buffer);
// 转换为读取模式
buffer.flip();
System.out.println("before get():" + buffer);
System.out.println((char) buffer.get());
System.out.println("after get():" + buffer);
// get(index)不影响position的值
System.out.println((char) buffer.get(2));
System.out.println("after get(index):" + buffer);
byte[] dst = new byte[10];
// position移动两位
buffer.get(dst, 0, 2);
// 这里的buffer是 abcdef[pos=3 lim=6 cap=32]
System.out.println("after get(dst, 0, 2):" + buffer);
System.out.println("dst:" + new String(dst));
System.out.println("--------Test put-------");
ByteBuffer bb = ByteBuffer.allocate(32);
System.out.println("before put(byte):" + bb);
System.out.println("after put(byte):" + bb.put((byte) 'z'));
// put(2,(byte) 'c')不改变position的位置
bb.put(2, (byte) 'c');
System.out.println("after put(2,(byte) 'c'):" + bb);
System.out.println(new String(bb.array()));
// 这里的buffer是 abcdef[pos=3 lim=6 cap=32]
bb.put(buffer);
System.out.println("after put(buffer):" + bb);
System.out.println(new String(bb.array()));
System.out.println("--------Test reset----------");
buffer = ByteBuffer.allocate(20);
System.out.println("buffer = " + buffer);
buffer.clear();
buffer.position(5); // 移动position到5
buffer.mark();//记录当前position的位置
buffer.position(10); // 移动position到10
System.out.println("before reset:" + buffer);
buffer.reset();//复位position到记录的地址
System.out.println("after reset:" + buffer);
System.out.println("--------Test rewind--------");
buffer.clear();
buffer.position(10); // 移动position到10
buffer.limit(15); // 限定最大可写入的位置为15
System.out.println("before rewind:" + buffer);
buffer.rewind(); // 将position设回0
System.out.println("before rewind:" + buffer);
System.out.println("--------Test compact--------");
buffer.clear();
// 放入4个字节,position移动到下个可写入的位置,也就是4
buffer.put("abcd".getBytes());
System.out.println("before compact:" + buffer);
System.out.println(new String(buffer.array()));
buffer.flip(); // 将position设回0,并将limit设置成之前position的值
System.out.println("after flip:" + buffer);
// 从Buffer中读取数据的例子,每读一次,position移动一次
System.out.println((char) buffer.get());
System.out.println((char) buffer.get());
System.out.println((char) buffer.get());
System.out.println("after three gets:" + buffer);
System.out.println(new String(buffer.array()));
// compact()方法将所有未读的数据拷贝到Buffer起始处。
// 然后将position设到最后一个未读元素正后面。
buffer.compact();
System.out.println("after compact:" + buffer);
System.out.println(new String(buffer.array()));
}
| limit(), limit(10)等 | 其中读取和设置这4个属性的方法的命名和jQuery中的val(),val(10)类似,一个负责get,一个负责set |
|---|---|
| reset() | 把position设置成mark的值,相当于之前做过一个标记,现在要退回到之前标记的地方 |
| clear() | position = 0;limit = capacity;mark = -1; 有点初始化的味道,但是并不影响底层byte数组的内容 |
| flip() | limit = position;position = 0;mark = -1; 翻转,也就是让flip之后的position到limit这块区域变成之前的0到position这块,翻转就是将一个处于存数据状态的缓冲区变为一个处于准备取数据的状态 |
| rewind() | 把position设为0,mark设为-1,不改变limit的值 |
| remaining() | return limit - position;返回limit和position之间相对位置差 |
| hasRemaining() | return position< limit返回是否还有未读内容 |
| compact() | 把从position到limit中的内容移到0到limit-position的区域内,position和limit的取值也分别变成limit-position、capacity。如果先将positon设置到limit,再compact,那么相当于clear() |
| get() | 相对读,从position位置读取一个byte,并将position+1,为下次读写作准备 |
| get(int index) | 绝对读,读取byteBuffer底层的bytes中下标为index的byte,不改变position |
| get(byte[] dst, int offset, int length) | 从position位置开始相对读,读length个byte,并写入dst下标从offset到offset+length的区域 |
| put(byte b) | 相对写,向position的位置写入一个byte,并将postion+1,为下次读写作准备 |
| put(int index, byte b) | 绝对写,向byteBuffer底层的bytes中下标为index的位置插入byte b,不改变position |
| put(ByteBuffer src) | 用相对写,把src中可读的部分(也就是position到limit)写入此byteBuffer |
| put(byte[] src, int offset, int length) | 从src数组中的offset到offset+length区域读取数据并使用相对写写入此byteBuffer |
NIO之Reactor模式
Reactor 是一种开发模式,模式的核心流程:
注册事件 -> 扫描事件是否发生 -> 处理事件
单线程Reactor模式
- 服务器端的Reactor是一个线程对象,该线程会启动事件循环,并使用Selector(选择器)来实现IO的多路复用。注册一个Acceptor事件处理器到Reactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样Reactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。
- 客户端向服务器端发起一个连接请求,Reactor监听到了该ACCEPT事件的发生并将该ACCEPT事件派发给相应的Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将该连接所关注的READ事件以及对应的READ事件处理器注册到Reactor中,这样一来Reactor就会监听该连接的READ事件了。
- 当Reactor监听到有读或者写事件发生时,将相关的事件派发给对应的处理器进行处理。比如,读处理器会通过SocketChannel的read()方法读取数据,此时read()操作可以直接读取到数据,而不会堵塞与等待可读的数据到来。
- 每当处理完所有就绪的感兴趣的I/O事件后,Reactor线程会再次执行select()阻塞等待新的事件就绪并将其分派给对应处理器进行处理。
注意,Reactor的单线程模式的单线程主要是针对于I/O操作而言,也就是所有的I/O的accept()、read()、write()以及connect()操作都在一个线程上完成的。
但在目前的单线程Reactor模式中,不仅I/O操作在该Reactor线程上,连非I/O的业务操作也在该线程上进行处理了,这可能会大大延迟I/O请求的响应。所以我们应该将非I/O的业务逻辑操作从Reactor线程上卸载,以此来加速Reactor线程对I/O请求的响应。
多线程Reactor模式
与单线程Reactor模式不同的是,添加了一个工作者线程池,并将非I/O操作从Reactor线程中移出转交给工作者线程池来执行。这样能够提高Reactor线程的I/O响应,不至于因为一些耗时的业务逻辑而延迟对后面I/O请求的处理。
使用线程池的优势:
-
通过重用现有的线程而不是创建新线程,可以在处理多个请求时分摊在线程创建和销毁过程产生的巨大开销。
-
另一个额外的好处是,当请求到达时,工作线程通常已经存在,因此不会由于等待创建线程而延迟任务的执行,从而提高了响应性。
-
通过适当调整线程池的大小,可以创建足够多的线程以便使处理器保持忙碌状态。同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败。
改进的版本中,所以的I/O操作依旧由一个Reactor来完成,包括I/O的accept()、read()、write()以及connect()操作。
对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发或大数据量的应用场景却不合适,主要原因如下:
-
一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的读取和发送;
-
当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
主从多线程Reactor模式
Reactor线程池中的每一Reactor线程都会有自己的Selector、线程和分发的事件循环逻辑。
mainReactor可以只有一个,但subReactor一般会有多个。mainReactor线程主要负责接收客户端的连接请求,然后将接收到的SocketChannel传递给subReactor,由subReactor来完成和客户端的通信。
流程:
-
注册一个Acceptor事件处理器到mainReactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样mainReactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。启动mainReactor的事件循环。
-
客户端向服务器端发起一个连接请求,mainReactor监听到了该ACCEPT事件并将该ACCEPT事件派发给Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将这个SocketChannel传递给subReactor线程池。
-
subReactor线程池分配一个subReactor线程给这个SocketChannel,即,将SocketChannel关注的READ事件以及对应的READ事件处理器注册到subReactor线程中。当然你也注册WRITE事件以及WRITE事件处理器到subReactor线程中以完成I/O写操作。Reactor线程池中的每一Reactor线程都会有自己的Selector、线程和分发的循环逻辑。
-
当有I/O事件就绪时,相关的subReactor就将事件派发给响应的处理器处理。注意,这里subReactor线程只负责完成I/O的read()操作,在读取到数据后将业务逻辑的处理放入到线程池中完成,若完成业务逻辑后需要返回数据给客户端,则相关的I/O的write操作还是会被提交回subReactor线程来完成。
注意,所以的I/O操作(包括,I/O的accept()、read()、write()以及connect()操作)依旧还是在Reactor线程(mainReactor线程或 subReactor线程)中完成的。Thread Pool(线程池)仅用来处理非I/O操作的逻辑。
多Reactor线程模式将“接受客户端的连接请求”和“与该客户端的通信”分在了两个Reactor线程来完成。mainReactor完成接收客户端连接请求的操作,它不负责与客户端的通信,而是将建立好的连接转交给subReactor线程来完成与客户端的通信,这样一来就不会因为read()数据量太大而导致后面的客户端连接请求得不到即时处理的情况。并且多Reactor线程模式在海量的客户端并发请求的情况下,还可以通过实现subReactor线程池来将海量的连接分发给多个subReactor线程,在多核的操作系统中这能大大提升应用的负载和吞吐量。