UDP通信基础:UDP特点、UDP与TCP区别、UDP编程模型
说到UDP,我脑子里第一个蹦出来的词就是「快」。但快是有代价的,这个代价就是不可靠。嗯,咱们今天就把UDP这层窗户纸捅破,看看它到底是个什么脾气。
UDP到底是什么?
UDP的全称是User Datagram Protocol,用户数据报协议。说白了,它就是个「发出去就不管了」的协议。你想想看,寄快递的时候,你是选挂号信(TCP)还是平信(UDP)?挂号信每一站都要签收确认,平信扔进邮筒就完事了。
UDP就是那个平信。它把数据打包成一个数据报,扔到网络上,至于对方收没收到、收到的是不是完整的、顺序对不对——它一概不关心。
核心特点:无连接、不可靠、面向数据报、低延迟。
我在项目中遇到过好几次这样的场景:实时视频通话、在线游戏、DNS查询。这些场景里,偶尔丢一两个包问题不大,但要是因为等待重传导致卡顿,那用户体验就彻底完蛋了。
UDP与TCP的区别
很多新手问我:「老师,TCP那么可靠,为什么还要用UDP?」我的回答是:因为TCP的可靠是有代价的。
咱们直接看对比表,一目了然:
| 对比维度 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接(三次握手) | 无连接 |
| 可靠性 | 可靠传输,有确认重传 | 不可靠,尽力交付 |
| 数据边界 | 字节流,无边界 | 数据报,有边界 |
| 传输速度 | 较慢(有拥塞控制) | 快(无拥塞控制) |
| 应用场景 | 文件传输、网页浏览 | 音视频、游戏、DNS |
你仔细看最后一行。TCP适合那些「一个字节都不能错」的场景,比如你下载一个安装包,错一个字节就装不上。而UDP适合「快比准重要」的场景,比如视频通话,丢一帧画面,下一帧就补上了,用户根本感觉不到。
我曾经踩过一个坑:在一个物联网项目中,用UDP传输传感器数据,结果因为网络抖动,丢包率高达15%。后来我在应用层自己加了个简单的确认机制,才把问题解决。记住:UDP不保证可靠,但你可以自己在应用层实现可靠。
UDP编程模型
UDP的编程模型比TCP简单太多了。TCP要经历三次握手、四次挥手,还要处理各种状态机。UDP呢?直接发,直接收,完事。
我个人的习惯是,把UDP编程总结成四个步骤:
- 创建套接字——用socket()函数,指定SOCK_DGRAM
- 绑定地址——用bind()函数,服务端必须绑,客户端可选
- 发送数据——用sendto()函数,指定目标地址
- 接收数据——用recvfrom()函数,获取发送方地址
你看,没有listen(),没有accept(),没有connect()。这就是UDP的爽快之处。
下面是一个最简单的UDP服务端代码:
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
// 1. 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
return -1;
}
// 2. 绑定地址
struct sockaddr_in server_addr;
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 = htonl(INADDR_ANY);
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
close(sockfd);
return -1;
}
printf("UDP server listening on port 8888...\n");
// 3. 接收数据
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
while (1) {
memset(buffer, 0, sizeof(buffer));
int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&client_addr, &addr_len);
if (n < 0) {
perror("recvfrom");
continue;
}
buffer[n] = '\0';
printf("Received from %s:%d: %s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buffer);
// 4. 发送响应
sendto(sockfd, buffer, n, 0,
(struct sockaddr*)&client_addr, addr_len);
}
close(sockfd);
return 0;
}
这段代码我写过不下五十遍了。你注意看recvfrom的最后一个参数,它既是输入也是输出——传入地址结构体的大小,返回实际地址长度。这个细节我当年栽过跟头,没初始化addr_len,结果recvfrom一直返回-1。
客户端就更简单了:
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int main() {
// 创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
return -1;
}
// 设置服务器地址
struct sockaddr_in server_addr;
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("127.0.0.1");
// 发送数据
char *message = "Hello UDP!";
sendto(sockfd, message, strlen(message), 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
printf("Message sent: %s\n", message);
// 接收响应
char buffer[1024] = {0};
struct sockaddr_in from_addr;
socklen_t addr_len = sizeof(from_addr);
int n = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0,
(struct sockaddr*)&from_addr, &addr_len);
if (n > 0) {
buffer[n] = '\0';
printf("Response: %s\n", buffer);
}
close(sockfd);
return 0;
}
你看,客户端甚至可以不bind。系统会自动分配一个临时端口。但服务端必须bind,不然客户端怎么知道往哪发?
UDP的核心知识体系
为了让你更直观地理解UDP的整个知识脉络,我画了一张图:
避坑指南
最后,我把自己这些年踩过的UDP坑总结一下,你记好了:
- 数据报大小限制:UDP数据报最大65535字节,但实际网络MTU通常是1500字节。超过MTU的数据报会被分片,分片丢失会导致整个数据报丢失。我建议你控制在1472字节以内(1500 - 20字节IP头 - 8字节UDP头)。
- 接收缓冲区:recvfrom一次只能读取一个数据报。如果缓冲区太小,数据会被截断,剩余部分直接丢弃。所以缓冲区要开大一点,1024字节起步。
- 端口占用:UDP端口和TCP端口是独立的,但同一个端口不能被两个UDP套接字同时绑定。我遇到过好几次bind失败,都是因为上一个程序没关干净。
- 广播和组播:UDP支持广播和组播,TCP不支持。这是UDP的一大优势,但需要额外设置套接字选项。后面我会专门讲这个。
一个小技巧:调试UDP程序时,可以用nc命令快速测试。服务端:nc -ul 8888,客户端:nc -u 127.0.0.1 8888。比写代码快多了。
UDP看似简单,但用好它并不容易。它的「不可靠」既是缺点也是优点——正因为不可靠,它才能做到极致的低延迟。你想想看,实时语音通话里,你是愿意听到一个卡顿的完整句子,还是愿意听到一个流畅但偶尔丢字的话?答案不言自明。
好了,UDP的基础就聊到这儿。代码你拿去跑一跑,改一改,感受一下UDP的「快」和「野」。下次咱们再深入聊聊UDP的高级用法。
公众号:蓝海资料掘金营,微信deep3321