NIO10——Non-blocking Server

 即使你了解 Java NIO 的非阻塞功能是怎么工作的,设计一个非阻塞的 Java NIO server 依然很困难。和阻塞式的 IO 相比,非阻塞的 IO 有多个挑战。下面就讨论一下非阻塞的 Server 面临的主要挑战,并提供一些可能的解决方案。
 虽然这里描述的方案是围绕着 Java NIO 的,我想这些方案也同样适用于那些类似于 Selector 的结构的场景。这些数据结构是由底层操作系统支持的,因此也适用于其他的编程语言。

 1、Non-blocking IO Pipelines
 非阻塞的 IO pipeline 是一个处理非阻塞 IO 的组件链,包括以非阻塞的方式进行 IO 的读和写,这里有个示意图:
在这里插入图片描述
 组件使用 Selector 检测 Channel 中何时有要读取的数据,一旦检测到有数据,组件就读取这些输入的数据并根据输入生成一些输出,再将输出的数据写入通道。
 一个非阻塞的 IO pipeline 不必同时负责读和写,可以让一些 pipeline 只负责读数据,另外的一些 pipeline 只负责写数据。
 上图仅显示了一个组件。无阻塞 IO 管道可能具有多个组件来处理传入的数据。无阻塞 IO 管道的长度取决于管道需要执行的操作。
 一个非阻塞 IO 管道也可能同时从多个 Channel 中读取数据。例如,从多个 SocketChannel 中读取数据。
 上图中的控制流程得到了简化,它是一个通过选择器从通道中读取可读数据的组件。并不是 Channel 将数据推送到 Selector ,然后组件再从 Selector 中获取数据, 但是 Java NIO 也支持这样。

 2、Non-blocking 和 Blocking IO Pipelines
 非阻塞 IO 和阻塞 IO pipeline 的最大的区别就是如何从底层 Channel(Socket 或 File) 中读取数据。
 IO 管道通常从某个流(从 Socket 或 File)中读取数据,并将读取的数据拆分为一系列的合乎逻辑的消息。这类似于将数据流分解为若干个令牌然后使用令牌解析器进行解析。相反,如果将数据流分解为了更大的消息,那将调用将该流分解为消息阅读器消息的组件。 下面是消息阅读器将流分成消息的说明:
在这里插入图片描述
 阻塞的 IO pipeline 可以使用类似于 InputStream 的接口,但一次只能从底层 Channel 中读取一个字节,并且在数据准备好之前一直都是阻塞的。这形成了一个阻塞的消息阅读的实现。
 流的阻塞式的接口大大简化了消息读取的实现。阻塞式的消息读取不需要处理流中没有数据可读的情况,也不需要处理流中只读取到部分消息但在过后需要继续解析该消息的情况。
 同样,阻塞式的消息编写器(将消息写入流中的组件)也不必处理仅写入消息的一部分且稍后必须恢复消息写入的情况。

 3、Blocking IO Pipeline 的缺点
 尽管阻塞式的消息阅读器更易于实现,但它有一个不幸的缺点,即需要为每个需要拆分为消息的流分配一个单独的线程。之所以需要这样做,是因为每个流的 IO 接口都会阻塞,直到其中有数据可读取。这意味着单个线程无法尝试在从一个流中读取不到数据时,则从另一个流中读取。因为一旦线程尝试从流中读取数据,若读取不到数据,这个线程就会阻塞,直到该流中有数据可读取为止,也就是说至始至终这个线程都只能作用在这一个流上。
 如果这个 IO 管道是必须处理大量并发连接的服务器的一部分,则该服务器将会为每个活动的传入连接建立一个线程。如果服务器在任何时候都只有几百个并发连接,这可能不是问题。但是,如果服务器具有数百万个并发连接,则这种设计就不太适合了。每个线程都会占用 320K(32 bit JVM) 到 1024K(64 bit JVM) 的栈内存,因此,1百万个线程将占用近 1TB 的内存,这还是在服务器未使用任何内存处理传入的消息之前(比如在消息处理期间为使用的对象分配内存)。
 为了减少线程数量,许多服务器使用线程池的设计,线程池中的每个线程一次处理一个入站的连接,处理该连接时会去读取该连接的消息。入站的连接保留在一个队列中,线程池中的线程按入站连接放入队列的顺序处理来自每个入站连接的消息。如下图所示:
在这里插入图片描述
 但是,这样的设计要求入站的连接要适度地发送数据。如果入站的连接长时间处于非活动状态,则大量的非活动连接占用着线程,还是会阻塞线程池中的所有线程。 这意味着服务器的响应速度变慢甚至会不响应。
 一些服务器设计试图通过使线程池中的线程数具有一定的弹性来缓解此问题。 例如,如果线程池中的线程用完了,则线程池可能会再创建更多线程来处理负载。 此解决方案意味着需要大量的慢速连接才能使服务器无响应。 但是请记住,您可以运行多少个线程仍然存在上限。 如果有1百万个慢连接,则扩展效果仍然不佳。

 4、基本的非阻塞 IO Pipeline 设计
 一个非阻塞的 IO pipeline 可以使用单个线程从多个流中读取消息,这要求这些流能够切换到(或者说支持)非阻塞的模式。在非阻塞模式下,当试图从流中读取数据时,流可能会返回 0 个或多个字节。如果流中没有数据可以读取就返回 0 个字节,当有数据可读的时候就会返回 1 个或多个字节。
 为了避免检查要读取的 0 字节流,我们使用 Java NIO 选择器。 可以向一个选择器注册一个或多个 SelectableChannel 实例。 当调用选择器的 select() 或 selectNow() 时,选择器会提供实际上有待读取数据的 SelectableChannel 实例。 如下所示:
在这里插入图片描述
 5、读取部分消息
 当从 SelectableChannel 中读取数据块时,其实我们不知道这个数据块是比一个完整的消息多还是少,一个数据块可能只是一个消息的一部分,也可能正好是一个完整的消息,也可能是多于一个消息的数据,比如 1.5 或2.5 个消息,下面是各种部分消息的可能性:
在这里插入图片描述
 在处理读取的部分消息时有两个挑战:
 1️⃣检测数据块中是否有完整的消息
 2️⃣在消息的其他部分到达之前如何处理部分消息

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值