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

从今年 3 月份看到有人打算用 Rust 重写 ZeroMQ、我开始认真学习 Rust 语言,到后来 6 月份开始着手实现,再到如今 0.1 版即将达成,先后也有小半年了。今天,我打算在这里把当前的设计总结一下,也顺便试图招募志愿者一块儿来作开发。html

项目地址:https://github.com/zeromq/zmq.rs前端

没错木哈哈,被收编成了 ZeroMQ 官方项目了,因此必定来一块儿作哦。python

关于本文:8 月份的草稿啊!这都年末了,真是醉了。下个月(2015 年 1 月)北京有个 Rust 的聚会,打算分享这个项目,因此如今果断删掉未完成章节,把能发的先发出来。git

Rust 语言

突然意识到以前几篇《Rust 语言学习笔记》一直没有介绍过 Rust 语言,这里一并补齐。程序员

Rust 语言是几种下一代编程语言中较为优秀的一款系统级编程语言,最显著的特色就是运行时几乎不崩溃、高并发无数据竞争,还有就是跑的贼拉快。github

相较于应用级编程语言如 Java 或 Python,Rust 做为一款系统级编程语言,天生被设计用来开发系统软件诸如操做系统、设备驱动或编译器等,与 D 语言、Go 语言齐名(你们对 Go 语言是不是系统级编程语言有争议),是 C++ 继位者的候选人。所以,Rust 理所应当是编译型的语言。依托于 LLVM,Rust 在编译器(Rust 编译器前端系 Rust 语言自己实现,即所谓的自举)上大下功夫,经过“全部权”的概念将内存管理的最佳实践整合在了编译期,在编译期保证了运行时的内存安全,且没有使用垃圾回收机制(垃圾回收是 Rust 的一种可选的额外工具),运行速度不打折扣。这种对内存管理的高要求,也让程序员能够更容易地用 Rust 语言编写正确的高并发程序,天然地实现相似 Erlang 的并发模型。另外受 Haskell 的熏陶,Rust 语言对函数式编程也是很是友好的——我本身认为要比 Python 函数式多了。另外,Rust 的语法一点也不诡异,若是您写过 C/C++/Java/Python/Ruby,您会以为不少语法似曾相识,上手较快。编程

很少说了,毕竟不是《半小时,介绍 Rust》——有兴趣你们能够移步这里继续阅读。segmentfault

ZeroMQ

ZeroMQ 乍一看是一种消息队列,但实际上它并非传统意义上的消息队列——开一个消息服务器,全部消息都通过它,能够离线什么的。上述传统消息队列主要部分只是 ZeroMQ 指南中的一个叫作泰坦尼克的模式概念,ZeroMQ 的库中甚至都没有这种模式的实现。其实 ZeroMQ 是一种内嵌式的网络库,关注于怎样将程序连在一块儿。简单来讲,您能够认为 ZeroMQ 是对普通 socket 的一种封装和抽象,将常规的通讯工具封装成了 ZeroMQ 的 socket。您能够经过 ZeroMQ 的 socket 来实现通讯,ZeroMQ 的 socket 则内置了消息排队、流量控制、收发模型、断线重连等机制,方便您设计出本身适合的消息通讯模型。安全

介绍再细一点。服务器

每一个 ZeroMQ 的 socket 都有一个类型,决定了它内置的收发模型,好比建立一个用于发送请求的 REQ

pythonsocket = context.socket(zmq.REQ)

REQ 就限制了,这个 socket 必须得先发送一个消息,而后才能——且必须——接收一个消息。发一个请求、收一个响应,绝对不能乱套:

python# print(socket.recv_multipart()) -- 这会失败

socket.send_multipart(["Hello"])
# socket.send_multipart(["Hello2"]) -- 这会失败

print(socket.recv_multipart())
# print(socket.recv_multipart()) -- 这会失败

socket.send_multipart(["Hello2"])

固然了,在收发消息以前,咱们得先把咱们的 socket 跟别的 socket 连上才行:

pythonsocket.connect("tcp://192.168.3.27:8868")

意思就是说,跟监听在 192.168.3.27:8868 上的 ZeroMQ socket 创建 TCP 连接。另外,ZeroMQ 还支持进程间通讯(ipc)、进程内通讯(inproc)和多播。

有趣的是,ZeroMQ 容许先 connect,再建立监听端的 socket(由于内置了断线重连机制):

pythonserver_socket = context.socket(zmq.REP)
server_socket.bind("tcp://192.168.3.27:8868")

这个 REPsocket 也只是又一个普通的 ZeroMQ 的 socket,只不过 REP 的要求与 REQ 正好相反,必须得先收再发——收一个请求,返回一个响应。这里咱们就很少作示例了。

每个 ZeroMQ 的 socket 都(特例除外)能够屡次作 connect 或/和 bind,只要 socket 之间创建了链接,他们就能够在各自类型的约束下实现通讯。对 REQ 来讲,发送时会轮流使用全部链接,接收时必须从上一次发送去的链接来接收;而对 REP 来讲,全部链接上进来的请求都会被公平的排队,REP 会公平地接收,而发送时则保证将响应发送回请求的来源。这些就是所谓的收发模型,这对用户都是透明的,您只须要调用 sendrecv 就能够了。

