【反思】一个价值两天的BUG,无论工作还是学习C语言的朋友都看看吧!

 

博文原创,转载请联系博主!

 

使用C语言也有两个年头了,BUG写出来过不少,也改过不少BUG。但是偏偏就是有这么一个BUG让我手头的项目停工了两天,原因从百度找到谷歌,资料从MAN手册找到RFC也没有找到问题的原因,但是真正发现BUG原因之后实在是让自己汗颜。

不管如何,决定把这个BUG写进博文,也是给学习C语言的朋友们提个醒,查看BUG的眼光不要太高,思考问题要自底向上思考。

 

具体项目在我的github里:  https://github.com/yue9944882/HttpAccelerater

 

 

正文:

 

问题大致发现是这样的,在这个HTTP下载器中,实质上编程逻辑都是在传输层TCP套接字上完成的,具体细节就不多提,在通讯过程中是通过一对send和recv函数完成的,recv函数原型如下所示:(linux环境<sys/socket.h>)

 

int recv( _In_ SOCKET s, _Out_ char *buf, _In_ int len, _In_ int flags);

 

sockfd:   接收端套接字描述符

buff:     用来存放recv函数接收到的数据的缓冲区

nbytes:   指明buff的长度

flags:     一般置为0

返回值:  recv函数返回其实际copy的字节数

 
flags 说明 recv send
 MSG_DONTROUTE 绕过路由表查找      •
 MSG_DONTWAIT 仅本操作非阻塞    •       •
 MSG_OOB     发送或接收带外数据   •   •
 MSG_PEEK   窥看外来消息   •  
 MSG_WAITALL   等待所有数据    •  

 

  问题就是出在recv函数的返回值这里,因为和HTTP服务器的通讯过程中,服务器端所返回的内容不仅仅是包括一个所请求的文件数据,还有http的报头,而且在recv得到的数据中HTTP首部和实体是混合在一起的,所以就需要我们用\r\n\r\n四个字符作为标志来检测HTTP首部的结束,而且又因为recv得到的数据并不是完整地填充进接收数据的缓冲区中的,所以我们计算接收到的文件的第一段数据的偏移是这样的:

 

[ HTTP首部结束的偏移,实际读进缓冲区的偏移 ]

 

因为HTTP首部长度远远小于缓冲区长度就忽略首部填满缓冲区的情况。

 

recv函数是得到实际读进缓冲区偏移的关键,可是实际调试过程中每次recv返回的值都是0!

可是缓冲区里面却读进了请求的数据,里面也有完整的HTTP首部,这究竟是为什么呢?

 

  那么我们查看一下recv函数返回0的具体原因:

 

recv() returns 0 only when you request a 0-byte buffer or the other peer has gracefully disconnected.

 

  首先我们的缓冲区确确实实写进了数据,就谈不上0-byte buffer,另一种情况就是TCP连接的正常关闭,即服务器端发送FIN包,但是如下所示,我们的代码中有后续的while循环仍然接受到了服务器端传送的数据,代码如下所示:

 

    while(curPos<gURLinfo.llContentLen){
        dr=recv(sockdesc,recvBuf,4096,0);
        if(dr+curPos>gURLinfo.llContentLen){
            dw=pwrite(file,recvBuf,gURLinfo.llContentLen-headlength-curPos,curPos);    
            curPos+=dw;
            //printf("offset:\t%d\ndw:\t%d\n",curPos,dw);
            break;
        }else{
            dw=pwrite(file,recvBuf,dr,curPos);
            curPos+=dw;
        }
        //printf("offset:\t%d\ndw:\t%d\n",curPos,dw);
    }

 

  在这里recv返回的dr值竟然是正常的非零正值--从内核读取进缓冲区的字节数! 那么我们自然就会开始认为是recv函数的第一次使用才会返回0,之后的使用不会再出现问题。于是我就用了一个“弄巧成拙的办法”:首先使用bzero函数将缓冲区填充满0,再在缓冲区中寻找以‘\0’为结束标志进行扫描,扫描结束的时候得到缓冲区内实际字节的长度。但是实际测试的时候发现,这个办法用于下载纯粹的txt格式的文件是没有问题的,然而当下载二进制文件例如图片,压缩后文件的时候,就会出现问题!ps:这样下载下来的图片竟然还是偏红的,害得我去图片编码区RBG值寻找BUG真相,浪费了很多时间。

  既然总是返回0,我们来查看一下是否是有错误发生吧,于是加入了<errno.h>,结果输出出来了errno还是0,也就是无错误发生!

 

 

最终这个问题的解决过程是这样的:

 

1.使用wget下载完整的图片。

2.使用 vim -b 图片 和正常的图片进行对比(:%!xxd 查看),发现和正常图片不同之处,在于一些空白0字段的填充

3.进而发现还是第一次recv函数导致的文件内容不对

4.最终问题锁定在了这样一段代码,也是让我最汗颜的:

 

    if(dr=recv(sockdesc,recvBuf,4096,0)==-1){
        fprintf(stderr,"Header Recving Failure!\n");
        exit(-1);
    }

 

这是recv函数第一次接收读取字节数的代码,也就是这段代码导致了recv读取字节数的不可知,返回值永远为0。相信C语言的老手已经看出来了,这段代码的问题:

 

关系运算符优先级大于赋值运算符!!!

 

真正的正确的代码应该是这样写的:

 

    if((dr=recv(sockdesc,recvBuf,4096,0))==-1){
        fprintf(stderr,"Header Recving Failure!\n");
        exit(-1);
    }

 

C语言写多了,有些代码会越写越简练,比如声明和运算混写,函数参数局部全局混写,但是对于关系运算符的优先级是最不能忽略的,无论是哪个语言,哪怕是运算符关系符最混乱的perl,也要牢牢记住每个优先级和结合性!

 

 

 

posted @ 2015-09-12 12:20  Kimmin  阅读(6705)  评论(12编辑  收藏  举报