一、传输层协议TCP
1. 理解TCP协议

学习TCP协议,我们要和UDP一样回答两个问题。一、如何将报头和有效载荷分离 二、如何解决分用问题。
先回答第二个问题,分用问题可以根据目的端口号来解决,这一点和UDP协议是一样的。
在解决第一个问题之前,我们先来看一看TCP协议的格式。
TCP协议也具有源端口号和目的端口号,用来解决分用问题以及目标主机给源主机回复的问题。它还有一个32位的序号和确认序号,以及4位首部长度。暂且先看到这里。
首先TCP协议的标准报头长度固定是20字节,至于TCP的选项可能有也可能没有,如果有,报头的长度会大于20字节,这时候报头的长度是多少呢?
首先4位首部长度是用来表示TCP协议报头的总长度的(包含TCP协议中的选项),首部长度占4个bit,也就是说最多只能表示15个字节([0000,1111],即[0,15]),那这都不够标准报头长度的 。TCP规定,首部长度的单位是4字节,那么首部长度实际上能够表示的就是[0,60]个字节,而TCP协议的标准报头是20字节,那TCP协议的选项最多能有40个字节,TCP协议的报头长度范围在[20,60]之间。
现在再来谈谈TCP协议是如何解决报头和有效载荷分离的。
4位首部长度是用来表示报头的长度的,那么当拿到TCP报文的时候,就可以计算出报文的长度,又知道了报头的长度,那报文的长度减去报头的长度不就是有效载荷的长度了吗!这不就实现了报头和有效载荷的分离。
TCP报文中为什么没有报文总长度或者有效载荷的长度?如果没有拿到完整的报文怎么办呢?
答案是因为不需要,TCP协议是面向字节流的。
2. 理解TCP协议的可靠性
在网络通信中,远距离通信是存在丢包问题的 ,当客户端给服务端发送消息时,出现了丢包,客户端能知道自己的报文丢失了吗?答案是不能。
举个例子:今天你在家学了一天,感觉到饿了,你问你妈妈饭做好了没,你怎么知道你妈妈有没有听到你说的话呢?是不是得听到你妈妈给你的回复,你才能确认你妈妈听到了你说的话。
那存不存在百分百可靠的协议呢?
你妈妈给你回复了,只能确认你先前对你妈妈说的话你妈妈听到了,但你妈妈给你回复,她怎么确认你有没有听到呢?需要你给你妈妈进行回复,她才能确认她说的话你收到了,那么问题来了,你又怎么确认你说的话你妈妈有没有听到呢?这不就是鸡生蛋,蛋生鸡的问题了吗!
所以不存在百分百可靠的协议,最新的消息永远也没有办法确认对方是否收到。
但是能够确定的是只要得到应答,就能够确认历史报文百分百是被对方接收到了的。
但有一个问题,应答还需不需要应答呢 ?答案是不需要的,因为应答本质上是为了对历史报文进行确认的,确认对方是否收到了,而不是对自己进行确认。
就像你问你妈妈饭做好了没,你妈妈说还没呢,那你就知道你说的话你妈妈是听到了的,而你不需要再次进行应答。
那如果是服务端给客户端发送消息呢 ?原理是一样的,客户端就需要给服务端进行应答,来确认服务端给客户端发送的消息,客户端是拿到了的。
TCP的常规通信模式就是客户端给服务端发送报文,服务端需要对客户端进行应答,以此来确认客户端发送的报文是被服务端接收了的,服务端给客户端发送报文,客户端需要对服务端进行应答,以此来确认服务端的报文被客户端接收了。
这也就意味着对端必须给对方发送的有效消息进行应答,互相保证两个朝向上的可靠性。而且这是一种串行的模式,也就意味着效率低下。
总结:
1.消息!= 应答,消息是包含了用户信息的内容,应答就只是单纯的应答。
2.应答不是为了保证应答的可靠性,而是为了保证消息的可靠性。
3.应答方不确定应答对方是否收到,但是发送方有没有收到应答是能确认的。
4.收到应答,本质上是为了保证历史消息的可靠性。
TCP通信的一般模式
比如:客户端给服务端一次性发送多个报文,服务端并没有第一时间收到一个报文就开始应答,而是收到一部分或者全部的报文再开始应答,还是确认应答机制,但是这一次发送报文和回复应答在各自的过程中时间上就有了一定的重叠性,效率就高了。
接收方在进行应答时,是由自己的OS自动完成的。
那么,假如你和你的舍友要从北京去云南,你先走了一段时间,然后你的舍友才陆陆续续的出发,但是你能保证自己一定先到达云南吗?有没有可能你在路上玩了几天呢?有没有可能你坐的是火车,你舍友坐的是飞机呢?是不是不能按照你们出发的先后确定你们到达云南的顺序。所以这本身就是一种不可靠性 ,对于TCP协议来说,也是如此,一次性发送多个报文,也是无法确定到达的顺序 ,所以TCP协议要有序号。
再举个例子,有没有可能客户端发送了多个报文,比如4个,服务端只回复了3个应答,那能确认是哪一个报文没有被接收吗 ?不能,所以要有确认序号。
比如这四个报文的序号分别是100,200,300,400,服务端收到报文分别应答101,201,401,不就知道是哪一个报文没有被接收了吗!(原则上来讲,这样是不对的,TCP不是这样实现的)
确认序号ack_seq表明,该数字之前的报文,我已全部收到,下次发送,请从ack_seq开始发送。
上面的例子,如果是第3个报文丢失了,那么确认序号应该是300,而且不会应答401 ,因为确认序号表明之前的数据都已经接收到了。
现在提出两个问题:
1.客户端,服务端给对方互相发送的是什么 ?是报文,报文 = 报头 + 有效载荷,应答的一方可以没有有效载荷。
2.为什么要有两个序号呢?
可不可以用一个序号来实现确认应答机制呢 ?例如客户端在给服务端发送报文的时候,带上序号,服务端再给客户端应答的时候,用这个序号来作为确认序号发送给客户端呢?理论上是可以的,但是这样做效率太低了,每次都需要确认应答发送给客户端,服务端才能给客户端发送报文。
客户端给服务端发送报文,服务端需要对客户端进行应答,那么接下来如果服务端需要给客户端发送报文呢?要在服务端对客户端应答完之后再发送报文吗?这样不是串行,效率就低了。
那可不可以这样呢?客户端在给服务端发送报文的时候,服务端对客户端进行应答的同时发送报文呢 ?这样是不是效率就高了,这就叫做捎带应答。
这也是TCP的真实场景。那么服务端再给客户端进行应答就需要确认序号,同时给客户端发送报文就需要序号,所以这也是为什么要有两个序号的原因,就是为了提高效率。
前面我们说过,TCP协议是有发送和接收缓冲区的,源主机发送数据的时候,是把数据拷贝到了发送缓冲区里的,至于数据何时发送给目标主机,由TCP协议决定。TCP协议会把发送缓冲区里的内容拷贝到目标主机的接收缓冲区 ,但是缓冲区里的空间是有限的,如果目标主机的接收缓冲区的剩余空间不多了呢?源主机发送了大量的数据,目标主机无法接受这么多的数据,就会把一部分数据进行丢弃。
丢弃了就需要重新发送,那这样岂不是效率上就损失了。而且这一次丢弃数据理论上来说是不应该的,因为没有出现异常,所以这也是不可靠的一种情况。
那么,源主机应该如何才能衡量目标主机的接收能力呢?又如何得知目标主机的接受能力呢?
我们说了,源主机是把数据发送给目标主机的接收缓冲区的,所以衡量目标主机的接受能力就应该是目标主机的接收缓冲区剩余空间的大小。
那源主机如何得知目标主机的接受能力呢?
所以TCP协议报头部分有一个16位的窗口大小,这个窗口大小就是用来表示对端接收缓冲区的接受能力的。
源主机给目标主机发送数据,目标主机进行应答的过程当中就会填入自己接收缓冲区的剩余空间的大小。
我们把这种根据16位的窗口大小,调整发送数据速率的机制,叫做流量控制。
注 :流量控制是双向的,如果单向发送就只做单向流量控制。
窗口大小填写的是自己的接收缓冲区剩余空间的大小。
那如果目标主机的接收能力很强呢 ?就意味着源主机可以多发数据了。
流量控制可以兼顾可靠性(不合理的丢包导致重传)和效率(减少了不合理丢包导致重传的问题)。
目前解决TCP协议可靠性的两种途径就是序号确认应答机制和窗口大小。
3. TCP标志位
什么是标志位?
标志位就是结构体位段中的比特位。比特位的内容0表示无效,1表示有效。
那为什么要有标志位呢?
举个例子:客户端和服务端之间是一对多的关系,可以有多个客户端向服务端发起建立连接的请求,也可以断开连接,也可以发送正常的报文。所以报文是有类型的。
为什么要有类型呢 ?类型决定着接下来要做的动作。建立连接就要建立客户端与服务端之间的联系,断开连接就要断开客户端与服务端之间的联系。所以要在报头设置标志位,用来区分报文类型。
ACK是用来确认报文是否有效的,为1表示该报文是一个确认报文。
报文是可以捎带应答的,所以大部分情况下ACK都是1。
SYN是用来请求建立连接的,我们把携带SYN标识的称为同步报文段。
TCP是面向连接的,通信之前需要先进行三次握手,进行建立连接,这个过程由OS自己完成,但需要由用户手动触发。
客户端要给服务端发送消息之前,客户端要和服务端先建立连接,在建立连接时,客户端先给服务端发送报头 ,将SYN置为1表示要和服务端建立连接,服务端说可以啊,就对客户端进行应答 ,将ACK,SYN都置1,客户端接收到应答之后,对服务端进行确认 ,将ACK置为1,发送给服务端。至此,连接建立成功。
为什么是3次握手?
举个例子:你的班级上有一个很漂亮的女生,你很喜欢她,所以你就上去问她能不能做你女朋友,她说好呀,什么时候开始,你说就现在。至此你们就正式成为了男女朋友。
所以,三次握手是为了建立连接的共识。
FIN通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段。
客户端与服务端建立连接之后,就可以开始正常通信了 ,那断开连接的时候呢?就需要将FIN标志位置为1,表示要断开连接。
客户端给服务端发起断开连接的请求FIN,服务端进行确认ACK,服务端再给客户端发起断开连接的请求,客户端进行确认,至此断开连接成功。
这个过程就类似于夫妻之间离婚一样,当其中一方提出离婚时,另一方收到请求之后,也同意了离婚,等待另一方进行确认,确认成功后就可以断开连接了。所以四次挥手的本质是为了建立断开连接的共识。
那这个过程可不可以简化为3次挥手呢?
前面我们说过了,发送报文的同时可以同时捎带应答,那当客户端发起断开请求连接的时候,服务端也想断开连接呢 ?那不就可以在对客户端请求进行应答的同时发送断开请求吗!这不就是捎带应答吗!
那3次握手可不可以看成4次握手呢?
你想让人家女生做你女朋友,所以上去对人家表白,但是人家说她需要考虑一下,过了一会儿,她说可以,你收到应答,至此关系成立。
这不就是客户端发起建立连接的请求,服务端进行确认,再对客户端发起建立连接的请求,客户端确认的过程吗!这不就是四次握手吗!
三次握手的本质也是四次握手。之所以是三次握手是因为捎带应答了。
那为什么我们经常说是三次握手呢 ?因为服务端要无条件的接收客户端的请求,所以就可以捎带应答了。
PSH标志位是用来提示接收端应用程序立刻从TCP缓冲区把数据读走。
举个例子:客户端给服务端发送数据是不能无脑发送的,首先需要建立连接,其次还需要知道服务端的接收能力 ,假设服务端的接收能力为0呢,那么客户端就应该阻塞,停止发送,如果服务端一直不读取数据,那客户端要一直阻塞吗 ?当然不行了,所以客户端会给服务端发送不携带数据的报文,即报头(询问报文),询问服务端是否有接收数据的能力,如果一直没有,那这个时候就会将PSH标志位置为1,提醒接收端尽快读取数据,留出空间接收新的数据到来,再通过应答报文中的窗口告诉客户端服务端的接收能力(用来理解,窗口探测并不会设置PSH标志位,如果接收端一直不读取,那么将启动超时机制,终止进程)。
这个例子有点极端,是为了便于理解,实际上即便服务端有接收能力,也可以将PSH标志位置为1。
读取数据应该是应用层要处理的事情啊,应用层就是为了尽快做IO的,如果不做,那就是bug,假设现在服务端读取数据,但接收缓冲区里没有数据,那recvfrom的时候,进程不就阻塞了吗!当数据到来的时候,OS把进程唤醒,读取数据。
RST:对方要求重新建立连接,我们把携带RST标识的称为复位报文段。
我们说TCP协议发送数据之前,是要建立连接的 ,那客户端向服务端发起建立连接的请求,会只有一个客户端吗?答案是不是,可以有多个客户端发起建立连接的请求 ,那OS要不要对这些连接进行管理呢?答案是要的,先描述在组织。一定会在双方的内核中构建连接结构体,维护和建立连接是有成本的。
客户端在向服务端发起建立连接的请求时,需要进行三次握手,我们不害怕前两次连接失败,因为前两次失败只能说明请求连接失败了,客户端不会发送数据,成本较低,但是我们害怕第三次连接请求失败,因为第三次请求没有应答 ,我们不知道请求成功还是失败了,如果成功了,那一切好说,直接发送数据,通信即可,可如果失败了呢?站在客户端的视角,是不知道的,客户端会认为自己建立连接是成功了的,所以会直接发送数据,可对于服务端呢?服务端会认为第三次握手失败了呀,你怎么就直接给我发送数据了呢,所以服务端会给客户端发送报文,将RST标志位置为1,要求客户端释放连接,重新建立连接。
所以,TCP建立连接,即三次握手不一定百分百成功 。而对于客户端来说,认为自己只要将报文发送给服务端,就认为一次握手完成。
实际上第三次握手失败,与这个有着些许差别,因为TCP有重传机制,服务端在SYN_RCVD状态下会重传SYN+ACK,如果多次重传失败,服务端会主动关闭这个半连接,这时客户端发送数据,连接已不存在,就会收到RST。
URG是用来判断紧急指针是否有效。
我们说TCP是保证可靠性的,发送的数据是要按序到达接收缓冲区的,因此接收缓冲区可以被看做是字节流式的队列,那如果有数据想要被优先处理呢?那就只能插队了。那什么样的数据要被优先处理呢?举个例子理解。
比如说我们要把图库里的照片上传给云盘,这时候我们突然改变主意了,不想上传数据了,难道要等到全部的照片全部上传以后再停止上传吗?这显然是不对的,这样做不仅在浪费资源,而且毫无意义。这时候停止上传的需求就应该被插队,优先处理。但是这种情况不是主流。
我们把这种优先处理的数据叫做紧急数据(或带外数据),它不属于常规数据。
URG为0表示TCP协议中的16位紧急指针无效,为1表示有效。
16位的紧急指针表示的是当前的报文有效载荷中,特定的偏移量处,有紧急数据。
那有几个紧急数据呢 ?紧急数据不属于常规数据,插队也不能插太多,因为这会破坏TCP协议的可靠性。紧急数据只有一个字节,就可以用来作为状态码。


