Java NIO 网络编程学习小结(1)
什么是 Java NIO?
Java NIO全称为Java Non-blocking I/O,他是指jdk1.4 及以上版本里提供的新api(所以也被称为New I/O),为所有的原始类型(Boolean类型除外)提供缓存支持的数据容器,使用它可以提供非阻塞式的高伸缩性网络。NIO通常应用于高性能高并发的场景。
BIO、NIO、AIO的特点
BIO:同步阻塞,服务器为每一个连接分配一个线程,也就是当客户端有连接请求的时候就需要启动一个线程进行处理,如果该客户端不再进行请求,服务器对应的线程就会进入循环等待,造成了不必要的线程开销。适用于连接数目少而且固定的架构。
NIO:同步非阻塞,服务器不必为每一个客户端的连接请求分配与之唯一对应的线程,而是可以利用一个单独的线程来监听多个输入通道,可以注册多个通道(Channel)使用一个选择器(Selector)。然后选择器轮询到连接有IO请求就进行处理,这种方法避免了不必要的线程开销。适用于连接数目比较多而且连接比较短的架构。
AIO:异步非阻塞,AIO引入了异步通道的概念,采用了Proactor模式,有效的请求才可以启动线程。适用于连接数量比较多而且连接比较长的架构。(笔者此方面还有待学习👀)
关于BIO网络模型
BIO(Blocking I/O):服务器为每一个连接分配一个线程,也就是当客户端有连接请求的时候就需要启动一个线程进行处理,如果该客户端不再进行请求,服务器对应的线程就会进入循环等待,造成了不必要的线程开销。
BIO网络模型的实现分为以下几步:
- 服务器端监听建立连接请求
- 客户端向服务器端发起连接请求
- 服务器端启动新线程
- 线程响应客户端
- 线程等待客户端再次请求
BIO的阻塞体现在第五步之后,服务器端的线程持续等待客户端数据,发生阻塞。
关于NIO网络模型
NIO网络模型的实现分为以下几步:
- 循环检测注册时间就绪情况(Selector:注册建立连接事件)
- 客户端发送建立连接请求
- selector启动建立连接事件处理器
- 创建与客户端连接
- 响应客户端建立连接请求
- 注册连接的可读事件
- 客户端发送请求到selector
- selector启动连接读写处理器
- 读写处理器进行处理与客户端读写业务
- 读写处理器响应客户端请求
相比BIO网络模型来说,NIO网络模型的优点首先是非阻塞式I/O模型,再有就是比较节省资源
NIO的三大核心
1.Buffer
NIO是面向缓冲区的,也就是在进行数据写入及访问操作的时候是通过缓冲区进行操作的。Buffer有四个重要的熟属性:Capacity-容量、Limit-上限、Mark-标记、Position-位置
-
Capacity:代表缓冲区的容量,设定之后不可以修改。
-
Limit:在写模式下:limit 代表的是最大能写入的数据,这个时候 limit 等于 capacity。
在读模式下:limit代表的是实际数据的个数(因为此时buffer不一定是满的)。
-
Mark:使用mark()方法,可以标记当前的位置(即mark值为当前position),当在之后调用reset()方法的时候,position就会被重置到mark的位置。(mark<=position)
-
Position:初始值为0,每写入一个值之后就自动+1,指向下一次写入的位置。
通过以下使用ByteBuffer的例子进行解释:
public class Buffertest {
public static void main(String[] args) {
/**
* 初始化了长度为10的byte类型的buffer(也可以初始化其他类型,如Int、Double等,除了Boolean类型)
* 此时:position属性指向0,limit属性指向10,capacity属性指向10
* */
ByteBuffer byteBuffer=ByteBuffer.allocate(10);
/**
* 向byteBuffer中写入三个字节,
* 此时:position指向3,limit指向10,capacity指向10
*/
/**ERROR:
* 此处有报错提示:Usage of API documented as @since 1.6+
* 需要点开file-Project Structure-module 将该项目的 language level调整到JDK6以上
*/
byteBuffer.put("abc".getBytes(Charset.forName("UTF-8")));
/**
* 将byteBuffer从写模式转化为读模式
* 此时:position指向0,limit指向3,capacity指向10
*/
byteBuffer.flip();
/**
* 从byteBuffer中读取一个字节
* 此时:position指向1,limit指向3,capacity指向10,
*/
byteBuffer.get();
/**
* 调用mark方法标记当前位置
* 此时:mark指向1,position指向1,limit指向3,capacity指向10
*/
byteBuffer.mark();
/**
* 先调用get方法读取下一个字节
* 此时:mark指向1,position指向2,limit指向3,capacity指向10
* 再调用reset方法将position重置到mark位置
* 此时:mark指向1,position指向1,limit指向3,capacity指向10
*/
byteBuffer.get();
byteBuffer.reset();
/**
* 调用clear方法,将所有属性重置
* 此时:position指向1,limit指向10,capacity指向10
*/
byteBuffer.clear();
}
}
2.Channel
Channel组件分为三类:FileChannel、DatagramChannel、ServerSocketChannel / SocketChannel
- FileChannel:文件通道,用于文件的读和写。(文件类)
- DatagramChannel:用于UDP连接的接收和发送(UDP类)
- ServerSocketChannel / SocketChannel:TCP连接的服务器端 / 客户端,ServerSocketChannel 负责监听某端口的请求。(TCP类)
Channel可以理解为通道,用于读取和写入,进行读操作的时候,将Channel中的数据填入到Buffer,进行写操作的时候,将Buffer中的数据写入到Channel中。
Channel 具有双向性、非阻塞性、操作唯一性。
服务器端通过Socket创建Channel:
ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
服务器绑定端口:
/**
*InetSocketAddress类用于包含IP地址以及端口号,常用于Socket编程
*/
//InInetSocketAddress的参数为端口号,这里以8000端口为例
serverSocketChannel.bind(new InetSocketAddress(8000));
服务器监听客户端连接,建立SocketChannel连接:
SocketChannel service=serverSocketChannel.accept();
客户端连接远程主机以及端口:
//这里InetSocketAddress第一个参数为ip地址、第二个为端口号,这里以localhost为例
SocketChannel Client=SocketChannel.open(new InetSocketAddress("127.0.0.1",8000));
以上代码需要使用try-catch语句处理或抛出IOException。
Channel设置为非阻塞方式使用ServerSocketChannel对象即:serverSocketChannel.configureBlocking(false);进行属性设置。
3.Selector
Selector(选择器)是Java NIO中能够检测一到多个通道,并能够知晓通道是否为各种事件主备就绪的组件。这样,一个单独的线程可以管理多个Channel,从而管理多个网络连接。Selector用于I/O就绪选择。
为何要使用Selector?
只需要极少的线程来处理通道,甚至可以只用一个线程处理所有的通道,节省系统开销。
Selector的使用:
创建Selector:
Selector selector = null;
try {
selector=Selector.open();
} catch (IOException e) {
e.printStackTrace();
}
将channel注册到selector上,监听读就绪事件:
/**
*这里需要用到channel对象中的register方法
*与Selector一起使用时,Channel必须处于非阻塞模式下
*socket通道可以转换为非阻塞模式,而FileChannel不可以
* -->(FileChannel不能与Selector一起使用)
*/
channel.configureBlocking(false);
SelectionKey selectionKey=channel.register(selector,SelectionKey.OP_READ);
注意register()方法的第二个参数,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件,分别是SelectionKey(选择键)中的四个状态常量:
- Connect -->连接就绪
- Accept -->接收就绪
- Read -->读就绪
- Write -->写就绪
一个通道准备好接收新进入的连接称为“接收就绪”;一个有数据可读的通道可以说是“读就绪”;等待写数据的通道可以说是“写就绪”。
SelectionKey(选择键)中还有其他的有价值的属性,获得已经就绪的SelectionKey的set集合,使用selector.selectedKeys(); 获取。
Set<SelectionKey> selectionKeys=selector.selectedKeys();
使用以下方法可以集合中就绪事件的个数:
/**
* select()方法:源码中解释为集合中的就绪事件的个数
* @return The number of keys, possibly zero,
* whose ready-operation sets were updated
*/
int selectNum=selector.select();
NIO编程实现步骤
- 创建Selector
- 创建ServerSocketChannel,并绑定监听端口
- 将Channel设置为非阻塞模式
- 将Channel注册到Selector上,监听连接事件
- 循环调用selector的select方法,检测就绪情况
- 调用selectedKeys()方法获取就绪的Channel集合
- 判断就绪事件种类,调用业务处理方法
- 根据业务需要决定是否再次注册监听事件,若需要,自第三步操作重复执行
NIO网络编程缺陷
- NIO的类库和API繁杂,使用麻烦:需要熟练掌握Selector 、ServerSocketChannel 、ByteBuffer等
- 需要具备其他的额外技能:要熟悉Java多线程编程,因为NIO涉及到Reactor模式,必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序
- 开发工作量和难度都非常大:例如客户端面临断连重连、网络断开等情况。
- 最重要的一点就是Java NIO的Bug:比如Epoll Bug,它会导致Selector空轮询,直到CPU占用100%,而现在,只是空轮询发生的概率降低而已,并没有得到完善的解决。
(使用Java NIO在本地实现了简单的聊天室功能的NIO网络编程实战以及上述内容的详细使用放于下篇文章 笔者小白 望不吝赐教)