什么是协议

作者: | 更新日期:

关于协议的本质,我有一些不成熟的想法,先抛砖引玉吧。

本文首发于公众号:天空的代码世界,微信号:tiankonguse

大家好,这里是tiankonguse的公众号(tiankonguse-code)。
tiankonguse曾是一名ACMer,现在是鹅厂视频部门的后台开发。
这里主要记录工作中的技术架构与经验、计算机相关的技术、数学、算法、生活上好玩的东西。

这篇文章从公众号tiankonguse-code自动同步过来。
如果转载请加上署名:公众号tiankonguse-code,并附上公众号二维码,谢谢。

零、背景

最近几篇文章打算记录一下协议相关的内容。
作为第一篇,自然就是介绍什么是协议了。

警告:下面的介绍大多是tiankonguse胡思乱想出来的观点,网上不一定能找到理论依据。
很多名词也是我自己瞎编的,所以你们就假设我在胡扯吧。

一、协议无处不在

网络通信中常听说的协议有ARP协议、IP协议、ICMP协议、UDP协议、TCP协议、FTP协议、HTTP协议等等。
计算机通过这些协议识别接收到的数据,从而可以进行交换数据。
可以看出一种协议会嵌套另一个协议,但是我们只用同一层的协议来交换数据,而其他层的协议我们不能识别,也不能感知。
PS:TCP/IP协议是个例外,和其它层数据有耦合,下面会提到。

向下看,字符或编码中值与含义的映射是一种协议,电子电路中低电平高电平与01也是一种协议。
向上看,XML、JSON、protobuf、各种编程语言都可以称为是一种协议。
这些协议实际上是一种约定规则,通过约定规则,我们把可以把看不懂的串转化为可以看懂的结构。
计算机语言协议的结构具有状态,且这个状态可以根据自身的描述自我更新,这就和状态机不谋而合和了,原来状态机也是协议。

再看看生活中,合同也是一种协议,法律也是一种协议,汉字是一种协议,语言也是一种协议。
有了这些协议,我们可以生活,可以说话。
为什么其他动物说话你听不懂? 那是因为你不了解他们语言对应的协议。

我甚至在想,是不是世界万物都是在协议下运行的。
比如物体间的引力是某种协议,原子的组成,元素的组成是不是又是另一种协议。
整个世界都是都是在协议下互相识别,互相通信,自我更新状态的。

二、通信协议

通信协议一般是定长的或者头部定长,我们把这种协议称为定长结构性协议。
定长结构性协议是万物通信的基础。
比较优秀的通信协议的前几字节,一般会有个字段标示整个包的长度,这个长度可以快速判断包的完整性。

当需要通信时,定长结构性协议需要进行序列化为某种介质,比如01串,当收到介质后就需要进行反序列化为结构化协议。
计算机上大多数通信的介质就是01串,而人类语言的通信则是通过转化为振动波(能量)进行传递。
这里重点介绍计算机中的通信协议吧。

1. IP 协议

不同机器之间通信需要IP协议。
学习IP协议,可以快速浏览一下rfc791。

