实现通讯协议
介绍
在网络编程中,两台服务器互相交换数据,需要遵循特定的规范,这种规范就是协议。
日常的研发工作中,我们通常使用的常见协议,比如HTTP,JSON-RPC,SOAP(早期)。
UDP 视频音频流,丢一点不太影响整体。
常用协议的优势:
方便与其他语言和框架对接。
有成熟的框架可以用。
升级也比较方便。
自定义协议的方向:
性能要求非常苛刻,以至于JSON等序列化会严重拖慢性能。
数据本身带有大量的二进制内容,不适合使用文本格式
其他目的(协议保密,反侦测等)
协议的基本单元-消息(message)
在TCP/IP设计之初,TCP用于传输可靠的数据流,而UDP用于传输不可靠的数据报文。但对于一个通信协议来说,我们不可能传输无限长的数据,我们一定需要一个时机,让服务器可以停下来,处理已经收到的数据,而不用等待后续的数据。这表示,我们发送的数据,一定可以通过某种方法切分成小段小段的数据,从而每个小段可以单独解析、单独处理。这样的最小的切分单元就叫做消息。一般来说,将多个消息拼在一起,不会影响每个消息单独表达的意思;而如果将一个消息再切小,就会破坏这个消息的完整性,就没法进行处理了。
对于UDP来说,整个数据报文可以是一个消息,这样不需要额外的工作。但是UDP是不可靠的,通常我们都不希望我们的协议工作在一个不可靠的基础上,那样太复杂了。使用TCP的话,如果我们在发送完一个消息之后马上关闭写的方向(TCP的两个方向可以独立进行关闭),我们也可以让对端服务器知道我们的消息已经发送完成了,但是这样我们每个连接上都只能发送一个消息。一些老的协议的确是这样工作的,比如HTTP/1.0。短连接比较灵活,但也有非常多的问题,比如TCP协议要求连接关闭之后,协议双方还需要保持WAIT状态一段时间(一般为2分钟),这段时间内相应的端口号是不能使用的,这样很容易导致客户端源端口耗尽。而且建立连接和断开连接的额外开销、TCP连接的慢启动、操作系统的连接数限制也都是需要考虑的问题。
在单独的一个连接上处理多个甚至无限数量的消息是现代协议发展的趋势,要实现这个目的,我们必须要能正确地从数据流当中将消息进行分片。TCP的传输是一个数据流的模型,我们不应该对数据何时到来、以什么形式到来做任何假设,它不会替我们保留任何除了数据本身以外的信息,比如我们发送’abcdefg’,再发送’1234567’,从原理上来说,接收方有可能分两次接受到两组数据,也可能一次接收到了’abcdefg1234567’,也有可能收到了’abcd’,然后’efg1234567’,甚至更复杂的其他情况。要正确切分出每个消息,我们必须精心设计消息的结构,让它在被连接在一起的情况下仍然能正确区分成每个消息。
要注意的是,一个消息不见得很小,而是只要它是一个不可分割的整体,就应该看成是一个消息。HTTP/1.1中每个HTTP请求和每个HTTP回复都可以看成是一个消息,它的大小可以非常大。虽然不能分割,一边接收一边处理却是可以做到的。
数据流的切分与消息解析机制
有许多种常用的方法可以将数据流正确切分成消息。
比如说对于基于文本的协议,以换行符为标记,将文本拆分成行,每行是一个完整的消息;这要求消息当中不能再包含换行符。
再比如说JSON-RPC或者SOAP中,利用JSON或XML的结构化特性,区分出多个首尾相连的JSON或XML的边界;这要求消息必须严格符合JSON或者XML的格式。
对于二进制数据来说,最简单的方法是在数据开头明确写出这个消息的长度,然后直接按照这个长度进行切分,这样在消息内部可以使用任意的数据和数据格式,而且协议解析的速度远远比其他方法来得快。
切分成正确的分段的下一步,就是解析消息。一般来说我们很难想像所有的消息只有一种类型,大部分时候我们都需要不同类型的消息,这样我们需要一个字段来表示消息的类型。同一类型的消息,可能在版本升级之后,有了新功能,所以格式也会发生变化,我们可以使用不同的类型来表示新格式,但一种更清晰的方法是通过一个独立的字段来表示消息格式的版本。
许多情况下,对消息有明确的标识是很有用的。设想对于我们发送的每个请求消息,服务器都需要给出一个相应的应答消息,由于可以在同一个连接上发送多个请求,我们需要能够区分服务器给出的应答是针对哪一个请求的。一种方案是服务器对每个请求消息都给出恰好一个应答消息,而且顺序与请求的顺序完全相同,这样客户端只要按照接收到应答的顺序与发送请求的顺序一一对应起来,就能知道每个应答消息对应哪个请求。Redis和HTTP/1.1使用的就是这样的机制。但它也有一些缺点:
每个请求必须有且仅有一个响应消息,而不能没有或者是有一系列的消息。
前一个请求的响应发出之前,后一个应答的响应不能发出。比如Redis当中在使用BRPOP之后,BRPOP响应之前,不能使用同一个连接发送更多的请求。
只能支持客户端单方向请求服务器,而不能实现服务器请求客户端,或者从服务器推送数据。
JSON-RPC等协议则使用了另一套机制,在request当中加入id,然后再reply当中使用相同的id,来表示request与reply的关联关系。这样就解决了前面的问题:发送请求的顺序可以与发送应答的顺序不同;请求可以没有或者有多个应答消息;可以双向发送请求和应答。
总结来说,我们在消息头当中需要类型、长度、版本、消息ID四个字段。