NIO 与Netty学习
Netty导学
- 互联网环境下,分布式系统大行其道,而分布式系统的根在于网络编程。而Netty是JAVA领域网络编程的王者。
- nio基础
- non-blocking io 非阻塞io
- 三大组件
1.1 核心一 Buffer(缓冲区)
- buffer则用来缓冲读写数据。
- 常见的buffer有
- ByteBuffer(抽象类)
- MappedByteBuffer
- DirectByteBuffer
- HeapByteBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloagBuffer
- DoubleBuffer
- CharBuffer
1.2 核心二 Channel(通道)
1. 基础概念
-
Channel :是由java.nio.channel包定义的,表示IO源于目标打开的连接。Channel类似于传统的“流”,是读写数据的双向通道;只不过Channel本身不能直接访问数据,Channel只能与Buffer交互。尅从channel将数据读入buffer,也可以将buffer的数据写入channle。channel比stream更为底层
-
NIO的通道类似于流,但区别如下:
- 通道可同时进行读写,而流只能读或者写;
- 通道可实现异步读写数据
- 通道可从缓冲读取数据,也可写数据到缓冲;
- BIO中的stream是单向的,如FileInputStream对象只能进行读取数据的操作,而NIO中的通道是双向的,可读也可写;
- 常用的Channel实现类
- FileChannel:用于读取、写入、映射和操作文件。
- DatagramChannel:通过UDP读写网络中的数据通道
- SocketChannel: 通过TCP读写网络中的数据;
- ServerSocketChannel可监听新进来的TCP连接,对每一个新进来的连接都会创建一个SocketChannel。
1.3 核心三 Selector
selector从字面意思不好立即,需要结合服务器的设计演化来理解它的用途
1.3.1 服务器设计的演化
- 多线程版设计
多线程版的缺点
- 内存占用高
- 线程上下文切换成本高
- 只适合连接数少的场景
- 线程池版设计
线程池版缺点
- 阻塞模式下, 线程仅能处理一个socket链接(处理socket1时,不能处理socket3)
- 仅适合短连接场景
- selector版设计
selector的作用就是配合一个线程来管理多个channel,获取这些channel上发生的事件,这些channel工作在非阻塞模式下,不会让线程吊死在一个channel上,适合连接数特别多,但流量低的场景(low traffic)。
调用selector的select()会阻直到channel发生了读写就绪事件,这些事件发生,select方法就会返回这些事件交给thread来处理。
1.3.2 Channel与Bytebuffer的基本使用
- 从channel读取数据,即向buffer写入数据。
- buffer切换读写模式
- flIp
- clear/compact();
- ByteBuffer的正确使用方式
- 向buffer中写入数据,例如调用channel.read(buffer)
- 调用flip()切换至读模式
- 从buffer读取数据,例如调用buffer.get()
- 调用clear()/compact()切换至写模式
- 重复 1-4步骤
- ByteBuffer的内部结构
1.3.2 文件编程
- FileChannel只能工作在阻塞模式下。
- 获取
- 通过FileInputStream(只能读)、FileOutputStream(只能写)、RandomAccessFile(根据mode)获取channel;
- 强制写入
OS出于性能考虑,会将数据缓存,不是立刻写入磁盘。可以调用force(true)方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘。 - transferTo,底层会使用零拷贝,所以效率高。传入上限是最大只能为2G数据;
- Paths,Files,
-Files. copy,move,delete,
2 网络编程与NIO
2.1 网络编程
2.1.1 非阻塞vs阻塞
1 阻塞
- 在没有数据可读时,包括数据复制过程中,线程必须阻塞等待,不会占用CPU,但线程相当于闲置。
- 32位JVM一个线程32k,64位JVM一个线程1024k,为了减少线程数,需要采用线程池技术
- 但即便采用了线程池,如果有很多线程简历,但长时间inactive,会阻塞线程池中的所有线程 2 非2 非阻塞
- 在某个channel没有可读事件时,线程不必阻塞,它可以去处理其他刻度时间的channel;
- 数据复制过程中,线程实际还是阻塞的(AIO改进的地方)
- 写数据是,线程只是等待数据写入channel即可,无需等待channel通过网络把数据发送出去
3 多路复用
线程必须配合selector才能完成对多个channel可读可写事件的监控,这称之为多路复用 - 多路复用仅针对网络IO,普通文件IO没法多路复用
- 如果不用selector的非阻塞模式,那么channel读取到的字节很多时候是0,而selector保证了有刻度事件才去读
- channel输入的数据一旦准备好,会触发selector的可读事件
4 阻塞 - 默认情况下:ServerSocketChannel的accept方法和SocketChannel的read方法,都是阻塞的。阻塞是让出CPU,非阻塞是CPU空转,不会让出CPU。
- 非阻塞模式,没有连接请求,没有读请求时候也在不断循环,这样是不合理的。CPU空转过高的使用方式也是不合理的。
2.1.1 selector编程
- channel注册到selector上;
1 外部调用
final SelectionKey selectionKey = ssc.register(selector, SelectionKey.OP_ACCEPT, null);
2 内部实际是, 即selector有register方法
k = ((AbstractSelector)sel).register(this, ops, att);
执行register时,内部创建 SelectionKeyImpl(channel, selector,attach);
并将 SelectionKeyImpl对方存放到selector中的channelArray数组中
3 channel本身也维护了1个keys数组(类型:SelectionKey);
4.所以channel selector 是相互引用的
5. 事件的类型
3. SocketChannel
- ServerSocketChannel
configureBlocking 可设置通道为非阻塞模式;
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(9999));
// 当设置为false时, 则影响到 SocketChannel accept = serverSocketChannel.accept();,即获取连接的方法不会阻塞了。若无真实连接进来则会返回null;
serverSocketChannel.configureBlocking(false);
// 若SocketChannel 设置为非阻塞,则read方法就是非阻塞的了
temp.configureBlocking(false);
ByteBuffer allocate = ByteBuffer.allocate(1024);
int len = temp.read(allocate);
// 在非阻塞模式下,若未读到数据则返回0
4. Selector
- 简单非阻塞模式的缺点
- 没有连接请求时也一直在循环;
- 没有读取请求时也一直在循环;
上述两个情况都造成了系统CPU的浪费,CPU空转;
- 改进方式
Selector:用于管理Channel,监测channel上是否有事件发生,连接事件、可读事件。无活可干时则阻塞等待; - SelectionKey
- 通过SekectionKey 在将来事件发生时,可以知道是什么事件,并且知道是哪个Channel发生的事件。
- 事件类型:
- accept (OP_ACCEPT = 1 << 4,16)
ServerSocketChannel,会在有连接请求时触发 - connect (8)
是客户端,连接建立后触发。 - read (1)
表示有数据可读了 - write (4)
表示数据可以写了
- accept (OP_ACCEPT = 1 << 4,16)
- 事件使用
- ServerSocketChannel 关注 连接事件;
- 事件发生后,要么处理要么cancel,不能置之不理
// 没有事件发生,则阻塞等待,只有当有事件发生时才继续执行
int select = selector.select();
// 有事件未处理时 则是不会阻塞的,
SelectionKey.cancel() 也可取消事件
只会向 selectedKey 集添加事件,不会从中删除
// 即发生事件时,要么处理,要么删除
// cancel 当客户端断开时,会触发read事件;所以要捕获异常并cancel该key; 将key及其对应的channel从selectors中真正删除。
- accept 在非阻塞模式且无连接进入的情况下回返回null;
所以处理完一个key要将其删除; - 处理客户端断开连接
- 不管客户端是正常还是异常断开,在服务端都会收到一个读事件
- 客户端正常断开,服务端收到的read结果为 -1;也要进行key.cancel()处理