转载自:https://blog.csdn.net/qq_34827674/article/details/105331617
1.TCP基本认识
TCP头部格式:
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决不丢包的问题。
控制位:
- ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN包之外该位必须设置为 1 。
- RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
- SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
- FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。
Q: 为什么需要 TCP 协议? TCP 工作在哪一层?
A: IP 层是「不可靠」的,它不保证网络包的交付、不保证网络包的按序交付、也不保证网络包中的数据的完整性。如果需要保障网络数据包的可靠性,那么就需要由上层(传输层)的 TCP 协议来负责。因为 TCP 是一个工作在传输层的可靠数据传输的服务,它能确保接收端接收的网络包是无损坏、无间隔、非冗余和按序的。
Q:什么是 TCP ?
A:TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
- 面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
- 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
- 字节流:消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。并且消息是「有序的」,当「前一个」消息没有收到的时候,即使它先收到了后面的字节,那么也不能扔给应用层去处理,同时对「重复」的报文会自动丢弃。
Q:什么是 TCP 连接?
A:简单来说就是,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。
所以我们可以知道,建立一个 TCP 连接是需要客户端与服务器端达成上述三个信息的共识。
- Socket:由 IP 地址和端口号组成
- 序列号:用来解决乱序问题等
- 窗口大小:用来做流量控制
Q:如何唯一确定一个 TCP 连接呢?
A:TCP 四元组可以唯一的确定一个连接,四元组包括如下:
- 源地址
- 源端口
- 目的地址
- 目的端口
源地址和目的地址的字段(32位)是在 IP 头部中,作用是通过 IP 协议发送报文给对方主机。
源端口和目的端口的字段(16位)是在 TCP 头部中,作用是告诉 TCP 协议应该把报文发给哪个进程。
Q:有一个 IP 的服务器监听了一个端口,它的 TCP 的最大连接数是多少?
A:服务器通常固定在某个本地端口上监听,等待客户端的连接请求。因此,客户端 IP 和 端口是可变的,其理论值计算公式如下:
对 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数,约为 2 的 48 次方。当然,服务端最大并发 TCP 连接数远不能达到理论上限。
- 首先主要是文件描述符限制,Socket 都是文件,所以首先要通过
ulimit
配置文件描述符的数目; - 另一个是内存限制,每个 TCP 连接都要占用一定内存,操作系统的内存是有限的。
Q: UDP 和 TCP 有什么区别呢?分别的应用场景是?
A:
- 连接
TCP 是面向连接的传输层协议,传输数据前先要建立连接。
UDP 是不需要连接,即刻传输数据。 - 服务对象
TCP 是一对一的两点服务,即一条连接只有两个端点。
UDP 支持一对一、一对多、多对多的交互通信 - 可靠性
TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。
UDP 是尽最大努力交付,不保证可靠交付数据。 - 拥塞控制、流量控制
TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。 - 首部开销
TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是20
个字节,如果使
用了「选项」字段则会变长的。
UDP 首部只有8
个字节,并且是固定不变的,开销较小。
- 传输方式
TCP 是流式传输,没有边界,但保证顺序和可靠。
UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。 - 分片不同
TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输
层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完
数据,接着再传给传输层,但是如果中途丢了一个分片,则就需要重传所有的数据包,这样传输
效率非常差,所以通常 UDP 的报文应该小于 MTU。
TCP 和 UDP 应用场景:
由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:
- FTP 文件传输
- HTTP / HTTPS
由于 UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:
- 包总量较少的通信,如 DNS 、SNMP 等
- 视频、音频等多媒体通信
- 广播通信
Q: 为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?
A:原因是 TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录UDP 的首部长度。
2.TCP 连接建立
2.1TCP 三次握手过程和状态变迁
TCP 是面向连接的协议,所以使用 TCP 前必须先建立连接,而建立连接是通过三次握手来进行的。
从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的。
一旦完成三次握手,双方都处于 ESTABLISHED 状态,此时连接就已建立完成,客户端和服务端就可
以相互发送数据了。
Q: 如何在 Linux 系统中查看 TCP 状态?
A: TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看。
Q: 为什么是三次握手?不是两次、四次?
A:
1.阻止重复历史连接的初始化(主要原因)
2.同步双方的初始序列号
3.避免资源浪费
- 阻止重复历史连接的初始化(主要原因)
客户端连续发送多次 SYN 建立连接的报文,在网络拥堵情况下:
- 一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端;
- 那么此时服务端就会回一个
SYN + ACK
报文给客户端; - 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户
端就会发送RST
报文给服务端,表示中止这一次连接。
如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接:
- 如果是历史连接(序列号过期或超时),则第三次握手发送的报文是
RST
报文,以此中止历史
连接; - 如果不是历史连接,则第三次发送的报文是
ACK
报文,通信双方就会成功建立连接;
所以,TCP 使用三次握手建立连接的最主要原因是防止历史连接初始化了连接。
- 同步双方的初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
- 接收方可以去除重复的数据;
- 接收方可以根据数据包的序列号按序接收;
- 可以标识发送出去的数据包中, 哪些是已经被对方收到的;
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN
报文的时候,需要服务端回一个 ACK
应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当
服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。
而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
- 避免资源浪费
如果只有「两次握手」,当客户端的 SYN
请求连接在网络中阻塞,客户端没有接收到 ACK
报文,就会重新发送 SYN
,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的ACK
确认信号,所以每收到一个 SYN
就只能先主动建立一个连接,这会造成什么情况呢?
如果客户端的 SYN
阻塞了,重复发送多次 SYN
报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
Q: 为什么客户端和服务端的初始序列号 ISN 是不相同的?
A: 如果一个已经失效的连接被重用了,但是该旧连接的历史报文还残留在网络中,如果序列号相同,那么就无法分辨出该报文是不是历史报文,如果历史报文被新的连接接收了,则会产生数据错乱。所以,每次建立连接前重新初始化一个序列号主要是为了通信双方能够根据序号将不属于本连接的报文段丢弃。
另一方面是为了安全性,防止黑客伪造的相同序列号的 TCP 报文被对方接收。
Q: 既然 IP 层会分片,为什么 TCP 层还需要 MSS 呢?
MTU
:一个网络包的最大长度,以太网中一般为1500
字节;MSS
:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度;
如果在 TCP 的整个报文(头部 + 数据)交给 IP 层进行分片,会有什么异常呢?
当 IP 层有一个超过 MTU
大小的数据(TCP 头部 + TCP 数据)要发送,那么 IP 层就要进行分片,把数据分片成若干片,保证每一个分片都小于 MTU。把一份 IP 数据报进行分片以后,由目标主机的 IP层来进行重新组装后,再交给上一层 TCP 传输层。
这看起来井然有序,但这存在隐患的,那么当如果一个 IP 分片丢失,整个 IP 报文的所有分片都得重传。
因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。当接收方发现 TCP 报文(头部 + 数据)的某一片丢失后,则不会响应 ACK 给对方,那么发送方的TCP 在超时后,就会重发「整个 TCP 报文(头部 + 数据)」。
因此,可以得知由 IP 层进行分片传输,是非常没有效率的。
所以,为了达到最佳的传输效能 TCP 协议在建立连接的时候通常要协商双方的 MSS 值,当 TCP 层发现数据超过 MSS 时,则就先会进行分片,当然由它形成的 IP 包的长度也就不会大于 MTU ,自然也就不用 IP 分片了。
经过 TCP 层分片后,如果一个 TCP 分片丢失后,进行重发时也是以 MSS 为单位,而不用重传所有的分片,大大增加了重传的效率。
什么是 SYN 攻击?如何避免 SYN 攻击?
A:
SYN 攻击
我们都知道 TCP 连接建立是需要三次握手,假设攻击者短时间伪造不同 IP 地址的 SYN
报文,服务端每接收到一个 SYN
报文,就进入SYN_RCVD
状态,但服务端发送出去的 ACK + SYN
报文,无法得到未知 IP 主机的 ACK
应答,久而久之就会占满服务端的 SYN
接收队列(未连接队列),使得服务器不能为正常用户服务。
- 避免 SYN 攻击方式一
其中一种解决方式是通过修改 Linux 内核参数,控制队列大小和当队列满时应做什么处理。
- 当网卡接收数据包的速度大于内核处理的速度时,会有一个队列保存这些数据包。控制该队列的
最大值如下参数:
net.core.netdev_max_backlog - SYN_RCVD 状态连接的最大个数:
net.ipv4.tcp_max_syn_backlog - 超出处理能时,对新的 SYN 直接回报 RST,丢弃连接:
net.ipv4.tcp_abort_on_overflow
- 避免 SYN 攻击方式二
我们先来看下 Linux 内核的 SYN
(未完成连接建立)队列与 Accpet
(已完成连接建立)队列是如何工作的?
正常流程:
- 当服务端接收到客户端的 SYN 报文时,会将其加入到内核的「 SYN 队列」;
- 接着发送 SYN + ACK 给客户端,等待客户端回应 ACK 报文;
- 服务端接收到 ACK 报文后,从「 SYN 队列」移除放入到「 Accept 队列」;
- 应用通过调用
accpet()
socket 接口,从「 Accept 队列」取出连接。
应用程序过慢: - 如果应用程序过慢时,就会导致「 Accept 队列」被占满。
受到 SYN 攻击:
- 如果不断受到 SYN 攻击,就会导致「 SYN 队列」被占满。
tcp_syncookies
的方式可以应对 SYN 攻击的方法:
net.ipv4.tcp_syncookies = 1
- 当 「 SYN 队列」满之后,后续服务器收到 SYN 包,不进入「 SYN 队列」;
- 计算出一个
cookie
值,再以 SYN + ACK 中的「序列号」返回客户端, - 服务端接收到客户端的应答报文时,服务器会检查这个 ACK 包的合法性。如果合法,直接放入到
「 Accept 队列」。 - 最后应用通过调用
accpet()
socket 接口,从「 Accept 队列」取出的连接。
3.TCP 连接断开
3.1 TCP 四次挥手过程和状态变迁
每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手。
主动关闭连接的,才有 TIME_WAIT 状态。
Q: 为什么挥手需要四次?
A:再来回顾下四次挥手双方发 FIN
包的过程,就能理解为什么需要四次了。
- 关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。 - 服务器收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK
和 FIN
一般都会分开发送,从而比三次握手导致多了一次。
Q: 为什么 TIME_WAIT 等待的时间是 2MSL?
或者Q: 为什么需要 TIME_WAIT 状态?
A:
1.防止旧连接的数据包
2.保证连接正确关闭
先来看一下MSL:
MSL
是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文是基于 IP 协议的,而 IP 头中有一个 TTL(Time To Live)
字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL消耗为 0 的时间,以确保报文已被自然消亡。
2MSL
的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
在 Linux 系统里 2MSL
默认是 60
秒,那么一个 MSL
也就是 30
秒。Linux 系统停留在TIME_WAIT 的时间为固定的 60 秒。
其定义在 Linux 内核代码里的名称为 TCP_TIMEWAIT_LEN:
#define TCP_TIMEWAIT_LEN (60*HZ) /* how long to wait to destroy TIME-WAIT
state, about 60 seconds */
如果要修改 TIME_WAIT 的时间长度,只能修改 Linux 内核代码里 TCP_TIMEWAIT_LEN 的值,并重
新编译 Linux 内核。
回到这个问题,主要有两个原因:
- 防止旧连接的数据包
假设 TIME-WAIT 没有等待时间或时间过短,被延迟的数据包抵达后会发生什么呢?
- 如上图黄色框框服务端在关闭连接之前发送的
SEQ = 301
报文,被网络延迟了。 - 这时有相同端口的 TCP 连接被复用后,被延迟的
SEQ = 301
抵达了客户端,那么客户端是有可能正常接收这个过期的报文,这就会产生数据错乱等严重的问题。
所以,TCP 就设计出了这么一个机制,经过 2MSL
这个时间,足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的。
- 保证连接正确关闭
TIME-WAIT 另一个重要的作用是等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。
假设 TIME-WAIT 没有等待时间或时间过短,断开连接会造成什么问题呢?
- 如上图红色框框客户端四次挥手的最后一个
ACK
报文如果在网络中被丢失了,此时如果客户端TIME-WAIT
过短或没有,则就直接进入了CLOSED
状态了,那么服务端则会一直处在LAST_ACK
状态。 - 当客户端发起建立连接的
SYN
请求报文后,服务端会发送RST
报文给客户端,连接建立的过程就会被终止。
如果 TIME-WAIT 等待足够长的情况就会遇到两种情况:
- 服务端正常收到四次挥手的最后一个
ACK
报文,则服务端正常关闭连接。 - 服务端没有收到四次挥手的最后一个
ACK
报文时,则会重发FIN
关闭连接报文并等待新的ACK
报文。(最后一个ACK丢失,加上重发的FIN,一来一回需要等待2倍的MSL)
所以客户端在 TIME-WAIT
状态等待 2MSL
时间后,就可以保证双方的连接都可以正常的关闭。
Q: TIME_WAIT 过多有什么危害?
A:
1.内存资源占用
2.对端口资源的占用,一个 TCP 连接至少消耗一个本地端口
- 内存资源占用
服务端受系统资源限制:
由于一个四元组表示 TCP 连接,理论上服务端可以建立很多连接,服务端确实只监听一个端口但是会把连接扔给处理线程,所以理论上监听的端口可以继续监听。但是线程池处理不了那么多一直不断的连接了。所以当服务端出现大量 TIME_WAIT 时,系统资源被占满时,会导致处理不过来新的连接。
- 对端口资源的占用,一个 TCP 连接至少消耗一个本地端口
端口资源也是有限的,一般可以开启的端口为 32768~61000
,也可以通过如下参数设置指定
net.ipv4.ip_local_port_range
如果发起连接一方的 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。
客户端受端口资源限制:
客户端TIME_WAIT过多,就会导致端口资源被占用,因为端口就65536个,被占满就会导致无法
创建新的连接。
Q: 如何优化 TIME_WAIT?
A: 这里给出优化 TIME-WAIT 的几个方式,都是有利有弊
1.打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项
2.net.ipv4.tcp_max_tw_buckets
3.程序中使用 SO_LINGER ,应用强制使用 RST 关闭
- 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项
如下的 Linux 内核参数开启后,则可以复用处于 TIME_WAIT 的 socket 为新的连接所用。
有一点需要注意的是,tcp_tw_reuse 功能只能用于客户端(连接发起方),因为开启了该功能,在
调用connect() 函数时,内核会随机找一个 time_wait 状态超过 1 秒的连接给新的连接复用。
net.ipv4.tcp_tw_reuse = 1
使用这个选项,还有一个前提,需要打开对 TCP 时间戳的支持,即
net.ipv4.tcp_timestamps=1(默认即为 1)
这个时间戳的字段是在 TCP 头部的「选项」里,用于记录 TCP 发送方的当前时间戳和从对端接收到的最新时间戳。
由于引入了时间戳,我们在前面提到的 2MSL
问题就不复存在了,因为重复的数据包会因为时间戳过
期被自然丢弃。
- net.ipv4.tcp_max_tw_buckets
这个值默认为 18000,当系统中处于 TIME_WAIT 的连接一旦超过这个值时,系统就会将所有的TIME_WAIT 连接状态重置。
这个方法过于暴力,而且治标不治本,带来的问题远比解决的问题多,不推荐使用。
- 程序中使用 SO_LINGER ,应用强制使用 RST 关闭
我们可以通过设置 socket 选项,来设置调用 close 关闭连接行为。
struct linger so_linger;
so_linger.l_onoff = 1;
so_linger.l_linger = 0;
setsockopt(s, SOL_SOCKET, SO_LINGER, &so_linger,sizeof(so_linger));
如果l_onoff
为非 0, 且l_linger
值为 0,那么调用close
后,会立该发送一个RST
标志给对端,该
TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT
状态,直接关闭。
但这为跨越TIME_WAIT
状态提供了一个可能,不过是一个非常危险的行为,不值得提倡。
Q: 如果已经建立了连接,但是客户端突然出现故障了怎么办?
TCP 有一个机制是保活机制。这个机制的原理是这样的:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
net.ipv4.tcp_keepalive_time=7200
net.ipv4.tcp_keepalive_intvl=75
net.ipv4.tcp_keepalive_probes=9
- tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就是 2 小时内如果没有任何连接相关的活动,则会启动保活机制
- tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
- tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。
也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。
这个时间是有点长的,我们也可以根据实际的需求,对以上的保活相关的参数进行设置。
如果开启了 TCP 保活,需要考虑以下几种情况:
- 第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。
- 第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。
- 第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。
针对 TCP 应该如何 Socket 编程?
- 服务端和客户端初始化
socket
,得到文件描述符; - 服务端调用
bind
,将绑定在 IP 地址和端口; - 服务端调用
listen
,进行监听; - 服务端调用
accept
,等待客户端连接; - 客户端调用
connect
,向服务器端的地址和端口发起连接请求; - 服务端
accept
返回用于传输的socket
的文件描述符; - 客户端调用
write
写入数据;服务端调用read
读取数据; - 客户端断开连接时,会调用
close
,那么服务端read
读取数据的时候,就会读取到了EOF
,待处理完数据后,服务端调用close
,表示连接关闭。
这里需要注意的是,服务端调用 accept
时,连接成功了会返回一个已完成连接的 socket,后续用来
传输数据。
所以,监听的 socket 和真正用来传送数据的 socket,是「两个」 socket,一个叫作监听 socket,一个叫作已完成连接 socket。
成功连接建立之后,双方开始通过 read 和 write 函数来读写数据,就像往一个文件流里面写东西一样。
Q: listen 时候参数 backlog 的意义?
A: Linux内核中会维护两个队列:
- 未完成连接队列(SYN 队列):接收到一个 SYN 建立连接请求,处于 SYN_RCVD 状态;
- 已完成连接队列(Accpet 队列):已完成 TCP 三次握手过程,处于 ESTABLISHED 状态;
int listen (int socketfd, int backlog)
- 参数一 socketfd 为 socketfd 文件描述符
- 参数二 backlog,这参数在历史版本有一定的变化
在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。
在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。
但是上限值是内核参数 somaxconn 的大小,也就说 accpet 队列长度 = min(backlog, somaxconn)。
Q: accept 发生在三次握手的哪一步?
A:
- 客户端的协议栈向服务器端发送了 SYN 包,并告诉服务器端当前发送序列号 client_isn,客户端
进入 SYN_SENT 状态; - 服务器端的协议栈收到这个包之后,和客户端进行 ACK 应答,应答的值为 client_isn+1,表示对
SYN 包 client_isn 的确认,同时服务器也发送一个 SYN 包,告诉客户端当前我的发送序列号为
server_isn,服务器端进入 SYN_RCVD 状态; - 客户端协议栈收到 ACK 之后,使得应用程序从
connect
调用返回,表示客户端到服务器端的单
向连接建立成功,客户端的状态为 ESTABLISHED,同时客户端协议栈也会对服务器端的 SYN 包进行应答,应答数据为 server_isn+1; - 应答包到达服务器端后,服务器端协议栈使得
accept
阻塞调用返回,这个时候服务器端到客户
端的单向连接也建立成功,服务器端也进入 ESTABLISHED 状态。
从上面的描述过程,我们可以得知客户端 connect 成功返回是在第二次握手,服务端 accept 成功返回是在三次握手成功之后。
Q: 客户端调用 close 了,连接是断开的流程是什么?
我们看看客户端主动调用了 close
,会发生什么?
- 客户端调用
close
,表明客户端没有数据需要发送了,则此时会向服务端发送 FIN 报文,进入
FIN_WAIT_1 状态; - 服务端接收到了 FIN 报文,TCP 协议栈会为 FIN 包插入一个文件结束符
EOF
到接收缓冲区
中,应用程序可以通过read
调用来感知这个 FIN 包。这个EOF
会被放在已排队等候的其他
已接收的数据之后,这就意味着服务端需要处理这种异常情况,因为 EOF 表示在该连接上再无额
外数据到达。此时,服务端进入 CLOSE_WAIT 状态; - 接着,当处理完数据后,自然就会读到
EOF
,于是也调用close
关闭它的套接字,这会使得
客户端会发出一个 FIN 包,之后处于 LAST_ACK 状态; - 客户端接收到服务端的 FIN 包,并发送 ACK 确认包给服务端,此时客户端将进入 TIME_WAIT 状
态; - 服务端收到 ACK 确认包后,就进入了最后的 CLOSE 状态;
- 客户端经过
2MSL
时间之后,也进入 CLOSE 状态;
Q: 如果客户端第四次挥手ack丢失,服务端超时重发的fin报文也丢失,客户端timewait时
间超过了2msl,这个时候会发生什么?
A: 当客户端 timewait 时间超过了 2MSL,则客户端就直接进入关闭状态。
服务端超时重发 fin 报文的次数如果超过 tcp_orphan_retries
大小后,服务端也会关闭 TCP 连接。
Q: 关于IP分片和TCP MSS分片,为什么IP按MTU分片,如果某一片丢失则需要所有分片都重传,而TCP MSS分片可以做到丢失后只发丢失的这一片?
A:
- 如果一个大的 TCP 报文是被 MTU 分片,那么只有「第一个分片」才具有 TCP 头部,后面的分片则没有 TCP 头部,接收方 IP 层只有重组了这些分片,才会认为是一个 TCP 报文,那么丢失了其中一个分片,接收方 IP 层就不会把 TCP 报文丢给 TCP 层,那么就会等待对方超时重传这一整个TCP 报文。
- 如果一个大的 TCP 报文被 MSS 分片,那么所有「分片都具有 TCP 头部」,因为每个 MSS 分片
都是具有 TCP 头部的TCP报文,那么其中一个 MSS 分片丢失,就只需要重传这一个分片就可以。