前言

在前几天陆续把webserver的线程池、http处理、日志系统、定时器、mysql连接池、配置识别几个模块的框架代码写好后,今天就正式开始调试了。刚开始遇到了日志文件路径和MySQL初始化两个小问题,但都比较好定位也容易解决,这里不再赘述。要是接下来运行时遇到的http报文解析问题。

关于状态机

在《Linux高性能服务器编程》中提到,http的处理可以看作状态机。对于状态机,我的理解是:

  • 整体架构是一个循环中的多分支if-elseif-else或者switch
  • 条件需要涵盖所有可能的状态,对else(或default)也要谨慎处理
  • 对每个状态做出相应的处理以转移到下一个状态或终止状态
  • 每次只能从一个状态转移到另一个状态

状态机最大的特点是确定性,在不存在随机因素的情况下,理论上可以退出所有的状态和状态转移过程。
在今天的教训之后也明白了关于状态机的另一个特点:状态的连续性。通俗来说,就是除了初始状态之外,每个状态都是之前所有状态的延续。如果一个状态链中间部分的某个状态节点出现小的bug,有可能在下一个状态立即露出端倪,但也更有可能这个bug卧薪尝胆,在之后好远的一个状态节点突然袭击导致程序崩溃。

苦痛经历

今天debug了一上午的bug就是这样一个卧薪尝胆的bug:在成功运行web服务器之后,网页总是显示不出来,在最开始的debug过程中发现直接原因是http报文解析结果不对。正常结果应该是GET方法对应的状态,再通过对应的命令将状态机转移到可写状态;但总是提前出现终止状态,以至于无法转移到可写状态给客户端发送网页内容。
http解析部分的主要状态转移是:解析请求行-解析header-(处理GET)或(解析body)或(终止)。由于程序总是在解析header之后转移到终止而不是处理GET状态,我断定是解析header部分出了问题,于是就在解析header函数里一行一行地debug。结果当然是没有一点卵用,看着奇奇怪怪的读取缓存区一脸懵逼。之后,我尝试扩大debug范围,结果当然还是无效。
直到休息一会儿,我再开始一行一行地debug时,最终在第二遍发现了罪魁祸首:

HTTP_STATE parseHeader(char* text) {
    if (*text = '\0') {  // 应该是 *text == '\0'当然最好写成 '\0' == *text
        if (content_len_ == 0) {
            return GET_REQUEST;
         } else {
            return CONTENT_STATUS;
        }
    } else {
        ...
    }
}

这个函数是解析头部信息的函数,基本逻辑就是根据传入的字符指针确定是哪一类头部然后读取,之后将字符指针往后移到下一个头部,状态类型不变;当传入的字符指针指向结尾'\0'时说明头部读取完毕,需要转移到下一个状态类型。
然而我粗心地把*text == '\0'写成了text = '\0',赋值表达式的返回结果是等号右边数据,也就是0,于是这条唯一通向GET状态的条件分支就被堵死了,我2h的时间也被这个小小的=号掠夺了。

悲惨教训

这时候我才明白,在网上看的一些开源项目里,为什么许多相等的条件判断都把常量写在==的左边。因为常量是右值,不能在=的右边,否则会编译错误。于是如果有人再像我一样粗心,做出少打一个=号的暴行时,就会被编译器精准地识别出来...虽然把常量写在=右边确实不符合现实习惯,但总比debug了2h才找到bug位置好得多。
这时候我又突然想到,早在看《Effective C++》时侯捷老师就有提到这个问题,甚至进一步深入探讨了函数和运算符重载时的情况,并给出了令函数返回const类型的解决方案:

class A { ... };
const A operator*(const A& lhs, const B& rhs); // √
A operator*(const A& lhs, const B& rhs); // ×

const可能会出现错误:

// (a * b) = c 给乘积赋值
A a, b, c;
if (a * b = c) { ... } 

Longfar
1 声望3 粉丝