前言
刚学网络编程时,是不是写了代码能运行但总感觉"不踏实"?比如消息乱码、程序莫名崩溃、想退出只能强关窗口?今天就以UDP通信为例,从最基础的代码开始,一步步教你优化,让程序既稳定又好用,小白也能看懂!
一、先看我之前代码的"小问题"
初学写的UDP代码往往能实现基本收发,但藏着不少"坑",比如:
- 收消息、发消息失败了不知道,程序闷头继续跑
- 上次的消息没清干净,新消息带着"脏数据"
- 字符串拼接一不小心就"撑爆"缓冲区
- 想退出只能关掉窗口,服务器还在傻等
下面就以服务器和客户端代码为例,手把手优化这些问题~
二、优化第一步:让程序"会报错"
问题:原始代码里`socket`、`recvfrom`这些函数失败了不提示,出问题根本不知道哪错了。
解决:给每个系统函数加"返回值检查",失败了就打印错误原因。
服务器端示例(创建套接字优化)
// 原来的代码(有风险)
int oldfd = socket(AF_INET,SOCK_DGRAM,0);
// 优化后(会报错)
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) { // 检查是否创建失败
perror("创建套接字失败"); // 打印具体错误(比如权限不够、系统资源不足)
return -1; // 失败就退出,不继续瞎跑
}
所有系统函数都要这样! 比如`bind`、`recvfrom`、`sendto`,加上检查后,程序出问题时你能立刻知道是"端口被占用"还是"网络断了"。
三、优化第二步:给缓冲区"洗澡"
问题:缓冲区里有上次残留的消息,新消息发过去就会"串味"(比如收到`"你好\345\270\255"`这种乱码)。
解决:每次收发消息前,用`memset`清空缓冲区,就像倒新水前先把杯子洗干净。
示例(收发前清空缓冲区)
char buff[1024]; // 存消息的缓冲区
// 发消息前清空
memset(buff, 0, sizeof(buff)); // 把buff里的所有字节都设为0
// 收消息前也清空
memset(buff, 0, sizeof(buff));
加上这行后,消息再也不会乱码啦!
四、优化第三步:字符串操作"稳一点"
问题:用`strcat`拼接字符串时,不管缓冲区够不够大硬塞,容易"撑爆"缓冲区导致程序崩溃。
解决:用`strncat`代替`strcat`,告诉它最多能接多长的字符串,防止溢出。
示例(安全拼接字符串)
// 原来的危险操作(可能撑爆缓冲区)
strcat(buff, "xixi");
// 优化后(安全拼接)
// BUFF_SIZE是缓冲区总大小,strlen(buff)是当前已有内容长度,-1留一个位置给结束符
strncat(buff, "xixi", BUFF_SIZE - strlen(buff) - 1);
这样就算要拼接的内容很长,也不会超过缓冲区大小,程序稳多了!
五、优化第四步:能"温柔退出"
问题:客户端想退出只能关窗口,服务器还在一直等消息,很不友好。
解决:加一个退出命令(比如输入`exit`),客户端发`exit`给服务器,服务器收到后回复确认,两边都优雅退出。
客户端示例(检测退出命令)
// 输入消息后检查是不是"exit"
if (strcmp(buff, "exit") == 0) {
printf("准备退出啦~\n");
sendto(sockfd, buff, strlen(buff), 0, &server_addr, server_len); // 告诉服务器要退出
break; // 跳出循环,准备关闭程序
}
服务器示例(收到退出命令后处理)
// 收到消息后检查是不是客户端要退出
if (strcmp(buff, "exit") == 0) {
printf("客户端要退出啦~\n");
sendto(sockfd, "exit_ack", 8, 0, &client_addr, client_len); // 回复确认
break; // 服务器也退出循环
}
这样退出时两边都有"仪式感",不会留下"孤儿进程"~
六、完整优化后代码(带超详细注释)
服务器端代码(udp_server.c)
cs
#include <myhead.h> // 我的头文件(包含必要的网络库)
#define PORT 8888 // 服务器端口号(自己选一个没被占用的)
#define IP "192.168.0.118" // 服务器IP(填自己电脑的IP)
#define BUFF_SIZE 1024 // 缓冲区大小(1024字节够用了)
#define EXIT_CMD "exit" // 退出命令
int main() {
// 1. 创建UDP套接字(相当于买个"电话")
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("创建套接字失败"); // 失败提示:比如权限不够
return -1;
}
// 2. 绑定IP和端口(告诉系统:这个IP+端口归我用)
struct sockaddr_in server_addr = {
.sin_family = AF_INET, // 用IPv4协议
.sin_port = htons(PORT), // 端口转成网络格式(必须!)
.sin_addr.s_addr = inet_addr(IP) // IP转成网络格式(填自己的IP)
};
socklen_t addr_len = sizeof(server_addr); // 地址结构的大小
// 绑定操作,失败就提示并关闭套接字
if (bind(sockfd, (struct sockaddr *)&server_addr, addr_len) == -1) {
perror("绑定失败(可能端口被占用了)");
close(sockfd); // 记得关"电话"
return -1;
}
printf("服务器启动成功!端口:%d,等着收消息~\n", PORT);
// 存客户端地址的变量(知道消息是谁发的)
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
char buff[BUFF_SIZE]; // 存消息的缓冲区
// 循环收消息(服务器要一直运行)
while (1) {
// 清空缓冲区(重要!避免旧消息残留)
memset(buff, 0, BUFF_SIZE);
// 接收客户端消息,同时获取客户端地址
ssize_t recv_len = recvfrom(sockfd, buff, BUFF_SIZE - 1, 0,
(struct sockaddr *)&client_addr, &client_len);
if (recv_len == -1) { // 检查接收是否失败
if (errno == EINTR) { // 被信号打断(比如按了Ctrl+C但没退出),继续等
printf("被打断了,继续等消息...\n");
continue;
}
perror("接收消息失败(可能网络断了)");
break; // 真失败了就退出循环
}
// 手动加字符串结束符(不然打印会乱)
buff[recv_len] = '\0';
// 检查客户端是不是要退出
if (strcmp(buff, EXIT_CMD) == 0) {
printf("客户端 %s:%d 要退出啦~\n",
inet_ntoa(client_addr.sin_addr), // 客户端IP
ntohs(client_addr.sin_port)); // 客户端端口
// 回复客户端"已收到退出请求"
sendto(sockfd, "exit_ack", 8, 0,
(struct sockaddr *)&client_addr, client_len);
break; // 服务器也退出循环
}
// 打印收到的消息(显示谁发的、发了啥)
printf("收到【%s:%d】的消息:%s\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port),
buff);
// 给消息加个小尾巴(安全拼接)
strncat(buff, "xixi", BUFF_SIZE - strlen(buff) - 1);
// 回复客户端
ssize_t send_len = sendto(sockfd, buff, strlen(buff), 0,
(struct sockaddr *)&client_addr, client_len);
if (send_len == -1) { // 检查发送是否失败
perror("发送消息失败(可能客户端跑了)");
break;
}
}
// 关闭套接字(用完记得收拾)
close(sockfd);
printf("服务器关闭啦~\n");
return 0;
}
客户端代码(udp_client.c)
cs
#include <myhead.h> // 我的头文件
#define PORT 7777 // 服务器的端口号(要和服务器一致)
#define IP "192.168.0.147" // 服务器的IP(填服务器的IP)
#define BUFF_SIZE 1024 // 缓冲区大小
#define EXIT_CMD "exit" // 退出命令
int main() {
// 1. 创建UDP套接字(用来和服务器聊天)
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd == -1) {
perror("创建套接字失败");
return -1;
}
// 服务器的地址信息(知道要发给谁)
struct sockaddr_in server_addr = {
.sin_family = AF_INET,
.sin_port = htons(PORT), // 服务器端口(转网络格式)
.sin_addr.s_addr = inet_addr(IP) // 服务器IP
};
socklen_t server_len = sizeof(server_addr);
char buff[BUFF_SIZE]; // 存消息的缓冲区
printf("客户端启动成功!输入消息发给服务器,输入exit退出~\n");
// 循环发消息
while (1) {
// 清空缓冲区
memset(buff, 0, BUFF_SIZE);
// 让用户输入消息
printf("请输入消息:");
if (fgets(buff, BUFF_SIZE - 1, stdin) == NULL) { // 读取输入
perror("输入失败(可能按了错误的键)");
break;
}
// 去掉输入里的换行符(fgets会把回车也读进来)
buff[strcspn(buff, "\n")] = '\0';
// 检查是不是要退出
if (strcmp(buff, EXIT_CMD) == 0) {
printf("准备退出,告诉服务器一声~\n");
sendto(sockfd, buff, strlen(buff), 0,
(struct sockaddr *)&server_addr, server_len);
break; // 跳出循环
}
// 发送消息给服务器
ssize_t send_len = sendto(sockfd, buff, strlen(buff), 0,
(struct sockaddr *)&server_addr, server_len);
if (send_len == -1) {
perror("发送失败(可能服务器没开)");
break;
}
// 清空缓冲区,准备收服务器回复
memset(buff, 0, BUFF_SIZE);
// 接收服务器回复
ssize_t recv_len = recvfrom(sockfd, buff, BUFF_SIZE - 1, 0,
(struct sockaddr *)&server_addr, &server_len);
if (recv_len == -1) {
if (errno == EINTR) { // 被信号打断,继续等
printf("被打断了,等服务器回复...\n");
continue;
}
perror("接收回复失败(可能服务器掉线了)");
break;
}
// 加结束符,打印回复
buff[recv_len] = '\0';
printf("服务器回复:%s\n", buff);
}
// 关闭套接字
close(sockfd);
printf("客户端关闭啦~\n");
return 0;
}
七、怎么运行?超简单步骤
- 改IP地址:
服务器代码里的`IP`改成自己电脑的IP(用`ifconfig`查看),客户端代码里的`IP`改成服务器的IP(如果在同一台电脑上就填一样的)。
- 编译代码(终端输入):
编译服务器
gcc udp_server.c -o server -Wall # -Wall会提示代码里的小问题
编译客户端
gcc udp_client.c -o client -Wall
- 运行程序:
- 先开服务器:./server(看到"服务器启动成功"就对了)
- 再开客户端:./client(另开一个终端)
- 客户端输入消息,服务器会回复带`xixi`的消息,输入`exit`优雅退出~
八、优化后你能感受到的变化
- 报错清晰:再也不是"程序崩了但不知道为啥",而是明确告诉你"端口被占用""网络断了"。
- 消息干净:没有乱码,收到的消息和发送的一致。
- 退出安全:输入`exit`两边都正常关闭,不会卡住。
- 程序稳定:再也不会因为字符串拼接、缓冲区没清等小问题崩溃了。
总结
从"能跑"到"跑稳",其实就差这几步优化:加错误检查、清缓冲区、安全操作字符串、加退出机制。这些技巧不仅适用于UDP,在其他网络编程中也很常用,学会了就能少踩很多坑~ 快去试试吧!