3. UDP通信流程:服务端与客户端
聊完了UDP协议本身,咱们得动手写代码了。这一章我带你走一遍UDP通信的完整流程——说白了,就是服务端和客户端各自该干哪些事。
我记得刚学Socket那会儿,总觉得流程很绕。后来发现,其实就几个步骤,跟打电话差不多。你想想看,打电话是不是也得先有设备、拨号、说话、挂断?UDP的Socket通信也是这个道理。
核心要点:UDP通信流程分为服务端和客户端两条线。服务端先启动,绑定端口,然后收/发数据;客户端随时可以发数据给服务端。两者都通过Socket这个“管道”操作。
3.1 服务端流程详解
服务端是UDP通信中的“守门员”。它得先上场,占好位置,然后等着对方把球踢过来。我个人习惯把服务端流程拆成五步,每一步都有它的道理。
3.1.1 创建Socket
第一步,调用 socket() 函数。这相当于你从运营商那里申请了一部电话机。在Linux下,代码长这样:
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(1);
}
AF_INET 表示IPv4,SOCK_DGRAM 就是UDP。第三个参数传0,系统会自动选UDP协议。嗯,这里要注意:返回值是文件描述符,后面所有操作都靠它。
3.1.2 绑定端口
有了Socket,你得告诉操作系统:“我要用这个端口”。这就是 bind() 的作用。我在项目中遇到过有人忘了绑定端口,结果客户端死活连不上——因为系统随机分配了一个端口,客户端根本不知道往哪发。
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 = INADDR_ANY;
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind failed");
close(sockfd);
exit(1);
}
htons() 把端口号转成网络字节序,INADDR_ANY 表示监听所有网卡。绑定成功后,这个端口就是服务端的“门牌号”了。
小技巧:端口号建议选1024以上的,避免跟系统服务冲突。我一般用8000-9000之间的端口,不容易撞车。
3.1.3 接收数据
绑定完端口,服务端就进入“等待模式”。用 recvfrom() 接收数据,它会阻塞直到有数据到达。你想想看,这就像你坐在电话旁等铃响。
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&client_addr, &addr_len);
if (n < 0) {
perror("recvfrom error");
} else {
buffer[n] = '\0';
printf("收到来自 %s:%d 的消息: %s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buffer);
}
recvfrom() 不光能拿到数据,还能知道是谁发的——客户端的IP和端口都保存在 client_addr 里。这一点很重要,因为后面回复数据时要用到。
3.1.4 发送数据
收到数据后,服务端通常要回复。用 sendto() 把数据发回去。目标地址就是刚才收到的 client_addr。
const char *reply = "消息已收到";
int sent = sendto(sockfd, reply, strlen(reply), 0,
(struct sockaddr*)&client_addr, addr_len);
if (sent < 0) {
perror("sendto error");
}
这里有个坑:sendto() 的最后一个参数是地址长度,必须跟 recvfrom() 返回的一致。我曾经传错了长度,结果数据发不出去,排查了半天才发现是这里的问题。
3.1.5 关闭Socket
通信结束,记得关掉Socket。用 close() 释放资源。
close(sockfd);
这一步看似简单,但忘了做的话,端口会一直被占用。下次再启动服务端时,bind() 就会报错“Address already in use”。
避坑指南:我曾经在开发机上跑服务端,Ctrl+C 终止程序后没关Socket,结果端口被占用了好几分钟。后来我习惯在程序里加 setsockopt() 设置 SO_REUSEADDR,这样端口能立即重用。
3.2 客户端流程详解
客户端比服务端简单。它不需要绑定端口,直接发数据就行。说白了,客户端就是个“主动方”,想发就发。
3.2.1 创建Socket
跟服务端一样,先创建Socket。代码几乎一模一样:
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) {
perror("socket creation failed");
exit(1);
}
注意,客户端不需要 bind()。系统会自动给客户端分配一个临时端口,你只管发数据就行。
3.2.2 发送数据
客户端用 sendto() 把数据发给服务端。这里需要指定服务端的地址和端口。
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");
const char *msg = "Hello, UDP Server!";
int sent = sendto(sockfd, msg, strlen(msg), 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
if (sent < 0) {
perror("sendto failed");
}
你想想看,这里客户端不需要先建立连接,直接就把数据扔出去了。UDP就是这么“任性”。
3.2.3 接收数据
如果服务端回复了,客户端可以用 recvfrom() 接收。这一步是可选的——有些场景下客户端只管发,不管收。
char buffer[1024];
struct sockaddr_in from_addr;
socklen_t from_len = sizeof(from_addr);
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&from_addr, &from_len);
if (n > 0) {
buffer[n] = '\0';
printf("服务端回复: %s\n", buffer);
}
嗯,这里要注意:recvfrom() 会阻塞,如果服务端没回复,程序会一直等。我建议用非阻塞模式或者设置超时,避免程序卡死。
3.2.4 关闭Socket
跟服务端一样,用 close() 收尾。
close(sockfd);
3.3 服务端 vs 客户端:流程对比
为了让你看得更清楚,我把两者的流程放在一起对比一下:
| 步骤 | 服务端 | 客户端 |
|---|---|---|
| 1 | 创建Socket | 创建Socket |
| 2 | 绑定端口(必须) | 无需绑定(系统自动分配) |
| 3 | 接收数据(recvfrom) | 发送数据(sendto) |
| 4 | 发送数据(sendto) | 接收数据(recvfrom,可选) |
| 5 | 关闭Socket | 关闭Socket |
看出来了吗?服务端多了一个 bind() 步骤,而且通常是先收后发。客户端则是先发后收,甚至可以不收。
3.4 完整示例:一个简单的UDP回声服务
光说不练假把式。我给你写一个完整的回声服务——客户端发什么,服务端就原样返回。这是我在项目中调试网络时最常用的“Hello World”程序。
服务端代码(echo_server.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) { perror("socket"); exit(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 = INADDR_ANY;
if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
perror("bind"); close(sockfd); exit(1);
}
printf("UDP Echo Server 启动,监听端口 8888...\n");
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t addr_len = sizeof(client_addr);
while (1) {
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0,
(struct sockaddr*)&client_addr, &addr_len);
if (n < 0) { perror("recvfrom"); continue; }
buffer[n] = '\0';
printf("收到: %s\n", buffer);
sendto(sockfd, buffer, n, 0,
(struct sockaddr*)&client_addr, addr_len);
}
close(sockfd);
return 0;
}
客户端代码(echo_client.c):
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
int main() {
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0) { perror("socket"); exit(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 buffer[1024];
while (1) {
printf("请输入消息: ");
fgets(buffer, sizeof(buffer), stdin);
buffer[strcspn(buffer, "\n")] = '\0';
sendto(sockfd, buffer, strlen(buffer), 0,
(struct sockaddr*)&server_addr, sizeof(server_addr));
int n = recvfrom(sockfd, buffer, sizeof(buffer), 0, NULL, NULL);
if (n > 0) {
buffer[n] = '\0';
printf("回声: %s\n", buffer);
}
}
close(sockfd);
return 0;
}
运行方法:先在一个终端里编译运行服务端,再开另一个终端运行客户端。客户端输入什么,服务端就返回什么。试试看,你会对UDP通信有更直观的感受。
3.5 常见问题与避坑
最后,我总结几个新手容易踩的坑,都是我亲身经历过的:
- 端口被占用:服务端退出后,端口不会立即释放。可以用
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))解决。 - 数据丢失:UDP不保证送达。如果客户端发了数据但服务端没收到,检查防火墙和网络连通性。
- 缓冲区溢出:
recvfrom()的缓冲区要足够大。我一般用4096字节,UDP数据报最大65507字节,但实际应用中很少超过1500字节。 - 地址结构清零:每次使用
struct sockaddr_in前,记得用memset()清零。否则残留数据可能导致绑定失败。