文章目录
TCP
TCP是面向连接的、可靠的、基于字节流的传输层通信协议:

-
面向连接 :一定是一对一才能连接,不能像UDP协议可以一个主机同时向多个主机发送消息,即一对多是无法做到的
-
可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端
-
字节流 :消息是没有边界 的,所以无论消息有多大都可以进行传输。并且消息是有序的 ,当前一个 消息没有收到的时候,即使它先收到了后面的字节已经收到,那么也不能扔给应用层去处理,同时对重复的报文会自动丢弃
TCP头部格式

- 序列号 :在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题
- 确认应答号 :指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决不丢包的问题
- 控制位:
- ACK :该位为
1
时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的SYN
包之外该位必须设置为1
- RST :该位为
1
时,表示 TCP 连接中出现异常必须强制断开连接 - SYN :该位为
1
时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定 - FIN :该位为
1
时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换FIN
位置为 1 的 TCP 段
- ACK :该位为
网络模型
谈一谈你对 TCP/IP 四层模型,OSI 七层模型的理解?
OSI参考模型
OSI(Open System Interconnect),即开放式系统互联。 一般都叫OSI参考模型,是ISO(国际标准化组织)组织在1985年研究的网络互连模型。ISO为了更好的使网络应用更为普及,推出了OSI参考模型。其含义就是推荐所有公司使用这个规范来控制网络。这样所有公司都有相同的规范,就能互联了。

TCP/IP五层模型
TCP/IP五层协议和OSI的七层协议对应关系如下:

TCP状态

- CLOSED: 表示初始状态
- LISTEN: 表示服务器端的某个SOCKET处于监听状态,可以接受连接了
- SYN_RCVD: 表示接收到了SYN报文
- SYN_SENT: 表示客户端已发送SYN报文
- **ESTABLISHED:**表示连接已经建立了
- **TIME_WAIT:**表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL后即可回到CLOSED可用状态了
- CLOSING: 表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报 文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接
- CLOSE_WAIT: 表示在等待关闭
如何在 Linux 系统中查看 TCP 状态?
TCP 的连接状态查看,在 Linux 可以通过 netstat -napt
命令查看:

TIME_WAIT
① 为什么需要 TIME_WAIT 状态?
主动发起关闭连接的一方,才会有 TIME-WAIT
状态。需要 TIME-WAIT 状态,主要是两个原因:
- 防止具有相同「四元组」的「旧」数据包被收到
- 保证「被动关闭连接」的一方能被正确的关闭,即保证最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭
② TIME_WAIT 过多有什么危害?
如果服务器有处于 TIME-WAIT 状态的 TCP,则说明是由服务器方主动发起的断开请求。过多的 TIME-WAIT 状态主要的危害有两种:
- 第一是内存资源占用
- 第二是对端口资源的占用,一个 TCP 连接至少消耗一个本地端口
第二个危害是会造成严重的后果的,要知道,端口资源也是有限的,一般可以开启的端口为 32768~61000
,也可以通过如下参数设置指定
shell
net.ipv4.ip_local_port_range
如果发起连接一方的 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。
客户端受端口资源限制:
- 客户端TIME_WAIT过多,就会导致端口资源被占用,因为端口就65536个,被占满就会导致无法创建新的连接
服务端受系统资源限制:
- 由于一个四元组表示 TCP 连接,理论上服务端可以建立很多连接,服务端确实只监听一个端口 但是会把连接扔给处理线程,所以理论上监听的端口可以继续监听。但是线程池处理不了那么多一直不断的连接了。所以当服务端出现大量 TIME_WAIT 时,系统资源被占满时,会导致处理不过来新的连接
③ 如何优化 TIME_WAIT?
这里给出优化 TIME-WAIT
的几个方式,都是有利有弊:
- 打开
net.ipv4.tcp_tw_reuse
和net.ipv4.tcp_timestamps
选项 net.ipv4.tcp_max_tw_buckets
- 程序中使用
SO_LINGER
,应用强制使用RST
关闭
④ 为什么 TIME_WAIT 等待的时间是 2MSL?
MSL
是 Maximum Segment Lifetime,报文最大生存时间 ,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL
字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
MSL 与 TTL 的区别 : MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。
TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
比如如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 Fin 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。
2MSL
的时间是从客户端接收到 FIN 后发送 ACK 开始计时的 。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
在 Linux 系统里 2MSL
默认是 60
秒,那么一个 MSL
也就是 30
秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
其定义在 Linux 内核代码里的名称为 TCP_TIMEWAIT_LEN:
shell
#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内核。
连接过程

