TCP协议

TCP

TCP特点

  1. TCP是面向连接的运输层协议
  2. 每一条TCP连接只能有两个端点(endpoint),每一条TCP连接只能是点对点的(一对一)
  3. TCP提供可靠交付的服务。通过TCP连接传送的数据,无差错、不丢失、不重复、并且按序到达
  4. TCP提供全双工通信。TCP允许通信双方的应用进程在任何时候都能发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。
    image

如果应用进程传送到TCP缓存的数据块太长,TCP就可以把它划分短一些再传送。如果应用进程一次只发来一个字节,TCP也可以等待积累有足够多的字节后再构成报文段发送出去。

可靠传输的工作原理

停止等待协议

缺点:信道利用率太低。要使用流水线传输!!!
每一条TCP连接有两个端点。TCP连接的端点叫做套接字(socket)或插口,即(IP 地址:端口号)
img
这里先假设A是发送方,B 是接收方。
image

a. A发送分组M1-->暂停-->收到M1的确认-->发送分组M2
b. A发送分组M1-->A 超过一段时间没收到M1的确认-->重传M1

  1. A发送完分组后,必须暂时保留已发送的分组的副本(为发生超时重传时使用)​。只有在收到相应的确认后才能清除暂时保留的分组副本。
  2. 分组和确认分组都必须进行编号。这样才能明确是哪一个发送出去的分组收到了确认,而哪一个分组还没有收到确认。
  3. 超时计时器设置的重传时间应当比数据在分组传输的平均往返时间更长一些。

img

连续ARQ协议

缺点:回退 N 帧重传会导致-->已正确接收的帧被重复发送
如帧 3、4 本身没问题,因帧 2 出错被连带重传

  • 发送方:
    发送方维护一个 发送窗口,窗口内的分组都可以连续发送出去,不需要等待对方的确认。
    每收到接收方对一个帧的确认(ACK ),发送窗口就 向前滑动一个帧的位置 ,释放已确认帧的资源,允许发送新的帧
    比如:一次性发送0 1 2 3 4;收到确认0,窗口向前滑动一个位置,可以发送5,释放0的资源

  • 接收方:
    对按序到达的最后一个帧发送确认
    若某帧出错,接收方会 丢弃该帧及后续失序帧(因依赖按序交付 ),且不回确认包
    比如:帧5出错,接收方丢弃帧5及后续失序帧(6 7),不回确认包

  • 重传机制
    发送方为每个帧设置 超时计时器,若超时未收到应答,则重传
    回退N帧:
    比如帧 2 出错,发送方超时后,会重传帧 2、3、4

TCP报文段的首部格式

一个TCP报文段分为首部[固定20字节+可选最大40字节]和数据两部分

img

  1. 源、目的端口号:各占2字节,即16位,2的16次方,即端口号的范围是0~65535

  2. 序号seq:占4字节,即32位。对每个字节进行编号

  3. 确认号ack:占4字节,即32位。是期望收到对方下一个报文段的第一个数据字节的序号
    若确认号 = N,则表明:到序号N - 1为止的所有数据都已正确收到。
    只有在ACK = 1时,ack才有意义

  4. 数据偏移Header Length/offset:占4位。表示首部长度,当数据偏移的值为40时,首部长度为40字节,固定头部为20字节,可选字段为20字节

  5. Flags保留:占6位,保留为今后使用 + 控制位:占6位
    紧急URG:当URG=1时,表明此报文段中有紧急数据
    复位RST:表明TCP连接中出现严重差错​,必须释放连接,然后再重新建立运输连接。

SYN:<你好,交个朋友>
ACK:<好的,收到>
PSH: <请尽快处理>(强制推送数据,跳过缓存立即处理)
URG:<紧急紧急,我要插队>
RST: <有内鬼,终止交易>
FIN: <交易完毕,收工>

标志位 PSH(Push) URG(Urgent)
本质 数据交付控制:强制接收方立即交付数据 数据优先级控制:标记报文中的紧急数据区域
作用范围 整个报文段的数据 报文中特定字节范围(由紧急指针指定)
目标 跳过 TCP 缓存积累步骤,减少应用层处理延迟 确保关键数据优先传输(甚至插队普通数据)
  1. 窗口Window:占2字节,即16位。

例如,确认号ack是701,窗口字段是1000。这就表明,从701号算起,发送此报文段的一方还有接收1000个字节数据(字节序号是701~1 700)的接收缓存空间。

  1. 检验和Checksum:占2字节。 检验的范围包括首部和数据这两部分。
  2. 紧急指针Urgent Pointer:占2字节。紧急指针仅在URG = 1时才有意义
  3. 选项options:长度可变,最长可达40字节
    MSS是每一个TCP报文段中的数据字段的最大长度。数据字段加上TCP首部才等于整个的TCP报文段。
    窗口扩大:可以在双方初始建立TCP连接时进行协商,当它不再需要扩大其窗口时,可发送S = 0的选项,使窗口大小回到16。
  4. 时间戳timestamps:10字节,用来计算往返时间RTT

计算RTT
用于处理TCP序号超过2^32的情况,这又称为防止序号绕回 PAWS

TCP可靠传输的实现

以字节为单位的滑动窗口

TCP的滑动窗口是以字节为单位的。

现假定A收到了B 发来的确认报文段,其中窗口是20(字节)​,而确认号是31,那么A会发送31~50这20个字节的数据。
img

img

小于P1的是已发送并已收到确认的部分
大于P3的是不允许发送的部分。
P3 - P1 = A的发送窗口(又称为通知窗口)
P2 - P1 = 已发送但尚未收到确认的字节数
P3 - P2 = 允许发送但尚未发送的字节数(又称为可用窗口或有效窗口)

假定B收到了序号为31的数据,并把序号为31~33的数据交付主机,然后B删除这些数据。接着把接收窗口向前移动3个序号​,同时给A发送确认,其中窗口值仍为20,但确认号是34。
B还收到了序号为37, 38和40的数据,但这些都没有按序到达,只能先暂存在接收窗口中。
img
A在继续发送完序号42~53的数据后,指针P2向前移动和P3重合。发送窗口内的序号都已用完,但还没有再收到确认​。由于A的发送窗口已满,可用窗口已减小到零,因此必须停止发送。

由于一直没有收到B的确认,为了保证可靠传输,A只能认为B还没有收到这些数据。于是,A在经过一段时间后(由超时计时器控制)就重传这部分数据,重新设置超时计时器,直到收到B的确认为止。

TCP的缓存和窗口的关系

img

缓存空间和序号空间都是有限的,并且都是循环使用的。最好是把它们画成圆环状的
发送缓存用来暂时存放:
(1) 发送应用程序传送给发送方TCP准备发送的数据;
(2) TCP已发送出但尚未收到确认的数据。
发送窗口通常只是发送缓存的一部分。已被确认的数据应当从发送缓存中删除,因此发送缓存和发送窗口的后沿是重合的。

接收缓存用来暂时存放:
(1) 按序到达的、但尚未被接收应用程序读取的数据;
(2) 未按序到达的数据。
如果收到的分组被检测出有差错,则要丢弃。如果接收应用程序来不及读取收到的数据,接收缓存最终就会被填满,使接收窗口减小到零。

超时重传时间的选择

超时重传时间设置得太短,会引起很多报文段的不必要的重传,使网络负荷增大。
超时重传时间设置得过长,使网络的空闲时间增大,降低了传输效率。
运输层的超时计时器的超时重传时间究竟应设置为多大呢
报文段的往返时间RTT
报文段每重传一次,就把超时重传时间RTO增大一些。典型的做法是取新的重传时间为2倍的旧的重传时间。

选择确认SACK

查漏补缺
在options中
img
sysctl net.ipv4.tcp_sack = 1 # 启用SACK
只传送缺少的数据而不重传已经正确到达接收方的数据
img
如果要使用选择确认SACK,那么在建立TCP连接时,就要在TCP首部的选项中加上“允许SACK”的选项,而双方必须都事先商定好。

SACK 通过在 ACK 报文中携带已接收的数据块范围(如[a,b]、[c,d]),告知发送方:哪些数据已成功接收,哪些数据可能丢失。发送方无需重传已确认的块,只需重传未被 SACK 覆盖的部分,避免了传统 ACK 只能确认 “连续数据” 的局限性

TCP的流量控制

流量控制(flow control)就是让发送方的发送速率不要太快,要让接收方来得及接收

假设A向B发送数据。在连接建立时,B告诉了A:​“我的接收窗口rwnd = 400”​
发送方的发送窗口不能超过接收方给出的接收窗口的数值。请注意,TCP的窗口单位是字节,不是报文段。
img
接收方的主机B进行了三次流量控制。
第一次把窗口减小到rwnd =300,
第二次又减到rwnd = 100,
最后减到rwnd = 0,即不允许发送方再发送数据了
B向A发送的三个报文段都设置了ACK = 1,只有在ACK = 1时确认号字段才有意义

!!!

B向A发送了零窗口的报文段后不久,B的接收缓存又有了一些存储空间。于是B向A发送了rwnd = 400的报文段。然而这个报文段在传送过程中丢失了。A一直等待收到B发送的非零窗口的通知,而B也一直等待A发送的数据。如果没有其他措施,这种互相等待的死锁局面将一直延续下去。

TCP为每一个连接设有一个持续计时器(persistence timer)。
只要TCP连接的一方收到对方的零窗口通知,就启动持续计时器。
若持续计时器设置的时间到期,就发送一个零窗口探测报文段(仅携带1字节的数据)​,而对方就在确认这个探测报文段时给出了现在的窗口值。
如果窗口仍然是零,那么收到这个报文段的一方就重新设置持续计时器。
如果窗口不是零,那么死锁的僵局就可以打破了。

必须考虑传输效率

发送方不发送很小的报文段,接收方也不要在缓存刚有了一点小空间就急忙把这个很小的窗口大小信息通知给发送方。
应用进程把数据传送到TCP的发送缓存后,剩下的发送任务就由TCP来控制了。可以用不同的机制来控制TCP报文段的发送时机。

  1. 只要缓存中存放的数据达到MSS字节时,就组装成一个TCP报文段发送出去。
  2. 由发送方的应用进程指明要求发送报文段,即TCP支持的推送(push)操作
  3. 发送方的一个计时器期限到了,这时就把当前已有的缓存数据装入报文段(但长度不能超过MSS)发送出去

在TCP的实现中广泛使用Nagle算法。
若发送应用进程把要发送的数据逐个字节地送到TCP的发送缓存,则发送方就把第一个数据字节先发送出去,把后面到达的数据字节都缓存起来。
当发送方收到对第一个数据字符的确认后,再把发送缓存中的所有数据组装成一个报文段发送出去,同时继续对随后到达的数据进行缓存。
只有在收到对前一个报文段的确认后才继续发送下一个报文段。当数据到达较快而网络速率较慢时,用这样的方法可明显地减少所用的网络带宽。
到达的数据已达到发送窗口大小的一半或已达到报文段的最大长度时,就立即发送一个报文段。这样做,就可以有效地提高网络的吞吐量。

接收方等待一段时间,使得或者接收缓存已有足够空间容纳一个最长的报文段,或者等到接收缓存已有一半空闲的空间时,接收方发出确认报文。

TCP的拥塞控制

流量控制是 “微观” 调控:
针对单个连接的接收能力进行优化,确保接收方不会被压垮。
拥塞控制是 “宏观” 调控:
考虑整个网络的承载能力,避免过多发送方集体导致网络瘫痪。

维度 流量控制(Flow Control) 拥塞控制(Congestion Control)
目标 防止接收方缓冲区溢出 防止网络因过载导致吞吐量下降
控制方 接收方(通过 rwnd 告知发送方) 发送方(根据网络反馈自主调整)
触发条件 接收方缓冲区不足 网络出现拥塞迹象(如丢包、延迟增加)
核心机制 TCP 窗口机制(滑动窗口协议) 慢启动、拥塞避免、快重传、快恢复
衡量指标 接收窗口大小(rwnd) 拥塞窗口大小(cwnd)、往返时延(RTT)
典型算法 Nagle 算法(避免小数据包) Reno、Cubic、BBR 等拥塞控制算法
影响范围 点对点连接(单个发送方与接收方) 整个网络(所有共享相同链路的发送方)

慢开始(slow-start)
拥塞避免(congestion avoidance)
快重传(fast retransmit)
快恢复(fast recovery)

发送方的发送窗口 <= 拥塞窗口
发送方控制拥塞窗口的原则是:
只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去。
但只要网络出现拥塞,拥塞窗口就减小一些,以减少注入到网络中的分组数。
拥塞的判断方法:发送方设置的超时计时器时限已到但还没有收到确认。
慢开始:由小到大逐渐增大拥塞窗口数值,指数增长
拥塞避免:让拥塞窗口cwnd缓慢地增大,线性增长
img
当cwnd < ssthresh时,使用上述的慢开始算法。
当cwnd > ssthresh时,停止使用慢开始算法而改用拥塞避免算法。
当cwnd = ssthresh时,既可使用慢开始算法,也可使用拥塞避免算法。
img

cwmd为24时出现了拥塞,于是把ssthresh设置为12(现为cwnd的一半),并把cwnd重新设置为1。

MSS(最大段大小)在连接建立时已确定

慢启动不改变 MSS,只改变发包数量:
例如,无论 cwnd 是 1 还是 100,每个数据包的大小始终是 MSS(如 1460 字节),只是同一时间发送的数据包数量变多了。

MSS相当于是一辆车的载重,cwnd相当于几辆车

慢启动和拥塞避免算法的核心目标之一,就是通过动态调整发送窗口(cwnd) 来试探网络的承载能力,并确定一个合理的慢启动阈值(ssthresh)

快重传算法首先要求接收方每收到一个失序的报文段后就立即发出重复确认,而不要等待自己发送数据时才进行捎带确认
img
img

慢启动阶段:
  cwnd=1 → 2 → 4 → 8 → 16(达到 ssthresh=16,进入拥塞避免)。
拥塞避免阶段:
  cwnd=17 → 18 → 19 → 20(此时网络出现轻微拥堵,接收方收到失序包,发送重复 ACK)。
快重传触发:
  发送方收到 3 个重复 ACK,确认数据包丢失,立即重传丢失包,同时进入快恢复。
快恢复阶段:
  ssthresh = 20 / 2 = 10,
  cwnd = 10 + 3 = 13(假设 MSS=1),
  以 cwnd=13 为起点,进入拥塞避免,继续 “加法增大”:14 → 15 → 16 → ……

慢启动:指数增长,快速探测容量;
拥塞避免:线性增长,预防拥堵;
快重传:通过重复 ACK [3个] 快速发现丢失包,立即重传;
快恢复:重传后调整阈值,避免窗口骤降,快速恢复传输。

image

TCP三次握手和四次挥手

img

  1. 第一次握手:客户端发送 SYN 包(SYN=1)
    随机生成初始序列号
    协商
    窗口大小(Window):客户端告知服务器自己能接收的数据缓冲区大小
    MSS(最大段大小):声明每次接收的最大 TCP 数据段长度
    时间戳(Timestamp):用于计算 RTT(往返时间),优化传输性能。
    是否启用选择性确认(SACK)功能

  2. 第二次握手:服务器发送 SYN+ACK 包(SYN=1, ACK=1)

  3. 第三次握手:客户端发送 ACK 包(ACK=1)

客户端状态变化:CLOSED → SYN_SENT → ESTABLISHED
服务器状态变化:CLOSED → LISTEN → SYN_RCVD → ESTABLISHED

img
为什么A在TIME-WAIT状态必须等待2MSL的时间呢?这有两个理由。

  1. 为了保证A发送的最后一个ACK报文段能够到达B。
    这个ACK报文段有可能丢失,因而使处在LAST-ACK状态的B收不到对已发送的FIN+ACK报文段的确认。
    B会超时重传这个FIN+ACK报文段,而A就能在2MSL时间内收到这个重传的FIN+ACK报文段。
  2. 防止旧数据包干扰新连接
    由于 IP 数据包可能因路由循环等原因在网络中滞留,TIME-WAIT 状态能确保这些旧数据包(如之前的 FIN 或 ACK)在新连接建立之前被丢弃。
    若新连接在原连接关闭后立即建立,且使用相同端口号,滞留的旧数据包可能会干扰新连接,而 2MSL 的等待期可大大减少这种可能性。
    MSL 是 Maximum Segment Lifetime 的缩写,意为最大段生存时间,是一个 TCP 段在网络中可以存活的最长时间

一次HTTP请求

img

前面3个包是TCP3次握手

包4是客户端发起的HTTP请求
包5收到后,马上回了个ACK包,表示收到了HTTP请求
包6是服务器返回的HTTP响应
包7是客户端对包6的回应
包8是服务器继续发送HTTP响应数据
包9是客户端对包8的回应

最后3个包是TCP四次挥手

  • 包5 和包6都是服务器的回应,为什么要分开?
    包5和包6 的seq和ack是一样的,因为它们是同一个TCP连接的确认和响应。
    Seq 相同:服务器的发送序列号未推进(两个包属于同一发送序列)。
    Ack 相同:服务器对客户端的确认状态未变化(两个包基于同一接收状态生成)。

先触发纯 ACK 回复(包 5):因服务器 “暂时没有数据要发” 或 “数据未准备好”,先快速确认客户端数据。
后触发带数据的 PSH+ACK(包 6):当服务器的应用层(HTTP 响应)准备好数据后,TCP 层复用之前的确认状态(Ack=80),同时发送数据(PSH 标志)。

  • 为什么包6 没有发送HTTP响应呢
    包 6 的真实内容:包含 HTTP 响应数据
    Len=237:表示包 6 携带了 237 字节的应用层数据
    PSH 标志:要求客户端立即将数据交给应用层(HTTP 解析)
    [TCP PDU reassembled in 8]:Wireshark 提示 “该 TCP 数据会在包 8 中重组”—— 因为 HTTP 响应可能分多个 TCP 包发送,需合并后才完整显示 HTTP 内容。
    Wireshark 为了让你看到完整的应用层协议(如 HTTP),会:
    先收集所有相关的 TCP 分片。
    重组后,在最后一个分片或关键包中显示完整的 HTTP 内容(包 8 就是重组后的结果)。
    所以包 6 虽然携带了 HTTP 数据,但因是 “分片”,Wireshark 没直接标为 HTTP,而是标为 TCP,同时提示 “在包 8 重组”。

点开包6,找到payload,可以看到确实有HTTP响应数据
img

  • 为什么不等HTTP响应都接受完了再统一回复ACK呢?
    TCP 靠 “收到数据就确认” 保证可靠,HTTP 靠 “在 TCP 之上拼接字节流” 实现消息完整

及时回 ACK 可让发送方(服务器)动态调整:
流量控制:通过 Win(窗口大小)告诉服务器 “我还能接收多少数据”。
拥塞控制:快速 ACK 让服务器知道 “网络通畅”,可继续发送数据。

  • 包11为什么是FIN+ACK而不是单纯的ACK?
    包 11 将FIN和ACK合并发送,是 TCP 协议在连接关闭阶段的优化行为,目的是减少网络包数量,提升效率。
    这一现象常见于通信双方 “同时准备关闭连接” 的场景,体现了 TCP 在可靠性基础上对性能的平衡。

疑问

为什么可选字段最大是40字节?

TCP首部的长度是用offset4来表示
因为Data Offset是4位,取值范围是5~15,为什么不能是0 1 2 3 4,因为固定头部是20字节,所以最小是20/4=5。
最大值是15
4=60字节,减去固定头部20字节,可选字段最大是40字节

NOP的作用是什么

NOP(No Operation)是一种特殊的选项,其作用是作为填充(Padding)来确保后续选项或 TCP 头部以 4 字节边界对齐,存在于options中。

MSS,WS和window的关系

Maximum Segement Size-->MSS: 最大段大小,告知对方自己接收缓冲区能处理的最大数据段长度(通常为 MTU-40 字节),在三次握手中协商,只在SYN报文中使用[即SYN和SYN+ACK]
Window Scale-->WS: WScale窗口大小,表示在通信过程中可以接收或发送的数据量,在三次握手中协商,只在SYN报文中使用[即SYN和SYN+ACK]
Window:窗口大小,实际窗口大小=窗口大小*2^WS cale

MSS限制单次发包的大小,Window和WS cale确定接收方的实际窗口大小

Python解析Pcap文件

'''
@File : pcap.py
@Time : 2025/06/23 16:23:50
@Version : 1.0
@Author : WiseHYH
@Contact : wiseshark@yeah.com
'''

from scapy.all import rdpcap, TCP, IP

# 读取PCAP文件
packets = rdpcap('pcap.pcap')

'''
options:
    Maximum Segement Size-->MSS: 最大段大小,告知对方自己接收缓冲区能处理的最大数据段长度(通常为 MTU-40 字节),在三次握手中协商,只在SYN报文中使用[即SYN和SYN+ACK]
    Window Scale-->WS: WScale窗口大小,表示在通信过程中可以接收或发送的数据量,在三次握手中协商,只在SYN报文中使用[即SYN和SYN+ACK]
    NOP(No Operation)是一种特殊的选项,其作用是作为填充(Padding)来确保后续选项或 TCP 头部以 4 字节边界对齐
    SACK:SYN包中进行协商,优化重传机制
Window:窗口大小,实际窗口大小=窗口大小*2^WS cale

'''
# 遍历每个数据包
for packet in packets:
    # 检查是否包含IP和TCP层
    if packet.haslayer(IP) and packet.haslayer(TCP):
        tcp = packet[TCP]
        
        # 打印IP信息
        print(f"源IP:Port--> {packet[IP].src}:{tcp.sport}, 目的IP:Port--> {packet[IP].dst}:{tcp.dport}")
        
        # 提取并打印TCP字段
        print(f"首部长度: {tcp.dataofs*4} 字节")
        
        # 解析标志位
        flags = []
        if tcp.flags & 0x01: flags.append('FIN')  # 0x01 = 1
        if tcp.flags & 0x02: flags.append('SYN')  # 0x02 = 2
        if tcp.flags & 0x04: flags.append('RST')  # 0x04 = 4
        if tcp.flags & 0x08: flags.append('PSH')  # 0x08 = 8
        if tcp.flags & 0x10: flags.append('ACK')  # 0x10 = 16
        if tcp.flags & 0x20: flags.append('URG')  # 0x20 = 32
        print(f"标志位: {', '.join(flags)}")
        
        print(f"窗口大小: {tcp.window}")
        print(f"校验和: 0x{tcp.chksum:04x}")
        
        # 仅当紧急指针不为0时打印
        if tcp.urgptr:
            print(f"紧急指针: {tcp.urgptr}")
        
        # 打印TCP选项(如果有)
        if tcp.options:
            print("选项:")
            for option in tcp.options:
                opt_name, opt_data = option
                print(f"  - {opt_name}: {opt_data}")
                
        print("-" * 50)  # 分隔线
posted @ 2025-06-26 17:56  WiseHYH  阅读(29)  评论(0)    收藏  举报