3、UDP Socket编程:服务端创建流程(socket、bind、recvfrom)、客户端创建流程(socket、sendto)、无连接特性详解。
聊到UDP,很多人的第一反应是“不可靠”。这话没错,但不够全面。我做了这么多年网络编程,UDP其实是个很有意思的协议——它把控制权完全交给你,让你在性能和可靠性之间做取舍。说白了,TCP像个保姆,什么都帮你管好;UDP像个快递员,把包裹扔给你就走,剩下的你自己看着办。
这一章,咱们就手把手把UDP的服务端和客户端搭起来。你会发现,UDP的代码比TCP简洁不少,但坑也不少。
3.1 UDP服务端创建流程
UDP服务端的创建,核心就三步:创建套接字、绑定地址、接收数据。没有listen,没有accept。为什么?因为UDP是无连接的,服务端不需要为每个客户端维护一个独立的连接。
3.1.1 创建套接字:socket()
这一步和TCP几乎一样,只是协议类型换成SOCK_DGRAM。
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
SOCK_DGRAM告诉内核:我要用UDP,数据报模式。第三个参数传0,让内核自动选择UDP协议。我个人习惯在这里显式检查返回值,因为很多嵌入式环境或者容器环境里,socket创建可能因为权限或资源限制失败。
3.1.2 绑定地址:bind()
bind的作用是告诉内核:这个套接字监听哪个IP和端口。UDP服务端必须bind,否则客户端不知道把数据发到哪。
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡
servaddr.sin_port = htons(8888); // 端口号
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind failed");
close(sockfd);
exit(EXIT_FAILURE);
}
这里有个细节:INADDR_ANY表示监听本机所有IP地址。如果你只想让某个特定IP接收数据,可以用inet_addr("192.168.1.100")。我在项目中遇到过一个问题——服务器有两块网卡,一块内网一块外网,结果bind了外网IP,内网客户端死活连不上。嗯,后来改成INADDR_ANY就解决了。
3.1.3 接收数据:recvfrom()
这是UDP服务端的核心函数。它不光接收数据,还能告诉你数据是谁发的。
char buffer[1024];
struct sockaddr_in cliaddr;
socklen_t len = sizeof(cliaddr);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr *)&cliaddr, &len);
if (n < 0) {
perror("recvfrom error");
} else {
buffer[n] = '\0';
printf("Received %ld bytes from %s:%d\n", n,
inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
printf("Data: %s\n", buffer);
}
注意cliaddr这个参数。它是个输出参数,recvfrom返回时,里面填了客户端的IP和端口。这样你就知道该给谁回数据了。
3.2 UDP客户端创建流程
UDP客户端更简单,两步走:创建套接字,然后直接发数据。不需要bind(虽然也可以bind),不需要connect。
3.2.1 创建套接字:socket()
和服务端一模一样:
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(EXIT_FAILURE);
}
3.2.2 发送数据:sendto()
sendto需要指定目标地址,因为UDP没有连接,内核不知道把数据发到哪。
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servaddr.sin_port = htons(8888);
char *message = "Hello, UDP server!";
ssize_t n = sendto(sockfd, message, strlen(message), 0,
(struct sockaddr *)&servaddr, sizeof(servaddr));
if (n < 0) {
perror("sendto error");
} else {
printf("Sent %ld bytes to server\n", n);
}
你看,客户端不需要bind。内核会自动给客户端分配一个临时端口。这个端口是随机的,范围通常在32768到60999之间(Linux默认)。
3.3 无连接特性详解
这是UDP和TCP最本质的区别。理解了这个,你就理解了UDP的灵魂。
3.3.1 什么叫“无连接”?
说白了,UDP在发送数据之前,不需要和对方建立一条专用的“通道”。你想想看,TCP发数据前要三次握手,维护连接状态,断开时要四次挥手。UDP呢?直接扔数据包,扔完就不管了。
我画了张图,帮你直观理解这个区别:
3.3.2 无连接带来的特性
这个特性带来了一系列连锁反应,我列个表给你看:
| 特性 | UDP | TCP | 影响 |
|---|---|---|---|
| 连接建立 | 不需要 | 三次握手 | UDP延迟更低,适合实时应用 |
| 状态维护 | 服务端无状态 | 内核维护连接表 | UDP服务端可轻松支持数十万客户端 |
| 数据边界 | 保留消息边界 | 字节流 | UDP一次recvfrom正好是一个完整消息 |
| 可靠性 | 不保证 | 保证有序、不丢失 | UDP需要应用层自己处理丢包 |
| 广播/组播 | 原生支持 | 不支持 | UDP适合一对多场景 |
3.3.3 无连接的实战影响
我在实际项目中遇到过几个典型场景,跟你分享一下:
- 场景一:DNS查询——DNS就是基于UDP的。客户端发一个查询包,服务端回一个响应包。如果丢包了,客户端超时重发。你看,应用层自己处理可靠性,UDP只负责传输。
- 场景二:视频直播——直播允许丢几帧画面,但不能卡顿。用UDP的话,丢包了就直接跳过,画面花一下而已。如果用TCP,丢包重传会导致延迟飙升,体验更差。
- 场景三:物联网传感器——温度传感器每秒上报一次数据。丢一两个包无所谓,反正下一秒还有新数据。用UDP省去了连接管理的开销,设备更省电。
3.4 完整示例:UDP回声服务
把上面的知识点串起来,写一个完整的UDP回声服务。客户端发什么,服务端原样返回。
服务端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in servaddr, cliaddr;
char buffer[1024];
socklen_t len;
// 1. 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
// 2. 绑定地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(8888);
if (bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
perror("bind");
close(sockfd);
exit(1);
}
printf("UDP echo server listening on port 8888...\n");
// 3. 循环接收并回显
while (1) {
len = sizeof(cliaddr);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr *)&cliaddr, &len);
if (n < 0) {
perror("recvfrom");
continue;
}
buffer[n] = '\0';
printf("Received: %s from %s:%d\n", buffer,
inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
// 原样返回
sendto(sockfd, buffer, n, 0,
(struct sockaddr *)&cliaddr, len);
}
close(sockfd);
return 0;
}
客户端代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int sockfd;
struct sockaddr_in servaddr;
char buffer[1024];
socklen_t len;
// 1. 创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket");
exit(1);
}
// 2. 设置服务端地址
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
servaddr.sin_port = htons(8888);
// 3. 发送数据
char *message = "Hello, UDP!";
sendto(sockfd, message, strlen(message), 0,
(struct sockaddr *)&servaddr, sizeof(servaddr));
printf("Sent: %s\n", message);
// 4. 接收回声
len = sizeof(servaddr);
ssize_t n = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr *)&servaddr, &len);
if (n > 0) {
buffer[n] = '\0';
printf("Received echo: %s\n", buffer);
}
close(sockfd);
return 0;
}
- UDP服务端:socket → bind → recvfrom(循环)
- UDP客户端:socket → sendto(直接发)
- 无连接意味着没有握手、没有状态、没有连接表
- UDP保留消息边界,一次recvfrom就是一个完整数据报
- 可靠性由应用层自己保证,UDP只负责尽力交付