首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【C++/Linux】TinyWebServer前置知识之TCP协议详解

【C++/Linux】TinyWebServer前置知识之TCP协议详解

作者头像
小文要打代码
发布2025-06-06 08:16:15
发布2025-06-06 08:16:15
17400
代码可运行
举报
文章被收录于专栏:C++学习历程C++学习历程
运行总次数:0
代码可运行

TCP协议

是一种面向连接的、可靠的、基于字节流的传输层协议。 工作机制:通过三次握手建立连接,四次握手断开连接。 面向连接:通信前必须“三次握手”建立连接,断开前“四次挥手” 可靠性保障:通过序列号、确认号、重传机制、校验等方式 有序交付:数据按顺序到达,不会乱(方法:序列号) 流量控制:控制发送速率(方法:滑动窗口) 拥塞控制:网络拥堵时主动降低数据传输速率(TCP慢启动等方法) 双向通信:全双工(双向传输) 错误检测:校验和保证数据不出错(方法:校验和等方法)

下面的图是TCP头部的规范定义,它定义了TCP协议如何读取和解析数据:

1.1.TCP 端口号

TCP的连接是需要四个要素确定连接唯一:

(源IP,源端口号)+ (目地IP,目的端口号)

所以TCP首部预留了两个16位作为端口号的存储,而IP地址由上一层IP协议负责传递

源端口号和目地端口各占16位两个字节,也就是端口的范围是2^16=65535

另外1024以下端口是系统保留的,从1024-65535是用户使用的端口范围。

1.2.TCP 的序号和确认号

32位序号 seq:Sequence number 缩写seq ,TCP通信过程中某一个传输方向上的字节流的每个字节的序号,通过这个来确认发送的数据有序,也可以理解为:本次传输数据的起始字节在整个数据流中的位置。比如现在序列号为1000,发送了1000,下一个序列号就是2000。

32位确认号 ack:Acknowledge number 缩写ack,TCP对上一次seq序号做出的确认号,用来响应TCP报文段,给收到的TCP报文段的序号seq加1。也可以理解为: 期望收到下一包的序号,用于确认已经收到数据的偏移序号。

1.3.数据偏移(4位首部长度)

占4位,它指出TCP报文段的数据起始处距离TCP报文段的起始处有多远。这个字段指出TCP报文段的首部长度(报头长度)。由于首部中还有长度不确定的选项字段,因此数据偏移字段是必要的, 注意,“数据偏移”的单位不是字节,而是4字节。由于4位二进制数能表示的最大十进制数字是15,因此数据偏移的最大值是60字节(15*4字节),这也是TCP首部的最大字节(即选项长度不能超过40字节=60-20)。

1.4.保留

填充为0

1.5.TCP 的标志位

6位标志位,它们中的多个可同时被设置为1。每个TCP段都有一个目的,这是借助于TCP标志位选项来确定的,允许发送方或接收方指定哪些标志应该被使用,以便段被另一端正确处理。

用的最广泛的标志是 SYN,ACK 和 FIN,用于建立连接,确认成功的段传输,最后终止连接。

URG:简写为U,紧急标志位,表示数据包的 紧急指针域有效,用来保证连接不被阻断,并督促中间设备尽快处理;

ACK:简写为.,确认标志位,对已接收的数据包进行确认;

PSH:简写为P,推送标志位,表示该数据包被对方接收后应立即交给上层应用,而不在缓冲区排队;

RST:简写为R,重置标志位,用于连接复位、拒绝错误和非法的数据包;

SYN:简写为S,同步标志位,用于建立会话连接,同步序列号;

FIN: 简写为F,完成标志位,表示我已经没有数据要发送了,即将关闭连接;

1.5.1.紧急 URG

