从0开始用C写一个后端框架

项目地址

后端框架跑分

Go有Gin,Java有Tomcat/Spring,Python有Django,PHP本身就是为后端而生,Node.js有http模块,而C/C++……有nginx(?)! 但是众所周知,nginx插件并不好写,而且nginx还要配置文件,实际上也没多少人用nginx插件来写Web后端。

所以我决定来做这件事,我对这个框架的目标是

  • 高性能,既然是用C写,性能一定是优先地位,争取暴打nginx
  • 易用,类似于FaaS的开发和使用体验,如果还能有一个开发用的cli就更好了
  • 灵活,低耦合,可插拔,高度自定义
  • 轻量,少依赖
  • 跨平台,至少支持Linux和macOS

架构设计

Socket

为了实现高性能,我们应该使用IO多路复用方案,例如Linux的epoll,BSD的kqueue,Windows的IOCP,好在libev可以做这件事,它在不同系统下会使用不同的系统调用,而且性能很强。

在操作socket方面,如果是静态文件可以使用sendfile来减少内核拷贝,也可以减少处理过程中请求和释放堆内存。

协议

另一方面就是HTTP协议解析,众所周知HTTP适合人类阅读但并不适合机器解析,我打算使用node.js的llhttp做这一部分,它基于llparser,生成C代码状态机,性能是足够的,如果以后有机会可以考虑手写或者用flex&bison。

开发体验

对于FaaS来说,开发者只需要编写一个函数注册到某个路径上,接收请求参数,返回响应内容即可。我们的框架也应如此,所以在解析请求完成之后需要靠路径去分发请求,一个合理的想法是用字典树,他对于长度为$N$的字符串完整匹配的时间复杂度是$O(N)$,非常快,但是空间复杂度略高,不过既然是性能优先,那没事了。

动态URL

另外就是动态请求的问题,URL可能是动态的,我的想法是优先匹配最长的,例如开发者注册了/user/profile/user,那么对于URL为/user/profile/hzy的请求则会调用前者提供的回调函数,而对于URL为/user/pose的则请求会调用后者,这样符合字典树的做法。不过有些时候开发者可能希望进行精确匹配,后期也许可以在设置中让开发者选择模糊匹配还是精确匹配。

蓝图?

我希望能引入类似于Flask中的蓝图的概念,不过从实现的角度来说,这也是增加开销的,实际上Flask的蓝图不能递归,是否真的有必要呢?以后再说吧。

单线程 VS 多线程 VS 多进程

向nginx学习,使用多进程架构,父进程只管理子进程并wait(),子进程accept(),这样连入的客户端会随机发往一个子进程上,每个子进程使用IO多路复用接收请求。nginx引入了一个互斥锁来减少“惊群”的开销,但是我暂时没看懂他是怎么做的,就先暂时这样吧。

当然进程数量也是可以设置的,如果开发者不希望使用多进程(例如开发调试的时候),那么就仅由主进程进行处理。既然使用了IO多路复用,那么单线程+CPU核心数的进程就足够了。

CPU进行多线程时切换上下文的开销并不小,考虑到默认情况下已经有CPU核心数量的进程了,默认就单线程吧。

不过可能有些用户希望以单进程多线程的方式运行,我个人对此并无意见。经过测试,可以直接多线程accept(),效果还是蛮好的,但是socket的分发不太随机,如果想要比较理想的负载均衡效果可能得自己实现一个阻塞队列来做,总之如果有时间的话可以考虑支持这种方式。

基础设施函数

主要是考虑静态目录,开发者应该能够直接将某个URL注册为静态资源,我们可以通过提供一些基础设施函数的方式来实现这一部分,用户只需要在handler中调用http_send_dir()即可将某个目录作为静态资源使用,同时还能提供部分文件类型的MIME。

另外就是数据库的操作,如果能封装成异步调用就更爽了。

异步!异步!还是异步!

对于网站后端,一个很重要的操作就是对数据库的增删改查,实际上这属于IO操作,大多数的后端在这一部分都使用同步的方法,也就是等待数据库响应,那确实很方便,但是性能就不行了(多线程也可以解决,但是会引入另外的问题)。前面说过了,我们目前不打算支持多线程(即使将来支持多线程也不推荐使用),所以我们更应该使用异步的方式去进行数据库操作。那么为了实现异步,用户提供的回调函数应该是

1
void handler(http_context_t *context);

当完成请求时,调用http_next(context)进行后续的操作。

所以为什么不直接让函数返回要响应的内容呢?这就是关键点了,因为对于异步函数来说,我们并不知道它什么时候才实际完成,所以我们将这个返回的时机留给用户来决定,也方便用户执行异步调用。可惜C语言没有提供像js一样的async/await语法糖和闭包,不然写起来应该很爽。