recvfrom函数如何正确使用?

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

recvfrom 是用于无连接(UDP)套接字编程的核心函数,它不仅可以从网络中接收数据,还能获取发送数据的源地址信息(即“谁”发的)。

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

函数原型

recvfrom 函数通常在 <sys/socket.h> 头文件中声明。

#include <sys/socket.h>
#include <unistd.h> // 用于 ssize_t 类型
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
  • 返回值:

    • 成功时,返回接收到的字节数
    • 如果连接被对端关闭,返回 0
    • 出错时,返回 -1,并设置 errno
  • 参数详解:

    1. sockfd:

      c语言recvfrom
      (图片来源网络,侵删)
      • 接收数据的套接字文件描述符,这个套接字必须是通过 socket() 函数创建的,并且已经通过 bind() 绑定了一个本地端口和地址。
    2. buf:

      • 一个指向缓冲区的指针,接收到的数据将被拷贝到这个缓冲区中,你需要确保这个缓冲区足够大,以存放可能的最大数据包。
    3. len:

      • buf 指向的缓冲区的长度(以字节为单位)。recvfrom 最多读取 len 个字节的数据。
    4. flags:

      • 控制接收行为的标志位,通常设置为 0,表示使用默认行为。
      • 常用标志位:
        • MSG_DONTWAIT: 使 recvfrom 变为非阻塞模式,如果没有数据可读,函数会立即返回 -1,并设置 errnoEAGAINEWOULDBLOCK,而不是阻塞等待。
        • MSG_PEEK: 查看数据,但不从套接字接收缓冲区中移除数据,下一次调用 recvfrom 时会再次读到同样的数据。
        • MSG_WAITALL: 等待直到请求的字节数 len 全部收到,或者发生错误/连接关闭,但这并不保证一定能读取到 len 个字节(对端关闭连接)。
    5. src_addr:

      c语言recvfrom
      (图片来源网络,侵删)
      • 一个指向 struct sockaddr 结构体的指针。recvfrom 会将发送方的地址信息(IP地址和端口号)填充到这个结构体中。
      • 重要提示:如果你不关心发送方的地址,可以将其设置为 NULL
    6. addrlen:

      • 这是一个输入/输出参数(value-result argument)。
      • 输入:在调用 recvfrom 之前,你需要将 src_addr 指向的结构体的长度赋值给 *addrlensizeof(struct sockaddr_in))。
      • 输出:函数返回后,*addrlen 会被设置为实际填充到 src_addr 中的地址信息的长度。
      • 如果你将 src_addr 设置为 NULLaddrlen 也必须设置为 NULL

工作流程

recvfrom 的工作流程可以概括为:

  1. 检查数据: 检查套接字 sockfd 的接收缓冲区中是否有数据。
  2. 阻塞/非阻塞:
    • 如果没有数据且 flags 中没有 MSG_DONTWAITrecvfrom阻塞,直到有数据到达。
    • 如果没有数据且设置了 MSG_DONTWAITrecvfrom 会立即返回 -1
  3. 接收数据: 如果有数据,它会将数据从内核缓冲区拷贝到用户提供的 buf 中,拷贝的字节数不超过 len
  4. 获取源地址: 它会获取数据包的源 IP 地址和端口号,并将这些信息填充到 src_addr 指向的结构体中。
  5. 返回: 返回实际接收到的字节数。

完整示例代码

下面是一个经典的 UDP 客户端/服务器示例,清晰地展示了 recvfrom 的用法。

