C语言UDPsocket如何实现通信?

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

我们将分步介绍,从最基础的 socket 创建,到发送和接收数据,最后是一个完整的客户端/服务器示例。

C语言 udpsocket
(图片来源网络,侵删)

核心概念与流程

UDP 编程的核心是 套接字,它是一个通信端点,应用程序通过它发送和接收数据。

服务器端流程

  1. 创建套接字:使用 socket() 函数创建一个 UDP 套接字。
  2. 绑定地址和端口:使用 bind() 函数将套接字与一个特定的 IP 地址和端口号绑定,这样客户端才知道数据该发到哪里。
  3. 接收数据:使用 recvfrom() 函数在一个循环中等待接收来自客户端的数据,这个函数会阻塞程序,直到有数据到达。
  4. 处理数据并发送响应:对收到的数据进行处理后,可以使用 sendto() 函数将响应数据发回给客户端。
  5. 关闭套接字:通信结束后,使用 close() 函数关闭套接字。

客户端流程

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

关键函数详解

socket()

创建一个套接字。

#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int socket(int domain, int type, int protocol);
  • domain: 地址族,对于网络通信,使用 AF_INET (IPv4) 或 AF_INET6 (IPv6)。
  • type: 套接字类型,对于 UDP,使用 SOCK_DGRAM (数据报)。
  • protocol: 协议,当 type 为 SOCK_DGRAM 时,通常设置为 IPPROTO_UDP
  • 返回值:成功返回一个套接字描述符(一个非负整数),失败返回 -1。

bind()

将套接字与一个本地地址(IP 和端口)绑定。

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
  • sockfd: socket() 返回的套接字描述符。
  • addr: 指向 sockaddr 结构体的指针,对于 IPv4,我们通常使用 struct sockaddr_in,然后将其地址强制转换为 struct sockaddr *
  • addrlen: addr 结构体的长度,即 sizeof(struct sockaddr_in)
  • 返回值:成功返回 0,失败返回 -1。

sendto()

通过 UDP 套接字发送数据。

C语言 udpsocket
(图片来源网络,侵删)
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: 指向目标地址(struct sockaddr_in)的指针。
  • addrlen: 目标地址的长度,即 sizeof(struct sockaddr_in)
  • 返回值:成功返回实际发送的字节数,失败返回 -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: 指向一个 struct sockaddr_in 结构体的指针,用于存储发送方的地址信息,如果不需要,可以设为 NULL
  • addrlen: 指向一个 socklen_t 变量的指针,它应该在调用时设置为 sizeof(struct sockaddr_in),调用后会被修改为实际地址的长度。
  • 返回值:成功返回接收到的字节数,如果对端关闭连接,返回 0(UDP 本身无连接,此情况较少见),失败返回 -1。

sockaddr_in 结构体

这是 IPv4 地址的核心结构体,定义在 <netinet/in.h> 中。

struct sockaddr_in {
    short            sin_family;   // 地址族,必须设置为 AF_INET
    unsigned short   sin_port;     // 端口号,需要用 htons() 转换
    struct in_addr  sin_addr;     // IP 地址,需要用 inet_addr() 或 inet_pton() 转换
    char             sin_zero[8];  // 填充字节,必须为 0
};
// in_addr 结构体只包含一个成员
struct in_addr {
    unsigned long s_addr; // 32 位 IPv4 地址
}

重要函数

  • htons(): Host to Network Short (16位),将主机字节序转换为网络字节序(大端序)。
  • htonl(): Host to Network Long (32位)。
  • inet_addr(): 将点分十进制的 IP 地址字符串(如 "127.0.0.1")转换为 in_addr 结构体所需的格式。
  • inet_pton(): 更现代、更安全的 IP 地址转换函数,支持 IPv4 和 IPv6。

完整代码示例

下面是一个简单的 "Echo" (回声) 服务器和客户端的例子,客户端发送一条消息,服务器接收后原样返回。

C语言 udpsocket
(图片来源网络,侵删)

