传输层的协议-TCP/UDP

传输层协议

既然了解了 IP协议 的内容,上一篇通篇没有说到数据的安全性,因为 IP协议 只负责将数据运输到对应的计算机而已,不会对数据做任何操作,也不会验证数据是否完整到达或者有没有被破坏,或者交给哪个程序进行处理。而这些操作,是由 传输层协议 来做保证的。

传输层目前我们最流行的协议应该是 TCPUDP 了吧。前者是有连接的情况下传输数据,而后者则不会考虑数据是否完整到达,但是效率会比 TCP 略胜一筹。当然两者都有合适的使用场景。TCP 很多时候会用于设备之间的连接和数据传递,比如仓库 PDA 的使用。而 UDP 则多用于通话、视频方面的应用,还有我们熟知的 微信QQ,也是使用 UDP 作为传输协议的,他利用了 UDP 的效率,然后在 应用层 适配了数据完整性的校验。所以很多时候我们会有这种场景,我这边发送的消息显示个红色的❌,但是其实对方已经接受到刚刚发送的消息了,但是我们以为没有接收到又重新发了一次。还有直播类型的 应用层协议 多用 UDP协议

那,既然 IP协议 已经将数据传送到当前服务器了,服务器就应该具有一定的程序进行处理,要不然服务器也会懵逼。这个过程就通过解开客户端发送的数据,里面包含了一个参数称为 端口号,服务器系统就会交给监听这个 端口 对应的应用程序进行处理。依然是拿快递行业做类比,我们知道我们的地址一般会写到家里或者附近的地方,IP协议 就是负责将这个件送给对应的这个区域而已,但是这个区域有那么多人,根本不知道要交给谁,所以这时候快递员就需要用电话联系快递单号上的收件人电话,然后将件交给收件人。那么这个 收件人电话 就相当于端口号了。

这些服务端处理的 程序,将会监听着服务器系统上的 空闲端口。当这些端口有数据进来的时候,自然他们就可以接收到数据进行处理。当然如果这个 端口 被占用的话,程序是会启动失败的。但是不同的协议是可以监听同一个端口的,比方说当前有个 Tomcat 程序监听着 8080,我们还可以用另外一个程序,以 UDP 的协议继续监听这个 端口

一般来说,0 ~ 1023 留给一些知名程序进行处理(比如 sshd HTTP),如果我们需要占用端口,一般从 1024 开始,最大的端口号是 65535。当然这些端口也可能被我们启动的某些比较有名的程序占用,比如 Redis 占用 6379Tomcat 占用 8080 等等。但是通常如果不需要这些程序的话,我们是可以使用这些端口的。

UDP协议

UDP(User Datagram Protocol) 的内容比较简单,先来说说 UDP

UDP协议 一般用来做 效率要求第一但数据不要求一定要完整 的场景,比如 通话 视频直播。这两项总不能用 TCP 来做,加入用 TCP 来做的话,有可能这个包不完整然后发送端又重新发了,那么将会导致 视频 重复播放某一帧,这显然是不允许的。

UDP协议 只实现了最简单的功能,也就是 应用程序 数据的传输,它可以保证收到的数据是完整的(先放下分包的问题),但是不能保证一定能到达接收方。因此我们也不用在我们自己的消息体中设置一个分隔符或者消息长度,因为收到的消息基本是一个固定的包,客户端是怎样的,接收端就是怎样的。

UDP首部信息

UDP 的首部信息包含四个内容:源端口号(2byte)目标端口号(2byte)包长度(2byte) 以及 校验和(2byte)。一共 8byte 的长度。

  • 源端口号:可选项,如果客户端不需要接收返回的消息,则可以设置为 0
  • 目标端口号:一般存储服务器处理程序监听的端口号;
  • 包长度:UDP头部信息 长度 和 数据部分 的长度之和;
  • 校验和:该字段为了提供可靠的 UDP首部数据 而设计的,协议的校验需要 源和目标的IP地址协议号、还有 发送接收端两边的端口号 参与计算(称为 伪首部)。这个校验和是为了验证 IP 端口 协议 三个关键值的正确以防止数据对其他的 应用 产生干扰而设置的。

UDP的分包问题

上面我们说了,UDP 可以保证接收到的信息是完整的。但是这里有个前提条件,就是不需要被 IP协议 进行分包的信息。比方说,我发送了 ABCD,接收端要么没收到,要么就收到 ABCD,毕竟这四个字符如果用 UTF-8 进行编码的话,也只是需要 4byte

而如果我们分包大于系统设置的默认 MTU 从导致一个消息被分成多个 分片 的情况下,要么所有 分片 都接受到了,系统重组成功返回给 应用程序,要么 分片 不完整或者受损导致无法重组从而系统将整个 UDP 消息丢弃。