当URG=1时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快发送(相当于高优先级的数据),而不要按原来的排队顺序来传送。例如,已经发送了很长的一个程序要在远地的主机上运行。但后来发现了一些问题,需要取消该程序的运行,因此用户从键盘发出中断命令。如果不使用紧急数据,那么这两个字符将存储在接收TCP的缓存末尾。只有在所有的数据被处理完毕后这两个字符才被交付接收方的应用进程。这样做就浪费了很多时间。当URG置为1时,发送应用进程就告诉发送方的TCP有紧急数据要传送。于是发送方TCP就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍然是普通数据。这时要与首部中紧急指针(Urgent Pointer)字段配合使用。

1.5.2.确认 ACK

仅当 ACK = 1时确认号字段才有效,当 ACK = 0时确认号无效。TCP 规定,在连接建立后所有的传送的报文段都必须把 ACK 置为1。

1.5.3.推送 PSH

当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能收到对方的响应。在这种情况下,TCP 就可以使用推送(push)操作。这时,发送方 TCP 把 PSH 置为1,并立即创建一个报文段发送出去。接收方 TCP 收到 PSH =1的报文段,就尽快地(即“推送”向前)交付接收应用进程。而不用再等到整个缓存都填满了后再向上交付。一般这个不需要手动执标志,TCP 默认实现。

1.5.4.复位 RST

