[错误经验 坑]关于UDP服务器和客户端通信使用的recvfrom的输出型参数len没有被初始化导致的问题

发布于:2025-04-11 ⋅ 阅读:(43) ⋅ 点赞:(0)

[错误经验 坑]关于UDP服务器和客户端通信使用的recvfrom的输出型参数len没有被初始化导致的问题
@水墨不写bug

在这里插入图片描述

一、困惑:

看到标题,你一定很困惑,输出型参数?为什么需要初始化?

二、解答:

Linux下搭建UDP通信的时候,会用到recvfrom函数,函数原型如下:

#include <sys/socket.h>

ssize_t recvfrom(
    int sockfd,                // 套接字文件描述符
    void *buf,                 // 接收数据的缓冲区
    size_t len,                // 缓冲区的最大容量
    int flags,                 // 控制接收行为的标志位
    struct sockaddr *src_addr, // 发送方的地址信息(输出参数)
    socklen_t *addrlen         // 地址结构体的长度(输入/输出参数)
);

recvfrom 是 Linux 网络编程中用于 接收数据报 的核心系统调用,常用于无连接的套接字(如 UDP)。它不仅能接收数据,还能获取发送方的地址信息。以下是其函数原型及各参数的详细说明:


(1)函数原型

#include <sys/socket.h>

ssize_t recvfrom(
    int sockfd,                // 套接字文件描述符
    void *buf,                 // 接收数据的缓冲区
    size_t len,                // 缓冲区的最大容量
    int flags,                 // 控制接收行为的标志位
    struct sockaddr *src_addr, // 发送方的地址信息(输出参数)
    socklen_t *addrlen         // 地址结构体的长度(输入/输出参数)
);

1. int sockfd

  • 作用:已创建的套接字文件描述符。
  • 要求
    • 必须为 无连接协议(如 UDP)的套接字,或未调用 connect 的面向连接协议套接字。
    • 若套接字已通过 connect 建立连接(如 TCP),应使用 recv 而非 recvfrom

2. void *buf

  • 作用:指向接收数据的缓冲区。
  • 要求
    • 缓冲区必须由用户预先分配内存,大小至少为 len 字节。
    • 数据以二进制形式存储,需根据协议自行解析。

3. size_t len

  • 作用:缓冲区 buf 的最大容量。
  • 要求
    • 必须小于或等于缓冲区的实际大小,否则可能引发缓冲区溢出。
    • 若数据长度超过 len,多余部分会被丢弃(UDP 下)。

