【反思】一个价值两天的BUG,不管工做仍是学习C语言的朋友都看看吧!

 

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

 

使用C语言也有两个年头了,BUG写出来过很多,也改过很多BUG。可是恰恰就是有这么一个BUG让我手头的项目停工了两天,缘由从百度找到谷歌,资料从MAN手册找到RFC也没有找到问题的缘由,可是真正发现BUG缘由以后实在是让本身汗颜。git

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

 

具体项目在个人github里:  https://github.com/yue9944882/HttpAccelerater编程

 

 

正文:vim

 

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

 

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

 

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,也要紧紧记住每一个优先级和结合性!