C语言如何实现UDP socket通信?

99ANYc3cd6
预计阅读时长 26 分钟
位置: 首页 C语言 正文

UDP 是一种无连接的、不可靠的、基于数据报的传输协议,相比于 TCP,它的开销更小,速度更快,但数据包可能丢失、重复或乱序,它适用于对实时性要求高、能容忍少量丢包的场景,如视频会议、在线游戏、DNS 查询等。

下面我将分为三个部分:

  1. 核心 API 函数介绍
  2. 完整的服务端和客户端示例
  3. 代码详解与常见问题

核心 API 函数

在 C 语言中,Socket 编程主要使用 <sys/socket.h><netinet/in.h> 等头文件提供的函数。

函数 功能 参数
socket() 创建一个套接字,返回一个文件描述符。 domain (地址族, 如 AF_INET), type (套接字类型, 如 SOCK_DGRAM), protocol (协议, 如 IPPROTO_UDP)
bind() 将套接字与一个特定的 IP 地址和端口号绑定。 sockfd (套接字描述符), addr (指向 sockaddr 结构的指针), addrlen (地址结构体长度)
sendto() 通过 UDP 套接字发送数据,数据包的目标地址和端口在函数中指定。 sockfd, buf (数据缓冲区), len (数据长度), flags, dest_addr (目标地址), addrlen
recvfrom() 通过 UDP 套接字接收数据,可以获取到数据包的来源地址和端口。 sockfd, buf (接收缓冲区), len (缓冲区长度), flags, src_addr (来源地址), addrlen (地址结构体长度)
close() 关闭套接字,释放资源。 sockfd

关键结构体:

  • struct sockaddr: 通用地址结构体,作为 bind, sendto, recvfrom 的参数类型。
  • struct sockaddr_in: 针对 IPv4 的地址结构体,我们通常用它来填充地址信息,然后通过强制类型转换传递给通用地址结构。
struct sockaddr_in {
    short            sin_family;   // 地址族 (Address Family), AF_INET
    unsigned short   sin_port;     // 端口号 (Network Byte Order)
    struct in_addr  sin_addr;     // IP 地址
    char             sin_zero[8]; // 填充字节,保持与 struct sockaddr 大小一致
};
struct in_addr {
    long s_addr; // IPv4 地址,以网络字节序存储
};

字节序转换:

网络数据流采用大端字节序(Big-Endian),而 x86 架构的计算机使用小端字节序(Little-Endian),在设置端口号和 IP 地址时,必须进行转换。

  • htons(): Host to Network Short (16位)
  • htonl(): Host to Network Long (32位)
  • ntohs(): Network to Host Short (16位)
  • ntohl(): Network to Host Long (32位)

完整示例:回显服务器

这是一个经典的例子:客户端发送一条消息给服务器,服务器收到后将原消息发回给客户端。

服务端代码 (udp_server.c)

服务端需要绑定一个固定的 IP 和端口,等待客户端的请求。

#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 sockfd;
    char buffer[BUFFER_SIZE];
    struct sockaddr_in servaddr, cliaddr;
    // 1. 创建 UDP socket
    // AF_INET: IPv4
    // SOCK_DGRAM: UDP
    // 0: 自动选择协议 (IPPROTO_UDP)
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    // 2. 设置服务器地址信息
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET; // IPv4
    servaddr.sin_addr.s_addr = INADDR_ANY; // 监听所有可用的网络接口
    servaddr.sin_port = htons(PORT); // 端口号,转换为网络字节序
    // 3. 将 socket 与服务器地址绑定
    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    int len, n;
    len = sizeof(cliaddr); // cliaddr 的大小
    // 4. 循环接收客户端数据
    while (1) {
        // recvfrom 会阻塞,直到收到数据
        // buffer: 存储接收到的数据
        // len: buffer 的大小
        // cliaddr: 存储客户端的地址信息
        n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0,
                     (struct sockaddr *)&cliaddr, &len);
        // 将接收到的数据以字符串形式结束
        buffer[n] = '\0';
        printf("Message from client: %s\n", buffer);
        printf("Client IP: %s, Port: %d\n", 
               inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
        // 5. 将收到的数据原样发回给客户端
        sendto(sockfd, (const char *)buffer, n, 0,
               (const struct sockaddr *)&cliaddr, len);
        printf("Echo message sent.\n");
    }
    // 6. 关闭 socket (理论上上面的 while(1) 不会退出)
    close(sockfd);
    return 0;
}

客户端代码 (udp_client.c)