除了请求-响应模式的 REQREP,ZeroMQ 经常使用的一些 socket 类型还有:高级请求-响应模式的 DEALERROUTER、发布-订阅模式的 PUBSUB、流水线模式的 PUSHPULL 等。

实际应用中,咱们一般会将上述这些基本模式综合使用,组成各类各样的高级模式——好比一开头提到的泰坦尼克模式——去解决不少实际中的问题。这些就很少说了,你们有兴趣能够再次移步这里来观摩学习。

Task 模型的小纠结

该进入正题了。这一部分跟以前的那篇英文博文的第一部分对应的。这一部分和接下来的一部分主要作两个关键的技术选型。

我对 zmq.rs 的设计“借鉴”了 libzmq 不少——为了保留借鉴痕迹便于参照,有些名词我连名字都没有改(呃,这句英文博文里没有……)。以前的几篇《Rust 语言学习笔记》中,其实我已经在试图搭 zmq.rs 的架子了,有个关键性的问题在当时就已经出现了:

怎么样正确使用 Rust 的 Task

目前来看,针对于个人项目,C++(libzmq 的实现语言)和 Rust 有两点重要的不一样:

  1. Rust 尚未提供一个可用的、用于轮询文件描述符的 select() 接口,以及
  2. Rust 默认提供了一个可选的库 libgreen ——提供了微线程的实现。

做为一个 Gevent 的重度用户,我天然而然地选择了微线程模型,经过建立大量 Task 来实现并发,底层与直接使用 select() 来实现异步并发并没有实质区别。这听起来很是理想,但遗憾的是 libgreen 并非 Rust 的默认选项—— libnative 才是。这样的话,若是用户选择默认使用 libnative,那么 zmq.rs 轻轻松松就能够帮用户建立数百个操做系统级的线程,由于 libnative 1:1 的模型下,一个 Task 就是一个线程嘛。

这就不是一件很使人愉快的事了。那咱们先放下这个,看看另一条路吧:彻底借(zhao)鉴(ban)libzmq,Rust 里缺什么再补什么,好比 select()。1:1 的 libnative 模型天然就不会有问题了,由于咱们将要本身从新实现异步,因此开启的 Task 数屈指可数。虽然没有了协做式的异步编码优点,但这条路上的代码也能够写的干净整洁——至少不会比 libzmq 差吧。但是,这个时候假如用户又选择了使用 libgreen,又会怎么样呢?咣当!几个协做式的异步 Task 在分别执行一段手动实现的异步代码,何其诡异!由于协做式的异步 Task(即微线程)是应该被主事件循环驱动的,而不是用来跑一个本身写的事件循环!微线程原本就是设计用来将异步代码同步化、提升可读性、下降编程复杂度,而不是像如今这样又全都搞回去了。libgreen 在底层已经使用了 libuv 做为主循环来调度全部的 Task,而咱们如今为了实现 select(),还得想办法把 libuv 底层的接口给暴露上来。最后,咱们还得使用这些接口,从新本身实现一遍异步任务调度。

就我来看,上述方法有多是在 libgreen 下实现 select() 的最合理的方法,但这让我以为很是别扭,感受好像把 1:1 的 libnative 跟 M:N 的 libgreen 作出统一的接口是个糟糕的主意。这里不深究了,问题交给 Rust 1.0 以后打算实现 select() 的人去吧。这里呢,我选择了第一种方案,也就是把 Task 当微线程的方案,由于目前来看这种方案须要写的代码更少,并且感受更像是 Rust 指望的样子。

补充:其实也没有必要整个程序全都一致要求要么 libnative 要么 libgreen 的,为何不能混着用呢?个人 zmq.rs 内部本身搞一个 libgreenScheduler 好了,本身强制使用微线程模型;调用的人爱咋咋地呗。因此也就不纠结了。

基于接口的继承

这个事儿也让我纠结了好久,也别扭了好久:Rust 里的继承只包括成员方法,不包括成员变量。换句话说,Rust 里没有类,只有接口(Trait)和数据结构(Struct),struct 能够实现 trait,trait 能够继承 trait,可是 struct 不能继承 struct。这让用惯了 Python 的我非!常!不!爽!怎么能这样呢,父类里根本不能定义数据!难道父类搞个多态还得每次要数据的时候调用一个 self.getData() 吗?这听说仍是面向对象编程的一个演化方向呢,求高人点解啊……

这里碰到的问题主要是定义 SocketBase 啦,人家 libzmq 里洋洋洒洒几个文件,轻松搞定了漂亮的继承关系。到我这里死活搞不漂亮了:做为父类的 SocketBase 必须得本身负责一部分数据,而子类跟 SocketBase 又是息息相关的。

怎么办嘞?

拆!仍是顺应了那句老话,先组合后继承。如今呢,个人 socket 长这样:

zmq.rs 的 socket 类图

因此呢,ReqSocketRepSocketbind() 里只有相同的一句:self.base.bind()connect() 也是相似。而对于 send()recv() 的实现,两种不一样的 socket 则有了不一样的实现,且调用 self.base 的函数也不尽相同。

用这种组合的方式当然解决了当下的问题,但不知之后能不能学会 Rust 里基于接口的继承。

相关文章
相关标签/搜索