【问题表现】hydra cgi底层库取出来的参数不对
check out hydra-release_src-3.10.0,拷贝问题参数到单元测试用例,编译运行,表现跟上面贴图一致,确认是库本身的问题。遗憾的是,cgi底层库单元测试本身的用例全都通过。
仔细确认检查,cgi底层库依赖hydra_uri_endecoder里面的ParamsStr2Map接口,用以参数解析,参数解析过程会用到DecodeUri接口,用以uri解码。cgi底层库最近增加了DecodeUri重载版本,并把最原始的DecodeUri接口用新的重载版本封装,基本确认就是这个接口有问题。遗憾的是,这个接口的单元测试也是通过。
修改引起错误的用例,并寻找规律,发现一个“规律”,当连续2个参数中第2个参数的value长度比第一个参数长度要小的时候会触发此问题,第2个参数的值会覆盖第一个参数的值。但是这个规律似乎又不太靠谱,,如果所示,为什么num的value(40)没有覆盖src的value?折腾了很久都没有结果。
当反复修改用例,并加大量debug log辅助检查后,发现一个“真正的规律”,当连续2个参数中第2个参数的value长度比第一个参数长度“小1”的时候会触发此问题,第2个参数的值会覆盖第一个参数的值。修改几组用例并测试,最后证明了此规律的真实性。
代码里面使用了string手册不推荐的做法,直接修改string的buffer,虽然不推荐,但是这个方法本身是“没有”问题的,只要事前事后做好“偏移”即可。现实中,为了减少string拷贝,经常这样做,屡试不爽。再次排除了代码本身有错的可能。
一开始没有想过要尝试其他重载版本,DecodeUri接口有3个重载版本,是层层依赖的,下次接口有问题,基本确认上层接口也有问题。没辙的时候就死马当活马医了,用最上层的string DecodeUri(conststring&uri) 接口编译单元测试并运行,结果奇迹真的出现了,运行结果居然正常!有点不合常理,但是必须尊重现实。
对比发现,这2个版本的区别是,一个返回应用,一个返回拷贝。难道map[DecodeUri(key)] = DecodeUri(value)这样的操作,对于DecodeUri返回引用还是拷贝有差别吗?按常理,map会在内部容器拷贝一份pair<DecodeUri(key)] , DecodeUri(value)>,事实上确实如此,但是跟预想的并不完全一样。
stringktmp, vtmp;
key2value[DecodeUri(key, ktmp)] = DecodeUri(value, vtmp);
难道key2value这个map在内部容器直接引用了ktmp, vtmp,而没有拷贝?
加日志检查,证实了猜想,map中“最后”一次插入的<key、value>的地址跟外部ktmp、vtmp两个string的地址分别相等。
心里终于有点眉目了,原来string使用了"写时拷贝"技术!测试string key = DecodeUri(key, ktmp)这一条语句,打印
查看,发现key跟ktmp的地址相等,进一步证实写时拷贝确实存在。
回顾之前的规律:“当连续2个参数中第2个参数的value长度比第一个参数长度“小1”的时候会触发此问题”,结合代码:
stringktmp, vtmp;
key2value[DecodeUri(key, ktmp)] = DecodeUri(value, vtmp);
DecodeUri第2个参数res引用的是vtmp,vtmp存储的是上一次的结果,第2次的结果(就是这里的uri)长度比上次小1,uri.size() + 1后刚好跟上次的结果长度相等! resize的时候,string发现传进来的size跟内部的size是一致的,认为这不是一个“写”操作,从而不会触发拷贝,结果悲剧的一幕就上演了!修改res的buffer会导致map中最后一次插入的value的buffer被修改,因为它们指向同一个地址。最终导致取出的参数不对。
1. 用相对低效但是靠谱的重载版本(string拷贝版:string DecodeUri(const string&uri) )
2. 强制触发“写时拷贝”,如:
DecodeUri((char*)res.data(), reslen, uri.data(), uri.size())
改为
DecodeUri((char*)&res[0], reslen, uri.data(), uri.size())
左值引用版本【】重载,会触发string内部的写时拷贝,问题得到根本解决