UDP Socket C语言如何高效通信?

99ANYc3cd6
预计阅读时长 31 分钟
位置: 首页 C语言 正文
  1. 核心概念与函数:介绍 UDP 编程需要用到的主要数据结构和函数。
  2. 完整服务端/客户端示例:提供一个可运行的、带详细注释的代码示例。
  3. 代码编译与运行:说明如何编译和运行这些程序。
  4. 常见问题与注意事项:总结一些开发中需要注意的点。

核心概念与函数

1 头文件

你需要包含以下头文件:

udp socket c语言
(图片来源网络,侵删)
#include <stdio.h>      // 标准输入输出
#include <stdlib.h>     // 标准库函数
#include <string.h>     // 字符串操作
#include <unistd.h>     // POSIX 系统调用 (如 close)
#include <sys/socket.h> // 套接字相关的系统调用和结构体
#include <netinet/in.h> // IP 地址和端口的结构体定义
#include <arpa/inet.h>  // IP 地址转换函数 (如 inet_pton)

2 关键数据结构

  • struct sockaddr: 通用套接字地址结构,作为参数传递给函数时,会被强制转换。
  • struct sockaddr_in: 用于 IPv4 的套接字地址结构,包含端口号和 IP 地址。
    struct sockaddr_in {
        short            sin_family;   // 地址族 (AF_INET)
        unsigned short   sin_port;     // 16位端口号,需要用 htons() 转换
        struct in_addr  sin_addr;     // 32位 IP 地址
        unsigned char    sin_zero[8];  // 填充字段,不使用
    };
  • struct in_addr: IPv4 地址结构。
    // in_addr 是一个联合体,通常用 s_addr 成员
    typedef uint32_t in_addr_t;
    struct in_addr {
        in_addr_t s_addr;
    };

