JAVA NIO - Buffer
在之前提到JAVA NIO中,引入了Buffer、Channel 、Selectors.
- Buffer:
- 定义:
Java NIO Buffers用于和NIO Channel交互。 我们从Channel中读取数据到buffers里,从Buffer把数据写入到Channels. Buffer本质上就是一块内存区,可以用来写入数据,并在稍后读取出来。 这块内存被NIO Buffer包裹起来,对外提供一系列的读写方便开发的接口。
- 属性:
- 定义:
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;//标志位
private int position = 0;//当前游标(指针)位置
private int limit;//最大容量
private int capacity;//当前容量
- 交互步骤:
1.申明缓存区大小(直接缓冲区(allocateDirect)或非直接缓冲区(allocate))1
2.把数据写入buffer;
3.调用flip;
4.从Buffer中读取数据;
5.调用buffer.clear()或者buffer.compact()。
理解:
Buffer缓冲区实质上就是一块内存,用于写入数据,也供后续再次读取数据。
这块内存被NIO Buffer管理,并提供一系列的方法用于更简单的操作这块内存。
position和limit的具体含义取决于当前buffer的模式。capacity在两种模式下都表示容量。
(其实这里有点像切片的概念,指针、界限、最大容量)
-
容量(Capacity)
作为一块内存,buffer有一个固定的大小,叫做capacit(容量)。也就是最多只能写入容量值得字节,整形等数据。一旦buffer写满了就需要清空已读数据以便下次继续写入新的数据
-
上限(Limit)
在写模式,limit的含义是我们所能写入的最大数据量,它等同于buffer的容量。 一旦切换到读模式,limit则代表我们所能读取的最大数据量,他的值等同于写模式下position的位置。换句话说,您可以读取与写入数量相同的字节数(限制设置为写入的字节数,由位置标记)。(0位至 之前写模式将指针挪到的 索引位)
-
位置(Position)
当写入数据到Buffer的时候需要从一个确定的位置开始,默认初始化时这个位置position为0,一旦写入了数据比如一个字节,整形数据,那么position的值就会指向数据之后的一个单元,position最大可以到capacity-1. 当从Buffer读取数据时,也需要从一个确定的位置开始。buffer从写入模式变为读取模式时,position会归零,每次读取后,position向后移动。
buffer 中的绝大多数操作都是围绕以上三个属性进行操作的,
举例 :
- 标记当前指针位置/回退到指针位
/**
* Sets this buffer's mark at its position.
*
* @return This buffer
*/
public final Buffer mark() {
mark = position;
return this;
}
/**
回退都指针位置
*/
public final Buffer reset() {
int m = mark;
if (m < 0)
throw new InvalidMarkException();
position = m;
return this;
}
- clear
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
应该明确认知clear只是改变有了游标位置等,并不会真实的清空了数据。
好比之前dubbo泛化调用中的destroy不会清除zk上的节点,而只是不watch,T_T。
- flip
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
数据存入:
当声明一组缓存区时,pos为0位。
每次读、写时,pos会+1滑动至下一个索引位;
JAVA NIO - Channel
在之前提到JAVA NIO中,引入了Buffer、Channel 、Selectors.
-
Channel:
-
定义:
Java NIO Buffers用于和NIO Channel交互,而Channel等价于之前的**DMA**,但不存在总线问题。 Channel表示IO源于目标打开的连接。Channel类似传统的‘流’ ,但Channel本身不能直接访问数据,
-
交互步骤:
1.Channel只能与Buffer进行交互,且本身不存储数据。
2.按照通信目标分为FileChannel(本地,File理解为FD)、SocketChannel(tcp client)、ServerSocketChannel(tcp server)、DatagramChannel(udp)每种类型交互存在差异;
-
-
通道获取方式:
- 可使用API直接对已有的流、socket进行get(),如本地I/O中的FileInputStream、FileOutputStream、RandomAccessFile合网络IO中的Socket、ServerSocket、DatagramSocket。
- 对各个通道进行open()调用。
- Files中的newByteChannel()
- 通道与缓冲区的交互支持 Scatter(分散读取) 与 Gather (Gather)1
Pipe 不做介绍,简单理解为Go里面的 <-Channel 以及 ->Channel 单向
JAVA NIO - Select(ableChannel)
在之前提到JAVA NIO中,引入了Buffer、Channel 、Selectors.
-
Channel:
-
定义:
Java NIO Select(ableChannel)是多路复用器。用于监控相应I/O的状态
-
分类:
- java.nio.channels.Channel 接口:
- SelectableChannel
- SocketChannel
- ServerSocketChannel
- DatagramChannel
- Pipe.SinkChannel
- Pipe.SourceChannel
- SelectableChannel
- java.nio.channels.Channel 接口:
-
-
交互方式:
- 当调用register(Selector sel , int pos) 将通道组成选择器时,选择器对通道的监听事件,通过params2进行指定。
- 可监听到的事件类型有。
– 1.读 SelectionKey.OP_READ(1)
– 2.写 SelectionKey.OP_WRITE(4)
– 3.连接 SelectionKey.OP_CONNECT(8)
– 4.接收 SelectionKey.OP_ACCEPT(16) - 通道与缓冲区的交互支持 Scatter(分散读取) 与 Gather (Gather)2
- 若注册 不止一个监听事件,在可以通过使用“|”操作符拼接
SelectionKey keyWithRW =serverSocketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
零拷贝(无CPU拷贝)
- 零拷贝是网络编程的关键,很多性能都是围绕该点。
- JAVA中,常用的零拷贝有mmap(内存映射)和sendFile。于操作系统而言,在内核缓冲区之间,没有数据是重复的(只有Kernel buffer 一份数据)。
@Test
public void testCopy() throws IOException {
RandomAccessFile file = new RandomAccessFile(new File("test.txt"), "rw");
byte[] arr = new byte[(int) file.length()];
file.read(arr);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
}
数据一共经过4次拷贝,线程经过3次切换。
mmap优化
- mmap通过内存映射,将文件映射都内存缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的零拷贝次。
sendFile 优化
- Linux2.1 版本提供了sendFile 函数,其基本原理如下:数据根本不通过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态无关,就减少了一次上下文切换,2.4中会从kernal buffer直接拷贝到协议栈。
mmap与sendFile区别
1)mmap适合小数据量读写,sendFile适合大文件传输。
2)mmap需要四次上下文切换,3次数据拷贝;sendFile需要3次上下文切换,最少2次数据拷贝。
- sendFile可以利用DMA方式,减少CPU拷贝,mmap则不能(必须由内核拷贝到socket缓冲区)
理论概念整体较为杂乱,构建了相应demo,对ApI进行了尝试调用,并做了一发简易的群聊整理NIO知识,完整代码 https://github.com/LikeElephantintheforest/netty NIO-Group-chat 分支,客户端telnet实现。
简介:
- 客户端可进行注册至服务端
- 服务端可打印客户端上行数据
- 服务端可将该消息准发至其他客户端
较为重要的API使用:
//1--服务端注册Selector , 监听客户端连接事件。
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
//2---通过该selector监听OP事件,可得知客户端建连。
boolean haveAccept = selector.select(10000) > 0;
//3----当客户端连接建立成功服务端知晓后,可对当前socket进行可读事件监听
if (e.isAcceptable()) {
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(Boolean.FALSE);
//be notified by OP_ACCEPT event,then register OP_READ on socket
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println(String.format("IP: %s sign up ", socketChannel.getRemoteAddress()));
}
//4-----当客户端有数据写入,服务端判定有可读时,将该消息广播至所有其他已注册的客户端。
if (acceptedMsg) {
for (SelectionKey select : selector.keys()) {
//except self
if (e != select) {
if (select.channel() instanceof SocketChannel) {
SocketChannel clientSocketChannel = (SocketChannel) select.channel();
clientSocketChannel.configureBlocking(Boolean.FALSE);
try {
clientSocketChannel.write(ByteBuffer.wrap(receiveMsg.getBytes()));
} catch (IOException ex) {
//
ex.printStackTrace();
}
}
}
}
}
·
1:字节缓冲区要么是直接的,要么是非直接的。
2:如果为直接字节缓冲区,则java虚拟机会尽最大努力直接在此缓冲区上执行本机I/O操作。再每次调用os的一个本机I/O操作时,虚拟机都会尽量避免将缓冲区的内容复制到中间的缓冲区中。
3:直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因为对应用程序造成的内存需求影响并不明显。
4:非直缓冲区必然存在读写时用户态与内核态的拷贝,程序无法直接通过内核态交交互OS。
5:直接缓冲区通过形成物理内存映射文件,交互操作系统物理内存,不做拷贝。 ↩︎ ↩︎·
分散读取(Scatter Reads):是指从Channel中读取的数据“分散”到多个buffer中。(有序)
聚集写入(Gathering Writes):是指将多个Buffer中的数据“聚集”到Channel中。 ↩︎