从0搭建博客:HTTP服务器
发布于:
# 前: 其实从tcp服务器做完之后我就觉得是一种疯狂堆业务逻辑的感觉。不过我还是会挑一些比较有趣,并且在其他平台看到 我过不太满意的实践经验 的部分,笼统的记录一下 # Http服务器 ## 目标是什么? 我觉得一个http服务器应该要和springmvc有差不多的功能:能解析请求,能把请求分发到对应的处理层,能返回html结果。 可能spring把session控制的功能也封装进去了,但我觉得这个不是http服务器的必选项,只是在其上层的一个简单应用,所以并不会在这一层完成。 ## 1. 解析请求 这部分很简单,抓一个请求看一下,然后写出来就好。 http请求分三部分: 请求行: GET /index.html HTTP/1.1 请求头: User-Agent: Mozilla/5.0 请求体: Helloworld http请求用的换行符号是 CRLF,说人话也就是\r\n 请求体非必须,如果有请求体那么请求头中会有一个Content-Length: xxxxx,浏览器和服务器会靠此读出数据,请求头和请求体之间有一个空行。 写一个这种解析器并不难,但一定要注意这个解析器必须要基于状态机。 因为http服务基于Tcp服务,所以一定会发生一个叫分包的东西...那么如何分包嘞?答案是不好说,实践中我发现是请求头一个包,请求体n个包(随大小而定);但是难保有什么抽象自制browser祭出抽象分包,来验证我的抽象自制服务器,所以谨慎一些吧>︿< > 我写的时候犯了傻,忘了tcp分包这个问题...哭 [这一部分](https://github.com/feiqi3/BlogServer/blob/main/FeiLib/include/Http/FHttpRequestParser.h)我简单的做了一个基于状态机的实现,做法是每次会把一个字符push进一个行缓冲,一旦在行缓冲中检测到一个crlf,那么就会把这个行缓冲送入解析器,解析器会更新解析上下文,判断现在在解析什么部分。 可能会有人想到只发一个请求头,然后不发请求体,让服务器就这么等着...然后OOM的做法 我的建议是做一个头解析超时的回调 ## 2. 请求处理 在spring中是由controller类来负责处理http请求,每一个controller类的每一个function都是对应着不同的http请求路径的处理,这些function会返回一个HttpResponse,。 那么就有两个问题产生: 0. 如何定义一个控制器 1. 如何优雅的分发http请求到Controller 2. 如何优雅的注册处理Controller处理函数 ### 控制器的定义 包含很多处理函数,每个处理函数返回一个httpResponse,接收一个httpRequest ### 分发请求! 什么东西定义了一个http请求? 一般来说是请求路径和请求方法,请求路径用于定位操作目标,请求方法给出操作方法。 这也就要求我们在创建一个请求处理器的时候,支持一个请求路径和请求方法的满射 在RESTful的指导思想下,我能给出如下的例子 ``` GET /articles → 获取所有文章 GET /articles/10 → 获取 ID 为 10 的文章 POST /articles → 创建新文章 PUT /articles/10 → 更新 ID 为 10 的文章 DELETE /articles/10 → 删除 ID 为 10 的文章 ``` 可以发现请求路径中存在变量,那么服务器也要支持取出这些变量,以便控制器的操作。 到了这一步我可以给一些大聪明想用trie树来做匹配的方法判死刑力! 因为我确实看到了有些自制的http服务框架用trie树做路径匹配,那么对于处理路径中 *变量(甚至通配符)* 这种常见情景直接束手无策,而且为了支持神秘莫测的utf8编码,这个trie树一定会是一个非线性的结构,也就是说Cache Not Friendly,对于一个高频操作,即做不到好用又做不到高性能,那就是大大的失败 那么先想想如何做的好用吧 springboot用的是一种叫ant-style的匹配方法(貌似在后续的版本改成正则匹配了?我还是喜欢以前的样子) 对于ant-style可以很快的写出一个正则,大概长这样...添加了手动转义'\' ``` (@)|(\\?)|(\\*)|\\{([^/{}\\\\]+|\\\\[{}])+\\} ``` > 窝补药学正则qwq 其中加入了捕获组来方便我们获取匹配到的通配符。 但是很快工程上的问题就会找上门来了...为什么尼玛std::regex编译不过这个正则??? 事实上你拿这个正则去网上的正则测试器上测试会得到一个五五开的通过情况 然后就会发现...正则这个东西竟然也是有方言的!最广泛使用的正则库叫PCRE,是兼容Perl语言正则语法的库(Perl Compatible Regular Expressions);文本编辑器中查找字符也用的是正则,像vscode用的是JS 正则,Sublime Text用的是Oniguruma,Notepad++用的是Boost.Regex... 然后性能测试能从这里看到: > https://github.com/rust-leipzig/regex-performance 所以我决定用G家的RE2 [这是我的实现](https://github.com/feiqi3/BlogServer/blob/main/FeiLib/src/Http/FPathMatcher.cpp),用RE2做的匹配器。 那么一个http的路径对应一个匹配器,每个进入的请求会去匹配每个已经注册进来的匹配器,如果匹配那么进一步调用处理函数。 这个过程蛮耗时的,虽然re2有编译机制能把正则编译为dfa让正则快很多,但是上述的匹配模型是一个O(N)的过程,性能依然不是那么乐观。 于是还要加入一层缓存,对于已经匹配成功的路径,把他加入缓存,网页一进来先找缓存,没命中再去进行匹配,这样性能就好了很多。 ### 离谱的注册 要把请求路径注册到路由器,如何优雅是个大问题。 Java的装饰器确实简化了很多问题,只要在函数前面加个"@map"就能让spring自动解析,C++没有装饰器,那么就是宏和黑魔法咯... [这个是我的注册](https://github.com/feiqi3/BlogServer/blob/main/FeiLib/include/Http/FController.h),emm感觉能用吧 下面是一个使用的例子 ```cpp #include "Http/FController.h" #include "Http/FHttpRequest.h" #include "Http/FHttpResponse.h" #include "Http/FPathVar.h" namespace Blog { class FileController : public Fei::Http::FControllerBase { public: FileController() :FControllerBase("File") {} Fei::Http::FHttpResponse getFile(const Fei::Http::FHttpRequest&, const Fei::Http::FPathVar&); REGISTER_MAPPING_BEGIN("/file","/assets") REGISTER_MAPPING_FUNC(Fei::Http::Method::GET, "/{name}", FileController, getFile); REGISTER_MAPPING_END }; REGISTER_CONTROLLER_CLASS(FileController) } ``` 这里把控制器类FileController注册进了router,这个控制器类有一个控制函数但是对应了两个匹配路径:'/file'和'/assets/{name}' ## 结尾 好像Http服务器确实也就这些值得记下来力 <iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=330 height=86 src="//music.163.com/outchain/player?type=2&id=27501008&auto=0&height=66"></iframe>