Skip to content

LV035-传输层端口UDP与TCP ​

一、 为什么还需要传输层 ​

1. IP 只把包送到主机,不送到程序 ​

前面几篇解决了"怎么把包送到目标主机":IP 负责端到端寻址,MAC 负责一跳一跳搬运。但目标主机上同时跑着很多程序——浏览器、聊天软件、终端——包到了主机,应该交给谁?

IP 层回答不了这个问题:它的头里只有主机地址,没有"程序"的概念。区分程序是传输层的工作。

2. 端口:同一台主机上的分机号 ​

传输层给每个程序分配一个端口号(16 位,0 ~ 65535),整个寻址链条到这才完整:

text
MAC 找到下一跳的设备  →  IP 找到最终的主机  →  端口找到主机上的哪个程序

端口号的约定俗成分三段:

范围名称典型用途
0 ~ 1023知名端口80 = HTTP、53 = DNS、69 = TFTP、67/68 = DHCP
1024 ~ 49151注册端口各种约定好的服务
49152 ~ 65535动态端口客户端临时使用,用完即弃

注意知名端口绑定的是服务端:客户端发请求时,目的端口是服务器的知名端口,源端口则是一个临时选的动态端口。所以抓包看方向,看端口就能分清。

3. 本工程用到的端口 ​

端口谁在用协议出处
8888板子的 TCP 升级服务器TCPLV020-网络通信实验 第三节
69PC 侧的 TFTP 服务器UDPLV025-基于TFTP的网络升级实验
67 / 68DHCP 服务器 / 客户端UDPLV040-DHCP与IP地址自动获取 第二节

二、 UDP:无连接的信封 ​

1. UDP 头只有 8 字节 ​

UDP(用户数据报协议)是传输层里最薄的协议,头部固定 8 字节:

字段长度说明
源端口2 字节发送方端口
目的端口2 字节接收方端口
长度2 字节头 + 数据的总长度
校验和2 字节覆盖头与数据

没有序号、没有连接状态、没有窗口。发个 UDP 包就像寄一封信:写好地址扔进邮筒,之后就不管了。

2. 三个"不保证" ​

特性含义后果
不保证送达丢了就丢了,没人重传可靠性要靠应用自己解决
不保证顺序后发的可能先到应用要自己编号或容忍乱序
按报文交付收发的单位是"一个包",边界保留发两次 512 字节,收到的一定是两包

第三条是 UDP 与 TCP 最本质的区别:UDP 保留报文边界。发一个 512 字节的 UDP 报文,对方一次性收到的就是这个 512 字节,不会和下一个包粘在一起。

3. 什么场景适合 UDP ​

  • 一问一答的小交互:DNS 查询、DHCP 地址申请。一个包问、一个包答,不值得先建连接。
  • 允许丢包的实时流:音视频、在线游戏。等重传还不如要新数据。
  • 链路足够可靠的局域网:丢包率本来就近乎零,可靠性的成本付得不值。

TFTP 选 UDP 就属于第一种和第三种的组合,见第五节。

三、 TCP:有连接的字节流 ​

1. 字节流与报文的区别 ​

TCP(传输控制协议)与 UDP 的世界观完全不同:它传输的是一条没有边界的字节流。

应用程序调三次 send(512),TCP 可能把它合成一个 1536 字节的段发出去,也可能切成六个 256 字节的小段;接收方读出来的只是"一串顺序正确的字节",至于这串字节是几次 send 凑出来的,接收方根本不知道。

这意味着应用层的"消息边界"必须自己定。本工程的做法就是 IAPBIN01 帧:用魔数定位起点、用长度字段声明边界——这正是 TCP 字节流上"自己定义协议"的典型做法(帧格式见 LV030-网络升级帧协议与三条通道 第二节)。

2. 三次握手与四次挥手 ​

TCP 通信前先建立连接,握手只需要三个包:

text
客户端                服务端
  │──── SYN ────────────▶│   "我要连你,我的起始序号是 X"
  │◀─── SYN + ACK ───────│   "好,我的起始序号是 Y,你的 X 收到"
  │──── ACK ────────────▶│   "你的 Y 收到,连接建立"

结束时因为双方要各自宣布"我不再发了",需要四个包(FIN → ACK → FIN → ACK)。

三次握手的目的不复杂:双方确认对方的存在,并交换各自的初始序号。它要证明"我能发给你、你也能发给我",两个包证明不了双向都通,所以是三个。

3. 序号、ACK 与重传 ​

连接建立后,可靠传输靠三个机制:

(1)序号:字节流里每个字节都有编号。报文段头里的序号就是"我这段数据的第一个字节是第几号"。

(2)ACK:接收方收到数据后回 ACK,告诉对方"第 N 号之前的字节我都收到了"。

(3)超时重传:发送方发出数据后启动定时器,超时没收到 ACK 就重发。

