前段时间又重新看了一遍socket编程,心血来潮写了一个mini型的HTTP服务器,这就是monkv。
monkv GitHub地址:https://github.com/cuihang/monkv
古人说的好,纸上得来终觉浅,绝知此事要躬行。很多东西的原理,大家都能侃侃而谈,但具体操作起来,会面临各种各样棘手的小细节。真正的高手,讲起大道理来未必有什么过人之处,因为大的道理谁都会说,真正的高手体现在对具体细节的处理上。所以在这里我记录一下在monkv开发过程中出现的各种小问题,作为自己的一种技术积累,也分享给大家,权作交流。
如何设置epoll
epoll有两种模式,ET和LT模式。至于具体的区别,网上遍地都是讲解,这里不展开,这里为什么会专门提到epoll模式的问题,因为在开发过程中,面临一个小问题,就是如何保证一个请求只被一个线程或者进程解析。
就算没有看过monkv的源码,也能猜到monkv的大体架构,无非是主进程或者主线程监听epoll上的客户端socket可读事件,如果有新的客户端可读事件,就新开一个进程或者线程来处理,这是最传统的多路复用模型。那么问题来了,假设一个客户端上有一个可读事件,我们新开一个线程来处理。然后这个客户端上又发生了一个可读,如果我们再开一个线程,就意味着两个线程同时在处理一个请求,这就需要涉及到复杂的同步机制。所以最好的办法就是保证一个请求只能被一个线程来处理。
这种情况下怎么办?可能有人会说设置et模式呀,请注意,单纯设置et模式也无法解决这个问题,因为et模式下,客户端新的可读事件也会被监听到。
最后的解决方案就是设置EPOLLONESHOT。具体可以google一下EPOLLONESHOT的用法。
如何保存客户端的解析状态
网络传输是完全不可控的。什么情况都可能发送。正常情况下,http报文会及时完整的传输到server端。server端进行解析就可以了,但如果server端得到的http报文是残缺的,又该如何解析呢?
注意一个问题,这里的残缺不是指顺序紊乱或者缺失,tcp是可靠的,这里的残缺值得是在某个监听事件里面得到的数据是残缺的,比如说某个可读事件上的数据是"GET / HT",很明显,这是个残缺的请求报文。并且可以猜想,该socket上下一个监听事件读出来的数据肯定是"TP/1.1\r\n......."。这种情况该如何处理呢?
所以需要保存解析状态,需要搞一个类似状态机的东西。否则就像刚才的情况,明明是一段正确的报文,但两次监听解析都显示报文错误。
还有一个问题,是状态机该如何保存的问题。epoll_event结构体中标识socket的字段是data,而data是一个union,包含标识文件描述符的fd和标识指针的ptr。
指针是最方便的,可以指向一个自定义的request结构体,将读取的数据,解析的状态都保存在request中。
如果用fd也行,那么需要单独维护一个fd到状态机的映射关系,这样根据这个fd就能确定该socket上的数据解析到哪一步了。
线程池的实现
一般不建议用多进程,因为进程的开销太大。从效率角度来说,采用多线程比较好,但一个请求开一个线程,请求结束销毁线程,这样的开销也有一点大,最好的方式就是用线程池。
网上很多线程池的实现代码。主要是通过互斥锁和条件变量的方式实现,monkv中的线程池也是这样实现的。有兴趣可以研究一下。
下一步的工作
monkv只是一个非常mini的server,目前只能解析get方法,只能处理静态资源,与其说是一个server,倒不如说是我目前的一个小玩具,离生产环境还有遥不可及的距离。其实写monkv就是写着玩,就是要做一个小模型玩具,只是说目前的monkv是一个粗糙的模型,我想把monkv细细打磨,通过写着玩来提高自己的网络编程能力。所以下一步想做以下工作
- 替换monkv的http解析方法,使之能够解析更多的http方法
- 添加对php lua的支持,使之能够处理动态脚本
- 如果一切顺利,还希望能够做成多进程,类似nginx那种派生出多个子进程的架构。不过这个就更加复杂了,目前在想如何处理惊群效应,能力有限还是没有太好的方案
想来想去,目前暂时就是这些问题。虽然monkv是我自己亲手写的,但我对monkv背后的技术真的理解吗?我想我还是不理解的,很多东西就是这样,你以为你明白了,再过些年回头看,你还是没明白,你之所以觉得自己明白,只是很巧合某些东西没有被暴露出来而已。任何一个技术点背后都有很多很多的细节。所以还是那句话,纸上得来终觉浅,绝知此事要躬行。