在rfc791中,可以看到IP协议的定长头部如下。

 0               1               2               3           
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|Version|  IHL  |Type of Service|          Total Length         |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|         Identification        |Flags|      Fragment Offset    |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|  Time to Live |    Protocol   |         Header Checksum       |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|                       Source Address                          |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|                    Destination Address                        |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|                    Options                    |    Padding    |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
            (from  https://tools.ietf.org/html/rfc791)             

IP协议头部定长20字节,包长度使用两字节储存,所以IP协议包最小是20字节,最大是65535字节。
当然每个设备支持的最大长度(MTU)不同,所以实际最大长度比较小,但是约定至少支持576字节。
为什么是576字节呢?
因为头部支持扩选项,包头加上扩选项后最长是60字节(IHL * 4),对齐是64字节,而大部分数据小于512字节,头部加数据就是576字节了(512 + 64)。

好吧,这里又出来一个512,这个数字哪来的呢,我也不知道。
有兴趣的人可以查一下资料或者历史背景,看看为什么是512。

IP头部的数据结构可以标示成这样

struct Ip{  
    uint8_t version:4;  
    uint8_t ihl:4;  
    uint8_t type;        //Type of Service  
    uint16_t length;     //Total Length 
    uint16_t identif;    //Identification  
    uint16_t flags:4;  
    uint16_t offset:12;  //Fragment Offset 
    uint8_t ttl;         //Time to Live  
    uint8_t protocol;  
    uint16_t checksum;  
    uint32_t src;        //Source Address   
    uint32_t dst;        //Destination Address  
    uint8_t options[44];  
    uint8_t* data;        
};  

当我们收到一个ip包时,我们就可以使用这个定长结构协议从二进制反序列化为数据结构,当需要发送ip包前,需要将这个数据节后序列化为二进制。
由于这个比较简单,这里就不实现检查ip包,序列化ip包,反序列化ip包了。

与IP同级的ICMP,RIP等协议,以及下层的ARP协议这里就不介绍了,总的来说就是一种定长结构化协议。

2. UDP 协议

网络通信一般是通过TCP和UDP通信的,这里先来介绍一下UDP协议。

UDP是一种无确认协议,即数据包发送出去后,没有人回包确认收到对应包了。

可以在rfc768中可以看到UDP协议的定长结构.

 0               1               2               3   
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|          Source Port          |       Destination Port        |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|           Length              |         Checksum              |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|                             data                              |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
          (form https://tools.ietf.org/html/rfc768)    

UDP头部的数据结构可以标示成这样

struct Udp{  
    uint16_t srcPort;    //Source Port  
    uint16_t dstPort;    //Destination 
    uint16_t length;      
    uint16_t checksum;   
    uint8_t* data;        
};  

可以看到,头部定长8字节,分别包括源端口、目的端口、包长度、校验位,然后就是数据了。
可能有人看到这里就会有疑问了:怎么没有源ip和目标ip呢?
这是因为目前的网络体系底层都是通过ip协议进行通信的,所以上层就不需要冗余储存这两个字段了。

另外,看到包是两字节的,所以UDP包最大是65535了,当然因为头部的缘故实际比这个稍微小一点。

最后一个注意点就是检验和需要加上ip包的一些数据。
合起来就是下面的样子了。

 0               1               2               3   
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|                       Source Address                          |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|                    Destination Address                        |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|     zero      |   protocol    |          UDP length           |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|          Source Port          |       Destination Port        |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|           Length              |         Checksum              |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|                             data                              |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
          (form https://tools.ietf.org/html/rfc768)    

当然,加的IP头部并不是真实的IP头部,而是IP头部的部分数据。
从这里也可以看出UDP协议和IP协议是强耦合的。

3. TCP 协议

上面介绍了UDP协议,这里看看TCP协议。
在rfc793中可以看到TCP协议的定长结构。

 0               1               2               3           
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|          Source Port          |       Destination Port        |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|                        Sequence Number                        |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|                    Acknowledgment Number                      |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|  Data |           |U|A|P|R|S|F|                               |  
| Offset| Reserved  |R|C|S|S|Y|I|            Window             |  
|       |           |G|K|H|T|N|N|                               |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|           Checksum            |         Urgent Pointer        |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|                    Options                    |    Padding    |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
|                             data                              |  
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+  
        (from https://tools.ietf.org/html/rfc793)  

TCP协议和UDP协议类似,区别就是有确认机制,可以拆包进行流量控制。
确认机制说明了这个协议是存在状态的,协议一旦存在状态,就会变得复杂起来。
这个状态不是协议自身更新自己的,而是需要上层储存这个状态,一会下面谈自更新协议时就可以明白什么意思了。

另外,由于这里有状态的含义,我们就可以利用这个状态转换来攻击TCP服务器,比如只发送建立连接请求,然后上层会储存这个状态,等待下个状态请求到来。
而我们不发送下个状态的请求,并且不断的发第一个状态的请求,那么这个服务器就会储存大量的第一个状态数据资源有限的,量大了就可以使服务不能正常服务了。

最后这里我就不画TCP的数据结构了。
看看TCP的头部,其实可以发现,可UDP一样,没有来源和目标IP,甚至连TCP包的长度都没有了。
这些没有的信息也需要依靠下层的IP协议传递过来。

4. http协议

http协议本来是应用层的协议,但是也用来传输数据的,所以需要划分为通信协议。

默认http协议有两部分组成:包头key-value和包体内容。
头部会有一个字段Content-Length或者Transfer-Encoding来标示http协议是怎么传输的以及怎么检查包接收完毕。
Content-Length含义为http包的大小,Transfer-Encoding的值一般为chunked,代表使用流式的方式。
关于这个协议具体可以参考RFC2616(https://tools.ietf.org/html/rfc2616)。

关于检查包是否接收完毕这里可以看出有一些问题的。
首先是性能问题,不像IP协议或者TCP协议,我们在指定的位置可以得到包的长度,然后就可以检查包是否接收完成。

对于HTTP协议,我们需要解析包头,然后得到对应的长度。
很多私有的http服务器为了简单,没有解析包头,而是直接字符串查找Content-Length或者Transfer-Encoding。
这个时候假设我们的包体中含有这些字符串,就完成了http注入请求,甚至可以让私有的http服务器不可用。
我赶紧看看线上的代码,还好,这里都是客户端,虽然检查回包都是使用字符串查找,但是请求的服务我们是信任的,所以也就不需要考虑安全问题了。

对于服务器回的http包,一般是json,很多客户端为了得到这个json还是使用字符串查找左大括号来完成的。
这个时候又可以发现一个问题了,我们在http包头中包含这个字符的话,业务肯定就会出问题了。

关于chunked包需要展开说明一下。
举个例子大家就都懂了。

HTTP/1.1 200 OK  
Content-Type: text/plain  
Transfer-Encoding: chunked  
  
25  
This is the data in the first chunk  
  
1C  
and this is the second one  
  
3  
con  
  
8  
sequence  
  
0  

根据例子可以看出来,包体分成了很多小部分,每一小部分有一个数据大小,然后后面有对应大小的数据。
每一小部分使用空行分隔。
这样设计的好处是服务器不需要知道整个http回包有多大,可以有一部分数据就发送一部分数据。

好了,看来HTTP协议是一种简单的文本通信协议,这里就不多介绍了。

5. 私有协议

其实这里的私有协议和HTTP协议一样,也是TCP之上的应用协议。
应用之间用来通信时,需要一个基本信息: 包的长度,包体的含义,包的版本等等数据。

一般应用协议的组成像下面的样子:

struct Proto{  
    uint8_t  startMagic;   //起始魔数  
    uint16_t length;       //整个包的长度  
    uint8_t  version;      //当前协议版本  
    uint16_t cmd;          //包体的含义  
    //...                  其他字段,如预留字段  
    uint8_t* data;    
    uint8_t  endMagic;     //结束魔数    
};  

我们把业务数据序列后储存在data中,data的长度为真个包的长度减去包头的长度。
这时候我们通信时,就很容易判断包是否收完了,以及根据魔数简单的检查包是否是自己这个协议的包。

三、自描述应用协议

前面提到了通信协议是定长结构性协议,下面我们来看看自描述应用协议。
自描述应用协议一般没有固定的长度,但是自身是自描述的或者半自描述的。
这里我们定义应用协议为自解释结构性协议。

应用上由于各种使用场景不同,所以协议的偏重点也不同了。
大致分为两类: TLV解释结构性协议和文本解释结构性协议。

1. 文本协议

常见的文本协议有json和XML。
但是json和XML解析时存在性能问题,因为我们需要去探测对应结构的长度。
比如我们遇到了左双引号,可以知道后面的是字符串,但是字符串多长我们不知道,于是我们需要扫描比较字符串是否结束。
而对于TLV协议,我们直接copy对应的字符串即可。

JSON和XML的协议规则很简单,这里就说怎么序列化与反序列化了。
几年前我也写过json库,就是一个状态机,状态不断转移,大学编译原理课程上写过编译器语法分析的同学都应该秒杀这两个协议库的。

对于这两个文本协议,这里需要关注的是协议的自描述。
我们根据前几个字符来可以探测后面数据的类型了。
然后对于对象类型,协议本身储存了每个key的名字,对于XML有key的属性,实际上也可以转化为稍微复杂的json的,本质上没区别的。

另一个关注点就是这些协议是可递归的。
对于数组和对象的值,可以又是一个数组或对象,当然也可以是其他基本类型,那样就到的终点了。

这里总结一下这些文本协议有这样一些特征。

  1. 类型需要探测
  2. 名字的长度,对象和数组个数需要探测
  3. 名字也储存在协议中了

文本协议由于上面三个特征,导致解析性能低下,传递的包也很大,所以就有了下面的二进制协议。

2. 二进制协议

业界常用的二进制协议是protobuf和Thrift了,一些开源活动上曾提到过腾讯内部有JCE,OIDB等私有二进制协议。
其实这些二进制协议本质上没啥区别。

这些二进制协议一般也是自描述的。
前几字节就标示接下来数据的类型。

对于数字类型,可以直接得到对应的值。
对于字符串类型,后面的几个字节就是字符串的长度,然后是字符串的值。我们直接copy对于的值就行了。
对于数组类型,后面几个字节会标示数组的个数,然后递归去解析就行了。
对于对象类型,这里有个特殊的地方是没有储存key的名字,而是使用一个位置标记,位置到名字的映射是约定好的。

看到这里,其实可以发现,文本协议和二进制协议在结构上本身没有区别,只是在序列化时,采用不同的方式储存而已。
这样的好处就是解析快,储存小。
当然也有缺点:名字缺失,全是位置标记,不易阅读,需要借助解析器来查看对应的数据。

四、状态协议

上面扯了那么多, 实际上都是在说静态协议,即协议是一个静态的数据结构。

而状态协议是什么意思呢?
其实就是我们平常使用的各种语言跑的程序,当然世界万物实际上都可以归属于状态协议。
状态协议的最大特点就是具有一个状态,然后自己描述了怎么更新自身的内容,也就进行了状态转移。

平常常见的状态协议有lisp,javascript,php,java,c等。
这里我们来看看lisp语言吧。
lisp语言有以下两个规则,一个是数据,一个是函数。

规则如下:

  1. 数据 (list list ...)
  2. 函数 (funname paramlist)

有了上面两个规则,我们就可以做任何事情了。
当然我们需要提供几个默认函数:

  • quote定义表达式
  • atom判断list是否为空
  • eq 比较
  • car 得到列表的第一个元素
  • cdr 得到列表除第一个元素之后的所有元素组成的列表
  • cons 得到一个列表
  • cond 条件表达式

比如下面几个操作。

;;定义一个数据结构  
(quote (a b))  
  
;;调用比较函数  
(eq (quote a) (quote b))  
  
;;定义一个函数,调用定义函数和lambda函数。   
(define hello   
    (lambda (name)    
        (begin   
            (display "Hello ")   
            (display name)  
        )  
    )  
)  
  
;;有时候为了简单,常常省略lambda和begin  
(define (hello name) (display "Hello ") (display name))  
  
  
  
;;输出 "Hello tiankonguse"  
(hello "tiankonguse")  

有了这个规则后我们能干什么呢?

先看看简单的功能吧。

;;基本运算    
(+ 1 1) ;;1 + 1 => 2    
(* 2 3) ;;2 + 3 => 6    
(- 7 3) ;;7 - 3 => 4    
    
;;复杂点的    
(- 4 (+ 1 1)) ;; 4 - (1 + 1)  => 2    
    
    
;;基本函数    
(define (eq? a b) (= a b))    
(define true (eq? 1 1))    
(define false (eq? 1 0))    
(define (false? v) (not v))    
(define (atom l) (null? l))   ;;某些lisp没有atom    
(define (empty? l) (atom l))    
(define (first l) (car l))    
(define (other l) (cdr l))    
(define else true)    
(define (add a b) (+ a b))    
(define (list? l) (pair? l))    
    
    
;;定义if语法    
(define (if testExpr thenExpr elseExpr)    
    (cond     
        (testExpr thenExpr])    
        (true elseExpr)    
    )    
)    
    
;;定义最大值    
(define (max first second)     
    (if (> first second) first second)    
)    
    
;;定义map    
(define (map list fun)    
    (cond     
        ((empty list)  (quote ()))       
        (else (cons (fun (first list)) (map (other list) fun)))    
    )    
)    
    
;;定义range    
(define (rangeFun from to step cmp)    
    (cond    
        ((cmp from to) (cons from    
            (rangeFun (+ from step) to step cmp)    
        ))    
        (else (quote ()))    
    )    
)    
(define (range from to step)    
    (cond    
        ((> step 0) (rangeFun from to step (lambda (from to) (<= from to))))    
        (else (rangeFun from to step (lambda (from to) (>= from to))))    
    )    
)    
    
;;range应用    
(range 1 10 1)    
;;(1 2 3 4 5 6 7 8 9 10)    
    
(map (range 1 10 1) (lambda (a) (add a a)))    
;;(2 4 6 8 10 12 14 16 18 20)  

看到这里,我们发现lisp语言规则虽然简单,但是想做的任何事情都可以做到。
而且这个语言的规则也很简单,我们对这个语言进行反序列化就是词法分析了,拿这个语言来进行状态更新就实现解释器了。

五、Interrupted

这周本来想写三篇文章的: 什么是协议,协议反序列化,协议状态更新。
但是写的时候,发现后两篇文章如果写出来,贴的代码就会有点多,大家就会理解不动了。
于是就不写后两篇文章了,私底下我自己练习玩玩就行了。

上面的协议总结一下可以划分为四类:通信协议,文本协议,二进制协议,状态协议。
哦,状态协议大家还是忘记了吧,是我胡扯的。

对了,我的个人描述是分享经验、技术、算法、数学、生活上好玩的东西。
所以打算研究一些数学和算法,然后分享给大家。
另外我还建了一个免费的小密圈算法的世界,里面还没几个人。

六、其他文章


长按图片关注公众号,接受最新文章消息。

本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。

关注公众号,接收最新消息

tiankonguse +
穿越