当 RST = 1时,表名 TCP 连接中出现了严重错误(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立传输连接。RST 置为1还用来拒绝一个非法的报文段或拒绝打开一个连接。

1.5.5.同步 SYN

在连接建立时用来同步序号。当 SYN = 1而 ACK = 0时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使 SYN = 1和 ACK = 1,因此 SYN 置为1就表示这是一个连接请求或连接接受报文。

1.5.6.终止 FIN

发送端完成任务,表要求释放运输连接。

1.6.窗口

16位2字节,用于表示滑动 窗口大小,窗口大小最大为65535(2^16-1)字节。

接收方的流量控制手段,窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的字节。告诉发送端,接收端目前允许发送端数据量。大小两字节65535,在客户端与服务端 TCP 都允许的情况下,选项中可存在窗口扩展选项。 示例中:窗口大小65535,代表告诉发送方,从这个下一包0的序号开始,接收方只能接受65535个字节长度了(当然这里还没有算上扩展选项,稍后再讲)

1.7.校验和

16位2字节,检验和覆盖了整个的 TCP 报文段: TCP 首部和 TCP 数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。和 UDP 用户数据报一样,在计算检验和时,要在 TCP 报文段的前面加上12字节的伪首部。伪首部的格式和 UDP 用户数据报的伪首部一样。但应把伪首部第4个字段中的17改为6(TCP的协议号是6);把第5字段中的UDP中的长度改为TCP长度。接收方收到此报文段后,仍要加上这个伪首部来计算检验和。若使用TPv6,则相应的伪首部也要改变。

校验和错误的分组丢弃(因为源IP地址、源端口号或者协议字段可能被破坏)。

1.8.紧急指针

16位2字节,在紧急 URG 标志执1的时候有效,代表一个偏移量,和序号字段值相加,代表紧急数据最后一个字节的序号。

1.9.选项

长度可变,最长可达40字节。当没有使用“选项”时,TCP 的首部长度是20字节。其最大长度可根据 TCP 首部长度进行推算。TCP 首部长度用4位数据偏移表示,单位是4字节,那么选项部分最长为:(2^4-1)*4-20=40字节。

TCP 协议最初只规定了一种选项,即最长报文段长度(数据字段加上TCP首部),又称为 MSS。MSS 告诉对方 TCP “我的缓存所能接收的报文段的数据字段的最大长度是 MSS 个字节”。

新的RFC规定有以下几种选型:选项表结束,无操作, 最大报文段长度, 窗口扩大因子,时间戳。。。。。。。。

连接与断开

三次握手与四次挥手

在学习计算机网络之前,我们对于“三次握手”和“四次挥手”有所耳闻,其实这两个名词指的就是 TCP 连接与断开过程。

三次握手过程

代码语言:javascript
代码运行次数:0
运行
复制
客户端 <------------- 三次握手 -------------> 服务端
  | -------------> SYN, Seq=x -------------> |	
        #客户端发起连接,客户端数据包的初始序列号为x
  | <------- SYN+ACK, Seq=y, Ack=x+1 ------- |	
        #服务端响应客户端的连接请求,并且告知客户端我确认收到了请求。
        #服务端数据包的初始序列号为y,希望客户端的下一个数据包使用x+1
  | -------> ACK, Seq=x+1, Ack=y+1 --------> |
        #客户端响应服务端的连接请求,并告知服务端我确认收到了请求。
        #该数据包的序列号为x+1,希望服务端的下一个数据包使用y+1
        #连接建立,双方进入ESTABLISHED(连接状态)

三次握手是为了让客户端和服务端分别确认自己和对方接收和发送消息的能力是正常的。

一开始,客户端和服务端都处于 CLOSED 状态。先是服务端主动监听某个端口,处于 LISTEN 状态。

1.第一次握手:客户端会发送 SYN 报文给服务端,TCP 部首 SYN 标志位置为 1,并随机初始化首部序列号 seq=x;表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN_SENT 状态。

2.第二次握手:服务端收到客户端的 SYN 报文后,首先,服务端也随机初始化自己的 TCP 部首序列号 seq=y;其次,把首部的确认号填入 ack=x+1;接着,把部首 SYN 和 ACK 标志位都置为 1;最后、把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN_RCVD 状态。

3.第三次握手:客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次部首确认号填入 ack=y+1 ,最后把报文发送给服务端。这次报文可以携带客户到服务器的数据。

一旦完成三次握手,双方都处于 ESTABLISHED 状态,此致连接就已建立完成,客户端和服务端就可以相互发送数据了。

为什么是三次握手?不是两次、四次?

接下来以三个方面分析三次握手的原因:

三次握手才可以阻止重复历史连接的初始化(主要原因)

三次握手才可以同步双方的初始序列号

三次握手才可以避免资源浪费

总结:

两次握手:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;

四次握手:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。

原因一:阻止重复历史连接的初始化

简单来说,三次握手的首要原因是为了防止旧的重复连接初始化造成混乱。我们考虑一个场景,客户端先发送了 SYN(seq = 90) 报文,然后客户端宕机了,而且这个 SYN 报文还被网络阻塞了,服务端并没有收到,接着客户端重启后,又重新向服务端建立连接,发送了 SYN(seq = 100) 报文(注意!不是重传 SYN,重传的 SYN 的序列号是一样的)。

看看三次握手是如何阻止历史连接的:

客户端连续多次发送 SYN 报文建立连接,在网络拥堵情况下:一个“旧的 SYN 报文”比“新的 SYN报文”早到达了服务端,那么此时服务端就会回一个 SYN+ACK 报文给客户端,此报文中的确认号是 91(90+1)。客户端收到后,发现自己期望收到的确认号应该是 100+1,而不是 90+1,于是就会回复 RST 报文(复位链接)。服务端收到 RST 报文后,就会释放连接。后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手了。上述中的“旧的 SYN 报文”称为历史连接,TCP 使用三次握手建立连接最主要的原因,就是防止服务器初始化历史连接。

原因二:同步双方初始序列号

TCP 协议的通信双方, 都必须维护一个序列号,序列号是可靠传输的一个关键因素,它的作用:

1).接收方可以去除重复的数据;

2).接收方可以根据数据包的序列号按序接收;

3).可以标识发送出去的数据包中, 哪些是已经被对方收到的。

可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带“初始序列号”的 SYN 报文的时候,需要服务端回一个 ACK 应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送“初始序列号”给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。

“四次握手”其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了三次握手。

而“两次握手”只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。

原因三:避免资源浪费

如果只有“两次握手”,当客户端的 SYN 连接请求在网络中阻塞,客户端没有也不会接收到 ACK 报文,就会重新发送 SYN ,由于没有第三次握手,服务器不清楚客户端是否收到了自己发送的 ACK 确认信号,所以每收到一个 SYN 就只能先主动建立一个连接,这会造成什么情况呢?

