【反思】一个价值两天的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,也要牢牢记住每个优先级和结合性!
【反思】一个价值两天的BUG,无论工作还是学习C语言的朋友都看看吧!的更多相关文章
- 一个诡异的MySQL查询超时问题,居然隐藏着存在了两年的BUG
这一周线上碰到一个诡异的BUG. 线上有个定时任务,这个任务需要查询一个表几天范围内的一些数据做一些处理,每隔十分钟执行一次,直至成功. 通过日志发现,从凌晨5:26分开始到5:56任务执行了三次,三 ...
- 联想ERP项目实施案例分析(10):回到最初再反思IT价值
联想ERP项目实施案例分析(10):回到最初再反思IT价值 投入上千万(未来每年的维护费也非常高),投入一年实施时间,高级副总裁亲自挂帅,各级业务部门管理者亲自负责.骨干业务人员充当区域IT实施者/推 ...
- 一个历时五天的 Bug
一个程序员在没有成长成为架构师之前,几乎都要跟 Bug为伴,程序员有很多时间都是花在了查找各种 Bug上. 我印象深刻的一个Bug, 是一个服务器网络框架无锁队列的 Bug .那个 Bug 连续查找了 ...
- 一个月薪两万的Web安全工程师要掌握哪些技能?
作为一个薪水两万起步的工作,我想知道这些牛人们都会哪些技能呢? Web安全相关概念.熟悉渗透相关工具.渗透实战操作.关注安全圈动态.熟悉Windows/Kali Linux.服务器安全配置.脚本编程学 ...
- 对于长沙互联网发展,一个外来两年Java程序员的所见所感所愿
惟楚有材,于斯为盛 本文有感于2019长沙互联网求职招聘大会,内容比较多,但都是我自己的一些所见.所感和所愿. 2019年3月的最后一天,参加2019长沙互联网求职招聘大会,看到了很多的招聘企业,也看 ...
- Spring 循环引用(一)一个循环依赖引发的 BUG
Spring 循环引用(一)一个循环依赖引发的 BUG Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) Spring 循环 ...
- android一个下拉放大库bug的解决过程及思考
android一个下拉放大库bug的解决过程及思考 起因 项目中要做一个下拉缩放图片的效果,搜索了下github上面,找到了两个方案. https://github.com/Frank-Zhu/Pul ...
- 从一个跨二十年的glibc bug说起
1. 缘起 这几天调gcc 7.5.0 +glibc 2.23的交叉编译工具链,由于gcc 7.5.0的默认打开Werr,偶然发现了glibc一个隐藏了二十年的世纪大bug. 这个bug在glibc ...
- netty系列之:一个价值上亿的网站速度优化方案
目录 简介 本文的目标 支持多个图片服务 http2处理器 处理页面和图像 价值上亿的速度优化方案 总结 简介 其实软件界最赚钱的不是写代码的,写代码的只能叫马龙,高级点的叫做程序员,都是苦力活.那么 ...
随机推荐
- pods 遇到的问题
解决方法 : product -> sccheme ->pod 点击
- 定时器:Timer:System.Threading.Timer类(转)
最近的一个项目有一些地方需要用到定时功能,在设计过程中,突然发现.net的Timer类居然还有很多我以前没有用过的功能,这里就跟大家分享一下 注:这里的Timer类特指System.Threading ...
- Exception in thread "main" java.util.ConcurrentModificationException
package test; import java.util.ArrayList; import java.util.Iterator; import java.util.List; public c ...
- windows 10右键项添加Notepad++
1.打开注册表编辑器,开始->运行->regedit. 2.在HKEY_CLASSSES_ROOT→ * → Shell 下,在Shell下,新建项命名为Open With Notepad ...
- erlang实现一个进程池 pool
erlang的实现一个简单的进程池. erlang进程是非常轻量级的,这个进程池的主要目的是用一种通用的方式去管理和限制系统中运行的资源占用.当运行的工作者进程数量达到上限,进程池还可以把任务放到队列 ...
- [转]postman 官方文档解说
1. 安装 两种安装方式,我热衷于以chrome插件形式安装 Chrome插件 Mac App 2. 发送请求 Postman最基础的功能就是发送http请求,支持GET/PUT/POST/DELET ...
- iOS 递归锁
原理:递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型.NSRecursiveLock 与 NSLock 的区别在于内部封装的 pthread_mutex_t ...
- We7的区县站点群建设策略
一.解决门户和委办局.乡镇的互动 构建以区县政府门户为主站,各委办局.乡镇为子站的站点群体系: 基于统一的信息体系,实现分级授权.统一管理的功能.各网站能够有独立的页面展现和管理后台,同一时候网站之间 ...
- POJ 3253 Fence Repair(哈夫曼树)
Fence Repair Time Limit: 2000MS Memory Limit: 65536K Total Submissions: 26167 Accepted: 8459 Des ...
- 服务器buff管理
关于buff和玩家自动回血buff { m_StatusTimer.reset(pFT->createTimer()); m_StatusTimer->setInterval(); m_S ...