1. UDP协议与C/S模型基础解析
在Linux系统编程中,UDP(User Datagram Protocol)作为一种轻量级的传输层协议,因其简洁高效的特点,被广泛应用于实时性要求高但允许少量丢包的场景。与TCP协议相比,UDP不需要建立连接,也没有复杂的流量控制和重传机制,这使得它在某些特定场景下具有独特的优势。
1.1 UDP协议核心特性
UDP协议的设计哲学可以概括为"尽最大努力交付",其核心特性体现在三个维度:
-
无连接(Connectionless):通信双方不需要事先建立连接即可传输数据。每个数据包都是独立的,就像寄信不需要提前通知收件人一样。这种特性带来的直接好处是减少了握手和挥手的开销,但也意味着无法保证数据包的顺序和可靠性。
-
不可靠(Unreliable):UDP不提供数据确认、重传和排序机制。数据包可能丢失、重复或乱序到达。这种看似"缺点"的特性在某些场景下反而是优势,比如视频会议中,偶尔丢失几个数据包对整体体验影响不大,但等待重传会导致明显的卡顿。
-
数据报(Datagram):UDP以完整的数据包为单位进行传输,不会像TCP那样对数据进行拆分和重组。每个sendto调用对应一个独立的数据包,接收方通过recvfrom获取完整的数据单元。
提示:虽然UDP本身不可靠,但应用层可以通过添加序列号、确认机制等实现可靠传输。许多实时通信协议(如QUIC)就是在UDP基础上构建的可靠传输方案。
1.2 UDP适用场景分析
基于上述特性,UDP特别适合以下几类应用:
- 实时性要求高的应用:在线游戏、视频会议、VoIP等,这些场景中延迟比完整性更重要
- 广播/多播应用:如DHCP服务发现、视频直播等,一个发送方对应多个接收方
- 简单查询/响应应用:DNS查询、NTP时间同步等,通常只需要一次往返通信
- 传感器数据采集:物联网设备定期上报数据,即使偶尔丢失也不影响整体趋势
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. UDP编程模型详解
UDP采用客户端/服务器(C/S)模型,但与传统TCP的C/S模型有显著差异。由于不需要连接,UDP的客户端和服务端角色划分更加灵活,任何一端都可以主动发送数据。
2.1 客户端标准流程
UDP客户端的典型工作流程可以分为三个关键步骤:
2.1.1 创建套接字(socket)
c复制int fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
这里的关键参数解析:
AF_INET:表示使用IPv4地址族SOCK_DGRAM:指定数据报类型(UDP)0:自动选择协议(对于SOCK_DGRAM,默认为UDP)
2.1.2 绑定地址(bind,可选)
客户端通常不需要显式绑定地址,系统会自动分配临时端口。但在某些场景下可能需要固定客户端端口:
c复制struct sockaddr_in cliaddr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_ANY), // 任意本地接口
.sin_port = htons(12345) // 指定客户端端口
};
if (bind(fd, (struct sockaddr *)&cliaddr, sizeof(cliaddr)) < 0) {
perror("bind failed");
close(fd);
exit(EXIT_FAILURE);
}
2.1.3 发送数据(sendto)
c复制struct sockaddr_in servaddr = {
.sin_family = AF_INET,
.sin_port = htons(50000),
.sin_addr.s_addr = inet_addr("192.168.1.100")
};
char *message = "Hello UDP Server";
if (sendto(fd, message, strlen(message), 0,
(struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("sendto failed");
}
sendto函数的关键点:
- 每次调用都需要指定目标地址
- 可以随时改变目标地址,实现与不同服务器的通信
- 发送的数据长度不应超过底层网络MTU(通常1500字节减去IP和UDP头)
2.2 服务端标准流程
UDP服务端的流程与客户端类似,但通常需要绑定固定端口:
2.2.1 创建套接字
与客户端相同,使用socket函数创建SOCK_DGRAM类型的套接字。
2.2.2 绑定地址(bind)
服务端必须绑定到特定端口,以便客户端知道向哪里发送数据:
c复制struct sockaddr_in servaddr = {
.sin_family = AF_INET,
.sin_addr.s_addr = htonl(INADDR_ANY), // 监听所有接口
.sin_port = htons(50000) // 服务端口
};
if (bind(fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(fd);
exit(EXIT_FAILURE);
}
2.2.3 接收数据(recvfrom)
c复制struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
char buffer[1024];
ssize_t n = recvfrom(fd, buffer, sizeof(buffer), 0,
(struct sockaddr *)&cliaddr, &len);
if (n < 0) {
perror("recvfrom failed");
} else {
buffer[n] = '\0'; // 确保字符串终止
printf("Received %zd bytes from %s:%d\n", n,
inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
printf("Message: %s\n", buffer);
}
recvfrom的关键特性:
- 阻塞调用,直到收到数据或出错
- 通过src_addr参数返回发送方的地址信息
- addrlen参数必须初始化为src_addr结构的大小
3. 核心函数深度解析
UDP编程的核心在于sendto和recvfrom这两个系统调用,它们承担了所有数据传输工作。
3.1 sendto函数详解
c复制ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
3.1.1 参数解析
- sockfd:通过socket创建的UDP套接字描述符
- buf:待发送数据的缓冲区指针
- len:要发送的数据长度(字节数)
- flags:控制发送行为的标志位,常用值:
0:默认阻塞发送MSG_DONTWAIT:非阻塞发送MSG_CONFIRM:告诉链路层有数据要发送(用于ARP缓存)
- dest_addr:目标地址结构体指针
- addrlen:目标地址结构体长度
3.1.2 返回值处理
- 成功:返回实际发送的字节数(通常等于len)
- 失败:返回-1,并设置errno
EAGAIN/EWOULDBLOCK:非阻塞模式下发送缓冲区满ECONNREFUSED:目标端口没有监听服务(ICMP错误)EMSGSIZE:消息大小超过路径MTU
注意:UDP的sendto成功仅表示数据已交给网络栈,不保证对方收到。即使目标不可达,也可能返回成功。
3.2 recvfrom函数详解
c复制ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
3.2.1 参数解析
- sockfd:UDP套接字描述符
- buf:接收数据的缓冲区
- len:缓冲区最大容量
- flags:控制接收行为的标志位,常用值:
0:默认阻塞接收MSG_DONTWAIT:非阻塞接收MSG_PEEK:查看数据但不从缓冲区移除
- src_addr:用于存储发送方地址的结构体指针
- addrlen:值-结果参数,调用前初始化为src_addr的大小
3.2.2 返回值处理
- 成功:返回接收到的字节数(可能小于len)
- 失败:返回-1,并设置errno
EAGAIN/EWOULDBLOCK:非阻塞模式下无数据可读ECONNREFUSED:收到ICMP端口不可达错误EINTR:被信号中断
3.3 地址结构处理技巧
UDP编程中需要频繁处理sockaddr_in结构体,以下是一些实用技巧:
-
地址初始化:使用bzero或memset清空结构体,避免垃圾值
c复制struct sockaddr_in addr; bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; -
端口转换:使用htons/ntohs处理网络字节序
c复制addr.sin_port = htons(50000); // 主机序转网络序 uint16_t port = ntohs(addr.sin_port); // 网络序转主机序 -
地址转换:inet_addr/inet_ntoa处理点分十进制IP
c复制addr.sin_addr.s_addr = inet_addr("192.168.1.1"); char *ip_str = inet_ntoa(addr.sin_addr); -
通用地址结构:处理不同地址族时可以使用sockaddr_storage
c复制struct sockaddr_storage addr; socklen_t len = sizeof(addr); recvfrom(fd, buf, sizeof(buf), 0, (struct sockaddr *)&addr, &len);
4. 实战案例解析
4.1 基础版UDP通信实现
这个实现展示了最基本的UDP客户端-服务端交互模式,适合理解UDP通信的基本原理。
4.1.1 服务端实现关键点
c复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define BUFFER_SIZE 1024
#define PORT 50000
int main() {
int sockfd;
struct sockaddr_in servaddr, cliaddr;
char buffer[BUFFER_SIZE];
// 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 配置服务器地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
// 绑定套接字
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Server started on port %d\n", PORT);
socklen_t len;
ssize_t n;
while (1) {
len = sizeof(cliaddr);
n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&cliaddr, &len);
if (n < 0) {
perror("recvfrom failed");
continue;
}
buffer[n] = '\0';
printf("Received from %s:%d: %s\n",
inet_ntoa(cliaddr.sin_addr),
ntohs(cliaddr.sin_port),
buffer);
// 回显给客户端
sendto(sockfd, buffer, n, 0,
(const struct sockaddr *)&cliaddr, len);
}
close(sockfd);
return 0;
}
4.1.2 客户端实现关键点
c复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define BUFFER_SIZE 1024
#define PORT 50000
int main() {
int sockfd;
struct sockaddr_in servaddr;
char buffer[BUFFER_SIZE];
// 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 配置服务器地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
printf("UDP Client ready. Type messages to send...\n");
while (1) {
printf("Client> ");
fgets(buffer, BUFFER_SIZE, stdin);
// 移除换行符
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n') {
buffer[len-1] = '\0';
}
// 发送消息到服务器
sendto(sockfd, buffer, strlen(buffer), 0,
(const struct sockaddr *)&servaddr, sizeof(servaddr));
// 接收服务器响应
socklen_t serv_len = sizeof(servaddr);
ssize_t n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&servaddr, &serv_len);
if (n < 0) {
perror("recvfrom failed");
continue;
}
buffer[n] = '\0';
printf("Server response: %s\n", buffer);
}
close(sockfd);
return 0;
}
4.1.3 基础版实现注意事项
- 缓冲区管理:UDP不会粘包,但单个数据包不应超过MTU(通常1472字节)
- 字符串处理:recvfrom不保证字符串终止,需要手动添加'\0'
- 错误处理:所有系统调用都应检查返回值
- 地址重用:服务器重启时可能遇到"Address already in use"错误,可通过setsockopt设置SO_REUSEADDR解决
4.2 进阶版UDP通信实现
这个版本引入多线程,实现收发分离,更适合实际应用场景。
4.2.1 多线程服务端实现
c复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>
#define BUFFER_SIZE 1024
#define PORT 50000
int sockfd;
struct sockaddr_in client_addr;
void *send_thread(void *arg) {
char buffer[BUFFER_SIZE];
socklen_t len = sizeof(client_addr);
while (1) {
printf("Server> ");
fgets(buffer, BUFFER_SIZE, stdin);
size_t msg_len = strlen(buffer);
if (msg_len > 0 && buffer[msg_len-1] == '\n') {
buffer[msg_len-1] = '\0';
}
if (sendto(sockfd, buffer, strlen(buffer), 0,
(struct sockaddr *)&client_addr, len) < 0) {
perror("sendto failed");
}
if (strcmp(buffer, "quit") == 0) {
break;
}
}
pthread_exit(NULL);
}
void *recv_thread(void *arg) {
char buffer[BUFFER_SIZE];
socklen_t len = sizeof(client_addr);
while (1) {
ssize_t n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&client_addr, &len);
if (n < 0) {
perror("recvfrom failed");
continue;
}
buffer[n] = '\0';
printf("\nClient (%s:%d): %s\nServer> ",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buffer);
if (strcmp(buffer, "quit") == 0) {
break;
}
}
pthread_exit(NULL);
}
int main() {
pthread_t tid[2];
struct sockaddr_in servaddr;
// 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 配置服务器地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = INADDR_ANY;
servaddr.sin_port = htons(PORT);
// 绑定套接字
if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Multi-threaded UDP Server started on port %d\n", PORT);
// 首先接收一个客户端消息以获取客户端地址
socklen_t cli_len = sizeof(client_addr);
char init_buffer[BUFFER_SIZE];
ssize_t n = recvfrom(sockfd, init_buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&client_addr, &cli_len);
if (n < 0) {
perror("initial recvfrom failed");
close(sockfd);
exit(EXIT_FAILURE);
}
printf("Client connected from %s:%d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port));
// 创建发送和接收线程
if (pthread_create(&tid[0], NULL, recv_thread, NULL) != 0 ||
pthread_create(&tid[1], NULL, send_thread, NULL) != 0) {
perror("pthread_create failed");
close(sockfd);
exit(EXIT_FAILURE);
}
// 等待线程结束
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
close(sockfd);
return 0;
}
4.2.2 多线程客户端实现
c复制#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <pthread.h>
#define BUFFER_SIZE 1024
#define PORT 50000
int sockfd;
struct sockaddr_in servaddr;
void *send_thread(void *arg) {
char buffer[BUFFER_SIZE];
while (1) {
printf("Client> ");
fgets(buffer, BUFFER_SIZE, stdin);
size_t msg_len = strlen(buffer);
if (msg_len > 0 && buffer[msg_len-1] == '\n') {
buffer[msg_len-1] = '\0';
}
if (sendto(sockfd, buffer, strlen(buffer), 0,
(struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("sendto failed");
}
if (strcmp(buffer, "quit") == 0) {
break;
}
}
pthread_exit(NULL);
}
void *recv_thread(void *arg) {
char buffer[BUFFER_SIZE];
struct sockaddr_in from_addr;
socklen_t len = sizeof(from_addr);
while (1) {
ssize_t n = recvfrom(sockfd, buffer, BUFFER_SIZE, 0,
(struct sockaddr *)&from_addr, &len);
if (n < 0) {
perror("recvfrom failed");
continue;
}
buffer[n] = '\0';
printf("\nServer (%s:%d): %s\nClient> ",
inet_ntoa(from_addr.sin_addr),
ntohs(from_addr.sin_port),
buffer);
if (strcmp(buffer, "quit") == 0) {
break;
}
}
pthread_exit(NULL);
}
int main() {
pthread_t tid[2];
// 创建UDP套接字
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
// 配置服务器地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(PORT);
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
printf("Multi-threaded UDP Client connected to %s:%d\n",
inet_ntoa(servaddr.sin_addr),
ntohs(servaddr.sin_port));
// 创建发送和接收线程
if (pthread_create(&tid[0], NULL, recv_thread, NULL) != 0 ||
pthread_create(&tid[1], NULL, send_thread, NULL) != 0) {
perror("pthread_create failed");
close(sockfd);
exit(EXIT_FAILURE);
}
// 等待线程结束
pthread_join(tid[0], NULL);
pthread_join(tid[1], NULL);
close(sockfd);
return 0;
}
4.2.3 进阶版实现注意事项
- 线程安全:UDP套接字本身是线程安全的,多个线程可以同时调用sendto/recvfrom
- 资源清理:确保线程正确退出,避免僵尸线程
- 连接模拟:UDP无连接,但可以通过首次通信记录对端地址模拟连接
- 退出协调:使用quit命令协调两个线程的退出
- 输入输出:多线程下注意避免printf竞争导致输出混乱
5. 高级主题与性能优化
5.1 UDP缓冲区大小调整
UDP套接字有两个关键缓冲区参数影响性能:
c复制// 获取接收缓冲区大小
int recv_buf_size;
socklen_t optlen = sizeof(recv_buf_size);
getsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recv_buf_size, &optlen);
// 设置接收缓冲区大小(建议值:1MB)
int new_size = 1024 * 1024;
setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size));
// 同样可以设置发送缓冲区SO_SNDBUF
注意:实际缓冲区大小可能受系统限制,设置后应再次getsockopt确认
5.2 超时处理
UDP操作可以设置超时,避免无限阻塞:
c复制struct timeval tv;
tv.tv_sec = 5; // 5秒超时
tv.tv_usec = 0;
setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
5.3 错误检测与ICMP处理
UDP可以通过以下方式获取ICMP错误(如端口不可达):
c复制// 开启ICMP错误接收
int on = 1;
setsockopt(sockfd, SOL_IP, IP_RECVERR, &on, sizeof(on));
// 接收错误信息
struct sock_extended_err *serr;
struct sockaddr_in *sin;
struct msghdr msg = {0};
struct iovec iov;
char buf[1024], cbuf[1024];
iov.iov_base = buf;
iov.iov_len = sizeof(buf);
msg.msg_name = &sin;
msg.msg_namelen = sizeof(sin);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
msg.msg_control = cbuf;
msg.msg_controllen = sizeof(cbuf);
ssize_t n = recvmsg(sockfd, &msg, MSG_ERRQUEUE);
if (n < 0 && errno == EAGAIN) {
// 无错误
} else if (n < 0) {
perror("recvmsg error");
} else {
// 处理ICMP错误
struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
if (cm->cmsg_level == SOL_IP && cm->cmsg_type == IP_RECVERR) {
serr = (void *)CMSG_DATA(cm);
printf("ICMP error: code=%d, type=%d\n", serr->ee_errno, serr->ee_origin);
}
}
5.4 多播与广播
UDP支持一对多通信模式:
5.4.1 广播实现
c复制// 设置广播权限
int broadcast = 1;
setsockopt(sockfd, SOL_SOCKET, SO_BROADCAST, &broadcast, sizeof(broadcast));
// 发送广播消息
struct sockaddr_in bcast_addr = {
.sin_family = AF_INET,
.sin_port = htons(50000),
.sin_addr.s_addr = inet_addr("255.255.255.255")
};
sendto(sockfd, "Hello Broadcast", 15, 0,
(struct sockaddr *)&bcast_addr, sizeof(bcast_addr));
5.4.2 多播实现
c复制// 加入多播组
struct ip_mreq mreq;
mreq.imr_multiaddr.s_addr = inet_addr("239.255.255.250");
mreq.imr_interface.s_addr = htonl(INADDR_ANY);
setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq));
// 发送多播消息
struct sockaddr_in mcast_addr = {
.sin_family = AF_INET,
.sin_port = htons(50000),
.sin_addr.s_addr = inet_addr("239.255.255.250")
};
sendto(sockfd, "Hello Multicast", 15, 0,
(struct sockaddr *)&mcast_addr, sizeof(mcast_addr));
6. 常见问题与调试技巧
6.1 UDP数据包丢失分析
UDP数据包可能丢失的常见原因及解决方案:
-
接收缓冲区满:
- 现象:大量数据包丢失,但网络通畅
- 解决方案:增大SO_RCVBUF,提高接收处理速度
-
发送速率过快:
- 现象:小流量正常,大流量时丢包
- 解决方案:实现应用层流量控制,添加速率限制
-
ICMP错误未处理:
- 现象:特定对端通信失败
- 解决方案:启用IP_RECVERR获取错误信息
-
MTU限制:
- 现象:大包丢失,小包正常
- 解决方案:分片发送或设置更小的包大小
6.2 典型错误排查表
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| bind: Address already in use | 端口被占用或TIME_WAIT状态 | 设置SO_REUSEADDR选项 |
| sendto: Message too long | 数据超过MTU限制 | 减小数据包大小或启用分片 |
| recvfrom: Resource temporarily unavailable | 非阻塞模式无数据可读 | 检查SO_RCVTIMEO设置或使用select/poll |
| sendto: No route to host | 网络不可达 | 检查路由表和网络连接 |
| recvfrom返回0 | 对端发送了空包 | 正常现象,按业务逻辑处理 |
6.3 网络诊断工具
-
tcpdump:抓取原始UDP数据包
bash复制
tcpdump -i any udp port 50000 -vv -X -
netstat:查看UDP套接字状态
bash复制
netstat -anu -
ss:更现代的套接字统计工具
bash复制
ss -ulnp -
ping:测试基本连通性
bash复制
ping -c 4 目标IP -
traceroute:跟踪路由路径
bash复制
traceroute -U -p 50000 目标IP
6.4 性能优化建议
- 批量处理:在应用层合并小包,减少系统调用次数
- 零拷贝:对于高性能场景,考虑使用sendmsg/recvmsg与分散/聚集I/O
- 多线程:为每个CPU核心分配一个接收线程,减少锁竞争
- 亲和性:绑定线程到特定CPU核心,减少上下文切换
- 内核旁路:极端性能需求下,考虑DPDK等内核旁路技术
在实际项目中,我曾遇到一个UDP服务端在高负载下丢包严重的问题。通过以下步骤最终解决:
- 使用
netstat -su发现大量receive buffer errors - 将SO_RCVBUF从默认的208KB增大到1MB
- 为每个工作线程分配独立的套接字和CPU核心
- 实现应用层的流量控制机制
调整后,服务端能够稳定处理10万+ PPS的UDP流量
