第四节:UDP通讯——无连接的艺术
说到UDP,很多刚入行的朋友第一反应就是“不可靠”。嗯,这话没错,但也不全对。我做了十几年网络通讯,UDP其实是个很有意思的协议——它把选择权交给你,让你自己决定“可靠性”的尺度。
核心要点:UDP是面向无连接的传输层协议。它不建立连接,不保证顺序,不重传丢包。说白了,就是“发了就不管了”。
4.1 UDP的特点——为什么它还在被广泛使用?
你想想看,TCP那么可靠,为什么还要用UDP?我在项目中遇到过很多场景,比如视频监控、游戏同步、传感器数据采集——这些场景里,实时性比可靠性更重要。你愿意等一帧画面重传,还是愿意看到画面稍微花一下但保持流畅?
| 特性 | UDP | TCP |
|---|---|---|
| 连接建立 | 无连接 | 三次握手 |
| 可靠性 | 不保证 | 保证 |
| 传输效率 | 高 | 较低 |
| 数据边界 | 保留消息边界 | 流式传输 |
| 适用场景 | 实时音视频、广播 | 文件传输、网页 |
我个人习惯把UDP比作“寄明信片”——你写好地址扔进邮筒,至于对方收不收得到,邮局不负责。但正因为没有那么多确认和重传的流程,UDP的延迟可以做到非常低。
实战经验:我曾经在一个工业控制项目中,用UDP传输传感器数据。每秒2000个数据包,每个包只有几十字节。如果用TCP,光握手和确认就能把CPU吃满。换成UDP后,CPU占用率从85%降到了12%。
4.2 UDP客户端实现——最简单的网络通讯
UDP客户端其实就三步:创建套接字、发送数据、接收数据。没有connect,没有listen,干净利落。
// UDP客户端示例(C语言)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[1024] = "Hello, UDP Server!";
// 1. 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = inet_addr("192.168.1.100");
// 3. 发送数据
sendto(sockfd, buffer, strlen(buffer), 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
// 4. 接收响应(可选)
recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL);
close(sockfd);
return 0;
}
嗯,这里要注意:sendto函数每次都要指定目标地址。为什么?因为UDP没有连接,内核不知道你要发给谁。我刚开始学的时候也犯过这个错——以为发过一次就不用再指定地址了。
4.3 UDP服务端实现——被动接收的艺术
服务端更简单:绑定端口,然后收数据。不需要accept,因为UDP没有连接的概念。
// UDP服务端示例(C语言)
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
char buffer[1024];
// 1. 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 绑定地址和端口
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = INADDR_ANY;
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
// 3. 循环接收数据
while(1) {
memset(buffer, 0, sizeof(buffer));
recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&client_addr, &addr_len);
printf("收到来自 %s:%d 的消息: %s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buffer);
}
close(sockfd);
return 0;
}
避坑指南:我曾经在一个项目中,服务端用UDP接收数据,客户端发得快了,服务端来不及处理,结果缓冲区溢出丢包。解决方案有两个:要么加大接收缓冲区(setsockopt设置SO_RCVBUF),要么在应用层做流控。我个人建议后者——缓冲区再大也有上限。
4.4 UDP广播与组播——一对多的通讯模式
这是UDP最强大的特性之一。TCP做不到广播,因为TCP是一对一的连接。而UDP天生支持一对多。
4.4.1 广播——发给所有人
广播地址通常是xxx.xxx.xxx.255,或者255.255.255.255。你想想看,在一个局域网里,你发一个广播包,所有设备都能收到。这在设备发现、服务宣告等场景非常有用。
// 广播发送示例
int broadcast = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));
// 目标地址设为广播地址
server_addr.sin_addr.s_addr = inet_addr("192.168.1.255");
server_addr.sin_port = htons(9999);
sendto(sockfd, buffer, len, 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
实战经验:我在做智能家居项目时,用UDP广播做设备发现。新设备上电后,发一个广播包“我是谁,我在哪”,网关收到后回复配置信息。整个过程不到100毫秒,用户体验非常好。
4.4.2 组播——发给特定群体
广播有个问题:它会打扰到所有设备。组播就优雅多了——只有加入特定组播组的设备才会收到数据。组播地址范围是224.0.0.0到239.255.255.255。
// 加入组播组
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("224.0.0.1");
mreq.imr_interface.s_addr = INADDR_ANY;
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
// 发送组播数据
server_addr.sin_addr.s_addr = inet_addr("224.0.0.1");
sendto(sockfd, buffer, len, 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
我记得有一次做视频会议系统,用组播实现了多路视频分发。每个参会者加入一个组播组,摄像头数据通过组播发送,接收端只解码自己需要的流。相比TCP的点对点转发,网络带宽节省了70%以上。
总结一下:UDP虽然“不可靠”,但它的低延迟、无连接、支持广播组播的特性,让它在实时通讯领域无可替代。记住一句话:用UDP,你要自己负责可靠性——比如在应用层加序列号、超时重传、去重等机制。我见过太多人直接拿UDP当TCP用,结果丢包了还一脸懵。