C语言UDP Socket编程如何实现?

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

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

c语言 socket udp
(图片来源网络,侵删)

下面我将分步讲解 UDP 编程的核心概念、完整的服务端和客户端代码,以及代码的详细解析。


核心概念与函数

UDP 编程主要使用以下几个关键的 Socket API 函数:

  1. socket(): 创建一个套接字,用于网络通信。
  2. bind(): 将套接字与一个特定的 IP 地址和端口号绑定,对于服务器端是必须的,客户端通常不需要显式绑定(由操作系统自动分配)。
  3. sendto(): 向指定的 IP 地址和端口发送数据,这是 UDP 发送数据的核心函数。
  4. recvfrom(): 从网络中接收数据,并可以获取到数据发送方的 IP 地址和端口,这是 UDP 接收数据的核心函数。
  5. close(): (Linux/Unix) 或 closesocket(): (Windows) 关闭套接字,释放资源。
  6. inet_addr(): 将点分十进制的 IP 地址字符串(如 "127.0.0.1")转换为网络字节序的整数格式。
  7. htons(): 将主机字节序的端口号转换为网络字节序,网络协议规定端口号和 IP 地址都必须使用网络字节序(大端序)。

编程步骤

UDP 服务器端 编程步骤

  1. 创建套接字: 使用 socket() 函数创建一个 UDP 类型的套接字。
  2. 绑定地址和端口: 使用 bind() 函数将套接字与服务器本机的 IP 地址和监听端口号绑定。
  3. 接收数据: 在一个循环中,使用 recvfrom() 函数等待并接收客户端发送的数据。
  4. 处理数据 (可选): 对接收到的数据进行处理。
  5. 发送响应 (可选): 使用 sendto() 函数向客户端发送响应数据。
  6. 关闭套接字: 通信结束后,使用 close() 关闭套接字。

UDP 客户端 编程步骤

  1. 创建套接字: 使用 socket() 函数创建一个 UDP 类型的套接字。
  2. 发送数据: 使用 sendto() 函数向服务器的 IP 地址和端口发送数据,客户端不需要 bind()(操作系统会自动分配一个临时端口)。
  3. 接收响应: 使用 recvfrom() 函数接收服务器返回的响应数据。
  4. 关闭套接字: 通信结束后,使用 close() 关闭套接字。

完整代码示例

以下是一个经典的“回显服务器”(Echo Server)的例子,客户端发送任何消息,服务器都会将同样的消息返回给客户端。

