TCP全称是“传输控制协议(Transmission Control Protocol)”,是当今运用最多的传输层协议。TCP能被广泛使用,主要原因在于它保证了可靠性,并且为了传输效率也做到努力。本文会先介绍TCP报头的字段,并且渗透TCP保证可靠性的措施。
TCP报文的格式
先来见一见TCP报文
TCP报文的格式明显比UDP复杂,多了很多字段。也就注定了TCP的学习难度会远大于UDP。
所以,博主将逐个讲解报文的字段。并且介绍TCP保证可靠性的原因。
对于这个报文格式。
- 首先要解决的是有效载荷和报头如何分离。
- 其次、数据如何向上交付。
有效载荷和报头的分离问题?
udp是固定首部长度。而tcp首部长度是不固定的,因为包含了选项字段。
tcp的分离规则:先读取前20字节,获取首都长度。也就是4为首部长度。4位首部长度的单位是4字节表示的范围是[0,60],因为首部长度最少是20字节,所以修正一下范围为[20,60]
获取到首部长度后,就能做到有效载荷和报头的分离。
向上交付的问题?
数据的向上交付是依据目的端口。
应用层拷贝数据到传输层,会发生什么?怎么从传输层将数据交付?
传输层会存在大量的tcp数据报。所以要对这些报文进行管理。管理的方式就是sk_buffer是一个链表的形式。
报头的实质就是一个位段结构体,里面包含了每个字段的数据。
应用层将数据拷贝到传输层时,传输层会为数据申请一块专有的缓冲区,并且用sk_buffer结构体的data指针指向数据其实,tail指针指向数据结尾,最后将tcphdr报头拷贝到data指针之前,然后将data指针指到最前面。最后链接入链表。
往上层交付数据的时候,不再拷贝数据,而是改变head指针,移到有效载荷的起始位置。
流量控制
TCP为了报文可靠性,每次发送的消息,只有收到对方的回应后,才算真正发出去了。
在TCP传输层中存在发送缓冲区和接收缓冲区。如果对端是服务器,必然同时存在大量的报文在缓冲区中。而应用层来不及将接收缓冲区的数据读取,就会导致缓冲区的数据越来越多,剩余的空间越少。那么一旦缓冲区满了,如果再有数据到来,是不是只能丢弃?
但是数据跨越网络好不容易到达的对端,就被丢弃,显然是不合理的。因此在TCP层就会存在一个标志自己接收缓冲区大小的字段,也就是16位窗口。
所以——流量控制就是保证TCP可靠性的第一谈。
用法:
由于对端收到消息后,就会发送ACK报文(就是应答报文,裸的报头,将ack标记位设为1)
应答客户端的同时,服务器想通知自己的接受缓冲区大小,就会将16位窗口大小设置。然后在应答报文中携带窗口大小。
极端情况:对端接受缓冲区满了,数据会被放到发送缓冲区中。如果发送缓冲区也满了,那么上层就必须阻塞等待。
确认应答机制和发送数据的模式
TCP通信的双方地位是对等的,为了保证可靠的将数据发出,对端在接受到报文时,必须发送一个确认报文。
举一个简单的例子:电视剧特种兵作战时候,如果张三发现敌人,常常就会说:李四李四,A点发现敌人,收到请回答。这时候张三将自己的消息发送出去了,但是在没有收到李四的回答之前,它是不确定自己的消息真正的发出的,甚至过了一段时间没人回答就要重发。只有李四对张三的消息回复了,张三才能确定可靠的发送出去。
发送模式
但是这种发送模式的效率实在是太低了,同一时间端只能发送一份报文。如果是网络传输是几MB,而一份报文只有几K,那么发送的效率实在太低了。为了提高效率,一种发送时间可以重叠的机制被引入。
32位序号
数据被发出后,先发出的数据不一定先到达,主要是网络的波动和阻塞。也就是说,报文到达对端可能是无序的。而TCP是面向字节流的,上层只会一直收取数据,所以必须保证报文按照顺序组织。 因此引入32位序号。
保证报文按序到达——TCP保证可靠性的第二谈
所以通过32位序号就能给到达的报文排序,序号在前的报文自然就被排到前面。然后组织好顺序在往上交付,就是实现了按序到达。
32确认序号
服务器收到客户端的报文之后,并且能够根据报文的序号进行排序,保证序号的按序到达。那么下一步服务器就要对客户端进行ack。ack也必须保证到达客户端有顺序,所以再引入确认序号。
确认序号:序号+1,并且规定:确认序号之前的所有报文被对方全部收到了。
比如是确认序号是100,那么确认序号就是100+1,101。
为什么要规定确认序号之前的报文全被接受了?
允许少量的报文丢失。
如果101 201 301ack丢失了,但是401却收到了。客户端就能知道100 200 300 400的报文已经到达对方了,就不需要重新发送报文,是提高效率的做法。
有一个问题:为什么需要确认序号和序号呢?不能压缩成一个吗?
因为tcp是全双工,客户端给服务器发报文,同时服务器也会给客户端发报文。双发地位是对等的。为了提高效率,一种捎带应答的方式被使用。
如果需要发数据,同时又要ack。就会被合并到一份报文中,提高效率。
发报文需要序号标志位,ack也要标志位。所以不能省略。
讲完了窗口大小和确认序号,接下来来介绍一下标记位。
一台服务器同时会遇到许多的不同的连接,有建立连接的、断开连接的、异常处理的。为了区分这些不同类型的连接,对于每一份连接都应该有标记。
六位标记位
这些标记位分别是SYN、FIN、ACK、URG、PSH、RST
SYN和FIN是建立连接和关闭连接时候用到的标志位。ACK是应答标记。URG是紧急标记位。PSH是催促上层尽快取走数据。RST是重置连接。下面将介绍这些标记位的使用,及其场景。
URG:紧急标记位
如果URG被设置,通常会设置紧急指针。紧急指针是有效载荷中紧急事件位置的偏移量。
一个紧急指针只能标记一个字节
数据是按照顺序排队写入的,如果有异常事件触发,那么会需要设置紧急标记位。告知对端的上层先处理紧急消息,其它消息滞后处理。
在sento中的flage字段就有一个MSG_OOB表示带外字段,设置紧急事件。
场景:
- 需要暂停/终止服务器时,但是服务器正在排队处理之前的消息。这时候紧急指针的事件就会被优先处理。
- 询问服务器状况,如果服务器当前处理速度较慢,就可以发送带紧急指针的报文。服务器收到报文后优先回答对方我的状态。
PSH:推送
PSH被设置就是告知上层尽快把数据向上交付。
重新审视一下流量控制。
如果客户端得知对端的接受缓冲区变为0时,就会进入阻塞状态。
那么什么时候结束阻塞状态,总不能一直阻塞下去?
1.客户端定时给服务器发送报文,会得到ack并且获取服务器的窗口。这时候通常会带上PSH标记位,告诉服务器上层尽快处理缓冲区内容。
2.服务器的接受缓冲区由0变为非0时,会主动发送报文,告知缓冲区大小。
这俩种方式共同使用,都能让服务器知道什么时候停止阻塞。
场景:
像实时数据传输之类的,如直播、游戏等,报文通常都会携带PSH
RST:重置标记位
引入一个问题,三次握手必须成功吗?
答案是错的,三次握手不一定都会成功。就拿客户端主动向服务器发起三次握手为例子。
客户端的接受到服务器的ACK后,客户端就默认三次握手成功。而对于服务器而言,必须等到最后一份ACK收到之后,才认为建立连接完成。所以客户端和服务器认为的建立连接完成的时机是不一致的。
所以前俩次报文丢失是没关系的,因为对方都能可靠的知道,重新发送报文。
而最后一次ACK的丢失是无法预知的。
这就导致了一个问题:客户端认为已经建立连接,而服务器任务连接没建立完成。
如果客户端给服务器发了一条消息,而服务器尚在未连接状态,就会给客户端发送一个报文,将RST标记位设置,告知客户端重新建立连接。
场景:常用于链接建立认知不一致,重新建立请求!
简单谈完了标记位,下面来介绍一下TCP保证可靠性的第三谈——超时重传
超时重传
如果TCP请求被发出后,没有收到ack。有俩种情况
1.请求报文丢失
2.ack丢失。
如果是请求报文丢失,那么重新发送一份报文即可。
而ack丢失,那也要重新发送一份报文。而接受方会接受到同一份报文,就必须对报文去重,这里隐含着一个知识:TCP会短暂保存接受到的报文。
所以超时重传的重传机制很简单。
那什么时候重传呢?既不能太快也不能太慢。
规定以500MS为起始,如果还是没收到报文第二次2*500MS的时间发送报文。
第三次以4*500MS的间隔发送
超时的规则就是2^n *500MS
至此TCP报文部分就介绍完毕,并且渗透了TCP可靠性的原因。下文将继续介绍TCP保证可靠性、提高效率的策略