藏川线前段

--- 摄于 2017 年 9 月 藏川线前段

wasm 相关的内容,基本已经形成了 PR 或者已经合入了主支了,这一篇主要说的是 tentacle 最近的相关修改,这东西也是有点意思的,属于之前没做过,第一次尝试去做,内容属于知道的觉得很简单,不知道的觉得很神奇——就是同端口支持多协议到底是怎么回事?

网络协议到底是什么

这个问题其实可以扩大化,所谓协议到底是什么?很简单,它其实是一套行为规范,具体到内容,简单地说,就是一个消息怎么解析、打包、处理、响应。每一种协议都是一套消息处理的行为规范,而基于某个协议的上层协议实际上就是在前一种协议的基础上,加上自己的一套逻辑,进行抽象和嵌套,最后形成了自己的一套消息行为规范。

常见的网络协议,包括最底层的 TCP/UDP,以及基于它们的比如 TLS/HTTP/Websocket/KCP/QUIC,在某种程度上来说,后者其实底层都是前者,都是基于前者做的客制化而已。那么从理论上来说,在 TCP 层面上,都可以通过熟悉/解析他们的数据包,来识别正在通信的是什么协议。这其实就是同端口支持多协议核心的理论点,比较常见的场景就是代理,比如 Nginx 协议分发场景。

tentacle 的 TCP Upgrade

这次介绍的东西,实际上就是基于这个 PR,看上去代码很多,实际上只实现了一个功能,将基于 TCP 的所有协议都作为 Upgrade 对象,支持在同一个端口进行识别和建立连接通信。之前版本的 tentacle,支持 tcp/tls/ws 三种协议,但是,他们必须监听在不同的端口,支持的协议越多,占用的端口就越多,这样理论上问题不大,实际上对于同一个应用来说,多端口实现多协议会对运营和维护造成一定的障碍:

  • 远的不说,就说最基本的云服务器,任何一个端口的对外开放,都是需要谨慎的,有些流程比较长的公司,审批都是一项巨大的障碍。
  • 再说说用户端,它需要明确知道自己首先支持哪些协议,其次哪个协议在哪个端口,再次应该选择哪些协议,每一个问题都是对用户的一次阻拦,这是可以解决的问题,就尽量解决掉。
  • 从技术的角度上看,同一个底层协议支持多个上层协议的分发其实也是很自然的事情,属于会者不难,难者不会,并不复杂。

那么这个东西的实现,其实我在这篇文章中讲解不是很多,只挑部分重点核心代码,其余的并不十分重要,开始贴代码,原代码在这里,文章中有删减:

async fn protocol_select(stream: TcpStream, mut upgrade_mode: UpgradeModeEnum) {
	let mut peek_buf = [0u8; 16];
    stream.peek(&mut peek_buf).await?;
	loop {
        match upgrade_mode {
            UpgradeModeEnum::OnlyTcp => {
                进行 tcp 的下一步操作
                break
            },
            UpgradeModeEnum::OnlyWs => {
                进行 ws 的下一步操作
                break
            },
            UpgradeModeEnum::OnlyTls => {
                进行 tls 的下一步操作
                break
            },
            UpgradeModeEnum::TcpAndTls => {
				if peek_buf 是 tcp 消息 {
                    upgrade_mode = UpgradeModeEnum::OnlyTcp;
                    continue;
                } else {
                    upgrade_mode = UpgradeModeEnum::OnlyTls;
                    continue;
                }
            },
            UpgradeModeEnum::TcpAndWs => {
                if peek_buf 是 http 消息 {
                    upgrade_mode = UpgradeModeEnum::OnlyWs;
                    continue;
                } else {
                    upgrade_mode = UpgradeModeEnum::OnlyTcp;
                    continue;
                }
            },
            UpgradeModeEnum::All => {
                if peek_buf 是 http 消息 {
                    upgrade_mode = UpgradeModeEnum::OnlyWs;
                    continue;
                } else {
                    upgrade_mode = UpgradeModeEnum::TcpAndTls;
                    continue;
                }
            }
        }
    }
}

这段代码的核心就是:

  • 拿到建立连接的 TCP Stream,首先读前 16 个 bytes 的消息
  • 然后根据当前监听的需求,去尝试判断这 16 个 bytes 的格式特征
  • 然后分支跳动,最好确定它是什么协议,然后就可以根据最后确定的协议进行后续的处理了

这里面 loop 其实就是 goto,为了代码复用和好看,不然每个分支除了判断之外,都是重复的内容,其实有的时候,goto 语法是有很好的作用的,可惜 rust 基本不太可能支持了。不过 loop + break ‘label 也算是勉强能模拟 goto 了。

那么为什么是 16 bytes?很简单,拍脑袋,如果用正常的思路,它是需要集合所有支持的协议的第一个消息的内容,找到最小的消息,然后读这部分消息,判断是否是这个协议,然后以此类推,把所有协议都识别出来。这样是最正统的思路,但是,它的开销(包括识别协议的开销和程序员的开销)是相对比较大的:

  • 从性能的角度上看,如果能够在最小的读取前提下直接识别,这样是最好的
  • 其次,每个协议的第一个消息的大小是不一样的,甚至同协议的第一个消息大小也是不一样的,这样叠加的情况下,很难说,第一个消息的大小到底是多少,如果无法读到想要的长度,这个 TCP Stream 就只能 hang 在这里等超时了,这样的代价是接受不了的

最后

关于 lightclient 的进展,已经形成了 PR,期待它通过各种稳定性和功能性测试,正式合入主支,这样就就可以看看后续存在什么样的影响了,这是我比较期待的地方。

评论区

加载更多

登录后评论