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 不能获取正确的数据

Python网络编程之socket之send和recv原理剖析

C ++ Socket recv无法跟上某些计算机上的发送

socket编程·send和recv

通过 send() recv() 发送多条消息,Socket 编程,C