要检查通道是否准备好写入,可以使用选择器注册通道。 但是,我们不希望使用 Selector 注册所有 Channel 实例。 想象一下,如果所有 100*10000 个通道都在 Selector 中注册,然后调用 select() 时,大多数这些 Channel 实例都是可写入的(它们大多是空闲的,还记得吗?),然后还必须检查所有这些连接的 Message Writer 以查看它们是否有要写入的数据。
为了避免检查没有数据需要写入的通道的 Message Writer 实例,我们使用这两步方法:
当消息写入消息编写器时,消息编写器将其关联的 Channel 注册到选择器(如果尚未注册)。
当服务器有时间时,它会检查选择器以查看哪些已注册的 Channel 实例已准备好进行写入,对于每个写就绪通道,请求其关联的消息编写器将数据写入通道。 如果 Message Writer 已经将其所有消息写入了其 Channel ,则 Channel 将从 Selector 中注销。
这样,只有具有要写入消息的 Channel 实例才能实际注册到 Selector 。
总结非阻塞服务器需要不时检查传入数据,以查看是否收到任何新的完整消息。 服务器可能需要多次检查,直到收到一条或多条完整消息,仅仅检查一次是不够的。
同样,非阻塞服务器需要不时检查是否有任何要写入的数据。 如果是,则服务器需要检查相应的连接是否已准备好写入。 仅在第一次排队消息时检查是不够的,因为开始的时候消息可能只是数据的一部分。
总而言之,非阻塞服务器最终需要定期执行三个“管道”:
读取管道,用于检查来自打开连接的新传入数据。
处理管道,处理收到的任何完整消息的进程管道。
写入管道,检查是否可以将传出消息写入打开的连接。
这三个管道在循环中重复执行,还可能稍微优化它们的执行。 例如,如果没有排队的消息,可以跳过循环执行写入管道。 或者,如果我们没有收到新的完整消息,也许可以跳过处理管道。
这是一个完整服务器循环示意图:
如果仍然觉得这有点复杂,可以查看 GitHub 仓库:https://github.com/jjenkov/java-nio-server
也许看看代码有助于帮助理解。
GitHub 存储库中的非阻塞服务器实现使用具有 2 个线程的线程模型。 第一个线程接受来自 ServerSocketChannel 的传入连接。 第二个线程处理接受的连接,即读取消息,处理消息和将响应写回连接。 这个2线程模型如下所示: