通俗易懂超详细讲解TCP/UDP

TCP/UDP是传输层协议,本文主要探讨TCP/UDP的核心机制

一、 认识端口

端口号标识了一个主机上进行通信的不同的应用程序

在TCP/IP协议中, 用 "源IP", "源端口号", "目的IP", "目的端⼝号", "协议号" 这样⼀个五元组来标识⼀个通信。

端口号范围划分

0-1023:知名端口号,HTTP,SSH等应用层协议,它们的端口号是固定的

ssh服务器:22端口,ftp:21,telnet:23,http:80,https:443

1024-65535:操作系统动态分配的端口号,客户端程序的端口号,由操作系统从这个范围划分。

进程与端口号绑定问题

1.⼀个进程是否可以 bind 多个端⼝号

这个是可以的

实现原理:在网络编程中,一个端口对应一个"套接字(Socket)"。一个进程可以创建多个套接字,并分别调用 bind() 函数将它们绑定到不同的端口上。

比如:Web 服务器中,同一个进程可能同时监听 80 端口(HTTP)和 443 端口(HTTPS);还有多协议支持,一个进程可能同时绑定 TCP 8888 端口和 UDP 8888 端口。

2.一个端口是否可以被多个进程bind

通常不可以,但在特定条件下可以。

按照传统规则,一个端点(IP + 端口 + 协议)在同一时间内只能被一个进程占用,否则会报 Address already in use 错误。但以下几种情况允许"共享"或"共同绑定":

(1) 使用 SO_REUSEPORT 选项 (现代 Linux 特性)

这是最常见的实现方式。从 Linux 内核 3.9 版本开始,引入了SO_REUSEPORT 选项:

如果多个进程在 bind() 之前都设置了这个选项,它们就可以同时绑定到同一个 IP 和端口。

用途:内核会自动在这些进程之间进行负载均衡(将新连接分发给不同的进程),这常用于高并发服务器(如 Nginx、HAProxy)来提升性能。

(2) 父子进程继承 (Fork)

如果父进程先 bind() 并 listen() 了一个端口,然后调用 fork() 创建子进程,那么子进程会继承父进程的文件描述符。

此时,多个进程实际上在共享同一个监听套接字。

(3) 不同协议 (TCP vs UDP)

端号在 TCP 和 UDP 协议中是独立的命名空间。

进程 A 可以 bind TCP 8080,进程 B 可以同时 bind UDP 8080,两者互不干扰。

(4) 绑定不同的 IP 地址

如果一台机器有多个网卡或 IP 地址,进程 A 可以绑定 127.0.0.1:80,进程 B 可以绑定 192.168.1.10:80。虽然端口号相同,但由于 IP 地址不同,它们并不算冲突。

二、UDP:用户数据报协议

UDP 就像是"寄平信"。你把信投进邮筒,邮局尽力帮你送,但不会给你回执,信丢了也不会重发。

1.UDP报头

UDP 报头极其简单,只有固定的 8 个字节,分为四个字段(每个 2 字节):

  • 源端口号:发送方程序的端口。
  • 目的端口号:接收方程序的端口。
  • 长度:整个 UDP 报文的长度(报头 + 数据)
  • 校验和:检测报文在传输过程中是否出错(若出错直接丢弃)

2.UDP传输特点

  • 无连接:不需要握手,想发就发。
  • 不可靠:不保证到达,不保证顺序,没有确认机制
  • 面向报文:应用层给 UDP 多长,UDP 就发多长,不会拆分也不会合并。
  • 速度快:没有握手开销和复杂的控制逻辑,适合实时音视频、在线游戏、DNS查询

三、TCP:传输控制协议

TCP 就像是"打电话"。通话前要先拨号建立连接,通话中你会不断确认"听清了吗",如果没听清对方会重讲

1.TCP报头

TCP 报头较复杂,基础长度为 20 字节(含选项时更长):

  • 源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去

  • 序号 (Sequence Number):本段数据的第一个字节的编号(用于重组乱序包)

  • 确认号 (Acknowledgment Number):期望收到的下一个字节的编号(用于确认应答)

  • 标志位 (Flags): SYN (请求建立连接; 我们把携带SYN标识的称为同步报文段)、ACK (确认号是否有效)、FIN (释放连接) 、URG (紧急指针是否有效)、PSH ( 提示接收端应用程序立刻从TCP缓冲区把数据读走)、RST( 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段)

  • 窗口 (Window):告知对方,我现在的缓冲区还能收多少数据(用于流量控制)

  • 16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也包含TCP数据部分.

  • 16位紧急指针: 标识哪部分数据是紧急数据

2.TCP连接基本机制

2.1 确认应答

接收方每收到一段数据,就给发送方发一个 ACK,告诉它"我收到了",并告诉他下次你得给我从哪传

2.2序列号排序

TCP将每个字节的数据都进行了编号. 即为序列号.

每⼀个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下⼀次你从哪里开始发

网络会导致数据包"先发后到"或"重复",TCP 利用序列号对数据重新排序并去重

2.3超时重传

如果发送方在规定时间内没收到确认,就认为包丢了,会重新发送

3.TCP连接管理:三次握手与四次挥手

握手:建立连接

挥手:断开链接

我们可以将 TCP 通讯想象成两个人在用对讲机进行通话。

3.1 三次握手(建立连接)

核心目的 :确保双方的发送能力接收能力都是正常的。

  1. 第一次握手(Client -> Server)

    • 动作 :客户端发送 SYN 包。
    • 对话:A 说:"喂,B,你能听到我说话吗?"
    • 结论:B 明白 A 的发送能力正常,自己的接收能力正常。
  2. 第二次握手(Server -> Client)

    • 动作 :服务端回复 SYN + ACK 包。
    • 对话:B 说:"听到了!你能听到我说话吗?"
    • 结论:A 明白 B 的发送和接收都正常,A 自己的发送和接收也都正常。
    • 此时注意:B 还不敢确定 A 是否听到了自己的回复。
  3. 第三次握手(Client -> Server)

    • 动作 :客户端发送 ACK 包。
    • 对话:A 说:"我也听到了,咱们开始聊吧!"
    • 结论:B 明白 A 的接收能力也是正常的。
    • 结果:连接成功建立(Established)。

3.2 四次挥手(释放连接)

核心逻辑 :TCP 是双工通信,我没话说了不代表你也没话说了,所以两个方向的通道必须分别关闭

  1. 第一次挥手(Client -> Server)

    • 动作 :客户端发送 FIN 包。
    • 对话:A 说:"我的话说完了,我准备挂了。"
    • 状态:A 停止发送数据,但仍能接收数据。
  2. 第二次挥手(Server -> Client)

    • 动作 :服务端回复 ACK 包。
    • 对话:B 说:"收到了,但我这边还有数据没传完,你等我一下。"
    • 状态:此时连接处于"半关闭"状态,B 继续发送剩余数据。
  3. 第三次挥手(Server -> Client)

    • 动作 :服务端发送 FIN 包。
    • 对话:B 说:"好了,我也说完了。我也要挂了,再见!"
    • 状态:B 停止发送数据,等待 A 的最后确认。
  4. 第四次挥手(Client -> Server)

    • 动作 :客户端发送 ACK 包。
    • 对话:A 说:"收到,再见!"
    • 状态:A 会原地等待一会儿(TIME_WAIT),确保 B 收到消息。B 收到后直接断开,A 等待结束后也彻底断开。

3.3 关键问答

1. 为什么不能是"两次握手"?
  • 防止失效的连接请求突然到达
  • 如果 A 发出的第一个请求在网络中"堵车"了,A 又发了第二个请求并完成通话。
  • 当 A 挂断后,第一个请求才到达 B,B 回复后如果直接建立连接,B 就会一直空等 A 发送数据,造成资源浪费
  • 三次握手下,由于 A 不会给那个过期的请求发 ACK,B 没收到确认就不会开启连接。
2. 为什么挥手要"四次"而不是"三次"?
  • 因为 B 被告知断开时,可能还有数据在处理中
  • 当 B 收到 A 的 FIN 时,它只能先回一个 ACK 表示收到了请求。
  • B 必须等待自己的数据全部发送完毕后,才能发 FIN
  • 中间的两步(ACK 和 FIN)通常不能合并,因为发送 FIN 取决于 B 的数据是否传完。
3. 为什么 A 最后要等 2MSL(TIME_WAIT)?
  • 确保 B 收到最后一次 ACK
  • 如果 A 发的 ACK 丢了,B 会重发第三次挥手的 FIN
  • 如果 A 立刻消失,B 就永远收不到最后的确认,无法正常进入关闭状态。

4.滑动窗口(TCP性能提升)

如果在TCP协议下这样传数据,性能会很差,你传一个就得等半天

所以为了提升性能,我们一次发生多条数据那不就行了

窗口大小指的是无需等待确认应答而可以继续发送数据的最大值。上图的窗口大小就是4000个字节(四个段)。

• 发送前四个段的时候,不需要等待任何ACK,直接发送;

• 收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据;依次类推;

• 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉;

• 窗口越大,则网络的吞吐率就越高。

4.1滑动窗口丢包问题

4.1.1数据包已经抵达, ACK被丢了

