谈谈协议的设计

闲来无事,最近接了个公众号玩玩,当然肯定是基于silly的:)

最初的打算是开一个daemon,在收到微信sdk callback后根据好友发送的消息来做出不同的处理。比如根据输入关键字然后去我的blog上去爬取相关信息,每天定时把最新的blog文章做群发。

在实现http client过程中,需要解析dns。虽然gethostbyname可以用来解析域名,但是整个silly底层是基于异步来实现的,而gethostbyname则是以阻塞方式解析的,因此使用gethostbyname会极大地降低整个框架的吞吐量。

向dns服务器请求解析域名时,应该首先以udp方式请求,如果服务端回应超过512字节,则会置截断标志位。客户端发现截断标志位后应该以tcp的方式重新发送请求,在发送请求时,跟udp唯一的区别是,需要在数据包最开始附上两个字节的包长。

这是我第一次遇见一份协议同时适用于udp和tcp协议的。强烈的反差感让我不禁在思考,协议到底应该以什么样的方式进行组织,才可以更方便服务端的解析。

对比一下dns的udp和tcp的协议格式,同一个请求在通过tcp发送时需要增加两个字节的包头。那是因为tcp是属于字节流会粘包,而udp是按报文发送的,换句话说,你发出去是什么,对方就会接到什么。不会有粘包的顾虑,因此tcp需要包头来进行切饱。

当然在tcp情况下即使不用包头,dns请求协议也足够描述出一个完成的数据包。那么为什么要加两个字节的包长呢?

想到这里我就不得不佩服发明’协议栈’的哥们了,说得太形象了。协议栈通过抽象分层,然后明确每一层的作用,这样不但在设计代码时可以更好的解耦,在移植时也可以通过换掉其中某一层来达到快速移植的目的。

以dns的tcp协议为例,有了两个字节的包长就可以在逻辑层以下,抽象出组包层。

组包层的实现,屏蔽掉了底层通信细节,抛给逻辑层的都是一个一个完整的包。逻辑层的实现仅仅是拿一个完整的数据包去反序列化去处理,它并不关心数据是从哪里来的。

假如我们的dns不再通过tcp来通信,而是通过串口。我们所需要做的仅仅需要重新实现一个组包层,组包层从串口读出数据并切割出一个一个完整的数据包抛给逻辑层。而之前实现的逻辑层则一点都不需要改变,从而达到了代码的最大复用性。

这一点我感触颇深,在为silly移植各种协议的过程中,发现有好多库都是把socket直接做到协议里面的,而silly为了达到高吞吐量在c语言层使用的是异步逻辑,只有在lua层才可以进行阻塞访问。因此移植一些c协议库起来就颇为制肘。

因此,在设计协议时,一定要进行抽象分层,明确责任。这样在以后协议移植时会大大增加代码的复用性。



发表评论