Java NIO 网络编程学习小结(1)

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网络模型的实现分为以下几步:

  1. 服务器端监听建立连接请求
  2. 客户端向服务器端发起连接请求
  3. 服务器端启动新线程
  4. 线程响应客户端
  5. 线程等待客户端再次请求

BIO的阻塞体现在第五步之后,服务器端的线程持续等待客户端数据,发生阻塞。

关于NIO网络模型

NIO网络模型的实现分为以下几步:

  1. 循环检测注册时间就绪情况(Selector:注册建立连接事件)
  2. 客户端发送建立连接请求
  3. selector启动建立连接事件处理器
  4. 创建与客户端连接
  5. 响应客户端建立连接请求
  6. 注册连接的可读事件
  7. 客户端发送请求到selector
  8. selector启动连接读写处理器
  9. 读写处理器进行处理与客户端读写业务
  10. 读写处理器响应客户端请求

相比BIO网络模型来说,NIO网络模型的优点首先是非阻塞式I/O模型,再有就是比较节省资源

NIO的三大核心

1.Buffer

NIO是面向缓冲区的,也就是在进行数据写入及访问操作的时候是通过缓冲区进行操作的。Buffer有四个重要的熟属性:Capacity-容量、Limit-上限、Mark-标记、Position-位置

  1. Capacity:代表缓冲区的容量,设定之后不可以修改。

  2. Limit:在写模式下:limit 代表的是最大能写入的数据,这个时候 limit 等于 capacity。

    ​ 在读模式下:limit代表的是实际数据的个数(因为此时buffer不一定是满的)。

  3. Mark:使用mark()方法,可以标记当前的位置(即mark值为当前position),当在之后调用reset()方法的时候,position就会被重置到mark的位置。(mark<=position)

  4. 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

  1. FileChannel:文件通道,用于文件的读和写。(文件类)
  2. DatagramChannel:用于UDP连接的接收和发送(UDP类)
  3. 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(选择键)中的四个状态常量:

  1. Connect -->连接就绪
  2. Accept -->接收就绪
  3. Read -->读就绪
  4. 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编程实现步骤

  1. 创建Selector
  2. 创建ServerSocketChannel,并绑定监听端口
  3. 将Channel设置为非阻塞模式
  4. 将Channel注册到Selector上,监听连接事件
  5. 循环调用selector的select方法,检测就绪情况
  6. 调用selectedKeys()方法获取就绪的Channel集合
  7. 判断就绪事件种类,调用业务处理方法
  8. 根据业务需要决定是否再次注册监听事件,若需要,自第三步操作重复执行

NIO网络编程缺陷

  1. NIO的类库和API繁杂,使用麻烦:需要熟练掌握Selector 、ServerSocketChannel 、ByteBuffer等
  2. 需要具备其他的额外技能:要熟悉Java多线程编程,因为NIO涉及到Reactor模式,必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序
  3. 开发工作量和难度都非常大:例如客户端面临断连重连、网络断开等情况。
  4. 最重要的一点就是Java NIO的Bug:比如Epoll Bug,它会导致Selector空轮询,直到CPU占用100%,而现在,只是空轮询发生的概率降低而已,并没有得到完善的解决。

(使用Java NIO在本地实现了简单的聊天室功能的NIO网络编程实战以及上述内容的详细使用放于下篇文章 笔者小白 望不吝赐教)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值