如果客户端的 SYN 阻塞了,重复发送多次 SYN 报文,那么服务器在收到请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。

四次挥手

查看指令:

代码语言:javascript
代码运行次数:0
运行
复制
tcpdump -i eth0 'tcp port 80 and (tcp[tcpflags] & (tcp-fin|tcp-ack) != 0)'

因为是全双工,所以每个都需要单独关闭

代码语言:javascript
代码运行次数:0
运行
复制
客户端 <------------- 四次挥手 -------------> 服务端
  | ---------> FIN, Seq=x, Ack=y ------> |
    # 客户端发送FIN请求断开连接,希望服务端的下一个数据包使用y+1
    # 该包的序列号为x+2,延续数据传输后的序列号
    # FIN占用一个序列号,类似数据传输的1字节
  | <-------- ACK, Seq=y, Ack=x+1 -------- |
    # 服务端确认收到客户端的FIN,希望客户端的下一个数据包使用x+3
    # 该包的序列号为y+1
  | <--------- FIN, Seq=y+1, Ack=x+1 ------- |
    # 服务端发送FIN请求断开连接,希望客户端的下一个数据包使用x+3
    # 该包的序列号为y+1,延续服务端的序列号
  | --------> ACK, Seq=x+1, Ack=y+2 -------> |
    # 客户端确认收到服务端的FIN,希望服务端的下一个数据包使用y+2
    # 该包的序列号为x+1,连接即将关闭

四次挥手也就是客户端与服务器断开连接时,需要一共发送四个报文段来完成断开TCP连接。

初始时,客户端与服务器都处于 ESTABLISHED 状态,假如客户端发起断开连接的请求(服务器也可以发起),四次挥手过程如下:

1.第一次挥手:客户端发送 FIN 报文给服务端,TCP 部首 FIN 标志位置为 1,并随机初始化部首序列号 seq=u。之后客户端处于 FIN_WAIT_1 状态。

2.第二次挥手:服务器收到 FIN 报文后,立即发送一个 ACK 报文,部首确认号为 ack=u+1,序号设为 seq=v。表明已经收到了客户端的报文。之后服务器处于 CLOSE_WAIT 状态。

在第二次挥手和第三次挥手之间的时间段内,由于只是半关闭的状态,数据还是可以从服务器传送到客户端的。

3.第三次挥手:如果数据传送完毕,服务器也想断开连接,那么就发送 FIN 报文给客户端,并重新指定一个序号 seq=w,确认号还是ack=u+1,表明可以断开连接。

4.第四次挥手:客户端收到报文后,发出一个 ACK 报文应答,上一次客户端发送的报文序列号为 seq=u,那么这次序列号就是 seq=u+1,确认号为 ack=w+1。此时客户端处于 TIME_WAIT 状态,需要经过一段时间确保服务器收到自己的应答报文后,才会进入 CLOSED 状态。

服务器收到ACK报文后,就关闭连接,也处于 CLOSED 状态了。

特殊情况

同时关闭时的合并流程​​:

  1. A → B:FIN
  2. B → A:FIN + ACK(合并第二、三次挥手)
  3. A → B:ACK
1. 第一次挥手(服务端→客户端,FIN-ACK)​​
代码语言:javascript
代码运行次数:0
运行
复制
16:52:48.184922 IP 169.254.0.4.http > VM-8-13-centos.36828: 
Flags [F.], seq 217, ack 589, win 12288, length 0
  • ​关键字段​​:
    • Flags [F.]:FIN + ACK组合标志,表示服务端主动关闭连接。
    • seq 217:服务端当前数据序列号(发送FIN占用一个序列号)。
    • ack 589:确认客户端的最后一个数据包(期望接收客户端下一个序列号为589)。

