5天挑战网络编程 -DAY2(linux版)

📅 Day 2:另一种选择 —— 轻快高效的UDP


🎯 今日目标

  • 深入理解 TCP 与 UDP 的核心差异
  • 掌握 UDP 套接字编程的基本流程
  • 实现一个基于 UDP 的在线词典服务器
  • 理解 UDP 无连接通信的特点和应用场景

🧠 第一部分:理论基础

📘 TCP vs UDP 深度对比

特性TCPUDP
连接性面向连接(需要建立连接)无连接(直接发送数据)
可靠性可靠(确认、重传、流量控制)不可靠(尽力而为)
速度慢,开销大快,开销小
数据边界无(流式数据)有(数据报)
应用场景Web、文件传输、邮件视频、语音、游戏直播、DNS查询

🎯 UDP 的优势与劣势

优势:

  • 无需建立连接,通信开销小
  • 实时性好,延迟低
  • 支持一对多通信(广播、组播)

劣势:

  • 不保证数据到达
  • 不保证数据顺序
  • 不提供流量控制和拥塞控制

🧠 第二部分:重点函数详解

UDP 特有的关键函数

函数功能参数详细说明返回值
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen)向指定地址发送UDP数据报sockfd: 套接字描述符;buf: 发送缓冲区;len: 发送长度;flags: 发送标志;dest_addr: 目标地址;addrlen: 地址结构体大小成功返回发送字节数,失败返回-1
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen)接收UDP数据报并获取发送方地址sockfd: 套接字描述符;buf: 接收缓冲区;len: 缓冲区大小;flags: 接收标志;src_addr: 发送方地址结构体;addrlen: 地址结构体大小指针成功返回接收字节数,失败返回-1

与TCP的主要区别

特性TCPUDP
套接字类型SOCK_STREAMSOCK_DGRAM
连接建立需要 connect()不需要
数据接收recv()recvfrom()
数据发送send()sendto()
监听连接需要 listen()accept()不需要

🧱 第三部分:UDP 编程框架

UDP 服务器开发流程
1. 创建套接字 socket()
2. 绑定地址 bind()
3. 循环收发数据 recvfrom() / sendto()
4. 关闭套接字 close()
UDP 客户端开发流程
1. 创建套接字 socket()
2. 循环收发数据 sendto() / recvfrom()
3. 关闭套接字 close()

💻 第四部分:动手实战 —— UDP 在线词典

🧩 功能说明

  • 服务器内置简单的键值对词典(如 “hello” -> “你好”)
  • 客户端输入英文单词发送给服务器
  • 服务器查询词典并返回对应中文释义
  • 如果查不到,返回 “Not Found”

📁 文件结构

udp_server.c
udp_client.c

🖥️ udp_server.c(服务器端)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define PORT 8888           // 服务器监听端口号
#define MAX_BUFFER 1024     // 缓冲区大小

// 简单词典结构
typedef struct {
    char word[50];          // 英文单词
    char meaning[100];      // 中文释义
} Dictionary;

// 词典数据
Dictionary dict[] = {
    {"hello", "你好"},
    {"world", "世界"},
    {"computer", "计算机"},
    {"network", "网络"},
    {"programming", "编程"}
};
int dict_size = sizeof(dict) / sizeof(Dictionary);  // 词典大小

// 查找单词函数
char* lookup_word(char* word) {
    for (int i = 0; i < dict_size; i++) {
        if (strcmp(dict[i].word, word) == 0) {
            return dict[i].meaning;  // 找到返回释义
        }
    }
    return "Not Found";  // 未找到返回"Not Found"
}

