一、UDP协议的特点
UDP,全称是用户数据报协议。说白了,它就是个"只管发,不管到"的传输协议。
我在项目中用过不少UDP,最深的感受就是——它简单到让人有点不放心。但恰恰是这种简单,让它在某些场景下比TCP更香。
UDP的核心特点
- 无连接:发数据前不需要握手,直接扔出去就行
- 不可靠:数据丢了?乱序了?重复了?UDP不管
- 无状态:不维护连接状态,不记录收发进度
- 面向报文:应用层给多少数据,UDP就发多少,不拆分也不合并
- 支持广播和组播:这是TCP做不到的
重要:UDP头部只有8个字节,而TCP头部至少20个字节。开销小,延迟低,这是UDP最大的优势。
二、UDP Socket通信流程
UDP的通信流程,比TCP简单太多了。你想想看,TCP要三次握手、四次挥手,UDP呢?直接发就完了。
服务端流程
- 创建socket:
socket(AF_INET, SOCK_DGRAM, 0) - 绑定地址:
bind() - 接收数据:
recvfrom() - 发送数据:
sendto() - 关闭socket:
close()
客户端流程
- 创建socket:
socket(AF_INET, SOCK_DGRAM, 0) - 发送数据:
sendto() - 接收数据:
recvfrom() - 关闭socket:
close()
个人经验:客户端其实可以不调用bind(),系统会自动分配一个临时端口。但我建议你还是显式bind一下,方便调试时抓包定位。
三、UDP服务器的实现
写一个UDP服务器,核心就是recvfrom和sendto这两个函数。我刚开始用的时候,老是把它们的参数顺序搞混,后来干脆写了个小工具函数封装了一下。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
char buffer[BUFFER_SIZE];
// 1. 创建socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
// 2. 绑定地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind");
close(sockfd);
exit(1);
}
printf("UDP server listening on port %d...\n", PORT);
// 3. 循环接收数据
while (1) {
memset(buffer, 0, BUFFER_SIZE);
int n = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr*)&client_addr, &addr_len);
if (n < 0) {
perror("recvfrom");
continue;
}
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,传入时必须是sizeof(client_addr),传出时才是实际地址长度。我曾经在这里踩过坑,传了个0进去,结果recvfrom一直返回-1。
四、UDP客户端的实现
客户端更简单,连bind都不用。直接sendto就完事了。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define PORT 8888
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr;
socklen_t addr_len = sizeof(server_addr);
char buffer[BUFFER_SIZE];
// 1. 创建socket
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
// 2. 设置服务器地址
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
server_addr.sin_port = htons(PORT);
// 3. 发送数据
while (1) {
printf("Enter message: ");
fgets(buffer, BUFFER_SIZE, stdin);
buffer[strcspn(buffer, "\n")] = 0;
sendto(sockfd, buffer, strlen(buffer), 0,
(struct sockaddr*)&server_addr, addr_len);
// 4. 接收回显
memset(buffer, 0, BUFFER_SIZE);
int n = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr*)&server_addr, &addr_len);
if (n > 0) {
printf("Server echo: %s\n", buffer);
}
}
close(sockfd);
return 0;
}
小技巧:客户端也可以调用connect(),虽然UDP是无连接的,但connect之后可以用send()和recv()代替sendto()和recvfrom(),代码会简洁一些。不过要注意,connect之后就只能和这一个地址通信了。
五、UDP的广播与组播
这是UDP独有的能力。TCP是点对点的,UDP可以一对多。
广播
广播地址通常是xxx.xxx.xxx.255,或者255.255.255.255。发送广播前,需要设置socket选项:
int broadcast = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));
然后往广播地址发数据,同一网段的所有机器都能收到。
组播
组播比广播更灵活。它用D类地址(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 = htonl(INADDR_ANY);
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
经验之谈:我在做视频监控项目时,用组播把摄像头画面分发给多个客户端。如果当时用TCP,每个客户端都要单独建连接,服务器压力会大很多。组播一发,所有客户端都能收到,省事多了。
六、UDP的丢包与乱序
UDP不保证可靠传输,所以丢包和乱序是家常便饭。为什么会这样?因为UDP没有确认机制,也没有序号机制。
丢包的原因
- 网络拥塞,路由器丢包
- 接收缓冲区满了
- 数据包损坏被丢弃
应对策略
如果应用层需要可靠性,就得自己实现。我常用的方法:
- 超时重传:发送后启动定时器,没收到确认就重发
- 序号机制:每个包加个序号,接收方检测乱序和重复
- 应用层ACK:自己实现确认包
避坑指南:我曾经在一个项目中,直接用UDP传文件,结果大文件传输时丢包率高达30%。后来加了简单的超时重传机制,才把丢包率降到可接受范围。记住,UDP不保证可靠,应用层必须自己处理。
七、UDP与TCP的选择
| 场景 | 推荐协议 | 原因 |
|---|---|---|
| 文件传输、网页浏览 | TCP | 需要可靠传输 |
| 视频直播、语音通话 | UDP | 容忍丢包,要求低延迟 |
| DNS查询 | UDP | 一次请求一次响应,简单高效 |
| 物联网传感器数据 | UDP | 数据量小,连接开销不划算 |
| 远程控制 | TCP | 命令必须可靠到达 |
说白了,选TCP还是UDP,就看你能不能接受丢包。能接受丢包,就用UDP,快;不能接受,就用TCP,稳。
八、UDP Socket综合案例
最后,我写一个简单的聊天程序,展示UDP的实际应用。这个程序既可以是服务端,也可以是客户端,启动时通过参数指定模式。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 9999
#define BUFFER_SIZE 1024
void *receive_thread(void *arg) {
int sockfd = *(int*)arg;
struct sockaddr_in from_addr;
socklen_t addr_len = sizeof(from_addr);
char buffer[BUFFER_SIZE];
while (1) {
memset(buffer, 0, BUFFER_SIZE);
int n = recvfrom(sockfd, buffer, BUFFER_SIZE - 1, 0,
(struct sockaddr*)&from_addr, &addr_len);
if (n > 0) {
printf("\n[%s:%d] %s\n> ",
inet_ntoa(from_addr.sin_addr),
ntohs(from_addr.sin_port),
buffer);
fflush(stdout);
}
}
return NULL;
}
int main(int argc, char *argv[]) {
int sockfd;
struct sockaddr_in addr;
pthread_t tid;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(PORT);
if (bind(sockfd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
perror("bind");
close(sockfd);
exit(1);
}
printf("Chat started on port %d\n", PORT);
printf("Enter messages (format: IP:message)\n");
// 启动接收线程
pthread_create(&tid, NULL, receive_thread, &sockfd);
// 主线程负责发送
char input[BUFFER_SIZE];
while (1) {
printf("> ");
fflush(stdout);
fgets(input, BUFFER_SIZE, stdin);
input[strcspn(input, "\n")] = 0;
// 解析目标IP和消息
char *ip = strtok(input, ":");
char *msg = strtok(NULL, "");
if (ip && msg) {
struct sockaddr_in dest;
dest.sin_family = AF_INET;
dest.sin_addr.s_addr = inet_addr(ip);
dest.sin_port = htons(PORT);
sendto(sockfd, msg, strlen(msg), 0,
(struct sockaddr*)&dest, sizeof(dest));
}
}
close(sockfd);
return 0;
}
使用说明:启动两个终端,都运行这个程序。在其中一个输入"127.0.0.1:你好",另一个就能收到消息。这就是一个最简单的UDP聊天室。
UDP虽然简单,但用好了威力很大。我做了这么多年嵌入式,UDP在音视频传输、传感器数据采集、游戏通信这些场景下,几乎是首选。记住它的特点,选对场景,UDP就是一把利器。