客户端不需要绑定固定端口,操作系统会自动分配一个临时端口,它只需要知道服务器的 IP 和端口即可发送数据。

#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 sockfd;
    char buffer[BUFFER_SIZE];
    struct sockaddr_in servaddr;
    // 1. 创建 UDP socket
    if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    // 2. 设置服务器地址信息
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(PORT);
    // 将点分十进制 IP 地址转换为网络字节序的 32 位整数
    if (inet_pton(AF_INET, SERVER_IP, &servaddr.sin_addr) <= 0) {
        perror("invalid address/ Address not supported");
        close(sockfd);
        exit(EXIT_FAILURE);
    }
    // 3. 获取用户输入并发送
    while (1) {
        printf("Enter message to send (or 'exit' to quit): ");
        fgets(buffer, BUFFER_SIZE, stdin);
        // 去掉 fgets 读取的换行符
        buffer[strcspn(buffer, "\n")] = 0;
        if (strcmp(buffer, "exit") == 0) {
            break;
        }
        // 发送数据到服务器
        sendto(sockfd, (const char *)buffer, strlen(buffer), 0,
               (const struct sockaddr *)&servaddr, sizeof(servaddr));
        printf("Message sent.\n");
        // 接收服务器的回显
        int n;
        socklen_t len = sizeof(servaddr);
        n = recvfrom(sockfd, (char *)buffer, BUFFER_SIZE, 0,
                     (struct sockaddr *)&servaddr, &len);
        buffer[n] = '\0';
        printf("Echo from server: %s\n", buffer);
    }
    // 4. 关闭 socket
    close(sockfd);
    return 0;
}

代码详解与常见问题

如何编译和运行?

  1. 将上述两段代码分别保存为 udp_server.cudp_client.c

  2. 打开终端,使用 GCC 进行编译:

    # 编译服务端
    gcc udp_server.c -o server
    # 编译客户端
    gcc udp_client.c -o client
  3. 先运行服务端,它会开始监听端口并等待连接:

    ./server
    # 输出: Server listening on port 8080...
  4. 再运行客户端,它会连接到服务器并发送消息:

    ./client
    # 输出: Enter message to send (or 'exit' to quit): 

    在客户端输入 hello world,然后按回车,你会看到:

    客户端终端:

    Enter message to send (or 'exit' to quit): hello world
    Message sent.
    Echo from server: hello world
    Enter message to send (or 'exit' to quit): 

    服务端终端:

    Server listening on port 8080...
    Message from client: hello world
    Client IP: 127.0.0.1, Port: 54321  // 端口号是动态分配的
    Echo message sent.

常见问题与注意事项

  1. 为什么服务端用 INADDR_ANY INADDR_ANY (值为 0) 是一个通配符地址,表示服务器接受发送到其所有网络接口(如 0.0.1, 168.1.100 等)上的指定端口的 UDP 数据包,这使得服务器无论通过哪个 IP 地址都能被访问,非常灵活。

  2. recvfrom 的最后一个参数 addrlen 这个参数比较特殊,在调用 recvfrom 之前,你需要将 cliaddr 结构体的大小赋给它,在调用之后,它会被修改为实际写入的地址结构体的大小,这是一个“值-结果”(value-result)参数,通常的做法是定义一个变量 len = sizeof(cliaddr),然后将 &len 传递给函数。

  3. UDP 是无连接的 UDP 的 sendtorecvfrom 是成对出现的,你不需要像 TCP 那样先 connect,每次 sendto 都可以指定不同的目标地址,每次 recvfrom 也都可以从不同的来源接收数据。

  4. 缓冲区大小 BUFFER_SIZE 定义了能接收的最大数据包大小,如果客户端发送的数据超过了这个大小,超出部分的数据将被丢弃,可能会导致数据不完整,对于 UDP,需要应用层自己处理分包和组包的逻辑。

  5. 错误处理 示例中的 perror 是一个很好的错误处理方式,它会打印出函数名和系统返回的错误信息(如 "Address already in use"),在实际生产环境中,健壮的错误处理至关重要。

  6. 防火墙 如果程序无法连接,请检查系统的防火墙设置,防火墙可能会阻止特定端口(如 8080)的流量,你可能需要配置防火墙规则来允许 UDP 流量通过该端口。

-- 展开阅读全文 --
头像
Simulink转C代码的关键步骤与注意事项有哪些?
« 上一篇 02-23
LabVIEW如何生成C语言代码?
下一篇 » 02-23

相关文章

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

目录[+]