这两个函数都可以使用这个标志位,这个选项就可以带出带外数据。
4. 确认应答机制
比如:客户端给服务端发送数据,序号为1000,服务端给客户端确认应答,确认序号为1001,客户端再给服务端发送数据,这一次发送的数据就应该从1001开始发送,序号为1001,服务端继续给客户端应答,确认序号为2001。
在逻辑上我们可以认为,TCP的发送和接收缓冲区是一种线性结构 。但实际上TCP的发送和接受缓冲区不是线性的,它是一种队列结构,使用双链表的形式进行管理的,在OS中有一个数据结构struct iovec,它就相当于是一个数组,数组空间连续存储着TCP的队列结构,对外提供这个数组,内部进行物理不连续到逻辑连续的转化。
5. 超时重传机制
在了解超时重传机制之前,我们需要谈谈什么是丢包问题?
比如:客户端给服务端发送数据,服务端有没有收到,客户端知道吗?知道,通过确认应答就可以知道。那客户端发送的数据,丢失了,客户端知道吗?不可以,因为丢包了,服务端不会应答。
丢包的几种情况:
第一种:客户端给服务端发送数据,丢包了。
第二种:客户端给服务端发送数据成功了,服务端进行应答,但应答丢失了,导致客户端以为自己的数据丢失了。
丢包要么数据丢,要么应答丢,两者不会同时存在。最终的结果都是客户端无法收到应答。
所以,客户端无法判断自己的数据对方是否收到了 ,因此处理这种情况采用简单粗暴的方式,统一认为这个报文被丢包了。
所以,判断是否丢包是被约定出来的。
接着再来谈什么是超时重传。
超时重传 :约定一个特定的时间,时间以内返回应答,即成功了,如果超过这个时间,我们判定超时,就会重发。
那如果是报文没丢,应答丢了,重传不就会导致同一个报文被重复发送吗 ?这也是一种不可靠的表现,所以要有序号,可以进行序号去重。
所以,超时重传机制也是TCP可靠性的条件之一。
那这个超时的时间应该被设置为多少呢?
首先网络的通畅程度是浮动的 ,这是一个客观的事实。如果时间太短,会导致高频次的重传,如果时间太长,发送效率太低。所以,这个时间不能固定某一个值,而且发送速率也与网络的通畅程度有关 ,因此,超时时间也必须随着网络浮动,调整超时时长。
那超时时间如何确定呢 ?最理想的情况下,找到一个最小的时间,这个时间是确认应答能够在这个时间内返回的。但这样做肯定是不够的,TCP协议为了保证无论在任何环境下都能比较高性能的通信,会动态计算这个最大超时时间。
Linux中,超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
如果重发一次没有得到应答,等待2500ms后在进行重传,如果还没有得到应答,4500ms后进行重传(以指数增长的趋势),累计一定的重传次数后,TCP认为网络或者对端主机出现异常,就会强制关闭连接。