传输层TCP协议
1. TCP协议介绍
TCP(Transmission Control Protocol,传输控制协议)是一个要对数据的传输进行详细控制的传输层协议。 TCP 与 UDP 的不同,在于TCP是有连接、可靠、面向字节流的。具体来说,TCP设置了一大堆机制,来保证它的连接是可靠的,它的字节流传输是可靠的。
TCP协议结构:
16位源端口号: 表示数据从哪个进程来。
16位目的端口号: 表示数据到哪个进程去。
32位序号: todo
32位确认序号: todo
4位TCP报头长度: 表示该TCP报头有多少个32bit(4字节),所以TCP报头的最大长度是 4 ∗ 15 = 60 4*15=60 4∗15=60 字节。
6位标志位:
- URG: 紧急指针是否有效。
- ACK(Acknowledge character):确认号是否有效。
- PSH: 提示接收端应用程序立即从TCP缓冲区把数据读走。详细->PSH在流量控制的应用
- RST(Reset,连接重置标志位):对方要求重新建立连接。收到该标志的主机,要对异常连接重新释放,重新建立。(携带 RST标识的报文称为 复位报文段)
- SYN(Synchronize Sequence Numbers):请求建立连接。(携带 SYN标识的报文称为 同步报文段)
- FIN: 通知对方,本端口要关闭了。(携带 FIN标识的报文称为 结束报文段)
16位窗口大小: 标识了自己接收缓冲区的接收能力。详细-> 点击跳转
16位校验和: 发送端填充,CRC校验,接收端判断校验不通过,则认为数据有问题。(校验不仅包含TCP头部,也包含TCP数据)
16位紧急指针: 标识哪部分数据是紧急数据 (实际应用非常少) 。详细->点击跳转
40字节头部选项: todo
1.1 面向字节流
在应用层创建一个TCP的socket,在内核中会创建一个发送缓冲区和一个接收缓冲区。当调用write()
时,数据会先写入发送缓冲区中,如果发送的数据太长,数据就会被拆分成多个 TCP 数据包发出;如果发送的数据太短,就会先在发送缓冲区里等待,等待发送缓冲区堆积得差不多了,或等待其他合适的时机再发出去。
在接收数据时,数据从网卡驱动程序到达内核的接收缓冲区,再通过应用层调用 read()
从接收缓冲区拿走数据。
由于一个TCP连接,既有发送缓冲区,也有接收缓冲区。所以对于一个连接,既可以读,也可以写,这个概念叫做全双工。
由于缓冲区的存在, TCP 程序的读和写不需要一一匹配。
写 100 100 100 个字节数据时,可以调用一次
write()
写100
个字节,也可以调用 100 次write()
,每次写一个字节。读 100 100 100 个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次
read()
100 100 100 个字节, 也可以一次read()
一个字节,重复 100 100 100 次。
2. 确认应答(ACK)机制
我们在进行网络传输时,数据有可能会非常庞大,TCP 需要将一份数据拆分成多份,使用多个报文向对方端传输数据。那么 TCP 就需要确认对方端成功接收到了其中的所有报文而没有丢失。
但由于报文在网络中传输需要时间,主机双方不能实时确认每份TCP报是否被接收,每份报文都是过去发送的。所以需要 确认应答机制来保证数据成功被接收。确认应答机制认为,只要对方传来应答,就判断报文 100% 收到。
TCP为每个需要传输的数据的字节都进行了编号,主机向对方传输了多少数据,只需要告诉对方编号的范围即可。
确认应答机制过程:
主机A向主机 B 发送数据,数据被拆分成多份 TCP 报分别发送。主机A发送第一份数据,在报头中包含此次传输的数据字节序列号为 1-1000
,主机B接收到该TCP报后,向A发送确认应答的TCP报,且告知对方下一份数据的序列号应该从
1001
1001
1001 开始,这是因为主机B要让主机 A 明确我应答的是 传输 1-1000
数据的TCP报(因为实际传输中可能会有多个 TCP 报)。
但是在网络通信中,每一份发送端报文或接收端报文可能不会都安全地到达。如果发生了丢包,TCP要如何应对呢?
3. 超时重传机制
两个主机之间在进行网络传输时,可能会因为网络拥堵等原因造成 TCP 报丢失。在上面确认应答的机制的情况下,如果其中一份 TCP 报丢失了,TCP 就需要对丢失的 TCP 报进行补发。但首先,发送端要先知道接收端丢失了哪一份数据。
3.1 如何处理重传?
如果丢失的是发送端的报文。主机B接收不到报文,主机A迟迟等待不到应答,就会认为发送的报文丢失了,向主机B重新发送一个TCP报。
如果丢失的是应答报文,那么主机 A 迟迟等到不到带有下一份应发序列号的应答,就会认为先前发的报文丢失了(主机A并不知道丢失的是自己的报文还是对方的应答报文),向 B 主机重新发送一份与上一份相同的报文。
所以,无论是发送端报文丢失,还是接收端的应答报文丢失,其处理方法都是一样的。都是发送端重新传一份没有正常经过确认应答机制的报文。
并且如果是应答报文丢失,主机 B 可能会接收到多个相同的报文,TCP 协议就需要具有去重的功能,而这个去重的功能就是由上面(确认应答机制)提到的 TCP 为每个需要传输的数据的字节都进行了编号来实现的。
现在我们知道了超时重传的重传机制,那么 TCP 又是如何判断、处理超时的呢?
3.2 如何处理超时?
最理想的情况是找到一个最小的时间,保证 “确认应答一定能在这个时间内返回”。但这个时间的长短一定是动态的,因为随着网络环境的不同,网络通信的速度肯定也不同。如果超时时间设置得太长,会影响整体重传的效率;如果超时时间设置得太短,有可能发送端会频繁发送重复报文。
Linux 中(BSD Unix 和 Windows 也是如此),超时以500ms为一个单位进行控制,每次判断超时重传的超时时间都是 500ms 的整数倍。
如果判断超时重传一次后,仍然得不到应答,下次超时重传就是
2*500ms
以后,再下次就是4*500ms
,以此类推。当重传累计到一定的次数后,TCP 认为网络或者对端主机出现异常,就会强制关闭连接。
4. 连接管理机制
TCP 是一个有连接的协议。那么 TCP 是如何保证连接是可靠的呢?
TCP 的通信全过程:
服务器端状态转换:
[CLOSED -> LISTEN]
服务器端调用listen()
后进入LISTEN状态,等待客户端连接。
[LISTEN -> SYN_RCVD]
一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送SYN确认报文。
[SYN_RCVD -> ESTABLISHED]
服务端一旦收到客户端的确认报文,就进入ESTABLISHED状态,可以进行读写数据了。
[ESTABLISHED -> CLOSE_WAIT]
当客户端主动关闭连接(调用close()
),服务器会收到结束报文段,服务器返回确认报文段并进入CLOSE_WAIT。
[CLOSE_WAIT -> LAST_ACK]
进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据);当服务器真正调用close()
关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)。
[LAST_ACK -> CLOSED]
服务器收到了对FIN的ACK,彻底关闭连接。
客户端状态转换:
[CLOSED -> SYN_SENT]
客户端调用connect()
, 发送同步报文段。
[SYN_SENT -> ESTABLISHED]
connect()
调用成功,则进入 ESTABLISHED 状态,开始读写数据。
[ESTABLISHED -> FIN_WAIT_1]
客户端主动调用close()
时,向服务器发送结束报文段,同时进入FIN_WAIT_1。
[FIN_WAIT_1 -> FIN_WAIT_2]
客户端收到服务器对结束报文段的确认,则进入FIN_WAIT_2,开始等待服务器的结束报文段。
[FIN_WAIT_2 -> TIME_WAIT]
客户端收到服务器发来的结束报文段,进入TIME_WAIT,并发出LAST_ACK。
[TIME_WAIT -> CLOSED]
客户端要等待一个2MSL(Max Segment Life,报文最大生存时间)的时间,才会进入 CLOSED 状态。
4.1 三次握手
TCP 在进行建立连接时,假设是客户端主动向服务器建立连接:
- 首先客户端向服务器进行 SYN 请求建立连接。
- 服务器接收到客户端的请求后,向客户端发送报文,这个报文除了 SYN 以外,还有客户端对服务器的 ACK 。
- 客户端在接收到服务器的 SYN+ACK 后,此时对于客户端来说连接已经建立完成,所以在下一个报文中,对服务器的 SYN 的 ACK 报文中,是可以携带数据的(称为捎带报文,下面细讲)。
- 服务器在接收到客户端的 ACK 后,对于服务器来说,此时连接也建立完成。
注意在三次握手中,
accept()
函数并没有参与到三次握手当中,当连接建立完成时,accept()
才会返回。
服务器和客户端建立连接的时间,有一个非常短暂的时间差,客户端比服务器要早一个报文。那么假如最后一个 ACK丢包了怎么办?
此时对于客户端来说,连接已经建立完成(ESTABLISHED),是可以正常发数据的。客户端进入正常通信状态,向服务器发送第报文,随后服务器就受到报文,此时服务器还没有进入 ESTABLISHED 状态,但服务器端认得发送该报文的IP和端口,就是刚才还没有完成连接建立(对服务器来说)的客户端。于是服务器对客户端报文的应答就不会是 ACK ,而是 RST(reset,连接重置标志位),表示我服务器这里的三次握手还没有完成,需要重新走三次握手的操作。
如果客户端是浏览器,触发 RST 的效果是这样:
TCP三次握手的目的:
-
验证全双工——验证网络的连通性。
全双工,即同时既能读又能写。之所以要握三次手,就是因为双方都要验证自己既能对对方写(发送 SYN),又能对对方读(接收 ACK)。
-
建立双方通信的共识意愿。
并不是说客户端想要和服务器建立连接,服务器就必须要与其建立连接。在访问网页时,也能常常看到网页拒绝访问的错误。
4.2 四次挥手
四次挥手,别名连接终止协议,是TCP双方断开连接的一种机制。TCP的四次挥手是进行可靠断开连接的最小次数。
客户端主动退出连接(FIN),此时在客户端的本地体现为用 scokfd
调用了 close()
。但既然文件描述符都关闭了,客户端为什么还能接收到服务器的 ACK呢?
实际上调用close()
表示应用层的数据已经发送完成,缓冲区的内容也已经清理完毕了,但是对于操作系统来说,还需要维持一段时间的TCP可靠性,所以连接并没有完全关闭。
为什么是四次挥手而不是三次挥手?
在三次握手中,使用了SYN+ACK的方式保证了最小建立可靠连接的次数。那么,为什么断开连接不能使用 FIN+ACK的方式,使得四次挥手变成三次挥手呢?
由于断开连接是由应用层调用的,应用层此时并不知道底层的通信在进行着怎么样的数据传输,如果服务器端一接收到客户端发来的 FIN,就立马也准备关闭连接,那么可能会导致一方可能在确认关闭连接之前未能发送完所有数据。因为虽然说客户端知道自己已经没有数据要发送,但不代表服务器端就没有数据要发送给客户端,所以四次挥手确保一方发送完数据后再关闭连接,避免数据丢失和确保双方都完全关闭,维持TCP的可靠性和有序性。
由于TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个 FIN 来终止这个方向的连接。收到一个 FIN 只意味着这一方向上没有数据流动,一个 TCP 连接在收到一个 FIN 后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
在套接字的库中,提供了一个关闭sockfd的函数 shutdown()
。
#include <sys/socket.h>
int shutdown(int sockfd, int how)
shutdown()
与colse()
的不同在于,shutdown()
的how
变量用于控制如何关闭文件。填入SHUT_RD
表示关闭读,填入SHUT_WR
表示关闭写,填入SHUT_WDRW
的效果与cOlse()
相同(完全关闭文件)。而且,shutdown()
没有计数器机制,一个进程使用shutdown()
对一个文件进行关闭操作,其他所有进程都会受到影响。
4.3 TIME_WAIT状态
TCP协议规定,使用TCP的程序如果使用 Ctrl+c
终止程序或正常退出,属于主动关闭连接,而主动关闭连接的一方要处于 TIME_WAIT 状态,等待两个 MSL(Maximum Segment Lifetime,报文最长存活时间)的时间后,才能回到 CLOSED 状态。
MSL 在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在 Centos7、Ubuntu上默认配置的值是 60s。
使用
cat /proc/sys/net/ipv4/tcp_fin_timeout
可以查看系统默认 MSL 的值。
之所以 TIME_WAIT 的时间是 2MSL,是因为:
-
MSL 是报文的理论上的最大存活时间,在 2MSL 内能保证双方所有尚未被接收的或迟到的都已经消失。
否则服务器重启后,可能会收到上一个进程的迟到报文,虽然说这种数据很可能是错误的。
-
理论上能保证最后一个报文可靠到达。
假设最后一个 ACK 丢失, 那么服务器会再重发一个 FIN. 这时虽然客户端的进程不在了, 但是 TCP 连接还在, 仍然可以重发 LAST_ACK 。
TIME_WAIT状态在服务器关闭,但客户端还保持连接时,如果服务器想使用原来的端口号重启进程就会发生 bind 错误。但一个服务器端可能需要处理非常大量的客户端的连接,服务器主动关闭连接,就会产生大量的 TIME_WAIT 连接。如果新来的客户端连接的ip和端口号和 TIME_WAIT 占用的链接重复了,就会出现问题。
要想跳过 TIME_WAIT 状态,套接字也提供了一个函数接口,来设置socket的具体属性:
#include <sys/types.h>
#include <sys/socket.h>
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
将
optname
设置为SO_REUSEADDR
(值为1),表示允许创建端口号相同但IP地址不同的多个 socket 描述符。
4.4 CLOSE_WAIT状态
如果TCP连接通信结束后,没有 close()
套接字,导致四次挥手无法完成。此时 TCP 连接就会处于 CLOSE_WAIT 状态。由于进程结束后,TCP连接还要处理善后工作,所以即使程序退出,处于 CLOSE_WAIT 状态的 TCP 并不会退出,造成内存泄漏。所以在进行编码时,必须要防止没有 close()
套接字的错误,如果系统中存在大量的 CLOSE_WAIT 连接,也要进行释放。
5. 滑动窗口
在先前的确认应答机制中提到,TCP 通信的双方,对于每发的一个数据段,都要有一个 ACK 应答。但真实的情况中,不可能是“接收到一个 ACK 再发送下一个数据段”,这样效率太低。
真实的情况,一定是同时发送多个数据段,再同时接受多个 ACK 确认应答:
一次发送窗口大小的数据段,然后等待接收 ACK,等到接收到第一个 ACK 后,窗口向后滑动,继续发送第五段数据段,这便是滑动窗口。操作系统内核为了维护这个滑动窗口,需要使用 发送缓冲区 来记录当前有哪些数据没有被应答,只有确认应答过的数据,才能从缓冲区删除。
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值,图示窗口大小为 4000 字节(四个段)。窗口越大,则网络的吞吐率就越高。但窗口大小也不是不变的(下面细谈)。
窗口滑动示图
在线性表描述的滑动窗口中,表中的下标表示字节序号下标。其中窗口左边表示已发送已接收应答的数据包,窗口中表示已发送待接收应答的数据包,窗口右边表示未发送的数据包。所以说,滑动窗口只能向右边移动。
在实际通信中,接收方是有接收缓冲区的,那么接收方的接收能力就一定是动态的,所以滑动窗口的大小也不是一成不变的,那么发送方如何得知接收方的接收能力呢?
5.1 滑动窗口丢包情况
在超时重传机制中提过,无论是数据包丢失还是 ACK 丢失,发送方都会补发一份数据包给彼端。那么在滑动窗口的机制下,TCP又要如何处理丢包呢?
5.1.1 ACK丢失
如果是应答部分丢失,那么发送端无需重新补发,因为根据后续 ACK 的情况已经可以知道,对方收到了数据包,只是对方的 ACK 丢失了而已。滑动窗口机制使得双方通信中由于 ACK 丢包引起的超时重传大大减少。
图中主机B 1001 的ACK丢失,但主机A接收到了 2001 的ACK,说明在主机A的视角中,主机B确实收到了 1-1000 的数据包,也就无需补发了。
5.1.2 数据包丢失(高速重发控制)
如果是数据包丢失,接收端一次接收多个数据包,固然知道数据包的序号在什么范围内有缺失。此时接收端会暂时放弃对正常接收到的数据包做 ACK 应答,而是反复发送丢包序号的 ACK ,告知发送端发送的数据包出现了丢包,需要补发。接收端接收到三次相同的ACK时,就知道自己的哪一个数据包发生了丢包,需要补发。
虽然说这段时间里接收端没有对正常接收到的数据做 ACK ,但这些数据确实被收到了,存储在接收端的接收缓冲区中。接收端接收到补发的数据包后,这个补发的数据包的 ACK 会跳过正常被接收到的数据包(只要其连续),直接告知发送端窗口滑动后的数据序号,所以发送端也无需对接收端正常接收到的数据包补发,因为根据上面 ACK 丢失的案例,补发数据包的ACK能证明其他数据包已经被正常接收。
这种机制被称为高速重发控制,也叫快重传。
图中发生数据包丢失时,接收端会发送三次以上相同的 ACK 来提醒发送端,我要的数据包是“1001-2000”的数据。接收端在接收到三次相同的 ACK 时(从丢包后开始算),就知道自己发送的数据包丢失了。不等接收到窗口大小数量的 ACK,就对丢包的 1001 − 2000 1001-2000 1001−2000 的数据包进行补发。等待接收到补发数据包的ACK后,再进行滑动窗口(此时正常的数据包的 ACK 肯定到达或丢失),而这个补发数据包的ACK,是正常窗口滑动后的数据包的序号,因为参考上面 ACK 丢失的案例,这个 ACK 能证明其他的数据包已经正常被接收到了。
对于数据包的丢失,无所谓丢包的是在滑动窗口的最左边、中间、最右边。因为滑动窗口的滑动在收到对应字节序号时进行,无论是中间丢包,还是最右边丢包,随着时间推进窗口向右滑动,都会变成最左边丢包的情况。
当最左边丢包时,滑动窗口此时的左侧不再右移,等待最左侧的数据包补发后,才会更新位置。
高速重发机制是否和超时重传机制矛盾?
高速重发机制是接收到三次以上相同的 ACK 才会触发的重发机制,而超时重传的重发机制依据的是时间。在三次握手和四次挥手中,高速重发机制并不参与,也就没有矛盾的说法。
而在正常通信中,如果数据包丢失触发了接收端的高速重发机制,但告知需要重发的 ACK 丢失到不足三个,那么发送端就无法触发高速重发来补发数据包,此时就需要超时重传机制让发送方主动补发数据包。或者数据包发生了大量的丢失,不足以触发三次以上的ACK应答机制,也需要超时重传机制,让发送方主动补发数据包。所以两钟机制是可以共存的。
注意,数据包大量丢失会触发拥塞控制机制(慢启动、拥塞避免等),来缓解网络拥塞情况,所以实际情况会更复杂。
6. 流量控制
滑动窗口解决了数据包如何快速发送的问题,但接收端接收数据的速度是有限的,如果发送端发送的太快,导致接收端的接收缓冲区打满溢出,此后继续接收到的数据包将全部丢包。流量控制则是要解决怎么控制滑动窗口发送速度的问题。
TCP支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就做流量控制(Flow Control)
6.1 16位窗口大小
在TCP报头中,16 位窗口大小用来描述自己的接收能力(缓冲区剩余空间大小),滑动窗口的窗口大小通过 ACK 应答的 16 位窗口大小计算出来,发送方就能控制下一轮窗口大小,不至于发送的数据包超过对方的接收能力造成丢包的问题。
窗口大小字段越大,说明网络的吞吐量越大。而当接收端发现自己的接收缓冲区快满了,就会将窗口大小设置成设置成一个更小的值发送给发送端,让发送端调小自己的发送量。如果接收端缓冲区满了,就会将窗口置为 0
。这时发送方不再发送数据,,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
16 位窗口大小中的变量
int win_start = ack_seq
和int win_end = win_start + win
分别记录了窗口大小的字节序范围。而窗口的向右移动,实际就是对win_start++
、win_end++
。所以想要控制滑动窗口的变大和变小,只需要控制这两个变量的差值即可。
在三次握手的过程中,双方发给对方的 ACK 中的窗口大小就已经交换了双方的接收能力,所以在第一轮数据包中,滑动窗口的大小就已经得知了。
其他:
6.2 探测窗口
当窗口大小变为 0
,此时接收端的接收缓冲区已经满载。发送端要暂时停止发送数据包,接收端需要时间来给接收缓冲区腾出空间。当接收端的接收缓冲区腾出空间后,会给发送端发送一个窗口更新通知的报文,让发送端设置滑动窗口发送下一轮数据包。
但这个窗口更新通知有可能需要很长时间才能发出,或在网络传输的过程中丢包。所以发送端会等待一个超时重传时间,如果时间到了还没有收到窗口更新通知,就会给接收端发一个窗口探测,让接收端回应此时的窗口大小是多少。
7. 拥塞控制
TCP 有了滑动窗口来提升发送数据包的速度,也有流量控制机制来控制发送数据包的速度,但这两者只是解决了通信双方的效率和可靠性。在实际的网络中,存在很多很多的计算机,如果此时网络已经很拥堵,它们在不清楚网络状态的情况下,认为自己或对方的接收缓冲区还绰绰有余,就进行大量的数据发送,就有可能造成网络更加拥堵。
所以 TCP 还需要一个机制,用来控制或缓解多个 TCP 同时通信时造成的拥塞问题,这种机制就是拥塞控制。
7.1 慢启动
所谓慢启动,就是在刚开始通信时,先发送少量的数据,探探网络的拥塞情况,再决定按照多大的速度传输数据。
但是这种增长是指数级增长,一直按 2 n 2^n 2n 增长下去也不合理,且先前提到的窗口大小的概念也没用了。
所以这里还需要引入一个概念,拥塞窗口。在发送数据的开始,将拥塞窗口设为 1
,发送一个数据包。每接收到一个 ACK 应答,拥塞窗口就增加 1
。在发送新一轮的数据包时,将此时的拥塞窗口大小和接收端反馈的窗口大小作比较,取较小值作为实际发送的窗口大小。
同时,慢启动的中后期增长幅度太大,不能单纯让拥塞窗口加倍。所以这里需要引入一个慢启动的阈值,当拥塞窗口超过这个阈值的时候,拥塞窗口就不再指数级增长,而是改为线性增长。
在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回 1
。少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为是网络拥塞。
7.2 延迟应答
如果接收端在收到数据后立马返回 ACK ,此时返回的窗口大小可能就会比较小(因为接收缓冲区此时还什么数据都没读走),所以 ACK 的时机可以迟那么一会儿。一般延迟应答有两个因素要考虑:
数量限制:每个N个包就应答一次
时间限制:超过最大延迟时间就应答一次
对于数量限制和时间限制,依据操作系统不同而不同,但一般 N 取 2
,超时时间取 200ms
。
7.3 捎带应答
接收端不一定需要专门发一个空报头的 ACK 来应答,有时候 ACK 可以搭其他带有数据内容的报文坐顺风车,一起发给发送端。
8. TCP粘包问题
粘包,指的是数据包之间没有明确的边界。
对于UDP来说,由于UDP的报文有明确的长度,数据也是一个一个交给上层的,具有明确的数据边界。即UDP报数据要么是0个、1个、2个,没有半个数据。故使用UDP,无需在应用层解决粘包问题。
但是对于TCP来说,虽然站在传输层角度,TCP报文也是一个一个过来,按照序号排好放在缓冲区中等待读取或发送。但站在应用层的角度,看到的只是一串串连续的字节流,在应用层无法区分哪个部分从哪开始,是一个完整的应用层数据包。故使用TCP,需要在应用层解决粘包问题,规定两个包的边界。
- 对于定长的数据包,保证每次都按固定大小读取即可。
- 对于变长的数据包,则需要在包头位置,规定一个包总长度的字段,从而知道整个包的大小;也可以在包与包之间使用明确的分隔符。(如序列化和反序列化)
9. TCP异常情况
-
进程终止:进程终止会释放文件描述符,也会发送 FIN ,对于TCP来说和正常关闭没有什么区别。
-
机器重启/关机:正常关闭机器都需要等待操作系统处理善后工作,此时就是操作系统在关闭程序,也有关闭TCP连接的流程,和正常关闭没有区别。
-
拔网线/死机:这种情况TCP连接就无法走正常关闭流程,没有四次挥手。此时对方端还认为这个TCP连接正常通信,只是我们一直没有给对方发送数据。在TCP协议中,即使双方没有什么通信往来,TCP连接也要保持很久(几十分钟到几个小时)。
但是TCP也内置了一个保活定时器。当对方不再发报文给我们时,会定期询问对方连接是否还在,如果对方没有应答,也会把连接释放掉。
另外,应用层的某些协议,也有一些这样的检测机制。例如 HTTP 长连接中,也会定期检测对方的状态。例如 QQ,在QQ断线之后,也会定期尝试重新连接。
10. 全连接队列
全连接队列是服务器端在TCP连接的三次握手成功后,临时存放已完成连接请求的一个队列。临时存放,直到服务器调用accept()
将连接从队列中提取出来,进行后续的处理操作。
三次握手建立连接的过程和用户是否调用
accept()
无关。实际上,三次握手是conncet()
发起的,在服务器来不及调用accept()
的时候,底层的TCP listen sock允许用户继续进行三次握手,建立连接,连接成功后存储在全连接队列里。服务器有可能是太忙而没有调用
accept()
,当服务器解除忙碌调用accept()
后,会立马去全连接队列里,将新的sockfd
提取出来。
每个服务器套接字(每个监听的端口)都有自己的全连接队列,全连接队列并不是无限的,具有一个上限,这个上限就是 backlog + 1
。全连接队列的数量由 listen()
的第二个参数 int backlog
决定,全连接队列的节点不能为空(不能一个都没有),所以调用listen()
设置全连接队列的数量时,其数量必是 backlog + 1
。
全连接队列不能为空,会增加服务的闲置率,减少给用户提供服务的效率和体验,也不能太长,浪费空间,用户体验不好。
11. 半连接队列
半连接队列,是TCP针对存放TCP三次握手未完成的连接的队列,这个队列里的连接一般只会存在几十秒到几分钟,并不是很重要。
12. 其他
12.1 16位紧急指针
16位紧急指针标识了哪部分数据是紧急数据,而TCP报头中的 URG 标志位标识了16位紧急指针是否有效。16位紧急指针的内容是报文的起始位置的偏移量,标识了从报文开始的多少字节之后是紧急数据。
注意这里的“指针”并不是c语言中的地址,单纯是一个具有指向性的概念。
TCP中的紧急数据只有一个字节。 一般之所以这么小,是因为它通常用来告知对方突然的变更。如应用层在下载文件时,传输层TCP在发送大量的数据包。此时用户突然进行“暂停下载”或“删除任务”操作,携带紧急指针和有效URG的报头就会在接收缓冲区中“插队”进入上层,直接中断传输。
在 recv()
和 send()
函数中,它们的 flag
参数就可以设置为 MSG_OOB
(out-of-band,带外数据)进行传输紧急数据。