Netty 与 NIO 之前世今生
缓冲区(Buffer)
Buffer 操作基本 API
缓冲区本质是一个数组,在NIO中,所有数据都需要缓冲区来处理,读取数据指从通道中读取,写入到缓冲区,写入数据指从缓冲区读取,写入到通道。在BIO中,所有数据是读取或写入到Stream对象中。
继承关系如下
image.png
public class IntBufferDemo {
public static void main(String[] args) {
// 分配新的int缓冲区,参数为缓冲区容量
// 新缓冲区的当前位置将为零,其界限(限制位置)将为其容量。它将具有一个底层实现数组,其数组偏移量将为零。
IntBuffer buffer = IntBuffer.allocate(8);
for (int i = 0; i < buffer.capacity(); ++i) {
int j = 2 * (i + 1);
// 将给定整数写入此缓冲区的当前位置,当前位置递增
buffer.put(j);
}
// 重设此缓冲区,将限制设置为当前位置,然后将当前位置设置为0
buffer.flip();
// 查看在当前位置和限制位置之间是否有元素
while (buffer.hasRemaining()) {
// 读取此缓冲区当前位置的整数,然后当前位置递增
int j = buffer.get();
System.out.print(j + " ");
}
}
}
Buffer 的基本的原理
用三个字段来表示数组的状态变化
capacity:数组容量,初始值确定
limit:数据的个数,对象初始时和容量相等;flip时和当前位置相等
position:下一个数据的位置,对象初始化时或flip时,值为0;read或者write时,自动更新
三个之间的关系, 0 <= position <= limit <= capacity
public class BufferDemo {
public static void main(String args[]) throws Exception {
//这用用的是文件IO处理 文件内容是:tom
FileInputStream fin = new FileInputStream("E://test.txt");
//创建文件的操作管道
FileChannel fc = fin.getChannel();
//分配一个10个大小缓冲区,说白了就是分配一个10个大小的byte数组
ByteBuffer buffer = ByteBuffer.allocate(10);
output("初始化", buffer);//capacity: 10, position: 0, limit: 10
//先读一下
fc.read(buffer);
output("调用read()", buffer);//10, position: 10, limit: 10
//准备操作之前,先锁定操作范围
buffer.flip();
output("调用flip()", buffer);//10, position: 0, limit: 10
//判断有没有可读数据
while (buffer.remaining() > 0) {
byte b = buffer.get();
// System.out.print(((char)b));
}
output("调用get()", buffer);//capacity: 10, position: 10, limit: 10
//可以理解为解锁
buffer.clear();
output("调用clear()", buffer);//capacity: 10, position: 0, limit: 10
//最后把管道关闭
fin.close();
}
//把这个缓冲里面实时状态给答应出来
public static void output(String step, ByteBuffer buffer) {
System.out.println(step + " : ");
//容量,数组大小
System.out.print("capacity: " + buffer.capacity() + ", ");
//当前操作数据所在的位置,也可以叫做游标
System.out.print("position: " + buffer.position() + ", ");
//锁定值,flip,数据操作范围索引只能在position - limit 之间
System.out.println("limit: " + buffer.limit());
System.out.println();
}
}
缓冲区的分配
public class BufferWrap {
public void myMethod() {
// 分配指定大小的缓冲区
ByteBuffer buffer1 = ByteBuffer.allocate(10);
// 包装一个现有的数组
byte array[] = new byte[10];
ByteBuffer buffer2 = ByteBuffer.wrap( array );
}
}
缓冲区分片
对子缓冲区的改变,也是对缓冲区的改变,类似于思维导图的下钻。
public class BufferSlice {
static public void main( String args[] ) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate( 10 );
// 缓冲区中的数据0-9
for (int i=0; i<buffer.capacity(); ++i) {
buffer.put( (byte)i );
}
// 创建子缓冲区
buffer.position( 3 );
buffer.limit( 7 );
ByteBuffer slice = buffer.slice();
// 改变子缓冲区的内容
for (int i=0; i<slice.capacity(); ++i) {
byte b = slice.get( i );
b *= 10;
slice.put( i, b );
}
buffer.position( 0 );
buffer.limit( buffer.capacity() );
while (buffer.remaining()>0) {
System.out.print( buffer.get() + " ");//0 1 2 30 40 50 60 7 8 9
}
}
}
只读缓冲区
只读缓冲区和缓冲区是共用同一份数据,只不过只读缓冲区不允许修改,所以当缓冲区内容发生改变时,只读缓冲区内容也发生改变。
public class ReadOnlyBuffer {
static public void main( String args[] ) throws Exception {
ByteBuffer buffer = ByteBuffer.allocate( 10 );
// 缓冲区中的数据0-9
for (int i=0; i<buffer.capacity(); ++i) {
buffer.put( (byte)i );
}
// 创建只读缓冲区
ByteBuffer readonly = buffer.asReadOnlyBuffer();
// 改变原缓冲区的内容
for (int i=0; i<buffer.capacity(); ++i) {
byte b = buffer.get( i );
b *= 10;
buffer.put( i, b );
}
readonly.position(0);
readonly.limit(buffer.capacity());
// 只读缓冲区的内容也随之改变
while (readonly.remaining()>0) {
System.out.print( readonly.get() + " ");//0 10 20 30 40 50 60 70 80 90
}
}
}
直接缓冲区
直接操作机器内存,而不需要经过jvm内存再到机器内存,少一个缓冲区的内容拷贝过程。
/**
* 直接缓冲区
* Zero Copy 减少了一个拷贝的过程
*/
public class DirectBuffer {
static public void main( String args[] ) throws Exception {
//在Java里面存的只是缓冲区的引用地址
//管理效率
//首先我们从磁盘上读取刚才我们写出的文件内容
String infile = "E://test.txt";
FileInputStream fin = new FileInputStream( infile );
FileChannel fcin = fin.getChannel();
//把刚刚读取的内容写入到一个新的文件中
String outfile = String.format("E://testcopy.txt");
FileOutputStream fout = new FileOutputStream(outfile);
FileChannel fcout = fout.getChannel();
// 使用allocateDirect,而不是allocate
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true) {
buffer.clear();
int r = fcin.read(buffer);
if (r==-1) {
break;
}
buffer.flip();
fcout.write(buffer);
}
}
}
内存映射
对缓冲区的修改,文件内容也会直接发生修改。
public class MappedBuffer {
static private final int start = 0;
static private final int size = 26;
static public void main( String args[] ) throws Exception {
RandomAccessFile raf = new RandomAccessFile( "E://test.txt", "rw" );
FileChannel fc = raf.getChannel();
//把缓冲区跟文件系统进行一个映射关联
//只要操作缓冲区里面的内容,文件内容也会跟着改变
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,start, size );
mbb.put( 0, (byte)97 ); //a
mbb.put( 25, (byte)122 ); //z
raf.close();
}
}
选择器(Selector)
传统的 Server/Client 模式会基于 TPR(Thread per Request),为每个请求分配一个线程。
image.png
NIO 中非阻塞 I/O 采用了基于 Reactor 模式的工作方式,I/O 调用不会被阻塞,相反是注册感兴趣的特定 I/O 事件,如可读数据到 达,新的套接字连接等等,在发生特定事件时,系统再通知我们。
image.png
从图中可以看出,当有读或写等任何注册的事件发生时,可以从 Selector 中获得相应的 SelectionKey,同时从 SelectionKey 中可以找到发生的事件和该事件所发生的具体的 SelectableChannel,以获得客户端发送过来的数据。
使用 NIO 中非阻塞 I/O 编写服务器处理程序,大体上可以分为下面三个步骤:
1.向 Selector 对象注册感兴趣的事件。
- 从 Selector 中获取感兴趣的事件.
- 根据不同的事件进行相应的处理。
具体代码见javaIO的演进过程中NIO的服务端
通道(Channel)
通道类似于流,读取和写入数据都需要通过缓冲区。
通道类的继承关系
image.png
使用 NIO 读取数据
public class FileInputDemo {
static public void main( String args[] ) throws Exception {
FileInputStream fin = new FileInputStream("E://test.txt");
// 获取通道
FileChannel fc = fin.getChannel();
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据到缓冲区
fc.read(buffer);
buffer.flip();
while (buffer.remaining() > 0) {
byte b = buffer.get();
System.out.print(((char)b));
}
fin.close();
}
}
使用 NIO 写入数据
public class FileOutputDemo {
static private final byte message[] = { 83, 111, 109, 101, 32, 98, 121, 116, 101, 115, 46 };
static public void main( String args[] ) throws Exception {
FileOutputStream fout = new FileOutputStream( "E://test.txt" );
FileChannel fc = fout.getChannel();
ByteBuffer buffer = ByteBuffer.allocate( 1024 );
for (int i=0; i<message.length; ++i) {
buffer.put( message[i] );
}
buffer.flip();
fc.write( buffer );
fout.close();
}
}
IO多路复用
下面场景采用的是io多路复用
image.png客人:客户端请求
点餐内容:客户端发送的实际数据
老板:操作系统
人力成本:系统资源
菜单:文件状态描述符(FD)。操作系统对于一个进程能够同时持有的文件状态描述符的个数是有限制的,在 linux 系统中$ulimit -n 查看这个限制值,当然也是可以(并且应该)进行内核参数调整的。
服务员:操作系统内核用于 IO 操作的线程(内核线程)
厨师:应用程序线程(当然厨房就是应用程序进程咯)
多路复用 IO 技术最适用的是“高并发”场景,所谓高并发是指 1 毫秒内至少同时有上千个连接请求准备好
目前流行的多路复用 IO 实现主要包括四种:select、poll、epoll、kqueue。下表是他们的一些重要特性的比较:
image.png反应堆 Reactor
阻塞io
阻塞 I/O调用 InputStream.read()方法时是阻塞的,它会一直等到数据到来时(或超时)才会返回;同样,在调用 ServerSocket.accept()方法时,也会一直阻塞到有客户端连接才会返回,每个客户端连接过来后,服务端都会启动一个线程去处理该客户端的请求。阻塞 I/O 的通信模型示意图如下
image.png
如果你细细分析,一定会发现阻塞 I/O 存在一些缺点。根据阻塞 I/O 通信模型,我总结了它的两点缺点: 1. 当客户端多时,会创建大量的处理线程。且每个线程都要占用栈空间和一些 CPU 时间 2. 阻塞可能带来频繁的上下文切换,且大部分上下文切换可能是无意义的。在这种情况下非阻塞式 I/O 就有了它的应用前景。
非阻塞IO
工作原理如下
1.由一个专门的线程来处理所有的 IO 事件,并负责分发。
2.事件驱动机制:事件到的时候触发,而不是同步的去监视事件。
3.线程通讯:线程之间通过 wait,notify 等方式通讯。保证每次上下文切换都是有意义的。减少无谓的线程切换。
image.png
Netty 与 NIO
Netty 支持的功能与特性
Netty 是一个异步、事件驱动的用来做高性能、高可靠性的网络应用框架.
image.png
Netty 采用 NIO 而非 AIO 的理由
1.Netty 不看重 Windows 上的使用,在 Linux 系统上,AIO 的底层实现仍使用 EPOLL,没有很好实现 AIO,因此在性 能上没有明显的优势,而且被 JDK 封装了一层不容易深度优化
2.Netty 整体架构是 reactor 模型, 而 AIO 是 proactor 模型, 混合在一起会非常混乱,把 AIO 也改造成 reactor 模型看起 来是把 epoll 绕个弯又绕回来
3.AIO还有个缺点是接收数据需要预先分配缓存, 而不是NIO那种需要接收时才需要分配缓存, 所以对连接数量非常大 但流量小的情况, 内存浪费很多
4.Linux 上 AIO 不够成熟,处理回调结果速度跟不到处理需求,比如外卖员太少,顾客太多,供不应求,造成处理速度 有瓶颈(待验证)