那我们来推断一下,数据应该在多少字节的情况下能够安全的发送消息,首先我们知道一个 以太网帧1500byte,而 IP协议首部信息 占用了 20byte(没有可选参数的情况下),说明 IP数据包数据部分 只有 1480byte,而 UDP首部信息 的长度是 8byte,所以一个 MTU 中我们可以存放的空间是 1472byte

但是但是,我们之前已经说了,如果传播图中有个设备小于 MTU,则使用的是最小的 MTU 进行数据传输(木桶定理)所以即使是 MTU=1500 的情况下,如果需要转发的 设备 过多的话,这个值 1472byte 也不是一个稳定的值,所以我们只能在我们知道设备信息的情况下(比如办公室局域网),应用程序1472byte 进行分包发送数据。那如果需要发送到公网的数据咋办,我们就只能使用 Internet上的标准MTU值 576byte(其实也就是目前互联网上可能存在的旧设备链路处理的 MTU 值) 的内容进行数据传输,才不容易出现奇怪的问题。

1
2
3
4
5
6
7
8
9
10
11
Network             MTU (bytes)
-------------------------------
16 Mbps Token Ring 17914
4 Mbps Token Ring 4464
FDDI 4352
Ethernet 1500
IEEE 802.3/802.2 1492
PPPoE (WAN Miniport) 1480
X.25 576

via https://support.microsoft.com/en-hk/help/314496/the-default-mtu-sizes-for-different-network-topologies

TCP协议

UDP 只是一个简单的过度 传输层 的协议,但 TCP 就不一样了,TCP 会给予承诺说所有的数据使用 TCP 协议的话基本上是不会丢失的。即使 IP数据包 经过很多跳 路由器 被丢弃了,但是 TCP 的发送端会接听接收端的接收情况,如果一直在一定的时长内没有收到接收端的接收确认,他将会继续将这一部分数据进行 重发,直到所有数据发送完毕为止。

我们可以假想使用 TCP协议 连接的两个应用程序中连接了一条 管道(但其实 IP层 并没有任何的管道,都是靠一问一答的方式来实现这个虚拟的假想的),就类似于 水管 一样,水就是 数据,但是水是会连连不断的 传输 过来的,所以我们的 应用程序 就需要使用自己的一些规范,比如 分隔符 或者 换行符 来切割每次发送端发送的不在同一个业务内的 数据

TCP首部信息

TCP首部信息 相比 UDP首部信息 就要复杂很多,毕竟有来来回回的应答在。

TCP首部信息

简单的喵几个比较有意思的字段吧:

  • 序列号Seq:这个序列号表示传输到数据的哪个位置,但并不是从 0 开始的,而是一个随机数,再加上位置的长度,可以理解为一个相对位置吧;
  • 确认应答号ACK:应答的位置标示这个位置之前的数据已经被接收完整,发送端需要发送后面的数据;
  • 数据偏移:表示数据部分应该从 TCP包 的哪个位置开始算起,也可以当成 首部信息 的长度;
  • 保留位:拓展使用一般设置为 0
  • 控制位:这个值一共有 8 位,如果某一位上的值为 1 时,表示这个包属于哪种类型的,分别是:
    • CWR:这个字段和下面的 ECE 字段均用于 IP包ECN 字段;
    • ECE:1 表示出现网络拥堵,通知对方将 拥塞窗口 缩小;
    • URG:表示当前包中有紧急数据需要处理;
    • ACK:1 表示 ACK 字段有效,除了最初的建立连接以外,其他的包都需要把这个值设置为 1
    • PSH:1 表示需要把接收到的数据立刻传给上层应用协议,否则则先进行缓存;
    • RST:TCP 出现异常需要强制中断
    • SYN:用于建立连接,1 表示希望建立连接;
    • FIN:用于请求断开连接。
  • 窗口大小:8位,通知从应答号开始的位置可以接受的数据大小,TCP 不允许发送超过这个值的数据;
  • 校验和:跟 UDP 类似,主要用于鉴别程序漏洞造成的首部破坏;
  • 紧急指针:在控制位 URG1 时有效,一般我们浏览器在加载页面的时候,如果加载不出来我们手动点击了停止加载按钮的话,就会发送一个 URG=1 的包;
  • 可选选项:可以在此字段设置最大段的长度、窗口扩大值(用于拓展窗口大小字段)、时间戳(用于32位的序列号不够用的时候加以判断包的先后顺序)等等。

TCP连接的开始与结束

