【反思】一个价值两天的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,也要牢牢记住每个优先级和结合性!