在进入主题之前先看个 Java 网络编程的一个简单例子:代码很简单,客户端和服务端进行通信,对于客户端的每次输入,服务端回复 get。注意,服务端可以同时允许多个客户端连接。
服务端端代码:
- // 创建服务端 socket
- 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);
- }
- }
考虑到完整的 Java 示例代码太过庞大影响阅读,所以这里不完整贴出,如果需要在 github 直接下载,这里是下载地址。
可以看到,server 为了能够同时处理多个 client 的请求,需要为每个 client 开启一个 thread,这种 one-thread-per-client 的模式对于 server 而言压力是很大的。假设有 1k 个 client,对应的 server 应该启动 1k 个 thread,那么 server 所耗费的内存,以及 thread 切换时候占用的时间等等都是致命伤。即使使用线程池的技术来限制线程个数,这种 blocking-IO 的模型还是没办法支撑大量连接。
每个 client 都需要一个 thread 来请求处理。
上面这种 one-thread-per-client 的模式无法支撑大量连接的主要原因在于
会 阻塞 IO,即在
- readLine
没能够读取到数据的时候,会一直阻塞线程,使得线程无法继续执行,那么 server 为了可以同时处理多个 client,只能同时开启多个线程。
- readLine
所以,Java 1.4 之后引入了一套 NIO 接口。NIO 中最主要的一个功能就是可以进行非阻塞 IO 操作:如果没能够读取到数据,非阻塞 IO 不会阻塞线程,而是直接返回 0。这种情况下,线程通过返回值判断数据还没有准备好,就可以处理其他事情,而不会被阻塞。
上图是阻塞 IO 和非阻塞 IO 的区别,可以看出虽然 非阻塞 IO 并不会被阻塞,但是它仍然不断的调用函数检查数据是否已经可读,这种现象在代码中是以这种形式展现:
- while((str = read()) == 0) {
- }
- // 继续读取到数据之后的逻辑。
可以明白,虽然非阻塞 IO 不会阻塞线程,但是由于没有数据可读,线程也没有办法继续执行下面的逻辑,只能不断的调用判断,等待数据到来。这种情况下称为同步 IO。所以综上,NIO 本质上是一个非阻塞同步 IO。
由于 NIO 不会因为数据还没有到达而被阻塞,那么就没有必要每一个 client 都分配一个 thread 不断去轮询判断是否有数据可读。可以使用一个 thread 监听所有的 client 连接,由这个 thread 循环判断是否有某个 client 的数据可读,如果有就告知其他 thread 某个 client 连接由数据可读。这种行为就被称之为 IO 复用。 在 NIO 中提供了
类来监听所有 client 连接是否有数据可读。
- Selector
使用
来实现 IO 复用,只有一个 thread 需要关心数据是否到来,其他线程等待通知就好。如此一来,只有监听线程会一直循环判断,并不会占据太多 CPU 资源。提到 NIO 中的
- Selector
,不得不说一下 Linux 编程中的 IO 复用,因为 NIO 中的
- Selector
底层就是使用系统级的 IO 复用方案。
- Selector
Linux 系统的 IO 复用实现方案有 2 种:
- select
- epoll
在 Linux 2.6+ 的版本上 NIO 底层使用的是
,在 2.4.x 的版本使用的是
- epoll
函数。
- select
函数在性能方面比
- epoll
好很多,这里可以不关心 Linux 编程具体细节。值得一提的是,Java 的 netty 网络框架底层就是使用 NIO 技术。
- select
回顾一下 NIO 中:使用监听线程调用
函数来监听所有请求是否有数据到达,如果有数据则通知其他线程来读取数据。这里在线程读取数据的过程中,线程在数据没有读取完毕之前是处于阻塞状态,只有数据读取完毕之后线程才可以继续执行逻辑。之前说过,这种称之为同步 IO。JDK 7 中新增了一套新接口 AIO(Asynchronous IO)。
- select
AIO 有一个神奇的特性:当发起 IO 操作之后,线程不用等待 IO 读取完毕,而是可以直接返回,继续执行其他操作。等到数据读取完毕之后,系统会通知线程数据已经读取完毕。这种发起 IO 操作,但是不必等待数据读取完毕的 IO 操作称之为异步 IO。如果使用 AIO,一个线程可以同时发起多个 IO 操作,这就意味着,一个线程可以同时处理多个请求。著名的 web 服务器 Nginx 就是用了异步 IO。关于更多的细节,可以参考下我的另一篇文章 <Apache--MPMs && Nginx事件驱动>。
到目前为止,文章解释了阻塞/非阻塞 IO,同步/异步 IO 的区别,谈起 IO 模型,不可避免会涉及 Linux 的 5 种 IO 模型
除去信号驱动 IO没有提及,其他 4 种主要的 IO 模型都有所解释,理解了这些 IO 模型的概念对于编写代码有很大的帮助。
Java学习交流QQ群:589809992 禁止闲聊,非喜勿进!
来源: http://www.cnblogs.com/aishangJava/p/7595713.html