UDP Socket 编程笔记
一、UDP 基础知识
1. UDP 特点
-
无连接:无需建立连接即可通信
-
不可靠:不保证数据到达、不保证顺序
-
面向数据报:有明确的报文边界
-
高效:开销小,速度快
2. TCP vs UDP
| 特性 | TCP | UDP |
|---|---|---|
| 连接方式 | 面向连接 | 无连接 |
| 可靠性 | 可靠传输 | 不可靠 |
| 数据边界 | 流式(无边界) | 数据报(有边界) |
| 速度 | 较慢 | 较快 |
| 头部大小 | 20-60字节 | 8字节 |
3. 核心数据结构(同TCP)
struct sockaddr_in {
short sin_family; // AF_INET
unsigned short sin_port; // 端口号
struct in_addr sin_addr; // IP地址
char sin_zero[8]; // 填充
};
总结:UDP编程相对TCP更简单,但需要应用层处理可靠性问题。适合实时性要求高、可容忍少量丢失的场景(如视频流、DNS查询、在线游戏等)。

二、UDP 编程核心函数
1. 发送数据
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
-
dest_addr: 目标地址结构体 -
addrlen: 地址结构体长度 -
返回:实际发送的字节数,-1表示错误
2. 接收数据
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
-
src_addr: 用于保存发送方地址 -
addrlen: 输入输出参数,需要初始化 -
返回:实际接收的字节数,-1表示错误,0表示对方关闭连接
三、示例程序分析
示例1:简单回显服务器(带时间戳)
服务器端 (server.c)
cpp
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
typedef struct sockaddr *(SA);
int main(int argc, char **argv)
{
// 1. 创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 绑定地址
struct sockaddr_in ser, cli;
bzero(&ser, sizeof(ser));
bzero(&cli, sizeof(cli));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = inet_addr("192.168.14.128");
bind(sockfd, (SA)&ser, sizeof(ser));
// 3. 循环处理客户端请求
socklen_t len = sizeof(cli);
while (1) {
char buf[512] = {0};
// 接收数据(获取客户端地址)
recvfrom(sockfd, buf, sizeof(buf), 0, (SA)&cli, &len);
printf("recv:%s\n", buf);
// 添加时间戳
time_t tm;
time(&tm);
struct tm *info = localtime(&tm);
sprintf(buf, "%s %d:%d:%d", buf, info->tm_hour,
info->tm_min, info->tm_sec);
// 发送回客户端
sendto(sockfd, buf, strlen(buf) + 1, 0, (SA)&cli, len);
}
close(sockfd);
return 0;
}
客户端 (cli.c)
cpp
#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
typedef struct sockaddr *(SA);
int main(int argc, char **argv)
{
// 1. 创建UDP套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
// 2. 设置服务器地址
struct sockaddr_in ser;
bzero(&ser, sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_port = htons(50000);
ser.sin_addr.s_addr = inet_addr("192.168.14.128");
// 3. 发送10次数据
int i = 10;
while (i--) {
char buf[512] = "hello";
// 发送数据
sendto(sockfd, buf, strlen(buf), 0, (SA)&ser, sizeof(ser));
// 接收响应
bzero(buf, sizeof(buf));
recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
printf("recv:%s\n", buf);
sleep(1);
}
close(sockfd);
return 0;
}
示例2:UDP聊天程序(父子进程)
服务器端 (server.c - 聊天版本)
// 创建UDP套接字并绑定
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in ser, cli;
// ... 绑定代码 ...
// 首次接收获取客户端地址
char buf[512] = {0};
recvfrom(sockfd, buf, sizeof(buf), 0, (SA)&cli, &len);
// 创建父子进程
pid_t pid = fork();
if (pid > 0) { // 父进程:发送消息
while (1) {
bzero(buf, sizeof(buf));
printf("to B:");
fgets(buf, sizeof(buf), stdin);
sendto(sockfd, buf, strlen(buf) + 1, 0, (SA)&cli, sizeof(cli));
if (0 == strcmp(buf, "#quit\n")) {
kill(pid, 9);
exit(0);
}
}
} else if (0 == pid) { // 子进程:接收消息
while (1) {
bzero(buf, sizeof(buf));
recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
if (0 == strcmp(buf, "#quit\n")) {
kill(getppid(), 9);
exit(0);
}
printf("from B:%s", buf);
fflush(stdout);
}
}
客户端 (cli.c - 聊天版本)
// 代码结构与服务器端类似
// 父子进程分别处理发送和接收
实现原理:
-
父子进程分离了输入和输出
-
父进程负责读取用户输入并发送
-
子进程负责接收并显示消息
-
使用
#quit作为退出指令
示例3:UDP文件传输
服务器端 (文件接收)
int fd = open("2.png", O_WRONLY | O_CREAT | O_TRUNC, 0666);
while (1) {
char buf[1024] = {0};
int rd_ret = recvfrom(sockfd, buf, sizeof(buf), 0, (SA)&cli, &len);
if (rd_ret <= 0) {
break;
}
write(fd, buf, rd_ret);
// 发送确认
bzero(buf, sizeof(buf));
strcpy(buf, "go on");
sendto(sockfd, buf, strlen(buf), 0, (SA)&cli, len);
}
客户端 (文件发送)
int fd = open("/home/linux/1.png", O_RDONLY);
char buf[1024] = {0};
while (1) {
bzero(buf, sizeof(buf));
int rd_ret = read(fd, buf, sizeof(buf));
if (rd_ret <= 0) {
break;
}
// 发送文件数据
sendto(sockfd, buf, rd_ret, 0, (SA)&ser, sizeof(ser));
// 等待确认
bzero(buf, sizeof(buf));
recvfrom(sockfd, buf, sizeof(buf), 0, NULL, NULL);
}
// 发送结束标志
sendto(sockfd, buf, 0, 0, (SA)&ser, sizeof(ser));
特点:
-
使用固定大小缓冲区传输
-
简单的"go on"确认机制
-
发送0长度数据表示传输结束
四、UDP编程注意事项
1. 地址绑定
// 监听所有接口
ser.sin_addr.s_addr = INADDR_ANY;
// 监听特定IP
ser.sin_addr.s_addr = inet_addr("192.168.1.100");
// 仅本地通信(回环地址)
ser.sin_addr.s_addr = inet_addr("127.0.0.1");
2. 数据包大小限制
-
UDP数据包最大长度:65535字节
-
实际受MTU限制(通常1500字节)
-
建议应用层分片传输大文件
3. 可靠性问题解决方案
// 1. 超时重传
struct timeval tv;
tv.tv_sec = 5; // 5秒超时
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
// 2. 序列号机制
typedef struct {
uint32_t seq; // 序列号
uint32_t total; // 总包数
char data[1400]; // 数据
} udp_packet_t;
4. 并发处理
UDP本身是无连接的,可以通过以下方式处理多个客户端:
-
记录每个客户端的地址
-
使用多线程/多进程
-
使用
select()/poll()/epoll()多路复用
五、完整编程模板
服务器模板
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define PORT 50000
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr, client_addr;
socklen_t client_len;
char buffer[BUFFER_SIZE];
// 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(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
// 3. 绑定地址
bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
printf("UDP Server listening on port %d\n", PORT);
while (1) {
// 4. 接收数据
client_len = sizeof(client_addr);
int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr*)&client_addr, &client_len);
// 5. 处理数据
buffer[n] = '\0';
printf("Received from %s:%d - %s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buffer);
// 6. 发送响应
sendto(sockfd, buffer, n, 0,
(struct sockaddr*)&client_addr, client_len);
}
close(sockfd);
return 0;
}
客户端模板
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define SERVER_IP "127.0.0.1"
#define PORT 50000
#define BUFFER_SIZE 1024
int main() {
int sockfd;
struct sockaddr_in server_addr;
char buffer[BUFFER_SIZE];
// 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(PORT);
server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);
while (1) {
printf("Enter message: ");
fgets(buffer, BUFFER_SIZE, stdin);
// 3. 发送数据
sendto(sockfd, buffer, strlen(buffer), 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
// 4. 接收响应
int n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0, NULL, NULL);
buffer[n] = '\0';
printf("Server response: %s\n", buffer);
}
close(sockfd);
return 0;
}
六、常见错误及解决方法
1. 地址已在使用
# 错误:bind: Address already in use
# 解决:
netstat -anp | grep 50000 # 查看占用进程
kill -9 <PID> # 结束进程
# 或使用 SO_REUSEADDR
int opt = 1;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
2. 数据包丢失
-
实现应用层的确认重传机制
-
增加超时设置
-
使用更小的数据包大小
3. 端口选择问题
-
避免使用知名端口(0-1023)
-
确保客户端和服务器使用相同端口
七、进阶主题
1. 广播通信
// 设置广播权限
int broadcast = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));
// 广播地址
ser.sin_addr.s_addr = inet_addr("255.255.255.255");
2. 组播通信
// 加入组播组
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));
3. 非阻塞UDP
// 设置非阻塞模式
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);