下面我用嵌入式网络开发视角,把这几个东西讲清楚:
text
TCP 客户端 / TCP 服务端
UDP 客户端 / UDP 服务端
UDP 广播 Broadcast
UDP 组播 Multicast
它们本质都在这套网络协议栈里:
text
┌──────────────────────────────┐
│ 应用层:HTTP / MQTT / 自定义协议 │
├──────────────────────────────┤
│ 传输层:TCP / UDP │
├──────────────────────────────┤
│ 网络层:IP │
├──────────────────────────────┤
│ 链路层:Ethernet / Wi-Fi │
├──────────────────────────────┤
│ 物理层:网线 / 无线电 │
└──────────────────────────────┘
1. 先理解 IP、端口、Socket
网络通信最核心的是:
text
IP 地址 + 端口号
比如:
text
192.168.1.100:8080
其中:
text
192.168.1.100 是设备地址
8080 是应用程序端口
可以类比成:
text
IP 地址 = 一栋楼的地址
端口号 = 这栋楼里的某个房间号
Socket = 你打开的一条通信通道
例如:
text
电脑 IP:192.168.1.10
ESP32 IP:192.168.1.20
服务器端口:8080
ESP32 要连接电脑的 TCP Server,就是连接:
text
192.168.1.10:8080
2. TCP 和 UDP 最大区别
| 对比项 | TCP | UDP |
|---|---|---|
| 是否连接 | 有连接 | 无连接 |
| 可靠性 | 可靠,丢了会重传 | 不可靠,丢了就丢了 |
| 顺序 | 保证顺序 | 不保证顺序 |
| 数据形式 | 字节流 | 数据报 |
| 速度 | 较稳,开销较大 | 快,开销小 |
| 是否有粘包 | 有 | 没有 TCP 那种粘包 |
| 典型应用 | HTTP、MQTT、OTA、文件传输 | 视频、语音、广播发现、实时控制 |
| API | connect/send/recv |
sendto/recvfrom |
一句话:
text
TCP 像打电话:先接通,再连续说话,可靠。
UDP 像发快递/扔纸条:直接发,快,但不保证一定到。
3. TCP 服务端通信流程
TCP 是面向连接的。
TCP Server 的典型流程是:
c
socket()
bind()
listen()
accept()
recv()
send()
close()
画成流程图:
text
TCP Server
1. socket()
创建一个 TCP 套接字
2. bind()
绑定本机 IP 和端口
3. listen()
开始监听端口,等待客户端连接
4. accept()
阻塞等待客户端连接进来
5. recv()
接收客户端数据
6. send()
回复客户端数据
7. close()
关闭连接
3.1 socket() 是干啥的?
c
int listen_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
意思是创建一个 TCP socket。
参数含义:
text
AF_INET 使用 IPv4
SOCK_STREAM 使用 TCP 流式套接字
IPPROTO_IP 协议自动选择,一般就是 TCP
可以理解为:
text
我要开一个网络通信接口,准备用 TCP 通信。
3.2 bind() 是干啥的?
c
bind(listen_sock, (struct sockaddr *)&server_addr, sizeof(server_addr));
bind() 是把 socket 和本机端口绑定起来。
例如:
text
本机 IP:0.0.0.0
端口:8080
其中 0.0.0.0 表示:
text
监听本机所有网卡上的 8080 端口
比如你的电脑有:
text
Wi-Fi IP:192.168.1.10
有线网卡 IP:192.168.2.10
绑定 0.0.0.0:8080,就是两个网卡都能接收连接。
3.3 listen() 是干啥的?
c
listen(listen_sock, 5);
listen() 表示:
text
我这个端口现在开始接客了。
第二个参数 5 是 backlog,表示连接等待队列长度。
简单理解:
text
如果多个客户端同时连接,系统可以先排队几个。
3.4 accept() 是干啥的?
c
int client_sock = accept(listen_sock,
(struct sockaddr *)&client_addr,
&addr_len);
accept() 会阻塞等待客户端连接。
注意一个关键点:
text
listen_sock 只负责监听
client_sock 才负责和客户端真正通信
也就是说:
text
listen_sock = 门口保安,只负责等人进来
client_sock = 具体接待窗口,负责和某个客户端聊天
如果有 3 个客户端连接服务器,服务器通常会得到 3 个不同的 client_sock:
text
listen_sock
├── client_sock_1 和客户端1通信
├── client_sock_2 和客户端2通信
└── client_sock_3 和客户端3通信
3.5 TCP Server 完整通信图
text
TCP Server TCP Client
socket()
bind()
listen()
accept() <────── connect() ─────── socket()
recv() <────── send()
send() ──────> recv()
close() <────── close()
4. TCP 客户端通信流程
TCP Client 的流程比 Server 简单:
c
socket()
connect()
send()
recv()
close()
流程图:
text
TCP Client
1. socket()
创建 TCP socket
2. connect()
连接服务器 IP + 端口
3. send()
发送数据
4. recv()
接收服务器回复
5. close()
关闭连接
4.1 connect() 底层发生了什么?
TCP 的 connect() 不是简单发个数据,它会触发 TCP 三次握手。
text
Client Server
SYN ─────────────────────>
<──────────────────── SYN + ACK
ACK ─────────────────────>
连接建立成功
解释一下:
text
第一次:Client 说:我要连接你。
第二次:Server 说:我收到了,我也准备好了。
第三次:Client 说:好的,我们开始通信。
这就是 TCP 可靠连接的开始。
4.2 TCP 建立连接后怎么发数据?
连接建立后,两边都可以:
c
send()
recv()
这时候 TCP 是全双工通信。
也就是:
text
客户端可以发给服务端
服务端也可以发给客户端
双方可以同时收发
例如:
text
Client Server
send("hello") ───────────────>
<─────────────── recv("hello")
recv("ok") <─────────────── send("ok")
5. TCP 断开连接流程
TCP 断开连接通常是四次挥手。
text
Client Server
FIN ─────────────────────>
<──────────────────── ACK
<──────────────────── FIN
ACK ─────────────────────>
简单理解:
text
Client:我不发了。
Server:好的,我知道了。
Server:我也不发了。
Client:好的,连接关闭。
为什么不是一次就断?
因为 TCP 是全双工的:
text
A 不想发了,不代表 B 也不想发了。
6. TCP 的重要特点:可靠字节流
TCP 不是一包一包给你看的,而是连续字节流。
比如客户端这样发:
c
send(sock, "hello", 5, 0);
send(sock, "world", 5, 0);
服务端可能这样收到:
text
一次 recv 收到:
helloworld
也可能这样收到:
text
第一次 recv:hel
第二次 recv:loworld
也可能:
text
第一次 recv:hello
第二次 recv:world
这就是你之前问过的:
text
TCP 粘包 / 拆包
6.1 TCP 为什么会粘包?
因为 TCP 不关心你应用层的"一条消息"边界。
TCP 只保证:
text
数据可靠、有序地到达
但它不保证:
text
你 send 几次,对方 recv 就收到几次
所以 TCP 应用层必须自己设计协议格式。
常见方式:
text
方案1:固定长度
方案2:特殊结束符,例如 \r\n
方案3:帧头 + 长度 + 数据
方案4:帧头 + 长度 + 数据 + CRC
最常见的是:
text
┌──────┬──────┬──────────┬──────┐
│ 帧头 │ 长度 │ 数据内容 │ CRC │
└──────┴──────┴──────────┴──────┘
例如:
text
AA 55 00 05 68 65 6C 6C 6F CRC
7. UDP 服务端通信流程
UDP 是无连接的。
UDP Server 流程:
c
socket()
bind()
recvfrom()
sendto()
close()
画成图:
text
UDP Server
1. socket()
创建 UDP socket
2. bind()
绑定本机端口
3. recvfrom()
等待接收任意客户端发来的 UDP 数据
4. sendto()
回复指定客户端
5. close()
关闭 socket
7.1 UDP Server 为什么也要 bind?
因为服务端需要告诉系统:
text
我要接收发到本机 8080 端口的数据。
例如:
text
ESP32 UDP Server 绑定 8080 端口
那么别人发到:
text
ESP32_IP:8080
的数据,ESP32 才能收到。
7.2 recvfrom() 和 TCP recv() 区别
UDP 用:
c
recvfrom(sock, rx_buffer, sizeof(rx_buffer), 0,
(struct sockaddr *)&source_addr, &socklen);
因为 UDP 没有连接,所以接收数据时要同时知道:
text
是谁发来的?
IP 是多少?
端口是多少?
所以 recvfrom() 不光拿数据,也拿对方地址。
7.3 UDP Server 通信图
text
UDP Server UDP Client
socket()
bind()
recvfrom() <────── sendto()
sendto() ──────> recvfrom()
close() close()
注意:
text
没有 listen()
没有 accept()
没有 connect() 必须步骤
8. UDP 客户端通信流程
UDP Client 流程:
c
socket()
sendto()
recvfrom()
close()
也可以不调用 bind()。
为什么?
因为客户端发送时,系统会自动分配一个临时端口。
例如:
text
ESP32 UDP Client
本地临时端口:52341
目标服务器:192.168.1.10:8080
发送出去的数据大概是:
text
源 IP: ESP32_IP
源端口: 52341
目标 IP: 192.168.1.10
目标端口: 8080
服务端回复时,就回到:
text
ESP32_IP:52341
9. UDP 的重要特点:数据报
UDP 是一包一包的。
比如客户端:
c
sendto(sock, "hello", 5, 0, ...);
sendto(sock, "world", 5, 0, ...);
服务端通常会:
text
第一次 recvfrom:hello
第二次 recvfrom:world
UDP 保留消息边界,所以没有 TCP 那种粘包问题。
但是 UDP 有其他问题:
text
可能丢包
可能乱序
可能重复
包太大可能被 IP 分片
分片丢一个,整包就没了
所以 UDP 常用于:
text
实时性比可靠性更重要的场景
例如:
text
语音
视频
局域网设备发现
传感器快速上报
广播
组播
游戏同步
10. TCP Server / Client 和 UDP Server / Client 的核心区别
| 对比 | TCP Server | TCP Client | UDP Server | UDP Client |
|---|---|---|---|---|
| 是否连接 | 等待连接 | 主动连接 | 不连接 | 不连接 |
| 关键函数 | bind/listen/accept |
connect |
bind/recvfrom |
sendto |
| 通信对象 | accept 后固定对端 | 固定服务端 | 谁发来都能收 | 发给指定 IP/端口 |
| 可靠性 | 可靠 | 可靠 | 不可靠 | 不可靠 |
| 数据边界 | 字节流,无边界 | 字节流,无边界 | 数据报,有边界 | 数据报,有边界 |
| 是否适合大数据 | 适合 | 适合 | 不太适合 | 不太适合 |
11. UDP 广播 Broadcast
广播是 UDP 里很常用的功能。
它的作用是:
text
一台设备发一包,局域网内很多设备都能收到。
广播常用于:
text
局域网设备发现
自动寻找服务器
设备配网发现
遥控器寻找被控设备
11.1 广播地址是什么?
常见广播地址有两种:
1)受限广播地址
text
255.255.255.255
表示:
text
发给当前局域网内所有设备
通常不会被路由器转发。
2)定向广播地址
假设局域网是:
text
IP:192.168.1.x
子网掩码:255.255.255.0
那么广播地址是:
text
192.168.1.255
意思是:
text
发给 192.168.1.0/24 这个网段里的所有设备
11.2 广播通信流程
假设:
text
ESP32 想找局域网里的服务器
流程如下:
text
ESP32 UDP Broadcast Client 局域网内所有设备
socket()
setsockopt(SO_BROADCAST)
sendto(192.168.1.255:9999, "who is server?")
├── 设备A收到
├── 设备B收到
├── 服务器收到
└── 其他设备收到
服务器回复:
recvfrom()
sendto(ESP32_IP:临时端口, "I am server")
图示:
text
ESP32
│
│ UDP 广播:who is server?
▼
192.168.1.255:9999
│
├── 设备 A 收到
├── 设备 B 收到
├── 服务器收到并回复
└── 手机/电脑可能也收到
11.3 广播发送端流程
UDP 广播发送端一般是:
c
socket()
setsockopt(SO_BROADCAST)
sendto(broadcast_ip, port, data)
close()
关键是:
c
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));
如果不开 SO_BROADCAST,很多系统不允许你发广播包。
11.4 广播接收端流程
广播接收端就是普通 UDP Server:
c
socket()
bind(本地端口)
recvfrom()
只要绑定了对应端口,就能收到发往广播地址的 UDP 数据。
例如所有设备都监听:
text
9999 端口
发送端发:
text
192.168.1.255:9999
这些设备就都可能收到。
11.5 广播的特点
| 特点 | 说明 |
|---|---|
| 一发多收 | 局域网内多个设备可收到 |
| 不需要知道对方 IP | 适合设备发现 |
| 只能局域网 | 一般不会跨路由器 |
| 占用网络资源 | 所有设备都要处理广播包 |
| 安全性一般 | 局域网内别人也可能收到 |
| 基于 UDP | 不可靠,可能丢包 |
12. UDP 组播 Multicast
组播比广播更高级一点。
广播是:
text
发给局域网所有设备
组播是:
text
只发给加入某个组的设备
可以类比:
text
广播 = 小区大喇叭,所有人都听到
组播 = 加入某个微信群的人才收到
12.1 组播地址范围
IPv4 组播地址范围是:
text
224.0.0.0 ~ 239.255.255.255
常见例子:
text
224.0.0.1 本地网络所有主机
224.0.0.251 mDNS
239.x.x.x 常用于私有组播
自己做项目时,常用私有组播地址:
text
239.255.0.1
239.255.255.250
例如 SSDP/UPnP 常用:
text
239.255.255.250:1900
12.2 组播通信流程
假设有几个设备:
text
设备 A:加入组播组 239.1.1.1:5000
设备 B:加入组播组 239.1.1.1:5000
设备 C:没有加入
发送端:向 239.1.1.1:5000 发 UDP 数据
结果:
text
设备 A 收到
设备 B 收到
设备 C 收不到
图示:
text
发送端
│
│ UDP 组播数据
▼
239.1.1.1:5000
│
├── 设备 A:已加入组播组 → 收到
├── 设备 B:已加入组播组 → 收到
└── 设备 C:未加入组播组 → 不收
12.3 组播接收端流程
组播接收端比普通 UDP Server 多一步:
text
加入组播组
流程:
c
socket()
bind(本地端口)
setsockopt(IP_ADD_MEMBERSHIP)
recvfrom()
关键是 IP_ADD_MEMBERSHIP:
c
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("239.1.1.1");
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
意思是:
text
我要加入 239.1.1.1 这个组播组。
12.4 组播发送端流程
组播发送端和 UDP Client 差不多:
c
socket()
sendto(239.1.1.1:5000, data)
close()
可选配置:
text
设置 TTL
设置发送网卡
是否允许自己收到自己发出的组播
12.5 TTL 是什么?
TTL 是 Time To Live。
在组播里,TTL 决定组播包能传多远。
常见设置:
text
TTL = 1 只在本地局域网
TTL > 1 可能跨路由器,前提是路由器支持组播转发
嵌入式局域网项目一般设置:
text
TTL = 1
12.6 组播的特点
| 特点 | 说明 |
|---|---|
| 一发多收 | 多个组成员能收到 |
| 比广播更精准 | 只有加入组的设备接收 |
| 基于 UDP | 不保证可靠 |
| 需要加入组 | 接收端需要 IP_ADD_MEMBERSHIP |
| 常用于发现/媒体 | mDNS、SSDP、视频流、局域网发现 |
| 跨网段复杂 | 需要路由器支持组播 |
13. 广播和组播区别
| 对比 | 广播 Broadcast | 组播 Multicast |
|---|---|---|
| 发送目标 | 局域网所有设备 | 某个组播组成员 |
| IP 地址 | 255.255.255.255 或 192.168.1.255 |
224.0.0.0 ~ 239.255.255.255 |
| 接收端是否要加入组 | 不需要 | 需要 |
| 网络负担 | 较大 | 较小 |
| 精准程度 | 低 | 高 |
| 是否跨路由 | 一般不跨 | 可以,但需要支持 |
| 常见用途 | 简单设备发现 | mDNS、SSDP、视频、多设备同步 |
一句话:
text
广播:所有人都收到。
组播:加入群的人才收到。
14. TCP、UDP、广播、组播放一起对比
| 类型 | 是否连接 | 一对一/一对多 | 是否可靠 | 典型用途 |
|---|---|---|---|---|
| TCP Client/Server | 有连接 | 一对一 | 可靠 | HTTP、MQTT、OTA、文件 |
| UDP Client/Server | 无连接 | 一对一 | 不可靠 | 实时数据、小包通信 |
| UDP 广播 | 无连接 | 一对多,所有设备 | 不可靠 | 局域网设备发现 |
| UDP 组播 | 无连接 | 一对多,组成员 | 不可靠 | mDNS、SSDP、视频流 |
15. 用实际例子理解
例子 1:ESP32 连接云服务器
适合 TCP。
text
ESP32 云服务器
连接 Wi-Fi
socket()
connect(server_ip:1883) ────>
send(MQTT数据) ─────────────>
recv(MQTT响应) <────────────
这种场景需要可靠通信:
text
MQTT
HTTP
HTTPS
OTA
天气请求
云端控制
所以一般用 TCP。
例子 2:ESP32 局域网找电脑服务器
适合 UDP 广播。
text
ESP32 不知道电脑 IP,只知道大家在同一个 Wi-Fi 下。
ESP32 发广播:
who is my server?
电脑 UDP Server 收到:
我是服务器,我的 IP 是 192.168.1.10
ESP32 收到回复后,再连接:
192.168.1.10:8080
流程:
text
ESP32 局域网 电脑 Server
UDP Broadcast ────────> 所有设备
"who is server?" 收到
ESP32 <─────────────────────────────────────── UDP 单播回复
"server ip is 192.168.1.10"
ESP32 ───────────────────────────────────────> TCP connect
很多智能设备发现都是这个思路。
例子 3:多个设备同时接收控制命令
可以用 UDP 组播。
比如你有 10 个灯,每个灯都加入:
text
239.1.1.1:5000
控制器发:
text
turn_on
所有加入组的灯都收到。
text
控制器
│
│ sendto("turn_on", 239.1.1.1:5000)
▼
组播组
├── 灯1 收到
├── 灯2 收到
├── 灯3 收到
└── 灯10 收到
16. TCP 多客户端服务端模型
TCP Server 如果要支持多个客户端,不能只 accept() 一次。
常见模型有几种。
16.1 一个客户端一个任务/线程
text
主任务:
socket()
bind()
listen()
while(1) {
client_sock = accept()
创建新任务处理 client_sock
}
结构:
text
TCP Server
├── listen socket
├── client task 1
├── client task 2
├── client task 3
└── client task N
优点:
text
逻辑简单
每个客户端独立处理
缺点:
text
客户端多了占 RAM
嵌入式设备不适合无限创建任务
16.2 select/poll 模型
一个任务同时管理多个 socket。
text
while(1) {
select()
如果 listen_sock 可读:accept 新连接
如果 client_sock_1 可读:recv
如果 client_sock_2 可读:recv
}
优点:
text
省任务
适合多个连接
缺点:
text
代码复杂
状态机多
ESP32/LwIP 也可以用 select()。
17. UDP 为什么没有 accept?
因为 UDP 没有连接。
TCP 的连接关系是:
text
客户端 A 和服务端建立连接
客户端 B 和服务端建立连接
服务端 accept 出不同 client_sock
UDP 是:
text
谁给我这个端口发数据,我就收。
所以 UDP Server 只需要:
text
一个 socket 绑定一个端口
然后通过 recvfrom() 区分是谁发来的。
text
UDP Server 9999端口
recvfrom() 收到 A 的数据,source_addr = A
recvfrom() 收到 B 的数据,source_addr = B
recvfrom() 收到 C 的数据,source_addr = C
18. UDP 的 connect 是什么?
UDP 也可以调用 connect(),但它和 TCP 不一样。
UDP 的 connect() 不会三次握手。
它只是告诉系统:
text
这个 UDP socket 默认只和某个 IP:Port 通信。
调用 UDP connect() 后,可以用:
c
send()
recv()
不用每次 sendto() 填目标地址。
但是注意:
text
UDP connect 不代表真的建立连接
对方不在线也可能 send 成功
底层仍然是不可靠 UDP
19. 单播、广播、组播
网络发送目标有三类:
text
单播 Unicast
广播 Broadcast
组播 Multicast
19.1 单播 Unicast
一对一。
text
192.168.1.20 ─────> 192.168.1.10
TCP 和普通 UDP 都是单播。
19.2 广播 Broadcast
一对所有。
text
192.168.1.20 ─────> 192.168.1.255
├── 192.168.1.10
├── 192.168.1.11
├── 192.168.1.12
└── ...
19.3 组播 Multicast
一对一组。
text
发送到 239.1.1.1
├── 加入组的设备 A 收到
├── 加入组的设备 B 收到
└── 没加入组的设备 C 不收
20. 嵌入式开发常见选择
TCP 适合
text
HTTP 请求天气
HTTPS OTA 升级
MQTT 长连接
文件传输
云端通信
需要可靠传输的数据
例如:
text
ESP32 获取天气:
ESP32 TCP Client → HTTP Server
UDP 单播适合
text
低延迟状态上报
局域网遥控
简单传感器数据
允许偶尔丢包
自己实现 ACK/重传
UDP 广播适合
text
局域网发现设备
不知道目标 IP
设备第一次配网后找服务器
遥控器找小车
APP 找局域网设备
UDP 组播适合
text
多个设备同时接收同一消息
局域网服务发现
mDNS
SSDP
多设备同步
视频/音频局域网分发
21. 一个完整项目里的组合用法
实际产品中经常不是只用一种,而是组合使用。
例如一个 ESP32 设备:
text
1. 上电连接 Wi-Fi
2. UDP 广播:寻找局域网服务器
3. 收到服务器 UDP 回复,得到服务器 IP
4. TCP 连接服务器
5. 后续用 TCP/MQTT 长连接通信
流程图:
text
ESP32 局域网服务器
Wi-Fi 连接成功
UDP 广播:
"who is server?" ────────────>
<────────────── UDP 回复:
"I am 192.168.1.10"
TCP connect 192.168.1.10:8080 ─────>
TCP send/recv 正式通信
这个设计很常见。
因为:
text
UDP 广播负责发现
TCP 负责可靠通信
22. 对 ESP32 / LwIP 来说的注意点
22.1 TCP 注意点
text
1. recv() 可能一次收不完整
2. send() 不一定一次把所有数据写完
3. 需要处理断开重连
4. 需要心跳保活
5. 不要在一个任务里永久阻塞影响其他功能
6. 多客户端要考虑任务数量和 RAM
7. TCP 是字节流,必须设计应用层协议
22.2 UDP 注意点
text
1. UDP 可能丢包
2. 包不要太大,避免 IP 分片
3. 广播需要 SO_BROADCAST
4. 组播接收需要加入组
5. Wi-Fi 路由器可能限制广播/组播
6. UDP 没有连接状态,需要自己判断对方是否在线
22.3 广播注意点
text
1. 广播通常只在局域网有效
2. 有些路由器/AP 会过滤广播
3. 手机热点下广播行为可能不稳定
4. 广播太频繁会占网络资源
5. 广播接收端必须 bind 对应端口
22.4 组播注意点
text
1. 接收端必须 IP_ADD_MEMBERSHIP
2. 路由器/AP 可能对组播做限速或过滤
3. Wi-Fi 下组播速率可能比较低
4. 跨网段组播需要路由器支持
5. 局域网项目 TTL 一般设为 1
23. 最终总结
你可以这样记:
text
TCP Server:
绑定端口,监听,accept 客户端连接,然后 recv/send。
TCP Client:
知道服务器 IP 和端口,connect 过去,然后 send/recv。
UDP Server:
绑定端口,不需要连接,谁发来就 recvfrom 谁。
UDP Client:
不需要连接,直接 sendto 目标 IP 和端口。
UDP 广播:
发给整个局域网,适合"我不知道目标 IP,先找一下"。
UDP 组播:
发给加入某个组的设备,适合"一发多收,但不想所有人都收"。
再压缩成一句话:
text
TCP:可靠连接通信。
UDP:快速无连接通信。
广播:UDP 发给局域网所有设备。
组播:UDP 发给指定组里的设备。
从嵌入式产品开发角度:
text
要可靠、大数据、云通信:用 TCP。
要简单、低延迟、小数据:用 UDP。
要发现局域网设备:用 UDP 广播。
要多个设备同时接收:用 UDP 组播。
好,这次直接用具体 IP + 端口 + 数据流向来讲。
假设现在有 3 台设备在同一个 Wi-Fi 局域网:
text
路由器网关:192.168.1.1
电脑 Server:192.168.1.10
ESP32 A: 192.168.1.20
ESP32 B: 192.168.1.21
手机: 192.168.1.30
子网掩码:255.255.255.0
广播地址:192.168.1.255
1. TCP 客户端 / 服务端:具体 IP 端口流程
1.1 场景
电脑作为 TCP Server:
text
电脑 IP:192.168.1.10
监听端口:8080
ESP32 作为 TCP Client:
text
ESP32 IP:192.168.1.20
本地临时端口:系统自动分配,例如 52341
通信目标是:
text
ESP32 连接电脑的 192.168.1.10:8080
2. TCP Server 端具体做了什么?
电脑程序做:
c
listen_sock = socket(AF_INET, SOCK_STREAM, 0);
bind(listen_sock, 0.0.0.0:8080);
listen(listen_sock, 5);
client_sock = accept(listen_sock);
recv(client_sock, ...);
send(client_sock, ...);
这里最关键的是:
text
0.0.0.0:8080
它表示:
text
电脑所有网卡的 8080 端口都监听
如果电脑 Wi-Fi IP 是:
text
192.168.1.10
那么 ESP32 实际连接的是:
text
192.168.1.10:8080
3. TCP Client 端具体做了什么?
ESP32 做:
c
sock = socket(AF_INET, SOCK_STREAM, 0);
connect(sock, 192.168.1.10:8080);
send(sock, "hello", 5, 0);
recv(sock, ...);
ESP32 一般不用自己指定本地端口,系统会自动分配一个临时端口,比如:
text
52341
于是 TCP 连接建立后,真正的通信四元组是:
text
源 IP: 192.168.1.20
源端口: 52341
目标 IP: 192.168.1.10
目标端口:8080
可以写成:
text
192.168.1.20:52341 ---> 192.168.1.10:8080
4. TCP 三次握手的具体 IP 端口
TCP connect() 时,底层会三次握手。
text
ESP32 Client 电脑 Server
192.168.1.20:52341 ── SYN ───────────> 192.168.1.10:8080
192.168.1.20:52341 <─ SYN+ACK ─────── 192.168.1.10:8080
192.168.1.20:52341 ── ACK ───────────> 192.168.1.10:8080
握手成功后,连接建立。
之后双方就可以收发数据。
5. TCP 数据发送的具体流向
ESP32 发送:
c
send(sock, "hello server", 12, 0);
实际网络包大概是:
text
源 IP: 192.168.1.20
源端口: 52341
目标 IP: 192.168.1.10
目标端口:8080
数据:hello server
方向:
text
192.168.1.20:52341 ---> 192.168.1.10:8080
电脑 Server 收到后回复:
c
send(client_sock, "hello esp32", 11, 0);
实际网络包是:
text
源 IP: 192.168.1.10
源端口: 8080
目标 IP: 192.168.1.20
目标端口:52341
数据:hello esp32
方向:
text
192.168.1.10:8080 ---> 192.168.1.20:52341
注意这里:服务端回复的目标端口不是 8080,而是 ESP32 的临时端口 52341。
6. TCP 多客户端具体例子
假设有两个 ESP32 同时连接电脑 Server。
text
电脑 Server:
192.168.1.10:8080
ESP32 A:
text
192.168.1.20:52341 ---> 192.168.1.10:8080
ESP32 B:
text
192.168.1.21:52342 ---> 192.168.1.10:8080
电脑 Server 虽然只监听一个端口:
text
8080
但是它能区分两个客户端,因为 TCP 连接靠四元组区分:
text
连接 1:
192.168.1.20:52341 <--> 192.168.1.10:8080
连接 2:
192.168.1.21:52342 <--> 192.168.1.10:8080
所以 TCP Server 不是靠"一个端口只能连一个设备",而是靠:
text
源 IP + 源端口 + 目标 IP + 目标端口
来区分不同连接。
7. TCP 里 listen_sock 和 client_sock 的具体区别
电脑 Server 代码里通常有两个 socket:
text
listen_sock
client_sock
比如:
text
listen_sock 绑定:0.0.0.0:8080
它只负责:
text
等待客户端连接
当 ESP32 A 连接进来后:
text
accept() 返回 client_sock_1
这个 client_sock_1 对应:
text
192.168.1.20:52341 <--> 192.168.1.10:8080
当 ESP32 B 连接进来后:
text
accept() 返回 client_sock_2
这个 client_sock_2 对应:
text
192.168.1.21:52342 <--> 192.168.1.10:8080
可以这样理解:
text
listen_sock = 门口接待台,只负责等新连接
client_sock_1 = 和 ESP32 A 通信的专用通道
client_sock_2 = 和 ESP32 B 通信的专用通道
8. TCP 总流程完整图
text
电脑 Server:192.168.1.10:8080
ESP32 Client:192.168.1.20:52341
电脑 Server ESP32 Client
socket()
bind(0.0.0.0:8080)
listen()
accept() <──────────── connect(192.168.1.10:8080)
三次握手:
192.168.1.20:52341 -> 192.168.1.10:8080
recv() <──────────── send("hello")
send("ok") ───────────> recv()
close() <──────────── close()
9. UDP 客户端 / 服务端:具体 IP 端口流程
UDP 和 TCP 最大区别:
text
TCP:先 connect,建立连接,再 send/recv
UDP:不建立连接,直接 sendto/recvfrom
9.1 UDP Server 例子
电脑作为 UDP Server:
text
电脑 IP:192.168.1.10
监听端口:9000
电脑代码做:
c
sock = socket(AF_INET, SOCK_DGRAM, 0);
bind(sock, 0.0.0.0:9000);
recvfrom(sock, ...);
sendto(sock, ...);
绑定:
text
0.0.0.0:9000
表示:
text
接收所有发到电脑 9000 端口的 UDP 数据
9.2 UDP Client 例子
ESP32 作为 UDP Client:
text
ESP32 IP:192.168.1.20
本地临时端口:系统自动分配,例如 53001
ESP32 发送:
c
sendto(sock, "hello udp", 9, 0, 192.168.1.10:9000);
实际数据包:
text
源 IP: 192.168.1.20
源端口: 53001
目标 IP: 192.168.1.10
目标端口:9000
数据:hello udp
方向:
text
192.168.1.20:53001 ---> 192.168.1.10:9000
9.3 UDP Server 如何回复?
电脑 Server 用 recvfrom() 收数据时,不只是收到数据,还会收到对方地址:
text
对方 IP:192.168.1.20
对方端口:53001
所以电脑回复时:
c
sendto(sock, "udp ok", 6, 0, 192.168.1.20:53001);
实际数据包:
text
源 IP: 192.168.1.10
源端口: 9000
目标 IP: 192.168.1.20
目标端口:53001
数据:udp ok
方向:
text
192.168.1.10:9000 ---> 192.168.1.20:53001
10. UDP 和 TCP 端口行为的关键区别
TCP Server:
text
192.168.1.10:8080
客户端连接进来后,形成一条"连接"。
UDP Server:
text
192.168.1.10:9000
没有连接,谁给这个端口发数据,Server 就能收到。
比如:
text
ESP32 A:192.168.1.20:53001 ---> 192.168.1.10:9000
ESP32 B:192.168.1.21:53002 ---> 192.168.1.10:9000
手机: 192.168.1.30:53003 ---> 192.168.1.10:9000
电脑 UDP Server 用同一个 socket 收:
text
recvfrom() 收到 A 的数据,source = 192.168.1.20:53001
recvfrom() 收到 B 的数据,source = 192.168.1.21:53002
recvfrom() 收到手机数据,source = 192.168.1.30:53003
UDP Server 不需要 accept(),因为 UDP 没有连接。
11. UDP 完整通信图
text
电脑 UDP Server:192.168.1.10:9000
ESP32 UDP Client:192.168.1.20:53001
电脑 Server ESP32 Client
socket()
bind(0.0.0.0:9000)
recvfrom() <──────────── sendto(192.168.1.10:9000)
数据方向:
192.168.1.20:53001 -> 192.168.1.10:9000
sendto(192.168.1.20:53001) ───────────> recvfrom()
数据方向:
192.168.1.10:9000 -> 192.168.1.20:53001
12. UDP Client 是否必须 bind?
不一定。
情况 1:不 bind
ESP32 直接:
c
socket()
sendto(192.168.1.10:9000)
系统自动分配本地端口,比如:
text
53001
这适合普通 UDP Client。
情况 2:主动 bind 本地端口
例如你希望 ESP32 固定用本地端口:
text
192.168.1.20:7000
那 ESP32 可以:
c
bind(sock, 0.0.0.0:7000);
sendto(sock, ..., 192.168.1.10:9000);
此时发出的 UDP 包是:
text
192.168.1.20:7000 ---> 192.168.1.10:9000
这样服务端回复时,也会回到:
text
192.168.1.20:7000
什么时候需要 Client bind?
text
1. 你希望本地端口固定
2. 对方需要固定向你这个端口发数据
3. 做广播/组播接收
4. 做某些固定协议,例如 mDNS、SSDP
13. UDP 广播:具体 IP 端口流程
广播用于:
text
我不知道服务器 IP,但是我知道它可能在这个局域网里。
比如 ESP32 不知道电脑 Server 是:
text
192.168.1.10
它只知道大家都在:
text
192.168.1.x
那就可以发广播。
13.1 广播地址
当前网络:
text
ESP32 IP:192.168.1.20
子网掩码:255.255.255.0
广播地址就是:
text
192.168.1.255
也可以用:
text
255.255.255.255
更常见推荐:
text
192.168.1.255
13.2 广播发现服务器例子
约定:
text
所有服务器都监听 UDP 9999 端口
电脑 Server:
text
192.168.1.10:9999
ESP32 发送广播:
text
目标 IP:192.168.1.255
目标端口:9999
数据:who is server?
ESP32 本地端口假设是:
text
53001
实际 UDP 包:
text
源 IP: 192.168.1.20
源端口: 53001
目标 IP: 192.168.1.255
目标端口:9999
数据:who is server?
方向:
text
192.168.1.20:53001 ---> 192.168.1.255:9999
局域网内所有监听 9999 端口的设备都有机会收到。
比如:
text
192.168.1.10:9999 收到
192.168.1.21:9999 收到
192.168.1.30:9999 收到
但是只有真正的服务器回复:
text
源 IP: 192.168.1.10
源端口: 9999
目标 IP: 192.168.1.20
目标端口:53001
数据:I am server, ip=192.168.1.10
方向:
text
192.168.1.10:9999 ---> 192.168.1.20:53001
14. UDP 广播完整流程图
text
ESP32:192.168.1.20:53001
电脑 Server:192.168.1.10:9999
广播地址:192.168.1.255:9999
ESP32 局域网设备
sendto(192.168.1.255:9999, "who is server?")
│
│ 192.168.1.20:53001 -> 192.168.1.255:9999
▼
电脑 192.168.1.10:9999 收到
手机 192.168.1.30:9999 可能收到
ESP32 B 192.168.1.21:9999 可能收到
电脑回复:
192.168.1.10:9999 -> 192.168.1.20:53001
"I am server"
15. 广播发送端为什么要 SO_BROADCAST?
发送广播前通常要设置:
c
int broadcast = 1;
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));
因为系统默认不允许随便往广播地址发包。
发送端流程:
c
sock = socket(AF_INET, SOCK_DGRAM, 0);
setsockopt(sock, SOL_SOCKET, SO_BROADCAST, ...);
sendto(sock, "who is server?", ..., 192.168.1.255:9999);
接收端就是普通 UDP Server:
c
sock = socket(AF_INET, SOCK_DGRAM, 0);
bind(sock, 0.0.0.0:9999);
recvfrom(sock, ...);
16. 广播里的端口非常重要
广播不是说"发给所有程序"。
它是:
text
发给局域网所有设备的某个端口
例如:
text
192.168.1.255:9999
表示:
text
局域网所有设备上,监听 9999 端口的程序可以收到
如果某台设备没有程序绑定 9999 端口,它就算收到网络包,也会被系统丢掉。
所以广播必须约定端口:
text
发现服务器端口:9999
17. UDP 组播:具体 IP 端口流程
组播和广播不同。
广播是发给:
text
192.168.1.255
所有局域网设备都可能收到。
组播是发给:
text
239.1.1.1
只有加入这个组播组的设备才收。
17.1 组播例子
约定组播地址:
text
组播 IP:239.1.1.1
组播端口:5000
设备 A:
text
192.168.1.20
加入组播组 239.1.1.1:5000
设备 B:
text
192.168.1.21
加入组播组 239.1.1.1:5000
手机:
text
192.168.1.30
没有加入组播组
电脑发送组播:
text
源 IP: 192.168.1.10
源端口: 54000
目标 IP: 239.1.1.1
目标端口:5000
数据:turn on led
方向:
text
192.168.1.10:54000 ---> 239.1.1.1:5000
结果:
text
ESP32 A 收到
ESP32 B 收到
手机没加入组,收不到
18. 组播接收端具体流程
ESP32 A 想收到:
text
239.1.1.1:5000
需要做:
c
sock = socket(AF_INET, SOCK_DGRAM, 0);
bind(sock, 0.0.0.0:5000);
setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP, ...);
recvfrom(sock, ...);
注意顺序:
text
1. 创建 UDP socket
2. bind 到本地端口 5000
3. 加入组播组 239.1.1.1
4. recvfrom 等待数据
为什么要 bind 到 5000?
因为发送端发的是:
text
239.1.1.1:5000
接收端必须监听这个端口。
为什么还要加入组播组?
因为目标 IP 不是你的单播 IP,而是组播 IP:
text
239.1.1.1
系统需要知道:
text
我要接收发给 239.1.1.1 的数据
19. 组播发送端具体流程
电脑发送端:
c
sock = socket(AF_INET, SOCK_DGRAM, 0);
sendto(sock, "turn on led", ..., 239.1.1.1:5000);
它不一定要加入组播组。
发送组播和发送普通 UDP 很像,只是目标 IP 变成组播 IP:
text
普通 UDP:
192.168.1.10:54000 -> 192.168.1.20:5000
组播 UDP:
192.168.1.10:54000 -> 239.1.1.1:5000
20. 组播完整流程图
text
组播组:239.1.1.1:5000
ESP32 A:192.168.1.20,加入 239.1.1.1:5000
ESP32 B:192.168.1.21,加入 239.1.1.1:5000
手机: 192.168.1.30,没有加入
电脑: 192.168.1.10,发送者
电脑发送:
192.168.1.10:54000 -> 239.1.1.1:5000
数据:"turn on led"
结果:
ESP32 A 收到
ESP32 B 收到
手机不收
图:
text
电脑 192.168.1.10
│
│ UDP Multicast
│ 192.168.1.10:54000 -> 239.1.1.1:5000
▼
组播组 239.1.1.1:5000
├── ESP32 A 192.168.1.20 已加入 → 收到
├── ESP32 B 192.168.1.21 已加入 → 收到
└── 手机 192.168.1.30 未加入 → 不收
21. 单播、广播、组播用具体地址对比
21.1 UDP 单播
text
192.168.1.20:53001 ---> 192.168.1.10:9000
意思:
text
ESP32 发给电脑一个设备
21.2 UDP 广播
text
192.168.1.20:53001 ---> 192.168.1.255:9999
意思:
text
ESP32 发给 192.168.1.x 局域网所有设备的 9999 端口
21.3 UDP 组播
text
192.168.1.10:54000 ---> 239.1.1.1:5000
意思:
text
电脑发给加入 239.1.1.1 组播组的设备
22. 四种模式放一起看
| 模式 | 源地址 | 目标地址 | 说明 |
|---|---|---|---|
| TCP Client 连接 Server | 192.168.1.20:52341 |
192.168.1.10:8080 |
ESP32 连接电脑 TCP 服务 |
| UDP 单播 | 192.168.1.20:53001 |
192.168.1.10:9000 |
ESP32 给电脑发一包 |
| UDP 广播 | 192.168.1.20:53001 |
192.168.1.255:9999 |
ESP32 发给整个局域网 |
| UDP 组播 | 192.168.1.10:54000 |
239.1.1.1:5000 |
电脑发给组播组成员 |
23. 实际项目例子:ESP32 找服务器,然后 TCP 通信
这是非常常见的组合。
23.1 已知条件
电脑 Server:
text
IP:192.168.1.10
UDP 发现端口:9999
TCP 正式通信端口:8080
ESP32:
text
IP:192.168.1.20
23.2 第一步:ESP32 广播找服务器
ESP32 发 UDP 广播:
text
源:192.168.1.20:53001
目标:192.168.1.255:9999
数据:DISCOVER_SERVER
电脑收到后回复:
text
源:192.168.1.10:9999
目标:192.168.1.20:53001
数据:SERVER_IP=192.168.1.10,TCP_PORT=8080
23.3 第二步:ESP32 建立 TCP 连接
ESP32 拿到电脑 IP 和端口后:
text
connect 192.168.1.10:8080
实际 TCP 连接:
text
192.168.1.20:52341 <--> 192.168.1.10:8080
23.4 第三步:正式传数据
ESP32 发送传感器数据:
text
源:192.168.1.20:52341
目标:192.168.1.10:8080
数据:{"temp":26.5,"hum":60}
电脑回复:
text
源:192.168.1.10:8080
目标:192.168.1.20:52341
数据:OK
完整流程:
text
ESP32 电脑 Server
UDP 广播:
192.168.1.20:53001
-> 192.168.1.255:9999
"DISCOVER_SERVER"
收到广播
UDP 单播回复:
192.168.1.10:9999
-> 192.168.1.20:53001
"SERVER_IP=192.168.1.10,TCP_PORT=8080"
TCP 连接:
192.168.1.20:52341
-> 192.168.1.10:8080
TCP 数据:
192.168.1.20:52341
-> 192.168.1.10:8080
"{temp:26.5,hum:60}"
TCP 回复:
192.168.1.10:8080
-> 192.168.1.20:52341
"OK"
24. 实际项目例子:多个 ESP32 接收控制命令
假设你有 3 个 ESP32 灯:
text
ESP32 A:192.168.1.20
ESP32 B:192.168.1.21
ESP32 C:192.168.1.22
它们都加入组播:
text
239.1.1.1:5000
电脑控制器:
text
192.168.1.10
电脑发组播:
text
源:192.168.1.10:54000
目标:239.1.1.1:5000
数据:LED_ON
结果:
text
ESP32 A 收到 LED_ON
ESP32 B 收到 LED_ON
ESP32 C 收到 LED_ON
这比你一个个单播简单:
text
单播方式:
192.168.1.10 -> 192.168.1.20
192.168.1.10 -> 192.168.1.21
192.168.1.10 -> 192.168.1.22
组播方式:
192.168.1.10 -> 239.1.1.1
25. 很容易搞混的几个点
点 1:Server 的端口固定,Client 的端口通常随机
TCP 例子:
text
Server 固定:
192.168.1.10:8080
Client 随机:
192.168.1.20:52341
UDP 例子:
text
Server 固定:
192.168.1.10:9000
Client 随机:
192.168.1.20:53001
Client 本地端口通常由系统自动分配。
点 2:服务端回复时,目标端口是客户端的源端口
ESP32 发:
text
192.168.1.20:53001 -> 192.168.1.10:9000
电脑回复:
text
192.168.1.10:9000 -> 192.168.1.20:53001
不是回复到 ESP32 的 9000,除非 ESP32 自己也 bind 到 9000。
点 3:UDP 广播不是发给某个 IP
广播目标是:
text
192.168.1.255:9999
不是:
text
192.168.1.10:9999
所以它的意义是:
text
这个网段里谁监听 9999,谁就可能收到
点 4:UDP 组播不是广播
广播:
text
192.168.1.255:9999
所有设备都可能收到。
组播:
text
239.1.1.1:5000
只有加入 239.1.1.1 这个组的设备收到。
点 5:TCP 必须有 Server 先 listen
TCP Client 连接:
text
192.168.1.10:8080
如果电脑上没有程序监听 8080,ESP32 会连接失败。
常见错误:
text
Connection refused
Connection timeout
点 6:UDP sendto 成功不代表对方收到了
UDP 发送:
c
sendto(...)
返回成功,只代表:
text
你的数据交给本机协议栈了
不代表:
text
对方一定收到了
UDP 没有 TCP 那种确认机制。
26. 最后用一句话总结
text
TCP:
192.168.1.20:52341 连接 192.168.1.10:8080
建立可靠连接后,双方 send/recv。
UDP 单播:
192.168.1.20:53001 直接 sendto 到 192.168.1.10:9000
不建立连接,对方用 recvfrom 收。
UDP 广播:
192.168.1.20:53001 sendto 到 192.168.1.255:9999
整个 192.168.1.x 局域网里监听 9999 的设备都可能收到。
UDP 组播:
192.168.1.10:54000 sendto 到 239.1.1.1:5000
只有加入 239.1.1.1:5000 组播组的设备收到。
最实用的项目组合是:
text
UDP 广播/组播:负责发现设备
TCP:负责后续可靠通信
UDP 单播:负责低延迟小数据