3 核心函数

  • socket(): 创建一个套接字。
    int socket(int domain, int type, int protocol);
    // domain: 地址族 (AF_INET for IPv4)
    // type: 套接字类型 (SOCK_DGRAM for UDP)
    // protocol: 协议 (对于 SOCK_DGRAM 和 AF_INET,通常为 0)
    // 返回值: 成功返回套接字描述符(一个整数),失败返回 -1
  • bind(): 将套接字与一个特定的 IP 地址和端口号绑定。
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    // sockfd: socket() 返回的套接字描述符
    // addr: 指向 sockaddr_in 结构体的指针,包含要绑定的 IP 和端口
    // addrlen: addr 结构体的长度
    // 返回值: 成功返回 0,失败返回 -1
  • sendto(): 通过 UDP 发送数据,它需要指定数据要发送到的目标地址。
    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: 指向目标 sockaddr_in 结构体的指针
    // addrlen: 目标地址结构体的长度
    // 返回值: 成功返回发送的字节数,失败返回 -1
  • recvfrom(): 通过 UDP 接收数据,它会返回数据以及发送方的地址信息。
    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: 用于存储发送方地址的 sockaddr_in 结构体指针
    // addrlen: 指向 src_addr 结构体长度的指针(传入时是 sizeof(struct sockaddr_in),返回时会被系统填入实际长度)
    // 返回值: 成功返回接收到的字节数,失败返回 -1,连接对端关闭时返回 0 (TCP 特性,UDP 不适用)
  • `htons() / htonl()**: 主机字节序到网络字节序的转换。
    • htons(): host to network short (16位)
    • htonl(): host to network long (32位) 网络字节序是大端序,而不同主机的字节序可能不同,在设置端口号和 IP 地址时必须使用这些函数。
  • inet_pton() / inet_ntop(): IP 地址的点分十进制字符串与网络字节序的二进制格式之间的转换。
    • inet_pton(): presentation to numeric (字符串 -> 二进制)
    • inet_ntop(): numeric to presentation (二进制 -> 字符串)

完整服务端/客户端示例

这个例子实现了一个简单的回显服务器:客户端发送一条消息,服务器收到后,再将原消息发送回客户端。

1 UDP 服务端代码 (server.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    char buffer[BUFFER_SIZE] = {0};
    socklen_t client_addr_len = sizeof(client_addr);
    // 1. 创建套接字
    // AF_INET: IPv4
    // SOCK_DGRAM: UDP
    // 0: 自动选择协议
    if ((server_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    // 设置套接字选项,允许地址重用
    // 防止服务器快速重启时 "Address already in use" 错误
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    // 2. 绑定地址和端口
    server_addr.sin_family = AF_INET; // IPv4
    server_addr.sin_port = htons(PORT); // 端口号,转换为网络字节序
    server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
    // 绑定套接字
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    // 3. 循环接收和发送数据
    while (1) {
        // 接收客户端数据
        // recvfrom 会阻塞,直到收到数据
        int n = recvfrom(server_fd, buffer, BUFFER_SIZE, 0,
                         (struct sockaddr *)&client_addr, &client_addr_len);
        if (n < 0) {
            perror("recvfrom failed");
            continue; // 继续等待下一个消息
        }
        buffer[n] = '\0'; // 确保字符串正确终止
        printf("Received from %s:%d -> %s\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port),
               buffer);
        // 将接收到的数据回显给客户端
        sendto(server_fd, buffer, strlen(buffer), 0,
               (struct sockaddr *)&client_addr, client_addr_len);
        printf("Echoed back to client.\n");
    }
    // 4. 关闭套接字 (虽然上面的 while(1) 永远不会退出)
    close(server_fd);
    return 0;
}

2 UDP 客户端代码 (client.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 8080
#define SERVER_IP "127.0.0.1" // 本地回环地址
#define BUFFER_SIZE 1024
int main() {
    int sock_fd;
    struct sockaddr_in server_addr;
    char buffer[BUFFER_SIZE] = {0};
    // 1. 创建套接字
    if ((sock_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    // 设置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    // 将 IP 地址字符串转换为网络字节序的二进制格式
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        perror("invalid address/ Address not supported");
        exit(EXIT_FAILURE);
    }
    // 2. 循环发送和接收数据
    while (1) {
        printf("Enter a message to send (or 'exit' to quit): ");
        fgets(buffer, BUFFER_SIZE, stdin);
        // 如果输入 "exit",则退出程序
        if (strncmp(buffer, "exit", 4) == 0) {
            break;
        }
        // 发送数据到服务器
        sendto(sock_fd, buffer, strlen(buffer), 0,
               (struct sockaddr *)&server_addr, sizeof(server_addr));
        printf("Message sent.\n");
        // 接收服务器的回显
        int n = recvfrom(sock_fd, buffer, BUFFER_SIZE, 0, NULL, NULL);
        // 注意:这里我们不需要知道是谁发来的,因为我们只和一个服务器通信
        // 所以可以传入 NULL 和 NULL
        if (n < 0) {
            perror("recvfrom failed");
            continue;
        }
        buffer[n] = '\0';
        printf("Received from server -> %s\n", buffer);
    }
    // 3. 关闭套接字
    close(sock_fd);
    return 0;
}

代码编译与运行

  1. 保存文件:将服务端代码保存为 server.c,客户端代码保存为 client.c

  2. 编译:打开终端,使用 gcc 进行编译,需要链接 pthread 库(虽然这个例子没用,但通常网络程序会用到,养成好习惯)。

    # 编译服务端
    gcc server.c -o server
    # 编译客户端
    gcc client.c -o client
  3. 运行

    udp socket c语言
    (图片来源网络,侵删)
    • 首先启动服务端

      ./server

      你会看到输出:Server listening on port 8080...

    • 然后启动客户端(可以在新的终端窗口中启动):

      ./client

      客户端会提示你输入消息。

      udp socket c语言
      (图片来源网络,侵删)
    • 交互示例

      # 客户端终端
      Enter a message to send (or 'exit' to quit): Hello, UDP Server!
      Message sent.
      Received from server -> Hello, UDP Server!
      Enter a message to send (or 'exit' to quit): This is a test.
      Message sent.
      Received from server -> This is a test.
      Enter a message to send (or 'exit' to quit): exit
    • 服务端终端会显示:

      Server listening on port 8080...
      Received from 127.0.0.1:54321 -> Hello, UDP Server!
      Echoed back to client.
      Received from 127.0.0.1:54321 -> This is a test.
      Echoed back to client.

常见问题与注意事项

  1. 无连接性

    • UDP 是无连接的。sendto 的第一次调用并不会建立连接,它只是把数据包发出去。recvfrom 也可以接收来自任何地址的数据包,这使得 UDP 编程比 TCP 简单,但也需要开发者自己处理地址管理和数据包的来源验证。
  2. 不可靠性

    UDP 不保证数据包的顺序,也不保证数据包一定能到达,如果应用层需要可靠传输,必须自己实现序列号、确认、重传等机制(在 UDP 之上实现一个简单的可靠协议)。

  3. 数据包大小限制

    UDP 数据包有理论上的最大大小(约 65,507 字节,包括 8 字节的 UDP 头和 20 字节的 IP 头),在以太网等链路上,实际传输的有效载荷通常限制在 1472 字节左右,以避免分片,发送大数据时需要进行分片和重组。

  4. 阻塞 vs. 非阻塞

    • recvfrom 默认是阻塞的,即如果没有数据到达,程序会一直等待,可以通过 fcntlioctl 等函数将套接字设置为非阻塞模式,这样 recvfrom 会立即返回,如果没有数据则返回错误码 EAGAINEWOULDBLOCK
  5. 广播与多播

    • UDP 支持广播和多播,通过设置 sin_addr.s_addrINADDR_BROADCAST (广播地址) 或加入多播组,一个数据包可以发送到网络中的多个主机。
  6. 错误处理

    • 网络编程中,系统调用(如 sendto, recvfrom)可能会因为各种原因失败(网络中断、目标不可达等),必须检查它们的返回值,并根据错误码(通过 perrorstrerror 打印)进行适当的错误处理。
-- 展开阅读全文 --
头像
C语言UDP Socket编程如何实现?
« 上一篇 今天
织梦如何正确读取session数据?
下一篇 » 今天

相关文章

取消
微信二维码
支付宝二维码

目录[+]