int main() {
    int server_fd;                                  // 服务器套接字描述符
    struct sockaddr_in server_addr, client_addr;    // 服务器和客户端地址结构体
    socklen_t client_len = sizeof(client_addr);     // 客户端地址结构体大小
    char buffer[MAX_BUFFER];                        // 数据缓冲区

    // 1. 创建UDP套接字
    // 函数原型:int socket(int domain, int type, int protocol);
    // 参数说明:
    //   domain: 地址族,AF_INET表示IPv4
    //   type: 套接字类型,SOCK_DGRAM表示UDP数据报套接字
    //   protocol: 协议,0表示使用默认协议
    // 返回值:成功返回套接字描述符,失败返回-1
    server_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (server_fd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 设置服务器地址结构
    memset(&server_addr, 0, sizeof(server_addr));   // 清零地址结构体
    server_addr.sin_family = AF_INET;               // 设置地址族为IPv4
    server_addr.sin_addr.s_addr = INADDR_ANY;       // 绑定所有本地IP地址
    server_addr.sin_port = htons(PORT);             // 设置端口号(网络字节序)

    // 3. 绑定地址
    // 函数原型:int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    // 参数说明:
    //   sockfd: 套接字描述符
    //   addr: 指向地址结构体的指针
    //   addrlen: 地址结构体的大小
    // 返回值:成功返回0,失败返回-1
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(server_fd);
        exit(EXIT_FAILURE);
    }

    printf("UDP词典服务器启动,监听端口 %d...\n", PORT);

    // 4. 循环处理客户端请求
    while (1) {
        // 函数原型:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
        //                           struct sockaddr *src_addr, socklen_t *addrlen);
        // 参数说明:
        //   sockfd: 服务器套接字
        //   buf: 接收数据的缓冲区
        //   len: 缓冲区大小
        //   flags: 接收标志,通常为0
        //   src_addr: 用于存储发送方地址信息的结构体
        //   addrlen: 地址结构体大小的指针
        // 返回值:成功返回接收到的字节数,失败返回-1
        int n = recvfrom(server_fd, buffer, MAX_BUFFER, 0,
                        (struct sockaddr *)&client_addr, &client_len);
        if (n <= 0) continue;  // 接收失败,继续等待

        buffer[n] = '\0';  // 添加字符串结束符
        printf("收到来自 %s:%d 的查询: %s",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port),
               buffer);

        // 查找单词释义
        char* result = lookup_word(buffer);

        // 函数原型:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
        //                         const struct sockaddr *dest_addr, socklen_t addrlen);
        // 参数说明:
        //   sockfd: 服务器套接字
        //   buf: 发送数据的缓冲区
        //   len: 发送数据的长度
        //   flags: 发送标志,通常为0
        //   dest_addr: 目标地址结构体
        //   addrlen: 地址结构体大小
        // 返回值:成功返回发送的字节数,失败返回-1
        sendto(server_fd, result, strlen(result), 0,
               (struct sockaddr *)&client_addr, client_len);
        printf(" 返回结果: %s\n", result);
    }

    close(server_fd);
    return 0;
}

🖥️ udp_client.c(客户端)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

#define SERVER_IP "127.0.0.1"   // 服务器IP地址(本地回环地址)
#define PORT 8888               // 服务器端口号
#define MAX_BUFFER 1024         // 缓冲区大小

int main() {
    int client_fd;                              // 客户端套接字描述符
    struct sockaddr_in server_addr;             // 服务器地址结构体
    socklen_t server_len = sizeof(server_addr); // 服务器地址结构体大小
    char buffer[MAX_BUFFER];                    // 数据缓冲区

    // 1. 创建UDP套接字
    // 函数原型:int socket(int domain, int type, int protocol);
    // 参数说明:
    //   domain: 地址族,AF_INET表示IPv4
    //   type: 套接字类型,SOCK_DGRAM表示UDP数据报套接字
    //   protocol: 协议,0表示使用默认协议
    // 返回值:成功返回套接字描述符,失败返回-1
    client_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (client_fd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 2. 设置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));       // 清零地址结构体
    server_addr.sin_family = AF_INET;                   // IPv4地址族
    server_addr.sin_port = htons(PORT);                 // 端口号(网络字节序)
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP); // 服务器IP地址

    printf("UDP词典客户端启动!输入单词查询释义,输入'exit'退出。\n");

    // 3. 循环查询单词
    while (1) {
        printf("请输入要查询的单词: ");
        fgets(buffer, MAX_BUFFER, stdin);
        
        // 移除换行符
        buffer[strcspn(buffer, "\n")] = 0;

        if (strcmp(buffer, "exit") == 0) break;  // 输入exit退出

        // 发送查询请求到服务器
        // 函数原型:ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
        //                         const struct sockaddr *dest_addr, socklen_t addrlen);
        // 参数说明:
        //   sockfd: 客户端套接字
        //   buf: 发送数据的缓冲区
        //   len: 发送数据的长度
        //   flags: 发送标志,通常为0
        //   dest_addr: 目标地址结构体(服务器地址)
        //   addrlen: 地址结构体大小
        // 返回值:成功返回发送的字节数,失败返回-1
        sendto(client_fd, buffer, strlen(buffer), 0,
               (struct sockaddr *)&server_addr, server_len);

        // 接收服务器返回的结果
        // 函数原型:ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
        //                           struct sockaddr *src_addr, socklen_t *addrlen);
        // 参数说明:
        //   sockfd: 客户端套接字
        //   buf: 接收数据的缓冲区
        //   len: 缓冲区大小
        //   flags: 接收标志,通常为0
        //   src_addr: 用于存储发送方地址信息的结构体(这里可以为NULL)
        //   addrlen: 地址结构体大小的指针(这里可以为NULL)
        // 返回值:成功返回接收到的字节数,失败返回-1
        int n = recvfrom(client_fd, buffer, MAX_BUFFER, 0, NULL, NULL);
        if (n <= 0) {
            printf("接收数据失败\n");
            continue;
        }

        buffer[n] = '\0';  // 添加字符串结束符
        printf("查询结果: %s\n\n", buffer);
    }

    close(client_fd);
    return 0;
}