服务器端代码 (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 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_addr.s_addr = INADDR_ANY; // 绑定到所有可用的网络接口
    server_addr.sin_port = htons(PORT); // 端口号,转换为网络字节序
    if (bind(server_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    printf("UDP Server listening on port %d...\n", PORT);
    // 3. 循环接收数据
    while (1) {
        // 清空缓冲区
        memset(buffer, 0, BUFFER_SIZE);
        // 4. 接收客户端数据
        // recvfrom 会阻塞,直到收到数据
        int n = recvfrom(server_fd, buffer, BUFFER_SIZE, 0,
                         (struct sockaddr *)&client_addr, &client_addr_len);
        if (n < 0) {
            perror("recvfrom failed");
            continue; // 继续等待下一个数据包
        }
        printf("Received from %s:%d -> %s\n",
               inet_ntoa(client_addr.sin_addr),
               ntohs(client_addr.sin_port),
               buffer);
        // 5. 将接收到的数据回发客户端
        sendto(server_fd, buffer, strlen(buffer), 0,
               (struct sockaddr *)&client_addr, client_addr_len);
    }
    // 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 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};
    char *message = "Hello from UDP Client!";
    // 1. 创建套接字
    if ((sock_fd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 2. 设置服务器地址信息
    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("inet_pton failed");
        exit(EXIT_FAILURE);
    }
    // 3. 发送数据到服务器
    sendto(sock_fd, message, strlen(message), 0,
           (struct sockaddr *)&server_addr, sizeof(server_addr));
    printf("Message sent to server.\n");
    // 4. 接收服务器的响应
    int n = recvfrom(sock_fd, buffer, BUFFER_SIZE, 0, NULL, NULL);
    // 注意:如果不需要知道是谁发的,可以把 src_addr 和 addrlen 设为 NULL
    if (n < 0) {
        perror("recvfrom failed");
        exit(EXIT_FAILURE);
    }
    printf("Server replied: %s\n", buffer);
    // 5. 关闭套接字
    close(sock_fd);
    return 0;
}

如何编译和运行

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

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

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

    • 第一步:在一个终端窗口中启动服务器,服务器会一直运行,等待客户端连接。
      ./udp_server

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

    • 第二步:在另一个终端窗口中启动客户端。
      ./udp_client
    • 第三步:观察两个终端的输出。
      • 客户端终端会显示:
        Message sent to server.
        Server replied: Hello from UDP Client!
      • 服务器终端会显示:
        Received from 127.0.0.1:54321 -> Hello from UDP Client!

        (注意:客户端的端口号 54321 是系统临时分配的,每次运行可能不同)


常见问题与注意事项

  1. 字节序:在将端口号和 IP 地址填充到 sockaddr_in 结构体中时,必须使用 htons()inet_addr()/inet_pton() 等函数将主机字节序转换为网络字节序,这是一个非常常见的错误。
  2. 地址已在使用:如果服务器程序异常退出,端口可能处于 TIME_WAIT 状态,导致下次启动时 bind 失败,解决方法是在 bind 之前使用 setsockopt 设置 SO_REUSEADDR 选项,如示例代码所示。
  3. recvfrom 的阻塞recvfrom 是一个阻塞函数,如果没有数据到达,程序会卡在这里,如果需要非阻塞方式,可以使用 fcntlioctlsocket (Windows) 将套接字设置为非阻塞模式。
  4. 无连接性:UDP 是无连接的。sendto 可以向任何地址发送数据,而无需事先建立连接,同样,recvfrom 可以从任何地址接收数据。
  5. 数据大小限制:UDP 数据报有理论上的最大长度(约 64KB),但实际网络中为了分片和重组的效率,建议限制在 512 字节到 1472 字节之间(对于以太网 MTU 为 1500 字节的情况)。
  6. 错误处理:网络编程中,函数调用失败是常态,务必检查每个函数的返回值,并根据错误码(通过 perror 打印)进行适当的错误处理。
-- 展开阅读全文 --
头像
dede自增标签的使用
« 上一篇 03-02
Binsearch C语言如何高效实现?
下一篇 » 03-02

相关文章

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

目录[+]