2. 第二次挥手(客户端→服务端,FIN-ACK)​​
代码语言:javascript
代码运行次数:0
运行
复制
16:52:48.185369 IP VM-8-13-centos.36828 > 169.254.0.4.http: 
Flags [F.], seq 589, ack 218, win 237, length 0
  • ​关键字段​​:
    • Flags [F.]:客户端发送FIN并确认服务端的FIN。
    • seq 589:客户端当前数据序列号(发送FIN占用一个序列号)。
    • ack 218:确认服务端的FIN(服务端FIN的seq=217,因此确认号为217+1=218)。

​​3. 第三次挥手(服务端→客户端,ACK)​​
代码语言:javascript
代码运行次数:0
运行
复制
16:52:48.190279 IP 169.254.0.4.http > VM-8-13-centos.36828: 
Flags [.], ack 590, win 12288, length 0
  • ​关键字段​​:
    • Flags [.]:纯ACK包,确认客户端的FIN。
    • ack 590:确认客户端的FIN(客户端FIN的seq=589,因此确认号为589+1=590)。

整体流程

代码语言:javascript
代码运行次数:0
运行
复制
客户端 <------------- 三次握手 -------------> 服务端
  | -------------> SYN, Seq=x -------------> |	
  | <------- SYN+ACK, Seq=y, Ack=x+1 ------- |	
  | -------> ACK, Seq=x+1, Ack=y+1 --------> |
客户端 <------------- 数据传输 -------------> 服务端
  | -------> Seq=x+1, Ack=y+1 (1 byte) ----> |
  | <----------- ACK, Seq=y+1, Ack=x+2 ----- |
客户端 <------------- 四次挥手 -------------> 服务端
  | ---------> FIN, Seq=x+2, Ack=y+1 ------> |
  | <-------- ACK, Seq=y+1, Ack=x+3 -------- |
  | <--------- FIN, Seq=y+1, Ack=x+3 ------- |
  | --------> ACK, Seq=x+3, Ack=y+2 -------> |
四次挥手问题总结
为什么挥手需要四次?

再来回顾下四次挥手双方发 FIN 包的过程,就能理解为什么需要四次了。

关闭连接时,客户端向服务端发送 FIN 时,仅仅表示客户端不再发送数据了但是还能接收数据。 服务器收到客户端的 FIN 报文时,先回一个 ACK 应答报文,而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。

从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。

为什么 TIME_WAIT 等待的时间是 2MSL?

MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。

MSL 与 TTL 的区别:MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。

TIME_WAIT 等待 2MSL 有两个原因:

1:如果客户端最后一个 ACK 丢失,服务端需要重传 FIN,如果客户端直接进入 CLOSED 状态,那对于重传的 FIN,肯定是 RST 响应。

2:为了保证最后一个 ACK 正常的丢失,因为不确认对方是否收到,需要等待 1MSL,至于另一个MSL,能找到比较信服的解释是被动关闭方在收到 ACK 那一刻之前重发了 FIN,为了保证这个 FIN 正常丢失,需要再等1MSL。

在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-06-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.1.TCP 端口号
  • 1.2.TCP 的序号和确认号
  • 1.3.数据偏移(4位首部长度)
  • 1.4.保留
  • 1.5.TCP 的标志位
  • 1.5.1.紧急 URG
  • 1.5.2.确认 ACK
  • 1.5.3.推送 PSH
  • 1.5.4.复位 RST
  • 1.5.5.同步 SYN
  • 1.5.6.终止 FIN
  • 1.6.窗口
  • 1.7.校验和
  • 1.8.紧急指针
  • 1.9.选项
  • 连接与断开
    • 三次握手与四次挥手
    • 三次握手过程
    • 为什么是三次握手?不是两次、四次?
      • 原因一:阻止重复历史连接的初始化
      • 原因二:同步双方初始序列号
      • 原因三:避免资源浪费
  • 四次挥手
    • 特殊情况
    • 整体流程
      • 四次挥手问题总结
      • 为什么挥手需要四次?
      • 为什么 TIME_WAIT 等待的时间是 2MSL?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档