Socket 编程——recv() 无法获取所有数据
Posted
技术标签:
【中文标题】Socket 编程——recv() 无法获取所有数据【英文标题】:Socket Programming — recv() cannot get all data 【发布时间】:2021-04-25 02:26:23 【问题描述】:我正在学习C语言的socket编程,这是我在学习过程中遇到的一个难以理解的问题。
今天我尝试向托管 Apache 示例网站的测试服务器发送 HTTP 请求,然后接收来自测试服务器的响应。这是我的接收代码的一部分。
unsigned long recv_size = 0;
unsigned long response_size = 4096;
int ret = 0;
char *recv_buff = (char *)malloc(response_size);
while (1)
// ret = recv(socket, recv_buff, response_size, MSG_WAITALL); // cannot get all data
ret = read(socket, recv_buff, response_size); // same effect as the above
recv_size += ret;
if (ret < 0)
error(strerror(errno));
else if (ret == 0)
break; // all data recved
我用burpsuit测试的正常结果是这样的。
但是我用 C 语言程序收到的是不完整的数据。
我搜索了一个晚上的原因,但我仍然没有找到解决我的问题的方法。不管是把buff设置成超大尺寸还是其他方式,都无法接受完整的数据。
wireshark 监控的流量没问题,但是我的程序还是收不到完整的数据。有什么问题?
如果您知道原因,请告诉我。谢谢。 (o゜▽゜)o☆
更新
while
循环会执行两次,第一次ret
的值是3343
,第二次是0
,所以循环到这里就停止了。
【问题讨论】:
您在真实代码中使用recv_buff
执行的操作是否未包含在此处?因为每次调用read()
都会覆盖第一个ret
字节...
'if (ret == 0)' 不是 HTTP 1.1 的正确条件
....虽然 Content-Length 为 3041,但我希望所有数据都可以在一次读取中放入您的缓冲区中,(尽管,'cos TCP 流,不能远程保证)。为什么你没有打印出 'ret' 作为调试措施?
read
不必一次返回所有数据。允许返回随机数量的数据,你要做好准备。
@Shawn 由于recv_buff申请了内存,所以recv_buff直到调用recv()函数才进行任何操作。( ̄﹏ ̄)
【参考方案1】:
您可以在套接字上进行简短读取。
但是,您要处理的代码存在一些问题。
您正在分配大小为response_size
的缓冲区。您总是读取该数量,而不是将读取的数量减去您在之前的循环迭代中已经读取的数量。
这可能会导致您读取超出缓冲区末尾的内容,从而导致 UB(未定义行为)。
您的终止条件是if (ret == 0)
。如果另一个数据包“提前”到达,这可能会失败。你永远不会看到 ret
为 0,因为来自 next 数据包的部分数据会使其非零
这是更正后的代码:
#if 0
unsigned long recv_size = 0;
#endif
unsigned long response_size = 4096;
int ret = 0;
char *recv_buff = (char *) malloc(response_size);
#if 1
unsigned long remaining_size = response_size;
unsigned long offset = 0;
#endif
for (; remaining_size > 0; remaining_size -= ret, offset += ret)
ret = read(socket, &recv_buff[offset], remaining_size);
if (ret < 0)
error(strerror(errno));
更新:
上面的代码纠正了一些问题。但是,对于可变长度的源 [例如http
],我们一开始并不知道要阅读多少。
因此,我们必须解析标头并查找“Content-Length”字段。这将告诉我们要阅读多少。
因此,我们希望为标题提供面向行的输入。或者,管理我们自己的缓冲区
假设我们可以解析该值,我们必须等待空行表示有效负载的开始。然后,我们可以循环计算那个确切的数量。
这里有一些代码尝试解析头部并保存有效负载。我已经编码了它,但没有编译它。所以,你可以把它当作伪代码:
unsigned long recv_size = 0;
unsigned long response_size = 4096;
char *recv_buff = malloc(response_size + 1);
// line oriented header buffer
char *endl = NULL;
unsigned long linelen;
char linebuf[1000];
int ret = 0;
// read headers
while (1)
// fill up a chunk of data
while (recv_size < response_size)
recv_buff[recv_size] = 0;
// do we have a line end?
endl = strstr(recv_buff,"\r\n");
if (endl != NULL)
break;
ret = read(socket, &recv_buff[recv_size], response_size - recv_size);
if (ret < 0)
error(strerror(errno));
if (ret == 0)
break;
recv_size += ret;
// error -- no line end but short read
if (endl == NULL)
error(strerror(errno));
// copy header to work buffer
linelen = endl - recv_buff;
memcpy(linebuf,recv_buff,linelen);
linebuf[linelen] = 0;
// remove header from receive buffer
linelen += 2;
recv_size -= linelen;
if (recv_size > 0)
memcpy(recv_buff,&recv_buff[linelen],recv_size);
// stop on end of headers (back to back "\r\n")
if ((recv_size >= 2) && (recv_buff[0] == '\r') && (recv_buff[1] == '\n'))
memcpy(recv_buff,&recv_buff[2],recv_size - 2);
recv_size -= 2;
break;
// parse line work buffer for keywords ... (e.g.)
content_length = ...;
// save payload to file
while (content_length > 0)
// write out prior payload amount
if (recv_size > 0)
write(file_fd,recv_buff,recv_size);
content_length -= recv_size;
recv_size = 0;
continue;
recv_size = read(socket,recv_buff,response_size);
if (recv_size < 0)
error(strerror(errno));
if (recv_size == 0)
break;
更新 #2:
是的,伪代码运行困难,返回值都是乱码
好的,这是我在自己的http
服务器上测试过的简单实用的工作版本。
我必须为您没有发布的部分(例如connect
等)创建自己的例程。
在核心部分,可能对缓冲区滑动代码进行了细微调整 [它在一个位置滑动了额外的 2 个字节],但是,否则它与我之前的版本非常接近
// htprcv/htprcv.c -- HTTP receiver
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <error.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
typedef unsigned char byte;
#define HTPSLIDE(_rmlen) \
recv_size = htpslide(recv_buff,recv_size,_rmlen)
#define _dbgprt(_fmt...) \
fprintf(stderr,_fmt)
#if DEBUG || _USE_ZPRT_
#define dbgprt(_lvl,_fmt...) \
do \
if (dbgok(_lvl)) \
_dbgprt(_fmt); \
while (0)
#define dbgexec(_lvl,_expr) \
do \
if (dbgok(_lvl)) \
_expr; \
while (0)
#else
#define dbgprt(_lvl,_fmt...) \
do \
while (0)
#define dbgexec(_lvl,_expr) \
do \
while (0)
#endif
#define dbgok(_lvl) \
opt_d[(byte) #_lvl[0]]
byte opt_d[256];
char *opt_o;
#define HEXMAX 16
// htpconn -- do connect to server
int
htpconn(const char *hostname,unsigned short portno)
struct addrinfo hints, *res;
struct hostent *hostent;
int ret;
char portstr[20];
int sockfd;
/* Prepare hint (socket address input). */
hostent = gethostbyname(hostname);
if (hostent == NULL)
error(1,errno,"htpconn: gethostbyname -- %s\n",hostname);
memset(&hints, 0, sizeof hints);
hints.ai_family = AF_INET; // ipv4
hints.ai_socktype = SOCK_STREAM; // tcp
hints.ai_flags = AI_PASSIVE; // fill in my IP for me
sprintf(portstr, "%d", portno);
getaddrinfo(NULL, portstr, &hints, &res);
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sockfd < 0)
error(1,errno,"htpconn: socket\n");
// do the actual connection
ret = connect(sockfd, res->ai_addr, res->ai_addrlen);
if (ret < 0)
error(1,errno,"htprcv: read header\n");
return sockfd;
// htpslide -- slide buffer (strip out processed data)
size_t
htpslide(char *recv_buff,size_t recv_size,int slidelen)
size_t new_size;
if (slidelen > recv_size)
slidelen = recv_size;
new_size = recv_size - slidelen;
dbgprt(S,"htpslide: slidelen=%d recv_size=%zu new_size=%zu\n",
slidelen,recv_size,new_size);
memcpy(&recv_buff[0],&recv_buff[slidelen],new_size);
return new_size;
// _htphex -- dump a line in hex
void
_htphex(unsigned long off,const void *vp,size_t xlen)
const byte *bp = vp;
int idx;
int chr;
char hexbuf[200];
char alfbuf[200];
char *hexptr = hexbuf;
char *alfptr = alfbuf;
for (idx = 0; idx < HEXMAX; ++idx)
chr = bp[idx];
if ((idx % 4) == 0)
*hexptr++ = ' ';
if (idx < xlen)
hexptr += sprintf(hexptr,"%2.2X",chr);
if ((chr < 0x20) || (chr > 0x7E))
chr = '.';
else
hexptr += sprintf(hexptr," ");
chr = ' ';
*alfptr++ = chr;
*hexptr = 0;
*alfptr = 0;
_dbgprt(" %8.8lX: %s *%s*\n",off,hexbuf,alfbuf);
// htphex -- dump a buffer in hex
void
htphex(const char *buf,size_t buflen,const char *reason)
size_t off = 0;
size_t xlen;
if (reason != NULL)
_dbgprt("htphex: DUMP buf=%p buflen=%zu (from %s)\n",
buf,buflen,reason);
for (; buflen > 0; buflen -= xlen, buf += xlen, off += xlen)
xlen = buflen;
if (xlen > HEXMAX)
xlen = HEXMAX;
_htphex(off,buf,xlen);
// htpsym -- get symbol/value
int
htpsym(char *linebuf,char *sym,char *val)
char *cp;
int match;
dbgprt(H,"htpsym: PARAM linebuf='%s'\n",linebuf);
// FORMAT:
// foo-bar: baz
do
match = 0;
cp = strchr(linebuf,':');
if (cp == NULL)
break;
*cp++ = 0;
strcpy(sym,linebuf);
for (; (*cp == ' ') || (*cp == '\t'); ++cp);
strcpy(val,cp);
match = 1;
dbgprt(H,"htpsym: SYMBOL sym='%s' val='%s'\n",sym,val);
while (0);
return match;
// htprcv -- receive server response
void
htprcv(int sockfd,int fdout)
size_t recv_size = 0;
size_t response_size = 4096;
char *recv_buff = malloc(response_size + 1);
// line oriented header buffer
char *endl = NULL;
size_t linelen;
char linebuf[1000];
ssize_t ret = 0;
off_t content_length = 0;
// read headers
while (1)
// fill up a chunk of data
while (recv_size < response_size)
recv_buff[recv_size] = 0;
// do we have a line end?
endl = strstr(recv_buff,"\r\n");
if (endl != NULL)
break;
// read a chunk of data
ret = read(sockfd,&recv_buff[recv_size],response_size - recv_size);
if (ret < 0)
error(1,errno,"htprcv: read header\n");
if (ret == 0)
break;
recv_size += ret;
dbgprt(R,"htprcv: READ ret=%zd\n",ret);
dbgexec(R,htphex(recv_buff,recv_size,"htprcv/READ"));
// error -- no line end but short read
if (endl == NULL)
error(1,0,"htprcv: no endl\n");
// copy header to work buffer
linelen = endl - recv_buff;
memcpy(linebuf,recv_buff,linelen);
linebuf[linelen] = 0;
// remove header from receive buffer
linelen += 2;
HTPSLIDE(linelen);
// stop on end of headers (back to back "\r\n")
if ((recv_size >= 2) &&
(recv_buff[0] == '\r') && (recv_buff[1] == '\n'))
HTPSLIDE(2);
break;
// parse line work buffer for keywords ...
char sym[100];
char val[1000];
if (! htpsym(linebuf,sym,val))
continue;
if (strcasecmp(sym,"Content-Length") == 0)
content_length = atoi(val);
continue;
// save payload to file
while (content_length > 0)
// write out prior payload amount
if (recv_size > 0)
dbgexec(W,htphex(recv_buff,recv_size,"htprcv/WRITE"));
ret = write(fdout,recv_buff,recv_size);
if (ret < 0)
error(1,errno,"htprcv: write body\n");
content_length -= recv_size;
recv_size = 0;
continue;
// read in new chunk of payload
ret = read(sockfd,recv_buff,response_size);
if (ret < 0)
error(1,errno,"htprcv: read body\n");
if (ret == 0)
break;
recv_size = ret;
free(recv_buff);
// htpget -- do initial dialog
void
htpget(int sockfd,const char *hostname,const char *file)
char *bp;
char buf[1024];
ssize_t resid;
ssize_t xlen;
size_t off;
bp = buf;
if (file == NULL)
file = "/";
bp += sprintf(bp,"GET %s HTTP/1.1\r\n",file);
if (hostname == NULL)
hostname = "localhost";
bp += sprintf(bp,"Host: %s\r\n",hostname);
if (0)
bp += sprintf(bp,"User-Agent: %s\r\n","curl/7.61.1");
else
bp += sprintf(bp,"User-Agent: %s\r\n","htprcv");
bp += sprintf(bp,"Accept: */*\r\n");
bp += sprintf(bp,"\r\n");
resid = bp - buf;
off = 0;
for (; resid > 0; resid -= xlen, off += xlen)
xlen = write(sockfd,buf,resid);
if (xlen < 0)
error(1,errno,"htpget: write error\n");
// main -- main program
int
main(int argc,char **argv)
char *cp;
char *portstr;
unsigned short portno;
int sockfd;
int filefd;
char url[1000];
--argc;
++argv;
//setlinebuf(stdout);
setlinebuf(stderr);
for (; argc > 0; --argc, ++argv)
cp = *argv;
if (*cp != '-')
break;
cp += 2;
switch(cp[-1])
case 'd': // debug options
if (*cp == 0)
cp = "SHRW";
for (; *cp != 0; ++cp)
opt_d[(byte) *cp] = 1;
break;
case 'o': // output file
opt_o = cp;
break;
// get the remote host:port
do
if (argc <= 0)
strcpy(url,"localhost:80");
break;
strcpy(url,*argv++);
--argc;
while (0);
// get remote port number
portstr = strchr(url,':');
if (portstr != NULL)
*portstr++ = 0;
else
portstr = "80";
portno = atoi(portstr);
// open the output file (or send to stdout)
do
if (opt_o == NULL)
filefd = 1;
break;
filefd = open(opt_o,O_WRONLY | O_CREAT,0644);
if (filefd < 0)
filefd = 1;
while (0);
// establish connection
sockfd = htpconn(url,portno);
// send the file request
htpget(sockfd,NULL,"/");
// receive the server response
htprcv(sockfd,filefd);
close(sockfd);
return 0;
【讨论】:
请注意,OP 确实需要解析 Content-Length 的标头。 @MartinJames 是的,我刚刚意识到这一点,并打算调整我的答案 我刚刚尝试了您的第一个解决方案,但程序陷入了无限循环。 Σ(°△°|||)︴ret
第一次是3343
,但下一次和下一次的值变成0
,所以remaining_size-=ret
总是大于0
,无穷无尽循环。
您尝试过 second 解决方案吗?这就是我在更新部分提到的http
所需要的。
@CraigEstey 是的,伪代码很难运行,返回值都是乱码。 . .(⊙﹏⊙)以上是关于Socket 编程——recv() 无法获取所有数据的主要内容,如果未能解决你的问题,请参考以下文章
MFC中socket编程时recv设置超时属性后,如果超时返回值是啥?谢谢大家:)
linux epoll socket recv 不能获取正确的数据