服务器端 (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, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[BUFFER_SIZE] = {0};
    // 1. 创建套接字
    if ((server_fd = socket(AF_INET, SOCK_DGRAM, 0)) == 0) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }
    // 2. 设置套接字选项,允许地址重用(可选)
    if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
        perror("setsockopt");
        exit(EXIT_FAILURE);
    }
    address.sin_family = AF_INET; // IPv4
    address.sin_addr.s_addr = INADDR_ANY; // 绑定到所有可用的网络接口
    address.sin_port = htons(PORT); // 转换为网络字节序
    // 3. 绑定套接字到端口
    if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
    printf("Server listening on port %d...\n", PORT);
    // 4. 循环接收数据
    while (1) {
        // 清空缓冲区
        memset(buffer, 0, BUFFER_SIZE);
        // 使用 recvfrom 接收数据
        // src_addr 不为 NULL,addrlen 初始化为 sizeof(address)
        int n = recvfrom(server_fd, (char *)buffer, BUFFER_SIZE, 0,
                         (struct sockaddr *)&address, (socklen_t *)&addrlen);
        if (n < 0) {
            perror("recvfrom failed");
            // 可以选择继续循环或退出
            continue;
        }
        // 打印接收到的数据和客户端信息
        printf("Received from %s:%d\n",
               inet_ntoa(address.sin_addr), // 将网络地址转换为字符串
               ntohs(address.sin_port));    // 将网络字节序的端口号转换为主机字节序
        printf("Message: %s\n", buffer);
        // 可以在这里添加回复客户端的逻辑,使用 sendto
        // char *response = "Hello from server";
        // sendto(server_fd, response, strlen(response), 0, (struct sockaddr *)&address, addrlen);
    }
    // 5. 关闭套接字 (理论上不会执行到这里)
    close(server_fd);
    return 0;
}

客户端 (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 = 0;
    struct sockaddr_in serv_addr;
    char buffer[BUFFER_SIZE] = {0};
    // 1. 创建套接字
    if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
        printf("\n Socket creation error \n");
        return -1;
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);
    // 将 IP 地址从字符串转换为网络地址
    if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
        printf("\nInvalid address/ Address not supported \n");
        return -1;
    }
    // 2. 循环发送和接收
    while (1) {
        printf("Enter message for server: ");
        fgets(buffer, BUFFER_SIZE, stdin);
        buffer[strcspn(buffer, "\n")] = 0; // 去掉换行符
        // 发送数据到服务器
        // 注意:对于 UDP 客户端,bind 不是必须的,系统会自动分配一个临时端口
        sendto(sock, buffer, strlen(buffer), 0, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
        printf("Message sent.\n");
        // 接收服务器的回复
        // 对于客户端,我们通常不关心是谁发的(因为我们知道是服务器),src_addr 可以设为 NULL
        memset(buffer, 0, BUFFER_SIZE);
        int n = recvfrom(sock, (char *)buffer, BUFFER_SIZE, 0, NULL, NULL);
        if (n > 0) {
            printf("Server reply: %s\n", buffer);
        } else {
            perror("recvfrom failed or no reply");
        }
    }
    close(sock);
    return 0;
}

recvfrom vs. recv

这是一个非常常见的问题,理解它们的区别至关重要。

特性 recvfrom recv
用途 无连接 (UDP) 套接字 面向连接 (TCP) 套接字
连接状态 不需要 connect(),可以接收来自任何地址的数据 需要 connect() 建立连接后才能使用
源地址 可以获取发送方的地址信息 不能获取,因为连接已经建立,数据只来自已连接的对端。
参数 sockfd, buf, len, flags, src_addr, addrlen sockfd, buf, len, flags
本质 recvrecvfrom 的一个特例,当套接字是 SOCK_STREAM (TCP) 类型时,recv 内部会调用 recvfrom,并忽略地址参数。

常见错误及处理

  1. EAGAINEWOULDBLOCK:

    • 原因: 在非阻塞模式下调用 recvfrom,但当时没有数据可读。
    • 处理: 这是正常行为,不是错误,你的程序应该继续执行其他任务,或者稍后再试。
  2. ECONNREFUSED:

    • 原因: (UDP) 发送了一个数据包,但目标端口上没有应用程序在监听,ICMP "端口不可达" 消息会返回给发送方,导致下一次 recvfrom 失败。
    • 处理: 检查服务器是否正在运行,以及端口号是否正确。
  3. EMSGSIZE:

    • 原因: 接收到的数据包长度超过了套接字缓冲区的大小限制(由 SO_RCVBUF 选项控制)。
    • 处理: 增大套接字缓冲区大小,或者确保发送方不会发送过大的数据包。
  4. EFAULT:

    • 原因: bufsrc_addr 指向了无效的内存地址。
    • 处理: 检查指针是否正确初始化。

希望这份详细的解释能帮助你完全掌握 C 语言中的 recvfrom 函数!

-- 展开阅读全文 --
头像
C语言中sstring是什么?如何实现?
« 上一篇 04-14
overlay C语言如何高效实现内存复用?
下一篇 » 04-14

相关文章

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

目录[+]