[错误经验 坑]关于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
。
- 必须为 无连接协议(如 UDP)的套接字,或未调用
2. void *buf
- 作用:指向接收数据的缓冲区。
- 要求:
- 缓冲区必须由用户预先分配内存,大小至少为
len
字节。 - 数据以二进制形式存储,需根据协议自行解析。
- 缓冲区必须由用户预先分配内存,大小至少为
3. size_t len
- 作用:缓冲区
buf
的最大容量。 - 要求:
- 必须小于或等于缓冲区的实际大小,否则可能引发缓冲区溢出。
- 若数据长度超过
len
,多余部分会被丢弃(UDP 下)。
4. int flags
- 作用:控制接收行为的标志位,多个标志可通过
|
组合。 - 常见标志:
标志 说明 MSG_WAITALL
阻塞直到请求的 len
字节全部接收完成(通常用于流式协议如 TCP)MSG_DONTWAIT
非阻塞模式:若无数据可读,立即返回 EAGAIN
或EWOULDBLOCK
MSG_PEEK
窥视数据但不从内核缓冲区移除(下次调用仍可读到相同数据) MSG_TRUNC
若数据超过 len
,返回实际数据长度(需检查返回值是否大于len
)
5. struct sockaddr *src_addr
- 作用:输出参数,保存发送方的地址信息(如 IP 和端口)。
- 要求:
- 若无需获取发送方地址,可设为
NULL
(此时addrlen
也应为NULL
)。 - 需预先分配足够大的内存(如
struct sockaddr_in
或struct 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服务端和客户端)
//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)结构体的大小!
完~