关于HTTP解析的一点思考

2023-05-16

原文

似乎已经很久没有提到关于服务器的消息了,其实我一直都在写,只是有时事情比较多,会耽搁一点时间。

在使用C重写前,我就已经用Dlang实现了近2个版本的HTTP解析器,换成C之后,又换了几种思路,期间也参考现有的几种实现,可以说是有点积累,现总结成文,记录一下。

注:如下所指的HTTP均指代HTTP/1.1,不涉及HTTP/2的内容。

HTTP协议特征分析

HTTP/1.1是目前使用最为广泛的应用层协议之一,当然如今HTTP/2的标准已经出来,但是尚未普及,有些特性有待考验,我就不吃螃蟹了。。

HTTP协议是典型的Key-Value类型的文本协议,当然还有first-line和body,总体而言解析并不复杂,并且我们也不要求规整的错误提示和文法验证,因此LL(1)和LALR(1)在这里基本无用武之地。HTTP的大致格式如下:

HTTP请求:

method path version\r\n
key: value\r\n
key: value\r\n
...
key: value\r\n
\r\n
body

HTTP响应:

version status desc\r\n
key: value\r\n
key: value\r\n
...
key: value\r\n
\r\n
body

和redis的协议类似,HTTP协议中first-line和headers之间以及header与header之间均通过CRLF('\r''\n')分割,这个需要特别注意,解析的时候可能会带来一定的麻烦。我一直想不通为何不直接用LF分割,因为http-version,header-key,header-value内部并不允许出现LF,因此选择LF完全可以更加简单的解决问题,或许作者当初只用windows吧。

关于header的key和value的具体语义,才是HTTP协议的关键,内容繁杂,需要参考rfc文档,不过这个和HTTP协议并没有太大的关系(下文解释理由),因此这里不再提及。

HTTP解析器的职责

在进一步描述HTTP解析器之前,我们有必要先弄明白HTTP的职责,需要符合哪些约束条件。以下,以问答的形式进行阐述。

  1. HTTP解析器在web server中处于什么位置?

    简单来说,它位于TCP服务器和应用框架之间,负责两者之间的数据转换。具体场景为:当HTTP请求到达时,解析器需要根据获取到的buf,对数据进行解析,并最终转换成应用框架能够识别的数据结构,比如HttpRequest、HttpResponse等;反之,它需要将数据结构的内容转换成字符串,并存储在buf中,提交给TCP服务器。

  2. HTTP解析器是否需要处理header完整的语义?

    准确地说,这个问题没有标准答案,和性能指标相关,具体体现在服务器的并发模型。如果HTTP解析是在一个独立的线程中进行,那么可以适当解析header的具体语义;如果HTTP解析是main loop中进行的,那么应该避免进一步的解析,应该将这个过程充分延迟。

    总体来说,将header的语义解析延后是相对理性的做法,因为我们未必需要获取某些header的值,且总的解析时间都是包括在响应时间内的。我的选择是解析标准(有一定扩充)中指定的header的key,至于value,只做字符的合法性检查。

  3. HTTP解析器是否应该处理body?

    对于这个问题,我曾经思考了很久,至少我目前的答案是不需要,也不应该。理由大致如下:其一,为了支持multi-part,这部分的解析应该充分延迟;其二,为了应对巨大的body,相对合理的做法是采用类似java中stream之类的做法,而不是一次性分配一大块内存,然后存储起来慢慢解析。

  4. 常见的约束条件

    对于服务器的benchmark而言,尤其是只输出hello world的那种,HTTP解析器在很大程度上决定了性能,可以说,一个性能优异的解析器是高性能web服务器的必要条件,关于这点,大致可以从h2o server中看到点什么。简单来说,如果你想做一款能够应对C10K问题的server,那么解析器的吞吐量至少应该是1w/s,这个指标,对于C语言实现的模块而言,是相对容易的。

    除此之外,可用性和安全性,同样是不可忽视的需求,从HTTP解析器的角度考虑可用性,要求HTTP解析器能够应处理任何输入,做到不崩溃,这是最基本的要求。附带上安全性,那么就要求解析器能够识别非法的输入,并提供准确的错误信息。

HTTP解析器编写的几种方法

本节,我将从现存的几个开源实现进行分析,它们分别是http-parser(nodejs),picohttpparser(h2o server),以及本人的实现。

  1. picohttpparser