参考文档:https://www.cnblogs.com/jojop/p/14111160.html
TCP三次握手
开始客户端和服务器都处于CLOSED状态,然后服务端开始监听某个端口,进入LISTEN状态:
- 第一次握手(SYN=1, seq=x),发送完毕后,客户端进入 SYN_SENT 状态
- 第二次握手(SYN=1, ACK=1, seq=y, ACKnum=x+1), 发送完毕后,服务器端进入 SYN_RCVD 状态
- 第三次握手(ACK=1,ACKnum=y+1),发送完毕后,客户端进入 ESTABLISHED 状态,当服务器端接收到这个包时,也进入 ESTABLISHED 状态,TCP 握手,即可以开始数据传输

-
假设一开始客户端和服务端都处于
CLOSED
的状态。然后先是服务端主动监听某个端口,处于LISTEN
状态 -
【第一个报文】:客户端会随机初始化序号(
client_isn
),将此序号置于TCP
首部的「序号」字段中,同时把SYN
标志位置为1
,表示SYN
报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN-SENT
状态 -
【第二个报文】:服务端收到客户端的
SYN
报文后,首先服务端也随机初始化自己的序号(server_isn
),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入client_isn + 1
, 接着把SYN
和ACK
标志位置为1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于SYN-RCVD
状态 -
【第三个报文】:客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部
ACK
标志位置为1
,其次「确认应答号」字段填入server_isn + 1
,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据 ,之后客户端处于ESTABLISHED
状态 -
服务器收到客户端的应答报文后,也进入
ESTABLISHED
状态
第三次握手是否可以携带数据?
第三次握手是可以携带数据的,前两次握手是不可以携带数据的 。一旦完成三次握手,双方都处于 ESTABLISHED
状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。假设第三次握手的报文的seq
是x+1
:
- 如果有携带数据 :下次客户端发送的报文,
seq=服务器发回的ACK号
- 如果没有携带数据 :第三次握手的报文不消耗
seq
,下次客户端发送的报文,seq
序列号为x+1
① 服务端SYN-RECV流程

② 客户端SYN-SEND流程

-
场景1:sk->sk_write_pending != 0
这个值默认是0的,那什么情况会导致不为0呢?答案是协议栈发送数据的函数遇到socket状态不是ESTABLISHED的时候,会对这个变量做++操作,并等待一小会时间尝试发送数据。
-
场景2:icsk->icsk_accept_queue.rskq_defer_accept != 0
客户端先bind到一个端口和IP,然后setsockopt(TCP_DEFER_ACCEPT),然后connect服务器,这个时候就会出现rskq_defer_accept=1的情况,这时候内核会设置定时器等待数据一起在回复ACK包。
-
场景3:icsk->icsk_ack.pingpong != 0
pingpong这个属性实际上也是一个套接字选项,用来表明当前链接是否为交互数据流,如其值为1,则表明为交互数据流,会使用延迟确认机制。
为什么是三次握手?不是两次、四次?
TCP建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。不使用「两次握手」和「四次握手」的原因:
- 两次握手:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号
- 四次握手:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数
接下来以三个方面分析三次握手的原因:
- 三次握手才可以阻止重复历史连接的初始化(主要原因)
- 三次握手才可以同步双方的初始序列号
- 三次握手才可以避免资源浪费
原因一:避免历史连接
客户端连续发送多次 SYN 建立连接的报文,在网络拥堵情况下:
- 一个「旧 SYN 报文」比「最新的 SYN 」 报文早到达了服务端
- 那么此时服务端就会回一个
SYN + ACK
报文给客户端 - 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时),那么客户端就会发送
RST
报文给服务端,表示中止这一次连接

