Netty框架学习之路(一)—— Java网络IO模型

前言

《Unix网络编程:卷1》中介绍了5中IO模型,JAVA作为运行在宿主机上的程序,底层也遵循这5中I/O模型规则。这5中I/O模型分别是:

  • 阻塞式IO
  • 非阻塞式IO
  • I/O复用
  • 信号驱动式IO
  • 异步IO

按POSIX标准来分,IO分为同步和异步,上面的前4钟都属于同步IO。

在Unix系统中,操作系统的IO操作是一个系统调用recvfrom(),即一个系统调用recvfrom包含两步,等待数据就绪和拷贝数据。

同步及阻塞相关概念

同步和异步是指访问数据的机制,同步一般指主动请求并等待IO操作完毕的方式,当数据就绪后在读写的时候必须阻塞。

异步则指主动请求数据后便可以继续处理其它任务,随后等待IO操作完毕的通知,这可以使进程在数据读写时也不阻塞。

阻塞和非阻塞是指进程访问的数据如果尚未就绪,进程是否需要等待,简单说这相当于函数内部的实现区别,也就是未就绪时是直接返回还是等待就绪。

这四个概念两两组合便有了四种IO机制,我们以平时常见的下载文件场景梳理一下这四种机制:

  • 同步阻塞:
    小明同学提交完下载任务后,目不转睛地盯着下载进度条,直至完成。

  • 同步非阻塞:
    小明同学提交完下载任务后便去干别的事情了,每隔一段时间查看一下下载进度条,直至下载完成。

  • 异步阻塞:
    小明同学提交完下载任务后,什么事情都不干一直在等待下载完成的提示音。

  • 异步非阻塞:
    小明同学提交完下载任务后就去干别的事情了,只需要接收“叮”声通知即可。

IO模型

阻塞式IO

当应用程序发起IO请求之后,操作系统就要处理系统调用recvfrom(),在这个过程中,操作系统需要等待数据就绪(数据可能来自别的应用程序的输入或者网络),应用程序则不再处理别的事情,而是一直等待(即阻塞状态)数据就绪,然后操作系统完成IO操作,然后recvfrom()才方法返回,应用程序才继续执行,这就是阻塞式IO模型。

服务端代码:

//创建套接字,绑定并监听指定端口
ServerSocket serverSocket = new ServerSocket(20000);
//请求未到达之时,线程阻塞于此
client = serverSocket.accept();
// 客户端连接成功,输出提示
System.out.println("客户端连接成功");
// 启动一个新的线程处理客户端请求
new Thread(new ServerThread(client)).start();

// 子线程中处理客户端的输入
class ServerThread implements Runnable {
    .....
    @Override
    public void run() {
        boolean flag = true;
        while (flag) {
            // 读取客户端发送来的数据
            String str = buf.readLine();
            // 回复给客户端 get 表示收到数据
            out.println("get"); 
        }
    }
}

客户端代码:

Socket client = new Socket("127.0.0.1", 20000);
boolean flag = true;
while (flag) {
    // 读取用户从键盘的输入
    String str = input.readLine();
    // 把用户的输入发送给服务端
    out.println(str);
    // 接受到服务端回传的 get 字符串
    String echo = buf.readLine();
    System.out.println(echo);
    }
}

从上面的代码可知,每当有的client建立连接时,server就会为每个 client 开启一个 thread,这种one-thread-per-client 的模式对于 server 而言压力是很大的。即使使用线程池的技术来限制线程个数,这种 blocking-IO 的模型还是没办法支撑大量连接。

上面这种 one-thread-per-client 的模式无法支撑大量连接的主要原因在于 readLine 会阻塞IO,即在readLine没能够读取到数据的时候,会一直阻塞线程,使得线程无法继续执行,那么 server 为了可以同时处理多个 client,只能同时开启多个线程。

非阻塞式IO

针对上述阻塞式IO的短处,如果当数据准备完毕之前线程不被阻塞,那也就没必要为每一个client都创建一个线程了。

Java 1.4 之后引入了一套 NIO 接口。NIO 中最主要的一个功能就是可以进行非阻塞 IO 操作。如果没能够读取到数据,非阻塞 IO 不会阻塞线程,而是直接返回 0。这种情况下,我们可以根据线程的返回值判断数据是否准备好,就可以处理其他事情,而不会被阻塞。

那么问题来了,我们怎么知道数据有没有准备好呢?最直接的方法就是轮询,每隔一段时间便去检查数据是否准备好。

非阻塞 IO 并不会被阻塞,但是它仍然不断的调用函数检查数据是否已经可读,这种现象在代码中是以这种形式展现:

while((str = read()) == 0) {

}
// 继续读取到数据之后的逻辑。

可以看出,虽然非阻塞 IO 不会阻塞线程,但是由于没有数据可读,线程也没有办法继续执行下面的逻辑,只能不断的调用判断,等待数据到来。这种情况下称为同步 IO。所以综上,NIO 本质上是一个非阻塞同步 IO。

IO多路复用

由于 NIO 不会因为数据还没有到达而被阻塞,那么就没有必要每一个 client 都分配一个 thread 不断去轮询判断是否有数据可读。可以专门使用一个 thread 监听所有的 client 连接,然后由这个 thread 循环判断是否有某个 client 的数据可读,如果有就告知其他 thread 某个 client 连接由数据可读。这种行为就被称之为 IO 复用。在 NIO 中提供了 Selector 类来监听所有 client 连接是否有数据可读。

使用 Selector 来实现 IO 复用,只有一个 thread 需要关心数据是否到来,其他线程等待通知就好。如此一来,只有监听线程会一直循环判断,并不会占据太多 CPU 资源。提到 NIO 中的 Selector,不得不说一下 Linux 编程中的 IO 复用,因为 NIO 中的 Selector 底层就是使用系统级的 IO 复用方案。

Linux 系统的 IO 复用实现方案有 2 种:select和epoll。在 Linux 2.6+ 的版本上 NIO 底层使用的是 epoll,在 2.4.x 的版本使用的是 select 函数。epoll 函数在性能方面比 select 好很多,这里可以不关心 Linux 编程具体细节。值得一提的是,Java 的 netty 网络框架底层就是使用 NIO 技术。

信号驱动式IO

信号驱动式IO就是指进程预先告知内核,当某个描述符上发送事件时,内核使用信号通知相关进程。信号驱动式IO并没有实现真正的异步,因为通知到进程之后,依然是由进程来完成IO操作。

异步IO

上述IO多路复用模型中,当数据准备完毕便去通知其他线程开始读取数据,而读取数据的过程中,线程不能处理别的任务,也就是说IO多路复用其实是一种同步IO。

JDK 7 中新增了一套新接口 AIO(Asynchronous IO)。AIO 独特之处在于当发起 IO 操作之后,线程不用等待 IO 读取完毕,而是可以直接返回,继续执行其他操作。等到数据读取完毕之后,系统会通知线程数据已经读取完毕。这种发起 IO 操作,但是不必等待数据读取完毕的 IO 操作称之为异步 IO。如果使用 AIO,一个线程可以同时发起多个 IO 操作,这就意味着,一个线程可以同时处理多个请求。著名的 web 服务器 Nginx 就是用了异步 IO。

总结

到目前为止,我们介绍了同步和阻塞的概念和5种网络IO模型,理解了这些 IO 模型的概念对于编写代码有很大的帮助。

你可能感兴趣的:(netty学习之路,netty学习之路)