picohttpparser作为h2o的HTTP解析器,拥有比较出色的性能,当然这也正是h2o的目标。

pico项目的代码并不难理解,不到1000行,就是常规的线性解析,没有任何状态,所做的也只是“断句”,将HTTP请求/响应的各个成分断开,然后存储到用户指定的buf中。值得注意的是,如果我们的机器支持SSE4.2指令,那么可以使用pico的加速功能,关于这条指令,我会专门写一篇博客进行介绍。

然后,作者在README中给出了一张很给力的benchmark图,号称性能超过http-parser 3倍,开启加速后,结果是快5倍。

确实是很惊人的成绩,我粗略总结了下原因:

  1. 只进行断句,对header不做字符检查,不识别任何header-key;
  2. 不在解析器内部分配任何内存,即不调用任何malloc;
  3. 对buf的内容有一定假设,不判断buf是否读完。

大家确实可以在程序中用pico,但前提是需要做一些额外的工作:你需要在buf中存有足够的内容,因为pico对于非阻塞的适应性不好;你需要分配足够大的buf来存储header、path,谁都不知道到底会有多少条header;你需要在pico处理后,进一步识别数组中保存的是什么header,当然也免不了合法性检查。

漂亮的分数是有代价的,或许pico是为 单进程main loop + 线程池 这种模型准备的,但我肯定不会用它。

  1. http-parser

http-parser是nodejs的HTTP解析器,比较成熟,代码量在2000+,能够识别主要的method,header-key,能够在解析时完成path和host的解析,使用回调注册的方式进行方法触发(作者又写了个没有回调的版本,hl),包括内存分配,整体风格和libuv差不多。

话说作者原本的目标就是开发一个web server,然后加了v8引擎后,就成了nodejs。。。

整体的实现方式是添加大量状态,switch语句,比如如下形式:

H
HT
HTT
HTTP
HTTP1
HTTP1/

这种做法的优点是对非阻塞API的支持比较好,缺点是过于冗长,代码可读性不太高,对此作者采用了一些技巧,比如method根据第一个字母进行识别,貌似曾今见过有人拿这个黑nodejs。。

从性能指标上来看,http-parser的性能还是很优秀的,毕竟功能加强了很多。

不过,需要注意,switch的开销是存在的,尤其是在编译时不开优化,此时swich和if-else无异,即使开了优化,还是存在一定的开销,比如计算偏移,goto等。

  1. 基于协程的实现

协程的作用就是将非阻塞API的问题去除,那么基于协程的实现是最简单的,在使用Dlang开发的时候,我实现过对应的版本,代码量不足300行,不需要任何状态。

基于协程的实现,性能不会太低,虽然协程的resume和yield的开销介于普通调用和系统调用之间,但是时间比重占的并不高。不过使用协程有个致命的问题,那就是内存占用过大,大量应用是不太现实的,貌似lwan就用了这种方法,不知道基于ucontext的协程实现性能开销如何。

  1. 基于表驱动的实现

表驱动这个词,是我在看完《code complete》(代码大全)后,记得最牢的一个,因为它确实很有用,个别同学可别再把它和转移表或者跳转表的概念搞混了。

我当前的做法就是大量应用表驱动,主要体现在:

  1. 实现记录合法的ascii表,解析式的查询开销为O(1),而且是严格的;
  2. 事先对各个header-key做hash处理,在处理时,只需要对每个相关的字符做hash处理,并在读到分割符后,进行查表,开销为O(1),基本严格。

当然这也是http-parser的做法,只不过它并未使用hash来处理header-key。值得一体的是,我觉得可能hash在计算时,开销并不比switch小,有待进一步验证。

从目前的数据来看,当前实现的总体性能大概是http-parser的1.3倍,我使用了特殊的数据结构来尽可能避免在解析时分配内存,用time统计运行时间时,sys的开销基本为0。无论是出于时间开销,还是内存碎片的角度考虑,都应尽量避免大量分配小块内存。

优化建议

