--- 摄于 2017 年 9 月 藏川线前段
我们必须得承认,哪怕经历了几十年的构建,底层网络依然是不安全的存在,工程师在硬件不安全的基础上构建一个相对安全的网络环境,而相对安全的概念实际上是非常微妙的。我们暂且不关心硬件上的意外丢包、断电、链路损坏等场景,将硬件层当成是安全的,在应用层、网络层依然有各种意外情况发生,例如:
在工程上,依然只能用尽力而为的态度去做,假定底层网络不会出现上诉问题,那么它的正常打开、通信、延时、关闭流程需要一个标准的协议去定义。
记得在几个月之前,我在公司线下技术分享会中有过一个关于这个主题的分享,但那只是对于 Bitcoin 的网络协议的介绍,以及对未来我们自己 P2P 网络的畅想。更深入的地方因为本身没做过实现,就略过了。p2p 真正要实现的其实是无中心化、可自发现的一种网络,其他的根本不重要。
这系列讲的主要是 P2P 框架,对于具体协议的实现,只涉及多路复用协议 —— yamux 的实现问题,更上层的应用协议,就不在这个系列内了。
那么,为什么我们要实现首先实现一个框架,然后再在其之上构建用户层协议呢?
在应用层,想要保证网络协议的向后兼容,或者说,想要更轻松地切换用户层协议,就必须先对底层的通用协议进行统一抽象,然后在统一抽象之上,就可以定义用户层协议的版本、协议号、协议编码规则等等内容,同时,我们也可以在统一抽象之上,做单连接多协议并存的实现。
那么,不做抽象和做抽象对支持多协议并行,协议多版本并行的区别在什么地方呢?
不做抽象,如果要支持多协议并行,协议多版本并行,伪代码如下:
if proto_name = "a" {
if proto_version = "0.0.1" {
do_a_something()
} else if proto_version = ... {
do_a_something()
}
...
} else if proto_name = "b" {
if proto_version = "0.0.1" {
do_b_something()
} else if proto_version = ... {
do_b_something()
}
...
}
...
}
几乎所有判断,都必须在同一个地方去验证,然后根据分支去执行,如果版本号多或者并行协议多,那分支代码就会越来越膨胀,同时,当想要删除某个协议或者版本支持的时候,删除分支代码可能并没有想象中那么简单,可能并不是删分支这么简单,同样,要增加一个协议也是异常麻烦,这是因为多个协议耦合在一起了,随着时间推移,越来越少人能够完全理清每个分支的行为。
那么如果做框架抽象支持,代码会是怎么样的呢?
trait ProtocolMeta {
fn name() -> string {
...
}
fn version() -> Vec<String> {
...
}
}
trait ProtocalHandle {
fn connected() {
...
}
fn received() {
...
}
...
}
我会这么做,对协议应该有的元数据定义一个 trait,然后每个协议定义一个 handle trait 用来定义每个协议自己的特有行为,每个协议只需要根据自己版本去做自己的事就行了,在连接建立过程,就将会知道双方同一个协议支持的版本,之后的通信将根据协商的版本号进行。如果要删除某个版本的支持,只需要在对应的 version 分支中删除就可以了,如果想删除或者新增一个协议支持,只需要再次实现上诉两个 trait 然后插入框架的配置中就可以了。协议之间的耦合可以相对更轻松地拆除。
除了上面对多协议多版本的支持之外,还有一个功能就是隐藏一些业务层不关心的操作,包括多协议的路由问题、加密通信问题、连接管理问题。
业务端只需要知道,某个连接被打开了,这个连接中某个协议被打开了,这些信息就足够了,更多的细节问题,不需要放在业务层考虑,分层构建代码,让每一层专注于自己的实现,高内聚而低耦合。
比如,底层真实的连接可以做到与业务层无关,可以是 TCP、UDP 等任意协议。
这一篇还是没有进入正题,而是在阐述,我们要将 P2P 网络分层构建,虽然我们的真实网络场景是有各种不可预测的问题,但我们依然需要在其上构建一个相对安全的网络服务,将琐碎的事情通过层层封装进行过滤,留给上层更加干净的接口,让每一层专注于本层业务的实现。
请登录后评论
评论区
加载更多