🛠️ 第五部分:编译与运行

编译命令详解

# 编译UDP服务器程序
gcc udp_server.c -o udp_server

# 编译UDP客户端程序
gcc udp_client.c -o udp_client

运行方式

# 终端1:启动UDP服务器
./udp_server

# 终端2:启动UDP客户端
./udp_client

🧪 第六部分:测试与验证

示例交互

客户端输入:

hello
computer
xyz
exit

服务器输出:

UDP词典服务器启动,监听端口 8888...
收到来自 127.0.0.1:54321 的查询: hello 返回结果: 你好
收到来自 127.0.0.1:54321 的查询: computer 返回结果: 计算机
收到来自 127.0.0.1:54321 的查询: xyz 返回结果: Not Found

客户端输出:

UDP词典客户端启动!输入单词查询释义,输入'exit'退出。
请输入要查询的单词: hello
查询结果: 你好

请输入要查询的单词: computer
查询结果: 计算机

请输入要查询的单词: xyz
查询结果: Not Found

请输入要查询的单词: exit

📌 第七部分:UDP 编程注意事项

⚠️ 常见问题及解决方案

  1. 数据丢失问题

    • UDP不保证数据到达,需要应用层实现重传机制
    • 解决方案:添加确认和重传逻辑
  2. 数据包乱序问题

    • UDP不保证数据顺序,可能后发送的数据先到达
    • 解决方案:在数据包中添加序列号
  3. 数据包大小限制

    • UDP数据包有大小限制(通常64KB)
    • 解决方案:分割大数据包或使用TCP
  4. 广播和组播支持

    • UDP天然支持广播和组播通信
    • 可用于局域网发现、实时通信等场景

🎯 第八部分:UDP 应用场景

🎮 典型应用场景

应用类型说明为什么用UDP
实时游戏网络游戏、实时对战对延迟敏感,允许少量数据丢失
音视频传输视频直播、语音通话实时性要求高,少量丢包不影响体验
DNS查询域名解析查询响应快,无连接开销
广播通信局域网发现、时间同步支持一对多通信
IoT设备通信传感器数据上报数据量小,对实时性要求高

📌 总结

✅ 今天你学会了:

  • TCP 与 UDP 的核心差异和各自适用场景
  • UDP 套接字编程的基本流程和关键函数
  • sendto()recvfrom() 函数的详细使用方法
  • 实现了一个功能完整的 UDP 在线词典服务器
  • 理解了 UDP 无连接通信的特点和优势

🎯 明天我们将学习如何让服务器支持多个客户端并发连接,进入并发服务器的世界!


📌 Tips:

  • UDP 编程中要注意数据的可靠性和顺序问题
  • 多使用 man 命令查看函数文档
  • 理解 UDP 的无连接特性,这是它与 TCP 的根本区别
  • 实际项目中要根据业务需求选择合适的传输协议

👉 点个赞和关注,更多知识不迷路!!明天见!Day 3 更精彩:从一对一到一对多 —— 经典的并发服务器!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

饭碗的彼岸one

感谢鼓励,谢谢

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值