服务器端代码 (udp_server.c)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> // 用于 close()
#include <sys/socket.h> // 用于 socket(), bind(), sendto(), recvfrom()
#include <netinet/in.h> // 用于 struct sockaddr_in 和 htons()
#include <arpa/inet.h> // 用于 inet_addr()
#define PORT 8080
#define BUFFER_SIZE 1024
int main() {
    int server_fd, client_len;
    char buffer[BUFFER_SIZE];
    struct sockaddr_in server_addr, client_addr;
    // 1. 创建套接字
    // AF_INET: IPv4
    // SOCK_DGRAM: 数据报套接字 (UDP)
    // 0: 默认协议 (对于 SOCK_DGRAM, 默认就是 UDP)
    if ((server_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation 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; // 绑定所有可用的网络接口
    server_addr.sin_port = htons(PORT); // 绑定端口,htons将主机字节序转为网络字节序
    // 强制绑定端口,避免 "Address already in use" 错误
    int opt = 1;
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    if (bind(server_fd, (const 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) {
        client_len = sizeof(client_addr); // 必须先初始化这个长度
        // 4. 接收客户端数据
        // recvfrom 会将客户端的地址信息存入 client_addr
        int n = recvfrom(server_fd, (char *)buffer, BUFFER_SIZE, 0,
                         (struct sockaddr *)&client_addr, (socklen_t *)&client_len);
        buffer[n] = '\0'; // 确保字符串以 null 
        printf("Received from %s:%d -> %s\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port),
               buffer);
        // 5. 将收到的数据回显给客户端
        sendto(server_fd, (const char *)buffer, n, 0,
               (const struct sockaddr *)&client_addr, client_len);
        printf("Echoed back to client.\n");
    }
    // 6. 关闭套接字 (虽然上面的循环是永久的,但理论上应该在这里执行)
    close(server_fd);
    return 0;
}

客户端代码 (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 SERVER_PORT 8080
#define SERVER_IP "127.0.0.1" // 本地回环地址,用于测试
#define BUFFER_SIZE 1024
int main() {
    int client_fd;
    char buffer[BUFFER_SIZE];
    struct sockaddr_in server_addr;
    // 1. 创建套接字
    if ((client_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket creation failed");
        exit(EXIT_FAILURE);
    }
    memset(&server_addr, 0, sizeof(server_addr)); // 清空结构体
    server_addr.sin_family = AF_INET; // IPv4
    server_addr.sin_port = htons(SERVER_PORT); // 服务器端口
    // 将 IP 地址字符串转换为网络字节序的整数
    if (inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr) <= 0) {
        perror("invalid address/ Address not supported");
        exit(EXIT_FAILURE);
    }
    // 从标准输入读取用户输入
    printf("Enter a message to send to the server (or 'exit' to quit): ");
    while (fgets(buffer, BUFFER_SIZE, stdin) != NULL) {
        // 去掉换行符
        buffer[strcspn(buffer, "\n")] = 0;
        if (strcmp(buffer, "exit") == 0) {
            break;
        }
        // 2. 发送数据到服务器
        sendto(client_fd, (const char *)buffer, strlen(buffer), 0,
               (const struct sockaddr *)&server_addr, sizeof(server_addr));
        printf("Message sent.\n");
        // 3. 接收服务器响应
        int n = recvfrom(client_fd, (char *)buffer, BUFFER_SIZE, 0,
                         NULL, NULL); // 客户端不需要关心是谁发来的,所以可以传NULL
        buffer[n] = '\0';
        printf("Server echoed back: %s\n", buffer);
        printf("\nEnter another message (or 'exit' to quit): ");
    }
    // 4. 关闭套接字
    close(client_fd);
    return 0;
}

代码详解

服务器端 (udp_server.c)

  • struct sockaddr_in server_addr, client_addr;
    • 这是用于存储 IPv4 地址信息的结构体。server_addr 用于服务器自己的信息,client_addr 用于存储连接进来的客户端的信息。
  • socket(AF_INET, SOCK_DGRAM, 0)
    • AF_INET: 指定使用 IPv4 地址族。
    • SOCK_DGRAM: 指定创建数据报套接字,也就是 UDP。
  • INADDR_ANY
    • 这是一个特殊的 IP 地址,表示“所有可用的网络接口”,服务器通常用它来绑定,这样无论客户端是通过 0.0.1(本机)、168.1.100(局域网IP)还是公网IP访问,服务器都能收到。
  • htons(PORT)
    • host to network short,计算机内存中存储数据的方式(字节序)可能与网络标准(大端序)不同。htons 确保端口号在网络传输时是正确的格式,IP 地址的转换函数是 inet_addr
  • recvfrom(server_fd, buffer, ...)
    • 这是 UDP 接收数据的函数,它会阻塞程序,直到收到一个数据包。
    • buffer: 存放接收到的数据。
    • &client_addr: 一个指针,用于存放发送方的地址信息。
    • &client_len: 一个指针,指向 client_addr 结构体的大小。recvfrom 会填充这个地址信息。
  • sendto(..., &client_addr, ...)
    • sendto 不仅可以发送数据,还能指定要发送给谁,这里我们将收到的数据原封不动地发回给刚刚发消息的客户端。

客户端 (udp_client.c)

  • inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr)
    • presentation to network,这是将人类可读的 IP 地址字符串(如 "127.0.0.1")转换为 struct in_addr 所需的二进制格式,这是现代推荐的做法,比旧的 inet_addr 更安全,能处理 IPv6。
  • sendto(..., &server_addr, ...)

    客户端在发送数据时,必须明确指定服务器的地址和端口,这样数据包才能被正确路由。

    c语言 socket udp
    (图片来源网络,侵删)
  • recvfrom(..., NULL, NULL)
    • 在客户端,我们通常只关心从服务器收到的响应,而不关心这个响应来自哪个地址(因为我们只发给了服务器一个地址),可以将后两个参数设为 NULL,表示我们不关心这些信息。

如何编译和运行

你需要使用 GCC 编译器来编译这些 C 代码。

  1. 编译代码 打开终端,运行以下命令:

    # 编译服务器
    gcc udp_server.c -o udp_server
    # 编译客户端
    gcc udp_client.c -o udp_client
  2. 运行程序

    • 启动服务器

      c语言 socket udp
      (图片来源网络,侵删)
      ./udp_server

      你会看到输出:

      Server listening on port 8080...
    • 在另一个新的终端窗口中启动客户端

      ./udp_client

      你会看到提示:

      Enter a message to send to the server (or 'exit' to quit):
    • 进行交互: 在客户端输入一些文字,Hello, UDP Server!,然后按回车。

      • 客户端终端 会显示:

        Message sent.
        Server echoed back: Hello, UDP Server!
        Enter another message (or 'exit' to quit):
      • 服务器端终端 会显示:

        Received from 127.0.0.1:54321 -> Hello, UDP Server!
        Echoed back to client.

        (注意:客户端的端口号 54321 是每次运行时由系统随机分配的,所以你的可能会不同)

    • 输入 exit 即可退出客户端程序,服务器会继续运行,等待下一个客户端连接,你可以按 Ctrl+C 来终止服务器。


UDP 与 TCP 的关键区别总结

特性 UDP (User Datagram Protocol) TCP (Transmission Control Protocol)
连接性 无连接 面向连接
可靠性 不可靠 (不保证顺序、不保证不丢包、不保证不重复) 可靠 (通过确认、重传、排序机制保证数据完整有序)
速度 (开销小,没有建立连接和复杂的控制过程) (开销大,需要三次握手,有拥塞控制等机制)
头部开销 (8字节) (20字节以上,可选项会增大)
应用场景 实时视频/音频、在线游戏、DNS查询、广播 文件传输、网页浏览、电子邮件等要求可靠性的场景
编程模型 sendto / recvfrom connect / send / recv / read / write
-- 展开阅读全文 --
头像
织梦如何集成ueditor编辑器?
« 上一篇 02-13
UDP Socket C语言如何高效通信?
下一篇 » 02-13

相关文章

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

目录[+]