小白学UDP编程:从基础代码到优化实战(附完整可运行代码)

前言

刚学网络编程时,是不是写了代码能运行但总感觉"不踏实"?比如消息乱码、程序莫名崩溃、想退出只能强关窗口?今天就以UDP通信为例,从最基础的代码开始,一步步教你优化,让程序既稳定又好用,小白也能看懂!

一、先看我之前代码的"小问题"

初学写的UDP代码往往能实现基本收发,但藏着不少"坑",比如:

  1. 收消息、发消息失败了不知道,程序闷头继续跑
  2. 上次的消息没清干净,新消息带着"脏数据"
  3. 字符串拼接一不小心就"撑爆"缓冲区
  4. 想退出只能关掉窗口,服务器还在傻等

下面就以服务器和客户端代码为例,手把手优化这些问题~

二、优化第一步:让程序"会报错"

问题:原始代码里`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;
}

七、怎么运行?超简单步骤

  1. 改IP地址:

服务器代码里的`IP`改成自己电脑的IP(用`ifconfig`查看),客户端代码里的`IP`改成服务器的IP(如果在同一台电脑上就填一样的)。

  1. 编译代码(终端输入):

编译服务器

gcc udp_server.c -o server -Wall # -Wall会提示代码里的小问题

编译客户端

gcc udp_client.c -o client -Wall

  1. 运行程序:
  • 先开服务器:./server(看到"服务器启动成功"就对了)
  • 再开客户端:./client(另开一个终端)
  • 客户端输入消息,服务器会回复带`xixi`的消息,输入`exit`优雅退出~

八、优化后你能感受到的变化

  • 报错清晰:再也不是"程序崩了但不知道为啥",而是明确告诉你"端口被占用""网络断了"。
  • 消息干净:没有乱码,收到的消息和发送的一致。
  • 退出安全:输入`exit`两边都正常关闭,不会卡住。
  • 程序稳定:再也不会因为字符串拼接、缓冲区没清等小问题崩溃了。

总结

从"能跑"到"跑稳",其实就差这几步优化:加错误检查、清缓冲区、安全操作字符串、加退出机制。这些技巧不仅适用于UDP,在其他网络编程中也很常用,学会了就能少踩很多坑~ 快去试试吧!

相关推荐
DLGXY2 小时前
STM32——DMA数据转换、DMA+AD多通道(十五)
stm32·单片机·嵌入式硬件
来自晴朗的明天2 小时前
7、PCF8574 I2C 接口 GPIO 扩展电路
单片机·嵌入式硬件·硬件工程
qq_401700412 小时前
单片机调试进阶:IDE中的Register与Memory窗口以及断点与观察点 (Watchpoint)
单片机
繁星丶992 小时前
串口通信、TCP/UDP 通信和 MQTT 通信的概念与调试工具应用
单片机·tcp/ip·udp
傻童:CPU2 小时前
STM320F28377D的时钟配置
stm32·单片机·嵌入式硬件
小龙报2 小时前
【51单片机】串口通讯从入门到精通:原理拆解 + 参数详解 + 51 单片机实战指南
c语言·驱动开发·stm32·单片机·嵌入式硬件·物联网·51单片机
2023自学中3 小时前
imx6ull , 4.3寸800*480屏幕,触摸芯片型号 gt9147,显示触摸点的坐标数据
linux·嵌入式硬件
仰望星空的凡人3 小时前
探秘MCU最小系统中的晶振部分是如何工作的?
单片机·嵌入式硬件
羽获飞3 小时前
从零开始学嵌入式之STM32——8.流水灯
stm32·单片机·嵌入式硬件