开发完原型后,我做了下简单的benchmark,对比pico,竟然被完爆。。。然后花了2天时间进行分析优化,总结了点内容,或许对各位有一定帮助。

  1. switch的开销

    这个在上文中已经提及,一定要开优化,尽可能使case后的数字连续分布,主流的做法是写在enum内。总体而言,对于不是很复杂的if-else语句,使用switch替换,并不能带来显著提升,反而会使代码变得比较难读。

  2. 避免分配内存

    尤其是在循环内部,应该尽量避免分配频繁分配内存,这么做会造成内存碎片,并且malloc最终会使用系统调用,当初我的benchmark中,malloc就“贡献”了近1/4的时间开销。

  3. 如果你的程序适合用SSE4.2中的指令加速,那么可以一试,运用得当,应该能够加速3倍左右(intel官方给的数据)

  4. 对结构体中的内容做“本地化处理”

    尤其是需要在循环中大量操作结构体时,一定要注意,因为它也会“贡献”很大比重的开销。这里所谓“本地化”,即使用函数的局部变量代替结构体中的数据,在函数返回时,写入。示例如下:

struct XXX {
    int anum;
    ...
};
void foobar(struct XXX* x) {
    int anum = x->anum;
    for(int i = 0; i < 10000000; ++1) {
        ++anum;
    }
    x->anum = anum;
}

我还尚未仔细对比过gcc开-O2后是否会做相关工作(或许未来可以描述一下),所谓的本地化,一定要确保数据源和临时副本一致,切记。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

关于HTTP解析的一点思考 的相关文章

  • 使用意图过滤器从 URL 打开 Android 应用程序不起作用

    我有一个 Android 应用程序 人们用它来替代网站 因此 当用户遇到网站的 URL 时 我想为他们提供在我的应用程序中而不是在浏览器中 打开 URL 的选项 换句话说 我希望出现弹出窗口 让他们在我的应用程序和浏览器 可能还有其他应用程
  • 从 Django 基于类的视图的 form_valid 方法调用特殊(非 HTTP)URL

    如果你这样做的话 有一个 HTML 技巧 a href New SMS Message a 点击新短信打开手机的本机短信应用程序并预 先填写To包含所提供号码的字段 在本例中为 1 408 555 1212 以及body与提供的消息 Hel
  • 如何使用 python http.server 运行 CGI“hello world”

    我使用的是 Windows 7 和 Python 3 4 3 我想在浏览器中运行这个简单的 helloworld py 文件 print Content Type text html print print print print h2 H
  • 如何给所有HttpClient请求方法添加参数?

    我正在编写一些使用 Apache 的 Java 代码HttpClient版本4 2 2使用 RESTful 第三方 API 该 API 具有利用 HTTP 的方法GET POST PUT and DELETE 需要注意的是 我使用的是 4
  • 发送压缩文件 Spring

    我想通过我的 spring 控制器发送一个已经存在的压缩文件 但我不断收到这些错误消息org springframework web HttpMediaTypeNotAcceptableException Could not find ac
  • 如何自定义解析错误的 HTTP 400 响应?

    我编写了一个 REST API 服务 要求所有响应均为 JSON 但是 当 Go HTTP 请求解析器遇到错误时 它会返回 400 作为纯文本响应 而不会调用我的处理程序 例子 gt curl i H Authorization Basic
  • 从express.js 中删除所有标头

    我正在创建一个页面 其中有一些数据可以由另一个设备解析 我曾经使用 php 执行此操作 但现在将其移至 Node js 我需要从页面中删除所有标题 这样我就只有我的输出 此输出是对 GET 请求的响应 此刻我有 HTTP 1 1 200 O
  • 使用传输编码分块的 HTTP 响应中的最大块大小是多少?

    The w3 org RFC2616 http www w3 org Protocols rfc2616 rfc2616 sec3 html sec3 6 1似乎没有定义块的最大大小 但是如果没有最大块大小 则没有空间用于块扩展 必须有一个
  • 如何在C++中使用Curl获取HTTP响应字符串

    我对 HTTP 命令和 libcurl 库非常陌生 我知道如何获取 HTTP 响应代码 但不知道如何获取 HTTP 响应字符串 以下是我为获取响应代码而编写的代码片段 任何有关如何获取响应字符串的帮助将不胜感激 curl easy seto
  • GET 和 POST 方法的单独 Flask 路由

    在 Flask 中定义路由时 最好的做法是使用由多个 HTTP 方法定义的单个路由 并在该单个路由中使用显式逻辑处理不同的 HTTP 方法 例如 app route api users methods GET POST def users
  • 如何通过 HTTP POST 发送充满对象的 NSArray?

    我在 iPhone 端有一个产品 购物清单 由具有名称 product id 等的产品对象组成 我希望将此列表发送到服务器 在那里我将服务器上的列表与 iphone 中的列表进行比较 以合并所做的更改并将合并的列表发送回 iphone 如何
  • 在 Heroku 上获取客户端的真实 IP 地址

    在任何 Heroku 堆栈上 我想获取客户端的 IP 我的第一次尝试可能是 request headers REMOTE ADDR 当然 这是行不通的 因为所有请求都是通过代理传递的 所以替代方法是使用 request headers X
  • .NET 中有什么方法可以以编程方式侦听 HTTP 流量吗?

    我正在使用浏览器自动化来测试网站 但我需要验证来自浏览器的 HTTP 请求 即图像 外部脚本 XmlHttpRequest 对象 有没有一种方法可以以编程方式实例化代理以供浏览器使用以查看其发送的内容 我已经在使用 Fiddler 来监视流
  • 是否可以修改 $_SESSION 变量?

    恶意用户是否可以将 SESSION 在 php 中 变量设置为他想要的任何值 很大程度上取决于您的代码 有一点非常明显 SESSION username REQUEST username
  • 使用什么 API 在现有 MFC 应用程序中添加 HTTP 客户端支持?

    我最近接到一项任务 要添加与以下内容交互的能力网络地图服务 http en wikipedia org wiki Web Map Service到现有的 MFC 应用程序 我需要客户端 HTTP API 根据我的研究 领先的候选人似乎是CA
  • Web 客户端和 Expect100Continue

    使用 WebClient C NET 时设置 Expect100Continue 的最佳方法是什么 我有下面的代码 我仍然在标题中看到 100 continue 愚蠢的 apache 仍然抱怨 505 错误 string url http
  • 谁添加“_”单下划线查询参数?

    我有一个在 Apache 上运行的 PHP 服务器 我收到很多类似这样的请求 10 1 1 211 02 Sep 2010 16 14 31 0400 GET request 1283458471913 action get list HT
  • 如何在 PHP 中使用 file_get_contents 获取图像的 MIME 类型

    我需要获取图像的 MIME 类型 但我只有图像的正文file get contents 是否有可能获取 MIME 类型 是的 你可以这样得到它 file info new finfo FILEINFO MIME TYPE mime type
  • GET 和 POST 方法有什么区别? [复制]

    这个问题在这里已经有答案了 可能的重复 什么时候用POST 什么时候用GET https stackoverflow com questions 46585 when do you use post and when do you use
  • 如何在 Laravel 中使用 PUT http 动词提交表单

    我知道这个问题可能已经提出 但我就是无法让它发挥作用 如果有人可以帮助我 我将非常感激 我安装了 colletive form 但答案也可以是 html 表单标签 现在列出我的表格 我的路线和我的例外情况 Form model array

