提问人:Sgg8 提问时间:11/8/2023 最后编辑:Sgg8 更新时间:11/9/2023 访问量:209
用 C 语言解析 HTTP 请求的有效方法 [已关闭]
Efficient ways to parse an HTTP request in C [closed]
问:
我正在用 C 编写一个 HTTP 服务器。 请注意,我确实知道如何解析 HTTP 请求格式的字符串。这意味着我可以在收到标头后轻松解析它们。
我的挣扎如下:
HTTP 协议建立在 TCP 套接字之上。因此,不能保证客户端发送的请求在一次操作后就完好无损地交付。因此,我需要读取请求直到标头的末尾,get ,然后继续到正文,知道我应该读取多少数据。read()
Content-Length
read()
我正在使用非阻塞 IO,以防这对某些读者很重要。
为此,我有两个想法,每个想法都有其严重的缺点。
read()
一次一个字节,检查缓冲区的末尾是否是每次之后的 。然后获取并阅读正文。由于系统调用次数过多,效率非常低。"\r\n\r\n"
read()
Content-Length
read()
以较大的块读入缓冲区,每次检查是否读取请求的末尾,用于查找子字符串。找到子字符串后,将它后面的字符数保存在变量中,get .继续读取字符。由于每次都必须打电话,效率也很低。
strstr()
"\r\n\r\n"
"\r\n\r\n"
n
Content-Length
Content-Length - n
strstr()
read()
关于如何更有效地做到这一点有什么建议吗?
重要!我知道第二种方法更好。我正在寻找一些比我更好的新建议。
答:
谈到内容,我建议逐字节读取,并让用户定义最大大小,因为连接可以随时关闭,并且读取固定大小会禁用文件上传或更大的内容。 如果你想看看我的网络服务器,你可以复制你想要的任何部分 https://github.com/OUIsolutions/CWebStudio
我解析 http 其请求/request_parser CwebHttpRequest_parse_http_request的部分
评论
read()
您的问题是关于优化的
你问关于解析 HTTP 标头的问题,你描述了你的方法,总结一下就是:
- 您读入所有标题
- 将所有标头传递给标头解析函数
然后你解释你找到所有标头末尾的技术。然后你以问题结束:
关于如何更有效地做到这一点有什么建议吗?
对如何更有效地做到这一点没有具体限制。
在优化之前,您需要执行性能分析
您需要衡量软件的性能并确定代码中的热点。例如,代码花费的时间比您预期的要长。
为了便于论证,我们假设您正在有效地读取块中的套接字数据,以创建一个潜在的 HTTP 消息标头块,并且标头解析被证明是软件中的一个热点。
方法的潜在问题
如果你的标头解析被证明是一个瓶颈,并且你认为它与扫描标头的末尾有关,那么你需要一个假设来解释为什么它是一个瓶颈。
由于您没有发布任何代码,因此我们被迫根据您的一般描述进行推测。但是,这里有一些猜测:
使用
strstr()
进行扫描效率低下 此猜测基于以下事实:您使用字符串函数将 HTTP 标头视为 C 字符串。因此,您必须 nul terminate(在标头数据的末尾添加“\0”),然后大海捞针搜索 .
您需要从头开始,因为 HTTP 流水线可能会将多个请求塞入单个接收缓冲区中。比较测试代码必须扫描 nul 终止符,然后才能知道它完成,因此这使得循环条件稍微复杂一些。"\r\n\r\n"
可能的 O(n2) 行为
基于分析显示扫描是瓶颈的假设,一个原因可能是您正在将新接收的数据与以前接收的数据连接起来,以便您可以再次执行调用以查找标头的末尾,因为您以前没有找到它。
如果使用转换为字符串的旧缓冲区,而将新缓冲区转换为字符串,则这会导致对旧数据进行另一次扫描,以查找旧缓冲区的末尾,以便执行串联。
如果您在串联后再次从头开始呼叫,这会导致对旧数据进行另一次扫描,以发现您已经发现不存在的数据。strstr()
strcat()
strstr()
"\r\n\r\n"
解决假设问题
由于我们没有代码,因此为虚构的问题提供修复程序有点愚蠢。但是,为了后人,即使它不适用于您的问题,给出一个完整的答案仍然很有帮助。
您可以扫描并查看它是否后跟空行。
这简化了大部分扫描工作,成为简单的字符比较,并且具有良好的线性行为。'\n'
不要用于连接。
标头数据应复制到缓冲区中,该缓冲区预计大部分时间将保存所有标头(可能约为 16KB)。连接时,只需跳转到先前扫描的内容的末尾,然后从该点复制新读取的数据即可。strcat()
不要从头开始扫描标题的末尾。
相反,在串联完成后,从您离开的点开始扫描,这将是从新读取/复制的数据的开始。
不要解析两次
如果您已经删除了上述问题,那么在分析之后,您应该会从标头解析中看到相当良好的性能。
但是,仍然有可能提高效率。由于上面的建议已经在识别每个标题行的末尾,因此您可以将标头解析器构建到该扫描循环中。
找到 后,前面的字符应该是 ,这样您就知道刚刚扫描的标题行的长度。由于您现在只是在寻找,因此可以代替 ,这样就无需 nul 终止您的输入。\n
\r
\n
memchr()
strstr()
如果隐藏了行的每一端的位置,则还具有下一个标题行的开头。
当您到达空标题行时,您就知道您已经完成了对标题的解析。
这允许您在输入的单次扫描中解析标头并找到标头的末尾。
不复制数据
无需执行数据串联,只需分配一个大缓冲区来表示标头块,并将该大缓冲区用于调用。这样就避免了连接的需要。相反,当您未能找到标头的末尾时,直接从上一个区块读取的末尾开始调用缓冲区的偏移量。recv()
recv()
offset = 0;
bufsz = BUFSZ;
while (NOT_END_OF_HEADERS) {
if (bufsz > offset)
n = recv(sock, buf + offset, bufsz - offset, 0);
if (ERROR_OR_NEED_TO_STOP) HANDLE_IT;
RESUME_PARSE(buf + offset, buf + offset + n);
offset += n;
}
不要浪费内存
常规应用程序通常不会太担心占用太多内存。它们是生存期相对较短的程序,因此使用的内存会相对较快地交还给系统。
然而,嵌入式系统上长时间运行的程序通常需要更加吝啬。因此,除了 CPU 分析之外,还将对系统进行内存分析,并仔细检查内存占用。
这就是为什么嵌入式软件通常使用缓冲区结构,将较小的缓冲区链接在一起来表示流式消息,而不是连续的大缓冲区。这是为了更好地调整内存使用的大小,并可以避免与内存碎片相关的问题。
故事的寓意
优化应首先通过测量进行。在实际进行优化时,解决方案并不总是直截了当的。但是,解决性能瓶颈对于开发人员来说可能非常令人满意。
评论
read()
Content-Len
你可以两者兼而有之。以大块形式读取缓冲区,并从此缓冲区逐字节读取。您将拥有最少数量的系统调用,并且您将拥有单个字符(或行)解析器的简单性。您无需查找子字符串(即调用)。strstr
评论
strstr
\r\n