4. int flags

  • 作用:控制接收行为的标志位,多个标志可通过 | 组合。
  • 常见标志
    标志 说明
    MSG_WAITALL 阻塞直到请求的 len 字节全部接收完成(通常用于流式协议如 TCP)
    MSG_DONTWAIT 非阻塞模式:若无数据可读,立即返回 EAGAINEWOULDBLOCK
    MSG_PEEK 窥视数据但不从内核缓冲区移除(下次调用仍可读到相同数据)
    MSG_TRUNC 若数据超过 len,返回实际数据长度(需检查返回值是否大于 len

5. struct sockaddr *src_addr

  • 作用:输出参数,保存发送方的地址信息(如 IP 和端口)。
  • 要求
    • 若无需获取发送方地址,可设为 NULL(此时 addrlen 也应为 NULL)。
    • 需预先分配足够大的内存(如 struct sockaddr_instruct sockaddr_in6)。

6. socklen_t *addrlen

  • 作用
    • 输入:指定 src_addr 缓冲区的最大容量。
    • 输出:实际写入的地址结构体大小。
  • 要求
    • 必须初始化为 src_addr 缓冲区的实际大小(如 sizeof(struct sockaddr_in))。
    • 若实际地址长度超过 *addrlen,地址会被截断(可通过返回值检查)。

(2)返回值

  • 成功:返回接收到的字节数(>=0)。
  • 失败:返回 -1,错误码存储在 errno 中。
  • 特殊场景
    • UDP 数据报长度超过 len:返回实际接收的字节数(可能小于原始长度),需检查 flags 是否包含 MSG_TRUNC
    • 对端关闭连接(TCP):返回 0(需结合协议逻辑处理)。

三、考虑如下情景:(一套用于通信的UDP服务端和客户端)

//udp服务器

#include <iostream>
#include <string>
#include <cstring>

#include <cassert>

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>

int main()
{
    //1.创建套接字socket fd,并且填充结构体信息
    int udpsockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(udpsockfd < 0)
    {
        perror("create sockfd failed!");
        assert(false);   
    }
    std::cout<<"create success!\n"<<std::endl;

    struct sockaddr_in self;
    bzero(&self,sizeof(self));
    self.sin_family = AF_INET;
    self.sin_port = htons(8888);//暂时硬编码
    self.sin_addr.s_addr = INADDR_ANY;

    //2.绑定:设置进内核
    int res = ::bind(udpsockfd,(struct sockaddr*)&self,sizeof(self));
    if(res < 0)
    {
        perror("socket bind fail!");
        assert(false);
    }
    std::cout<<"bind success!\n"<<std::endl;

    //3.收发信息
    //服务器首先是收到信息,并且获得发送信息的客户端的sockaddr信息
    //然后才能往客户端发送信息
    while(true)
    {
        struct sockaddr_in client;
        socklen_t len = sizeof(client);///事先初始化为sizeof client
        std::cout<<"----"<<std::endl<<len<<std::endl<<"----"<<std::endl;
        char buf[1024] = {0};
        ssize_t n = recvfrom(udpsockfd,buf,sizeof(buf) - 1,0,(struct sockaddr*)&client,&len);
        std::cout<<"----"<<std::endl<<len<<std::endl<<"----"<<std::endl;

        //在这里就获取到了客户端sockaddr结构体信息了
        if(n < 0)
        {
            perror("recvfrom fail\n");
            assert(false);
        }
        else
        {
            buf[n] = 0;
            std::cout<<"GetFromClient: "<<buf<<std::endl;
            const char* buf = "hello client!\n";
            ssize_t m = sendto(udpsockfd,buf,strlen(buf),0,(struct sockaddr*)&client,len);
            if(m < 0)
            {
                perror("sendto fail!\n");
                assert(false);
            }
        }
        sleep(1);
    }

    return 0;
}
//udp客户端
#include <iostream>
#include <string>
#include <cstring>

#include <cassert>

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
int main()
{
    //1.创建套接字socket fd,并且填充结构体信息
    int udpsockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(udpsockfd < 0)
    {
        perror("create sockfd failed!");
        assert(false);   
    }
    std::cout<<"create success!\n"<<std::endl;
    //客户端不需要bind,OS自动绑定随机端口

    //2.填充server信息,在后面send的时候需要用到server信息
    struct sockaddr_in server;
    bzero(&server,sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(8888);                      //这里暂时先硬编码
    server.sin_addr.s_addr = inet_addr("127.0.0.1");

    //3.收发数据
    while(true)
    {
        std::string inbuf = "I am client,hello server!";

        ssize_t n = sendto(udpsockfd,inbuf.c_str(),inbuf.size(),0,(struct sockaddr*)&server,sizeof(server));
        if(n < 0)
        {
            perror("sendto fail!");
            assert(false);
        }
        else
        {
            char buf[1024] = {0};
            struct sockaddr_in tem;
            socklen_t len = sizeof(tem);/事先初始化为sizeof tem
            std::cout<<"----"<<std::endl<<len<<std::endl<<"----"<<std::endl;

            ssize_t m = recvfrom(udpsockfd,buf,sizeof(buf)-1,0,(struct sockaddr*)&tem,&len);
            std::cout<<"----"<<std::endl<<len<<std::endl<<"----"<<std::endl;

            if(m < 0)
            {
                perror("recvfrom fail!");
                assert(false);
            }
            else
            {
                buf[m] = 0;
                std::cout<<"GetFromServer: "<<buf<<std::endl;
                std::string toserver = "I Get your message\n";
                if(sendto(udpsockfd,toserver.c_str(),toserver.size(),0,(struct sockaddr*)&tem,len) < 0)
                {
                    perror("sendto fail!");
                    assert(false);
                }
            }
        }
        sleep(1);
    }
    return 0;
}

问题所在

实际在填入len这个参数的时候,需要首先把len初始化为sizeof(tem)(无论是客户端还是服务端),看似len是传地址,是一个输出型参数,但是实际上,它不仅仅起到输出型参数的作用。如果把len按照输出型参数来看待,那么不发送数据还好说,一旦在后文使用了这个len通过sendto来发送数据,就会报错:
在这里插入图片描述


四、结论:

在使用recvfrom之前,冷不能仅仅当作一个输出型参数来看待!也需要把len初始化为传进去的(sockaddr_in)结构体的大小!


完~
在这里插入图片描述