这时候通过后面的ACK判断就行了

4.1.2数据包丢了

这时候必须等待发送端再次发送1001数据包,然后就能把剩下的全处理了(因为被放在缓存区中)

4.2流量控制

我接受方处理能力有限,如果你发送方发太快那我不是处理不过来吗,那不肯定又是丢包又是重传的老麻烦了。

因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow

Control):

接收端将自己可以接收的缓冲区大小放入 TCP 首部的"窗口大小"字段,通过 ACK 端通知发送端。

  • 窗口大小字段越大,说明网络的吞吐量越高;

  • 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;

  • 发送端接收到这个窗口后,就会减慢自己的发送速度;

  • 如果接收端缓冲区满了,就会将窗口置为 0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。

4.3拥塞控制

拥塞控制解决的是发送端一开始发送太快的问题,你虽然有流量控制了,但你一开始传一堆不还是轧钢吗?

所以TCP引入慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;

拥塞控制是发送方根据"网络拥挤程度"来决定发多少数据。它引入了一个核心变量:拥塞窗口 (CWND, Congestion Window)
发送方的实际发送上限 = Min(RWND, CWND)

拥塞控制的四个阶段:
1. 慢启动 (Slow Start)
  • 策略:刚开始发数据时,不清楚网络状况,从小到大指数级增长。
  • 过程:收到 1 个 ACK,CWND 加 1。实际上每过一个往返时间(RTT),CWND 翻倍(1 -> 2 -> 4 -> 8...)。
  • 目的:快速探测网络的承载能力。
2. 拥塞避免 (Congestion Avoidance)
  • 触发点:当 CWND 达到一个阈值(ssthresh,慢启动门限)时。
  • 策略:从"指数增长"变为"线性增长"。
  • 过程:每过一个 RTT,CWND 只加 1。
  • 目的:在接近网络容量上限时,小心翼翼地试探,防止突然拥堵。
3. 拥塞发生时的处理(快重传与快恢复)

当网络真的出现丢包时,有两种触发逻辑:

  • 情况 A:超时重传(最严重)

    • 网络已经非常拥堵了。
    • 动作:ssthresh 砍半,CWND 直接降为 1,重新进入"慢启动"。
  • 情况 B:快重传(Fast Retransmit)

    • 接收方发现少了一个包,连续发 3 个同样的 ACK 告诉发送方。
    • 动作:发送方立即重传丢失的包,不需要等定时器超时。
    • 进入快恢复 (Fast Recovery):ssthresh 砍半,CWND 设为砍半后的值(而不是降为 1),直接进入"拥塞避免"阶段。

5.TCP 性能优化(延迟应答与捎带应答)

为了减少网络中"纯确认包(ACK)"的数量并提高带宽利用率,TCP 设计了延迟应答和捎带应答机制。


5.1 延迟应答 (Delayed Acknowledgment) ------ "等一等再回话"

1. 核心动机

如果接收方在收到数据后立即回发 ACK,会存在两个弊端:

  • 窗口太小:接收方缓冲区刚满,还没来得及被应用层取走,立即回发的窗口值很小,限制了发送方的速率。
  • ACK 包过多:如果每个数据包都对应一个 ACK,网络中会充斥着大量只有 40 字节(IP头+TCP头)且无载荷的空包。
2. 工作原理

接收方收到数据后,并不立刻发送 ACK,而是启动一个定时器等待一小段时间。在此期间:

  • 期待窗口增大 :等待应用层从缓冲区取走数据,从而在回发 ACK 时能通告一个更大的 Window
  • 期待累计确认 :如果在等待期间又收到了后续包,可以用一个 ACK 确认多个包
3. 触发规则(防止等待过久)

为了不触发发送方的"超时重传",延迟应答必须遵循以下限制:

  • 数量限制 :通常每收到 2 个 满长度的数据段,必须发送一个 ACK。
  • 时间限制 :通常最大延迟时间为 200ms(不同系统实现略有差异),超时必须发送。

5.2 捎带应答 (Piggybacking) ------ "顺风车机制"

1. 核心思想

在典型的"请求-响应"通信(如 HTTP、Telnet)中,接收方在收到数据后,往往很快就要给发送方回发数据。
捎带应答 允许将 ACK 信息(确认号、标志位等)直接"搭便车",放在接收方准备发回的数据报文中。

2. 对比流程
  • 无优化
    1. A -> B: 发送数据
    2. B -> A: 发送 ACK(纯确认包)
    3. B -> A: 发送响应数据
    4. A -> B: 发送 ACK(纯确认包)
  • 开启捎带应答
    1. A -> B: 发送数据
    2. B 触发延迟应答,等待一会儿...
    3. B 刚好要发响应数据,于是将 ACK 信息并入响应数据包
    4. B -> A: 发送 [响应数据 + ACK] (合并为一个包)