如果是两次握手连接,就不能判断当前连接是否是历史连接,三次握手则可以在客户端(发送方)准备发送第三次报文时,客户端因有足够的上下文来判断当前连接是否是历史连接:
- 如果是历史连接(序列号过期或超时),则第三次握手发送的报文是
RST
报文,以此中止历史连接 - 如果不是历史连接,则第三次发送的报文是
ACK
报文,通信双方就会成功建立连接
所以,TCP 使用三次握手建立连接的最主要原因是防止历史连接初始化了连接。
原因二:同步双方初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
- 接收方可以去除重复的数据
- 接收方可以根据数据包的序列号按序接收
- 可以标识发送出去的数据包中, 哪些是已经被对方收到的
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN
报文的时候,需要服务端回一个 ACK
应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。

四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了「三次握手」。而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
原因三:避免资源浪费
如果只有「两次握手」,当客户端的 SYN
请求连接在网络中阻塞,客户端没有接收到 ACK
报文,就会重新发送 SYN
,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的建立连接的 ACK
确认信号,所以每收到一个 SYN
就只能先主动建立一个连接,这会造成什么情况呢?如果客户端的 SYN
阻塞了,重复发送多次 SYN
报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。

即两次握手会造成消息滞留情况下,服务器重复接受无用的连接请求 SYN
报文,而造成重复分配资源。
TCP四次挥手
- 第一次挥手:FIN=1,seq=u ,发送完毕后客户端进入FIN_WAIT_1 状态
- 第二次挥手:ACK=1,seq =v,ack=u+1 ,发送完毕后服务器端进入CLOSE_WAIT 状态,客户端接收到后进入 FIN_WAIT_2 状态
- 第三次挥手:FIN=1,ACK=1,seq=w,ack=u+1 ,发送完毕后服务器端进入LAST_ACK 状态,客户端接收到后进入 TIME_WAIT状态
- 第四次挥手:ACK=1,seq=u+1,ack=w+1 ,客户端接收到来自服务器端的关闭请求,发送一个确认包,并进入 TIME_WAIT状态,等待了某个固定时间(两个最大段生命周期,2MSL ,2 Maximum Segment Lifetime)之后,没有收到服务器端的 ACK ,认为服务器端已经正常关闭连接,于是自己也关闭连接,进入 CLOSED 状态。服务器端接收到这个确认包之后,关闭连接,进入 CLOSED 状态

四次挥手过程:
- 客户端打算关闭连接,此时会发送一个 TCP 首部
FIN
标志位被置为1
的报文,也即FIN
报文,之后客户端进入FIN_WAIT_1
状态 - 服务端收到该报文后,就向客户端发送
ACK
应答报文,接着服务端进入CLOSED_WAIT
状态 - 客户端收到服务端的
ACK
应答报文后,之后进入FIN_WAIT_2
状态 - 等待服务端处理完数据后,也向客户端发送
FIN
报文,之后服务端进入LAST_ACK
状态 - 客户端收到服务端的
FIN
报文后,回一个ACK
应答报文,之后进入TIME_WAIT
状态 - 服务器收到了
ACK
应答报文后,就进入了CLOSE
状态,至此服务端已经完成连接的关闭 - 客户端在经过
2MSL
一段时间后,自动进入CLOSE
状态,至此客户端也完成连接的关闭
为什么挥手需要四次?
再来回顾下四次挥手双方发 FIN
包的过程,就能理解为什么需要四次了。
- 关闭连接时,客户端向服务端发送
FIN
时,仅仅表示客户端不再发送数据了但是还能接收数据。 - 服务器收到客户端的
FIN
报文时,先回一个ACK
应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送FIN
报文给客户端来表示同意现在关闭连接。
从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK
和 FIN
一般都会分开发送,从而比三次握手导致多了一次。
TCP优化
正确有效的使用TCP参数可以提高 TCP 性能。以下将从三个角度来阐述提升 TCP 的策略,分别是:
TCP三次握手优化

TCP四次挥手优化

TCP数据传输优化