这也就是我们最熟悉的 三次握手,四次挥手 了,由于 HTTP 的传输层用的是 TCP,又由于他是短连接的协议,所以如果我们没有设置 Connection: Keep-aliveKeep-Alive: timeout=20 这两个 HTTP Header 的话,那么一个页面的每一次连接(CSSJS 以及 异步调用),都需要经历 三次握手,四次挥手

那现在我们用一个图来描述这两个过程:

三次握手和四次挥手

那四次挥手的起因是因为,即使发送端发送了断开的请求,但是此时彼此都还需要处理剩下的数据包。只有当所有数据包已经处理完成了,才是真正断开的时机。

TCP数据传输的方式

TCP 是以 为单位进行数据传输的,在建立连接的 SYN 请求中,可以设置消息的最大长度 MSS(Maximum Segment Size),理想的长度是在传输链路中不需要被分包的长度,所以一般在第二次握手,服务端返回的 ACK+SYN消息 中,就会包含这个值。

TCP滑动窗口加快传输

滑动窗口 这个词可是听了很多遍,听起来就很牛逼的样子。

滑动窗口 之前,发送端服务端 需要一来一回的确认数据是否接收完整,比如:发送端 发送 1-1000 字节的数据,然后阻塞等待,服务端 返回说 “ 1-1000 字节我接受完整了,你下一个发 1001-2000 字节的数据吧”,这时候 发送端 才着手把 服务端 要求的数据发送出去。

滑动窗口 就不是这样了,而是先把 几个段 的数据发送出去,等 第一段 的数据接收到来自 服务端ACK 的时候,就发送下一个段的数据,有一个固定的窗口表示当前正在处理的数据的范围。这里有个网站很不错,滑动窗口演示 我截个图来看看。

Window

上方发送端灰色方框 就是表示一个 滑动窗口 的大小,这个 窗口 的第一个数据 接收到ACK 以后,窗口将会往下移动(而无需等待后面的段接收到 ACK),继续发送数据。

一旦出现丢失的问题,接收端则会一直发送应答确认,比如说 1-1000 丢失了,2001-3000 没有丢失,则 接收端 每接收一次 滑动窗口 后面的数据,就会应答一次 下一个是1-1000ACK发送端 在接收了 3次 这一个应答就会认为数据已经丢失,即会重新发送 1-1000 的数据,然后 服务端 因为已经接受了 2001-3000 的数据,所以就会跳跃回答 下一个是3001-4000ACK

至于 滑动窗口 的大小,在发送数据的过程中,发送端 会时不时的发送一个 窗口探测 的请求,来彼此调整 滑动窗口 的大小。

而为了防止 网络瘫痪 的局势出现,TCP 还有一个概念称为 慢启动,并且有另外的一个值 拥塞窗口。为了调节 滑动窗口 的容量值,在刚启动传输的时候,将 拥塞窗口 的容量值设置为 1,之后每收到一个 ACK 应答的时候,这个 拥塞窗口 的容量值就会按照 *1个数据段字节数 / 拥塞窗口字节数 * 1个数据段字节数* 的比例增大,增大到 ACK 返回超时情况时(并非上面说的收到三次 ACK 的情况,这个只是单纯的返回超时了),拥塞窗口 又会缩小为 当前滑动窗口容量1/2

拥塞窗口的值变化 via《图解TCP/IP》

提高网络利用率

发送端的Nagle算法

Nagle算法Nagle 发明,他的目的是减少网络中传输的小段的 TCP包,执行的方式也很简单,条件是 已发送的数据都已经收到ACK || 可以发送最大段长度数据的时候。如果此时需要发送的数据太小,则会先在 发送端 待一会再发送出去。这种方式虽然可以不用随时发送数据导致网络拥堵,但是有时候在需要比较快速传输数据的系统中却是一种障碍。

接收端的延迟确认应答

这个就是上图的 滑动窗口 的加强补充,接收端 没有必要为每一段数据进行应答,可以等待再接收多一点的数据再一起应答,只要应答的序列是递增的 发送端 就会认为已经收到数据了。最大延迟 0.5秒 再发送 ACK应答

但是但是,上面的两个算法放在一起,可能就是灾难了。比方说我 发送端 发送了一个 8字节 的数据以后还需要发送一个 6字节 的数据,但是 接收端 却认为在 0.5秒内 不需要立即应答,然后这个时间里就会导致 发送端接收端 相互等待的局面。所以日常开发中,我们可以关闭我们服务端的 tcpNoDelay 来提高接口响应的速度(-Dsun.net.httpserver.nodelay=true,如果使用的是 Tomcat,默认就是 true 的值)。

捎带应答

这个应用在上面 三次握手 的时候就可以看到了,接收端ACK 的时候顺带还加上了 SYN 的请求,合并成一个数据包进行发送。