随机推荐

  • strlen()函数详解

    头文件 xff1a include lt string h gt strlen 函数用来计算字符串的长度 xff0c 其原型为 xff1a unsigned int strlen char s strlen 用来计算指定的字符串s 的长度
  • 阿里云物联网平台基本设置-物模型

    陈拓 2019 12 14 2020 01 15 1 概述 如何让设备连接上云 xff1f 参考如下路径 本文以一个温度传感器为例 xff0c 演示创建产品 定义物模型 创建设备 虚拟设备调试 xff0c 这几部分 2 阿里云开通 2 1
  • Make与CMake

    1 Make与CMake 首先先来了解一下gcc xff0c gcc是GNU Compiler Collection 就是GNU编译器套件 xff0c 也可以简单认为是编译器 xff0c 它可以编译很多种编程语言 包括C C 43 43 O
  • C++学习(23)

    1 分析下述代码运行 xff1a include lt iostream gt using namespacestd int main int a 10 61 0 1 2 3 4 5 6 7 8 9 int p 61 a cout lt l
  • 史上最全最丰富的“最长公共子序列”、“最长公共子串”问题的解法与思路

    花了一天时间把一直以来的 最大子序列 最大递增子序列 最大公共子序列 最长公共子串 等问题总结了一下 其中参考了若干博文 xff0c 都备注引用 首先子序列是指一个一个序列中 xff0c 由若个数 xff08 字母 xff09 组成 xff
  • TCP协议拥塞控制算法(Reno、HSTCP、BIC、Vegas、Westwood)

    TCP协议拥塞控制算法 xff08 Reno HSTCP BIC Vegas Westwood xff09 一 TCP拥塞控制的研究框架 二 现有TCP拥塞控制的算法 xff08 Reno HSTCP Vegas Westwood xff0
  • C# Convert类

    Convert类常用的类型转换方法 方法说明Convert ToInt32 转换为整型 int Convert ToChar 转换为字符型 char Convert ToString 转换为字符串型 string Convert ToDat
  • try catch里面try catch嵌套

    try catch里面try catch嵌套 点击打开链接 try 与catch的作用 首先要清楚 xff0c 如果没有try的话 xff0c 出现异常会导致程序崩溃 而try则可以保证程序的正常运行下去 xff0c 比如说 xff1a t
  • mysql 中使用 where 1=1和 1=1 的作用

    Mysql中where 1 61 1 和count 0 使用小技巧 mysql中使用 where 1 61 1和 1 61 1 的作用
  • 面试题1:OS或者编译器怎么识别是全局变量还是局部变量

    OS或者编译器怎么识别是全局变量还是局部变量 操作系统内根本不关心你是什么变量 xff0c 它只管代理运行程序 xff0c 也就是进程 xff0c 负责这些进程之间的调度 xff0c 不过如果要说操作系统本身也是进程 xff0c 那倒可以理
  • 面试题4:数组、指针、引用的联系区别

    数组和指针 xff1f xff1f xff1f 从两个方面来看 xff0c 一是作为一个语言 xff0c 数组是必须要支持的一种数组类型 xff0c 原因很简单 xff0c 数组是线性表的直接体现 而从编译器设计者的角度来看 xff0c 如
  • c++ 容器类 概括性介绍

    C 43 43 中的容器类包括 顺序存储结构 和 关联存储结构 xff0c 前者包括vector xff0c list xff0c deque等 xff1b 后者包括set xff0c map xff0c multiset xff0c mu
  • 海康摄像头使用RTSP

    1 协议格式 海康威视IP摄像头rtsp协议地址如下 xff1a rtsp username passwd 64 ip port codec channel subtype av stream 主码流 xff1a rtsp admin 12
  • 树莓派串口连接ESP8266

    陈拓 chentuo 64 ms xab ac cn 2020 03 12 2020 03 12 1 概述 ESP8266是物联网行业广泛使用的WiFi模块 xff0c 小巧 功能强大 xff0c 而且价格低廉 通常用电脑进行ESP8266
  • Linux 创建TCP连接流程

    文章目录 Linux创建TCP的步骤服务端客户端TCP建立流程示例代码 Linux创建TCP的步骤 TCP编程需要客户端和服务器两套编码 xff0c 其创建TCP的流程也是不完全一致的 服务端 使用socket函数创建一个套接字使用sets
  • 结构体类型完全归纳

    结构体类型 目录 基本概述 一 结构体类型变量的定义方法及其初始化 1 定义结构体类型变量的方法 2 结构体变量的初始化 二 结构体变量的引用 三 结构体数组 1 定义结构体数组 2 结构体数组应用举例 四 指向结构体变量的指针 1 类型一
  • Ubuntu20.04LTS下安装Intel Realsense D435i驱动与ROS包

    文章目录 目标一 D435i简介二 环境配置三 RealSense的SDK2 0安装四 ROS包安装五 摄像机CV的ROS包节点 六 问题排查 目标 在Ubuntu20 04LTS系统下安装D435i的驱动SDK2和ROS包 xff0c 实
  • C# 调用NationalInstruments的dll报错问题 未能加载文件或程序集

    C 调用NationalInstruments的dll报错问题 问题原因 xff1a dll版本不匹配导致的 xff0c 需要做如下操作解决问题 未能加载文件或程序集 NationalInstruments Common Version 6
  • 需要授权的 API ,必须在请求头中使用 Authorization 字段提供 token 令牌

    需要授权的 API xff0c 必须在请求头中使用 添加字段 需要授权的 API xff0c 必须在请求头中使用 Authorization 字段提供 token 令牌 实现方法 通过 axios 请求拦截器添加 token xff0c 保
  • 关于HTTP解析的一点思考

    原文 似乎已经很久没有提到关于服务器的消息了 xff0c 其实我一直都在写 xff0c 只是有时事情比较多 xff0c 会耽搁一点时间 在使用C重写前 xff0c 我就已经用Dlang实现了近2个版本的HTTP解析器 xff0c 换成C之后