文档章节

基于TCP的应用层协议消息帧解析

lionets
 lionets
发布于 2016/09/07 19:37
字数 1216
阅读 110
收藏 0

TCP 特性

  1. 连接。
  2. 可靠的传输。确认送达,去重,有序传输
  3. 流。无边界

应用层协议按消息体格式分,可以分为面向消息的(message) 和 面向流(stream) 的,面向流的暂且不说,面向消息的更加常见,如客户端/服务器模型或订阅/发布模型。

但问题在于,可靠的 TCP 是传输流的,它本身并不含有消息边界。因此从字节流中区分独立的消息帧成了应用协议设计的一个基本任务。

常用的方法有以下两种:

长度前缀

以消息体字节长度为前缀并指定该前缀的大小和字节序。比如 \x0bhello, word 使用了一个字节指明后面字符串的长度为 11。(一般情况下更多会使用 I32 大小的前缀,并指定字节序)

这种方式只关心字节,因此可以看成是无类型的。

以 Thrift 的 TBinaryProtocol 为例。

service Ping {
    string ping(1:i32 x, 2:string y),
}

使用 (123, hello) 调用上面 Ping 接口的客户端发出的字节帧是这样的:

\x80\x01\x00\x01\x00\x00\x00\x04ping\x00\x00\x00\x00\x08\x00\x01\x00\x00\x00{\x0b\x00\x02\x00\x00\x00\x05hello\x00

其中,前四个字节是版本号和调用类型,服务端会用掩码做校验,来判断版本号的合法性。其中最后一个字节是调用类型,分为 CALL, EXCEPTION, REPLY, ONEWAY 四种。这四个起始字节可以看做是一个正确请求的开始。

接下来是一个 I32,用来指明后面 api 名称的长度,即 len('ping') == 4'。这是一个典型的长度前缀的应用。

ping 后面的字节是调用参数,每个参数都遵循这样的结构:

  • 前 3 个字节中,1 字节表示参数类型, 然后跟一个 I16 表示这个参数的位置序号
  • 如果 1 是定长类型的话, 接下来就是这个类型的值,如果 1 是变长类型的话,比如字符串,接下来就是一个 I32 表示字符串的长度,然后跟字符串的值。
  • 尾巴跟一个空字节,表示 TType.STOP

Thrift 不能以 String 类型传输非 ASCII 字符的问题也是这种标记方式造成的,因为非 ASCII 字符的字符数并不等于它们的字节长度,会导致接收端解析错误。解决方法,要么新建一种类型,比如 UTF8,要么把这些字符 encode 一下再传输,然后另一端再 decode 一下。

特殊分隔符

使用特殊分隔符做消息边界的方法一般用在文本格式的协议里,因为

  1. 文本消息里有特殊的转义字符,不用担心混淆的可能
  2. 文本消息类型单一,长度不定,使用分隔符显得十分简洁恰当
  3. 如果使用非显示字符做分隔符的话,打印出来的消息完全没有多余的控制信息,可读性好

以 HTTP 的 Headers 为例,它的基本单位是行,即以 \r\n 作为分隔符。

接收端解析的时候,与长度前缀不同,因为事先并不知道需要读取多少数据,所以往往要再维护一个 buffer,先把能读到的一段数据统一放入 buffer,然后拿出来遍历查找分隔符。正确截断消息帧后,再把多读的数据放回到 buffer 里去。比如 gunicorn 的 Request 类的 parse 方法,其 buffer 名为 unread,取其可以把多读出来的数据吐回去之意:

def parse(self, unreader):
    buf = BytesIO()
    self.get_data(unreader, buf, stop=True)

    # get request line
    line, rbuf = self.read_line(unreader, buf, self.limit_request_line)

    # proxy protocol
    if self.proxy_protocol(bytes_to_str(line)):
        # get next request line
        buf = BytesIO()
        buf.write(rbuf)
        line, rbuf = self.read_line(unreader, buf, self.limit_request_line)

    self.parse_request_line(bytes_to_str(line))
    buf = BytesIO()
    buf.write(rbuf)

    # Headers
    data = buf.getvalue()
    idx = data.find(b"\r\n\r\n")

    done = data[:2] == b"\r\n"
    while True:
        idx = data.find(b"\r\n\r\n")
        done = data[:2] == b"\r\n"

        if idx < 0 and not done:
            self.get_data(unreader, buf)
            data = buf.getvalue()
            if len(data) > self.max_buffer_headers:
                raise LimitRequestHeaders("max buffer headers")
        else:
            break

    if done:
        self.unreader.unread(data[2:])
        return b""

    self.headers = self.parse_headers(data[:idx])

    ret = data[idx + 4:]
    buf = BytesIO()
    return ret
    
def get_data(self, unreader, buf, stop=False):
    data = unreader.read()
    if not data:
        if stop:
            raise StopIteration()
        raise NoMoreData(buf.getvalue())
    buf.write(data)

其中 # Headers 块的代码就是不停读取数据,然后在里面找 \r\n\r\n, 因为这个空行是 headers 和 body 的分界。或者开头两个字节是 \r\n 的话,表示这个请求没有 header,直接返回。

特殊分隔符法带来的一个缺陷是对字符集的要求,http 的起始行和 headers 都要求使用 ASCII 字符。

但 http 并不是一个纯的字符分割协议,他在 headers 中提供了两个字段 Content-LengthContent-Type,用来支持二进制 body 传输。Content-Length 又是一个指明字节长度的字段。

© 著作权归作者所有

上一篇: git
lionets
粉丝 93
博文 101
码字总数 135303
作品 0
朝阳
程序员
私信 提问
谈谈应用层网络协议设计

对于初涉网络编程的开发人员来说,在通信协议的设计上一般会有所困惑。一般的网络编程书籍上也较少涉及这方面的内容。估计是觉得太简单了。这块确实是不难,但如果不了解,又很容易出篓子或者...

zhangyujsj
2016/09/13
104
0
原来你是这样的Websocket--抓包分析

版权声明: https://blog.csdn.net/ZARA0830/article/details/80380873 之前自己一个人负责完成了公司的消息推送服务,和移动端配合完成了扫码登录、订单消息推送、活动消息广播等功能。为了...

Andrewniu
03/13
0
0
刨根问底HTTP和WebSocket协议(二)

WebSocket WebSocket协议还很年轻,RFC文档相比HTTP的发布时间也很短,它的诞生是为了创建一种「双向通信」的协议,来作为HTTP协议的一个替代者。那么首先看一下它和HTTP(或者HTTP的长连接)...

umgsai
2016/09/18
0
0
爱创课堂每日一题七十八天-说说网络分层里七层模型是哪七层?

应用层:应用层、表示层、会话层(从上往下)(HTTP、FTP、SMTP、DNS)传输层(TCP和UDP)网络层(IP)物理和数据链路层(以太网)每一层的作用如下:物理层:通过媒介传输比特,确定机械及电...

全栈web笔记
2017/12/20
0
0
WebSocket的JavaScript例子

一个html5 WebSocket + JS的简单Echo例子,例子代码演示效果猛戳链接:websocket例子(打开页面,稍等一会) 使用一个文本编辑器,把下面代码复制保存在一个 websocket.html 文件中,然后只要在...

月下独酌100
2016/05/23
150
1

没有更多内容

加载失败,请刷新页面

加载更多

G1 垃圾收集器介绍-转

https://www.cnblogs.com/ASPNET2008/p/6496481.html

Java搬砖工程师
29分钟前
1
0
超高性能 key-value 数据库 Redis-基础数据结构

Redis的魅力 缓存大致可以分为两类:1.一种是应用内缓存,比如Map(简单的数据结构),以及EH Cache(Java第三方库);2.另一种 就是缓存组件,比如Memached,Redis;Redis(remote dictiona...

须臾之余
40分钟前
3
0
Mysql表分区的选择与实践小结

在一些系统中有时某张表会出现百万或者千万的数据量,尽管其中使用了索引,查询速度也不一定会很快。这时候可能就需要通过分库,分表,分区来解决这些性能瓶颈。 一. 选择合适的解决方法 1....

小谜弟
46分钟前
3
0
为 git 添加多个公秘钥

如果想为主机配置多个git设置,设置多个git公、秘钥,只需在生成密钥时指定密钥保持的文件即可,保证保存密钥的文件不同即可。 示例: ssh-keygen -t rsa -C "YOUR_EMAIL@YOUREMAIL.COM" -f...

niithub
46分钟前
2
0
walle-web 2.0安装流水

一、环境安装 VMware Workstation,centos7.6 64位,lnmp1.5 二、安装lnmp1.5 wget http://soft.vpser.net/lnmp/lnmp1.5.tar.gz -cO lnmp1.5.tar.gz && tar zxf lnmp1.5.tar.gz && cd lnmp1......

我心中有猛狗
48分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部