设计 zmq.rs ——用 Rust 实现的 ZeroMQ(二)

写在前面:html

Rust 1.0 临近,libgreen 因为统一接口代价太大以及其伪轻量级的事实被降级为不推荐的社区项目,zmq.rs 项目也面临着一次基于 mio 的从新设计——除非更合适的协程实现能当即出现。因此呢,草稿箱里积存了数月的“命令通道”部分再也不有意义了,但考虑到新的设计中也将有相似的概念,仍将其贴出来。git

命令通道(该设计即将删除!!)

以前的类图显示了几个重要的结构:socket 接口、不一样类型 socket 的实现,以及他们的共享功能 SocketBase。可是,这几个结构其实都运行在用户的 Task 里,也就是拥有 socket 的代码所在的 Task。前面也说了 zmq.rs 会建立好多 Task 的,下面就介绍几个跑在独立 Task 中的结构。github

咱们就按当时实现的顺序来吧。segmentfault

TcpListener 是用来接受链接请求的,每次调用 bind() 时,SocketBase 就会先建立一个 Task,而后在其中建立一个 TcpListener。咱们知道,Rust 中最经典的任务间通讯方式就是通道,无不例外,TcpListenerSocketBase 之间的数据交换也是经过通道,这个通道叫作命令通道并发

Task和命令通道示意图

命令通道目前是单方向的,命令通常由不一样的独立的 Task 发给 SocketBase 对象。上述类图中的 txrx 就是命令通道的两个端点,SocketBase.process_commands() 会从 rx 端接收命令并执行命令;而发送端 tx 有多个副本,是一个多发单收的通道,当建立 TcpListener 的同时,SocketBase 会克隆一个 tx 出来交给新建立的 TcpListener异步

TcpListener 在单独的 Task 中就能够随心所欲了。目前的实现是一个死循环,每次循环会异步阻塞在等待 bind() 给定端口上的新链接请求。每当有新的链接请求,TcpListener 都会接受,建立一个 TCP 链接,而后建立一个链接处理的任务(~~稍后提到~~),最后经过命令通道告知 SocketBaseTcpListener 会一直重复这样的行为,一直到……何时呢?socket

为了避免让 TcpListener 的死循环影响到程序的正常结束,咱们必须在 SocketBase 被销毁时(之后若是有了 unbind 还得再单独考虑),尽快跳出死循环。还好 Rust 的通道考虑到了这一点,提供了这样的功能:在一个通道的接收端被销毁后,继续往发送端发送数据的操做将会失败。因此咱们只要能往 tx 里发一个空白命令,若是发送失败就能够跳出死循环了。所以,我在 TcpListener 的阻塞调用 self.acceptor.accept() 上加了一个 1 秒(也许会改为 100 毫秒)的超时,而后每次循环都会发送一个空白命令。这样,当用户的 Task 退出以后,TcpListener 最多再存活 1 秒钟,而后也会自我销毁了。ide

看完了命令通道的发送端,咱们再来看一下接收端。spa

SocketBase 就没有 TcpListener 那么自由自在了,由于 SocketBase 是存活在用户的 Task 中,这也就意味着,它不能无节制的阻塞,否则用户还怎么作本身的事情。因此呢,当用户在执行本身的代码时,咱们只能等着先不去接收命令,这时命令就会在命令通道里排队。一旦 SocketBase 有机会执行了,好比用户调用了 recv() 或是 connect(),咱们就抓紧机会,先将待处理的命令一并处理干净,而后再去完成用户的请求。.net

这个设计跟 libzmq 是一致的:SocketBase.process_commands() 接受一个布尔型的参数 block,来决定是阻塞式地处理一个命令——若是没有命令就一直阻塞、等到一个再处理,仍是非阻塞式地尽可能去处理一个命令——若是有现成的命令就处理且返回真,不然当即返回假,毫不多等。处理命令是在一个循环里用非阻塞的方式进行的,每次循环先试图处理一个命令,而后一样是非阻塞地去调用底层。

翻天覆地

正如一开始所说,libgreen 已经不复存在了,因此以上全部基于轻量级协程的并发设计就不合适了,由于 Task 的开销太大——即便是继续使用 green-rs。关于这一段故事,我在这些幻灯片里也记录了,稍后我再把明天的录像发出来,你们有兴趣能够看看。

在新的设计出来以前,这个话题就先暂告一段落了。

相关文章
相关标签/搜索