目录
协议
应用层
xml
json
protobuffer
传输层
端口号(Port)
UDP 协议
UDP 协议端格式
完!
协议
网络通信中,协议是一个非常重要的概念。我们前面在网络原理中,就已经介绍了,为了统一各方网络,大佬们提出了协议,因为协议太多,对其进行了分层。
应用层
应用层,对应着应用程序,是跟我们程序员打交道最多的一层。调用系统提供的网络 API 写出的代码,都是属于应用层的。
应用层这里当然也有很多现成的协议,但是更多的,还是需要我们程序员根据实际的业务场景,自定义协议(网络传输的数据要怎么使用,当然也要考虑到数据是什么样的格式,里面包含那些内容~)
协议,其实就是一种与欸的那个,虽然存在很多的现有的协议(大佬们已经搞好了的),除此之外,我们程序员也可以自己来约定协议。
自定义协议,一般要约定好两方面的内容:
1. 服务器和客户端之间要交互那些信息
2. 数据是具体格式
(客户端按照上述约定发送请求,服务器按照上述约定来解析请求)
(服务器按照上述约定构造响应,客户端按照上述约定来解析响应)
举个栗子:
住酒店:
打开出行相关的 APP,显示出主页,主页里面就很显示出一些酒店的列表。
而且这些酒店都是在我们附近的(打开软件的时候,需要把我们的位置,告诉服务器)
显示的酒店列表中,也会包含一些信息 -- 酒店的名称 细节图片 相关评分 相关简介...
上述的这些信息,要按照什么样的格式来进行组织呢???是有一些固定的套路的~~~
确定如何组织数据格式这件事情,往往是需要客户端的程序员和服务器的程序员,这两伙人坐在一起,一起把这个事情给确定下来~~~(这里的格式怎么样约定都可以,只要是两伙程序员达成共识即可~~~)
一个简单粗暴但五脏俱全的栗子~~~
1. 请求,约定使用行文本的格式来进行表示:
userId,position\n (一个请求以 \n 为结尾,多个字段之间只用 , 来进行分割)
比如(1003,[经纬度]\n)
2. 响应,也是使用行文本来表示,一个响应中可能会包含多个酒店,每个酒店占一行,每个酒店都要返回 id,名称,图片,评分,简介
比如( 2001,A 酒店,[logo图片地址],4.9,五星级酒店\n
2002,B酒店,[logo图片地址],4.5,干净卫生的民宿\n
\n)
(若干行的最后,使用空行来作为所有数据的结束标志,上面的这一系列内容就是同一个响应中的数据了)
补充:
客户端和服务器之间往往要进行交互的是”结构化数据“(交互的数据是一个结构体 / 类,其中会包含很多个属性)网络传输的数据其实是”字符串“”二进制 bit 流“。
协议约定的过程,就是把结构化数据转换成字符串 / 二进制 bit 流的过程。
把结构化数据,转成字符串 / 二进制 bit 流,这个操作,称为”序列化“。
把字符串 / 二进制 bit 流还原成结构化数据,这个操作,称为”反序列化“。
序列化 / 反序列化具体要组织成什么样的格式,这里要包含那些信息,预定这两件事情的过程,就是自定义协议的过程。
为了让程序员更方便的去约定这里的协议格式,业界给出了几个比较好用的方案:
xml
大概的模样如下:
<> 称为标签(tag),一般都是成对出现的,分别为开始标签和结束标签。开始标签和结束标签中间夹着的就是标签的值,标签是可以嵌套的。(标签的名字 / 标签的值 / 标签的嵌套关系,都是程序员自定义的~)
xml 约定
优点:使得数据内容的可读性和拓展性都提升了很多,标签的名字能够对数据起到说明作用,后续要再增加一个属性,新添加一个标签即可,对已有代码影响不大~
缺点:冗余信息比较多,标签的描述性信息,占据的空间反而比数据本身还要多了~
json
大概的模样如下:
采用的是键值对结构:
键和值之间用:进行分割,键值对之间用 , 进行分割。
把若干个键值对使用 { } 括起来,此时就形成了一个 json 对象,还可以把多个 json 对象放到一起,使用 , 分隔开,并且整体使用 [ ] 括起来,就形成了一个 json 数组。
json 约定:
优点:可读性,扩展性都很好,而且对比 xml 来说,占用的空间更少了。
缺点:虽然 json 的确比 xml 占用的空间更少了,节省了宽带,但很明显,这里的宽带仍然还是有浪费的部分的,尤其是这种数组格式的 json,这种情况下往往传输的数据字段都是相同的,很多 key 关键字都是被重复传输的(id,name 等等...)
protobuffer
这种约定是更加节省宽带的方式,也是效率最高的方式。
protobuffer 只是在开发阶段(代码)定义出这里都有那些资源,描述每个字段的定义。程序真正运行起来的时候,实际传输的数据是不包含这些描述信息的。这样的数据都是按照二进制的方式来进行组织的。
这种方式虽然程序运行的效率会非常高,但其实并不太有利于程序员阅读~~~
虽然 protobuffer 运行效率更高,但是使用并没有比 json 更加广泛,只有一些对于性能要求非常搞的场景,才会使用 protobuffer。
应用层也很多线程的协议,HTTP 这种 后面详细介绍~
传输层
传输层中,虽然是系统内核已经实现好了的,但我们仍然需要重点进行关注,我们之前在网络编程中,使用的 socket API 就是传输层提供的
端口号(Port)
端口号,是一个 2 字节的整数。标识了一个主机上进行通信的不同的应用程序。
在 TCP/ IP 协议中,用源 IP,源端口号,目的 IP,目的端口号,协议号,这样一个五元组来标识一个通信。
端口号范围划分:
0 - 1023:是一些知名端口号,HTTP,FTP,SSH 等这些广为使用的应用层协议,他们的端口号的固定的。
1024 - 65535:操作系统动态分配的端口号。我们前面在网络编程中,客户端程序的端口号,就是由操作系统在这个范围内分配的。
一些有名的端口号:
SSH 服务器:22 端口
FTP 服务器:21 端口
telnet 服务器:23 端口
HTTP 服务器:80 端口
HTTPS 服务器:443 端口
我们写程序使用端口号的时候,需要避开这些知名端口号~
UDP 协议
前面我们已经提过,UDP 协议的四个特点:无连接,不可靠传输,面向数据报,全双工。
研究一个协议,我们主要是研究报文格式,基于报文格式,来了解这个协议的各个特性。
UDP 数据报 = 报头(重点) + 载荷(应用层数据包)
UDP 协议端格式
上面的这个图,并不准确,是为了我们书籍教材排版方便做出的妥协,其实应该是下面这样的:
UDP 报头中,一共有 4 个字段,每个字段 2 个字节(一共 8 个字节)
由于 UDP 协议中使用 2 个字节(16 位)来标识端口号,端口号的取值范围就是 0 - 655335(即这里最大值就是 64KB(2^10 = 1 KB),即,一个 UDP 数据报最大的长度,就是 64 KB,没办法更长了),一旦整个数据报的长度超出 64 KB,此时就可能导致数据出现阶段(数据后面的部分就没有了)
(总的 UDP 数据报最大长度是 64KB,则载荷部分实际能承担的最大长度,应该是 64KB - 8(减去报头的长度))
为了解决数据被截断的问题,有两个方案。
方案一:在应用层,把数据报进行拆分,之前一个数据报表示 N 个页面,拆分成每一个页面占用一个 UDP 数据报,甚至可以进一步的拆分成,一个页面对应多个 UDP 数据报。(开发和测试的成本都很大~~)
方案二:使用 TCP 代替 UDP,TCP 没有上述的长度限制~~
那为什么,不进行扩展呢???将 UDP 数据报,扩展成 4 个字节的长度呢???
技术上很容易实现,但要改,就要所有的系统一起改,如果一方改了,另一方不该,相互之间就无法进行通信了~~~大家都僵持住了...
校验和:验证数据在传输过程中是否正确~~~
前提是:数据在网络传输的过程中,是可能出错的!
网络数据传输:本质上其实是光信号 / 电信号 / 电磁波进行传输
上述信号都是很可能收到干扰的。
比如,使用高低电平来表示 0 1,外界如果加上一个磁场,就有可能把高电平变为低电平,低电平变为高电平,此时, 0 -> 1,1 -> 0 出现了比特翻转。现代的传输体系,其实有一系列的保护机制,来减少外界的干扰。
校验和的作用就是用来识别出当前的数据是否在传输过程中出现错误。
注意:网络中的校验和,并非是简单的按照数据的长度 / 数量来作为标准进行校验的,一定是数据的内容会参与到其中。
严格的来说,校验和只能用来“证伪”,即,只能是证明数据是出错了,没办法确保这个数据 100% 是正确的。但是实践中可以近似的认为检验和一致,数据就一致了。
UDP 中,校验和是使用比较简单的方法 - CRC 算法来完成校验的(循环冗余校验)
比如,要产生一个两字节的校验和:
加的过程中,产生的数据可能会比较大,超出了 short 两个字节的范围,溢出了,其实也无妨,这里不用管。
UDP 数据报发送方,在发送数据之前,先计算一边 CRC,把算好的 CRC 值放到 UDP 数据报中。(设这个 CRC 值为 value1)
接下来,这个数据报会通过网络传输到达接收端,接收端收到这个数据之后,也会按照同样的算法,再算一次 CRC 的值,得到的结果是 value2。比较自己计算的 value2 和收到的 value1 是否一致。如果是一致的,就说明数据大概率是 ok 的。如果不一致,则传输过程中一定出现了错误。
上述 CRC 算法,为什么说 value1 和 value2 如果是一致的,数据大概率是 ok 的呢???如果只有一个 bit 位发生反转,则 CRC 100% 能够发现错误。但如果恰好有两个 / 多个 bit 位发生反转,有可能恰好校验和仍然和之前的一样~~~(这种情况比较低,可以忽略不计,但如果希望这里有更高的检查精度,就需要使用其他更为严格的校验和算法了)
md5 算法,md5 算法的背后,是有一系列的数学公式来进行计算的,此处不讨论数学公式,来重点认识一下特点:
1. 定长:无论原始数据有多长,算出来的 md5 的最终值都是一个固定长度(md5 有 16 位版本,也有 32 版本,也有 64 位版本)
2. 分散:计算 md5 的过程中,原始数据,只要变化一点点,其算出来的 md5 值就会发生很大的变化差异。(这样的特性,使得 md5 也可以作为一个字符的 hash 算法)
3. 不可逆:给一个源字符串,计算 md5 值,过程非常简单(比 CRC 复杂一些,但整体比较简单)但是如果给一个算好的 md5 值,还原为原始的字符串,理论上无法完成~
原始的字符串 ==》 md5 这个过程,会有很多信息量损失了,无法直接还原~
(一些在线解密 md5 的网站,本质上是通过“打表”的方式完成 --> 服务器在不停的计算各种字符串的 md5 值,然后存储下来,我们输入 md5 值,他就直接去数据库里面查询 == 》 只能破解出一些常见的一般的字符串的 md5值~)