6.TCP 特性:面向字节流

"面向字节流"是指:TCP 不关心应用程序一次性发送了多少数据,也不关心数据的逻辑结构。在 TCP 看来,数据就是一串连续的、无结构的字节序列


6.1 类比UDP

1. UDP:面向报文(邮寄包裹)
  • 就像寄快递,你发一个包裹,对方收一个包裹。
  • 如果你发了 3 个包裹,对方必须分 3 次才能收完。
  • 每个包裹都有明确的边界。
2. TCP:面向字节流(自来水管)
  • 就像用水管供水。发送端是水龙头,接收端是接水盆。
  • 发送者可以一次注水 100 升,也可以分 100 次每次注水 1 升。
  • 对于接收者来说,他只看到盆里的水在增加,无法分辨这些水是分几次流进来的
  • 接收者可以根据自己的心情,用小勺舀水,或者直接拿大桶装,想取多少取多少。

6.2 内部实现机制:缓冲区 (Buffer)

TCP 能够实现"面向字节流",依靠的是内核中的发送缓冲区接收缓冲区

  1. 发送端

    • 应用层调用 write/send 写入数据。
    • 数据先被存入内核的发送缓冲区
    • TCP 根据网络状况(拥塞窗口、MSS、流量控制)来决定什么时候发、发多少。它可能会把一个大的应用层数据拆成多个包发送,也可能把多个小的数据合并成一个包发送。
  2. 接收端

    • TCP 将收到的网络包解包,按序号重组后放入接收缓冲区
    • 应用层调用 read/recv 读取数据。
    • 应用层可以一次读取 1 字节,也可以读取整个缓冲区,读取的长度与发送时写入的长度不需要一致

6.3 "粘包"问题 (Sticky Packets)

由于 TCP 没有消息边界,这就引出了开发者必须面对的"粘包/半包"问题。

  • 现象

    • 发送方发了两个消息:"Hello""World"
    • 接收方最终可能读到:
      • "HelloWorld" (两个合在一起了 ------ 粘包)
      • "HelloWo""rld" (一个被切开了 ------ 半包)
  • 原因

    • TCP 本身就没有"包"的概念,只有"字节"的概念。所谓的"粘包"其实是应用层没有正确处理字节流边界。

6.4 如何在应用层解决边界问题?

既然 TCP 不帮我们划分边界,应用层协议(如 HTTP、Redis 协议)必须自己制定规则:

  1. 固定长度
    • 规定每个消息固定为 128 字节,不够的补空格。
  2. 特殊分隔符
    • 在消息末尾加上特殊字符,如 \r\n(FTP、早期 HTTP 使用)。
  3. 自描述长度(最常用)
    • 在消息头部增加一个固定长度的字段,表示后续数据的字节数。
    • 流程:先读 4 字节获取长度 LLL -> 再读取 LLL 字节的数据。

7.TCP小结

为什么 TCP 这么复杂?

因为要保证可靠性,同时又尽可能提高性能。

可靠性:

• 校验和

• 序列号(按序到达)

• 确认应答

• 超时重发

• 连接管理

• 流量控制

• 拥塞控制

提高性能:

• 滑动窗口

• 快速重传

• 延迟应答

• 捎带应答

相关推荐
弹简特1 小时前
【JavaSE-网络部分04】网络原理-传输层:UDP + TCP 可靠性三大核心机制(确认应答 / 超时重传 / 连接管理)
网络·tcp/ip·udp
z10_142 小时前
住宅代理IP是什么?如何利用住宅代理做海外营销
网络·网络协议·tcp/ip
天荒地老笑话么2 小时前
NAT 下代理最佳实践:HTTP(S)_PROXY/NO_PROXY
网络·网络协议·http
消失的旧时光-194313 小时前
从 0 开始理解 RPC —— 后端工程师扫盲版
网络·网络协议·rpc
“αβ”14 小时前
网络层协议 -- ICMP协议
linux·服务器·网络·网络协议·icmp·traceroute·ping
wearegogog12315 小时前
基于C#的TCP/IP通信客户端与服务器
服务器·tcp/ip·c#
袁小皮皮不皮16 小时前
数据通信18-网络管理与运维
运维·服务器·网络·网络协议·智能路由器
Vect__18 小时前
UDP原理和极简socket编程demo
网络·网络协议·udp
小锋学长生活大爆炸20 小时前
【教程】查看docker容器的TCP连接和带宽使用情况
tcp/ip·docker·容器