乱序到达的段先缓存起来,按序号排好再交给上层。对应用来说,TCP 呈现的就是一条不丢、不乱、不重复的字节流——代价是头更大(20 字节起)、状态机更复杂、内存占用更高。

4. 滑动窗口与 MSS ​

两个决定传输效率的参数:

参数含义本工程的取值
接收窗口(RWND)接收方允许对方"最多未确认多少字节"TCP_WND = 4 × MSS = 2144 字节
最大报文段(MSS)一个段最多装多少数据,握手时各自宣布TCP_MSS = 536(lwIP 默认值)

滑动窗口的作用类比"发票报销":发出去的段相当于垫出去的钱,窗口就是"最多能垫多少"——垫满之前不必等报销回来,所以链路能跑满。

MSS 的细节(为什么是 536 而不是 1460、对分段数量的影响)见 LV015-lwIP配置与初始化 第二节。

TCP 三次握手与 UDP 一问一答的对比

四、 UDP 与 TCP 的对照 ​

对比项UDPTCP
连接无连接先握手建连接
传输单位报文(保留边界)字节流(无边界)
可靠性不保证送达、不保证顺序不丢、不乱、不重复
头部长度固定 8 字节20 字节起
状态开销无每个连接一个控制块(tcp_pcb)
本工程用法TFTP(69)、DHCP(67/68)升级通道(8888)

选择标准就一句话:宁可自己处理丢包也要省掉连接开销,选 UDP;字节序不能错、丢了必须补上,选 TCP。

五、 在本工程里的落地 ​

1. TFTP 为什么用 UDP ​

TFTP 是"一问一答的小交互"的极端形态:每块 512 字节,收到一块回一个 ACK,服务器没收到 ACK 就重发。可靠性由协议自己在应用层实现了,所以不需要 TCP 再保证一遍——再用 TCP 就是两套重传互相打架。

而且 UDP 无连接的特性让"服务器收到 RRQ 后换一个端口(TID)回数据"这种玩法成为可能,这在 TCP 的固定四元组连接里是做不了的(细节见 LV025 第五节)。

2. 升级通道为什么用 TCP ​

反过来,升级通道传的是"不允许错一个字节"的固件,而且 PC 侧要写脚本——TCP 的字节流语义让脚本只需要 sendall() 了事,重传、排序、流量控制全部由协议栈兜底。

板侧为此付出的代价是一个 tcp_pcb 控制块(MEMP_NUM_TCP_PCB = 5)以及回调式编程的复杂度,这在 LV010-lwIP协议栈移植 里已经讲过。

3. ping 为什么用不上传输层 ​

ping 用的是 ICMP,它直接坐在 IP 层上(协议号 1),根本没有端口。所以:

  • "板子的 8888 端口被防火墙拦了"这类说法对 ping 不成立——防火墙拦不拦 ICMP 是另一组规则;
  • ping 通不代表 TCP 能连上,两者验证的是不同的层。

这就是为什么排错要"先 ping、再 nc":ping 通了说明网络层以下都好,再往上才是端口的事(工具用法见 LV045-网络调试工具与抓包)。

4. lwIP 里的对应文件 ​

协议内核文件本工程的应用
TCPsrc/core/tcp.c、tcp_in.c、tcp_out.cport/iap_tcp.c(8888 端口服务器)
UDPsrc/core/udp.cDrivers/BSP/Ota/ota_tftp.c(TFTP 客户端)
ICMPsrc/core/ipv4/icmp.c无应用代码,协议栈自动应答

lwIP 提供的是 RAW API:没有 socket、没有 read/write,所有收发都是回调(tcp_accept、tcp_recv、udp_recv)。回调运行在主循环上下文,所以移植那篇的硬约束"回调里只做 O(1) 搬运"在 UDP 与 TCP 上同样成立。

六、 小结与参考资料 ​

1. 本篇小结 ​

  • IP 把包送到主机,端口把包送到程序;寻址链是 MAC → IP → 端口。
  • 知名端口绑服务端(80、53、69、67/68),客户端用动态端口;看端口就能分清抓包方向。
  • UDP 是无连接的"信封":8 字节头、不保证送达与顺序、保留报文边界,适合小交互与可靠链路。
  • TCP 是有连接的"字节流":三次握手交换初始序号,靠序号 + ACK + 超时重传保证不丢不乱,靠滑动窗口跑满链路;应用层消息边界要自己定。
  • 本工程的选型:TFTP 用 UDP(应用层自己做停等重传),升级通道用 TCP(字节流 + 协议栈兜底可靠性),ping 用 ICMP(根本不到传输层)。
  • lwIP 只有 RAW API,收发全部走回调;tcp_pcb 数量受 MEMP_NUM_TCP_PCB = 5 限制。

2. 参考资料 ​


本文档由 markdowncli 技能辅助生成