在C中解析HTTP请求的有效方法[关闭]

pwuypxnk  于 2023-11-16  发布在  其他
关注(0)|答案(3)|浏览(132)

已关闭。此问题为opinion-based。目前不接受回答。
**要改进此问题吗?**更新此问题,以便editing this post可以使用事实和引文来回答。

6天前关闭
Improve this question
我正在用C写一个HTTP服务器。注意,我知道如何解析 * HTTP请求格式的字符串 *。这意味着我可以在收到它们之后轻松地解析出头部。
我的奋斗目标如下:
HTTP协议是建立在TCP套接字之上的。因此,客户端发送的请求不能保证在一个read()操作之后就能完整地传递。因此,我需要读取请求的头部,获取Content-Length,然后继续到read()主体,知道我应该读取多少数据。
我使用nonblocking IO,以防对某些读者有影响。
对此,我有两个想法,每一个都有其严重的缺点。

  1. read()一次一个字节,每次在每个read()之后检查缓冲区的结尾是否为"\r\n\r\n"。然后获取Content-Length并读取主体。由于read()系统调用的数量非常低效。
    1.以较大的块读入缓冲区,每次使用strstr()检查是否读取了请求的末尾,以查找"\r\n\r\n"子字符串。当找到子字符串"\r\n\r\n"时,保存在变量n中读取的字符数,获取Content-Length。继续读取Content-Length - n字符。由于必须在每个read()之后调用strstr(),因此效率也很低。
    有什么建议可以更有效地做到这一点吗?

**重要!**我知道第二种方法更好。我正在寻找一些比我更好的新建议。

ql3eal8s

ql3eal8s1#

您的问题是关于优化的

你问了解析HTTP头的问题,并描述了你的方法,总结起来就是:

  • 你看了所有的标题
  • 将所有头传递给一个头解析函数

然后你解释了你找到所有头的结尾的技术。然后你以这个问题结束:
有什么建议可以更有效地做到这一点吗?
没有具体限制如何更有效地做到这一点。

优化前需要进行profiling

你需要测量软件的性能,并识别代码中的热点。例如,代码花费的时间比你预期的要长。
为了便于讨论,我们假设您正在高效地阅读套接字数据块,以创建一个潜在的HTTP消息头块,并且头解析是软件中的一个热点。

方法的潜在问题

如果你的头解析被证明是一个瓶颈,并且你认为它与扫描头的结尾有关,那么你需要一个假设来解释为什么它是一个瓶颈。
由于您没有发布任何代码,我们只能根据您的大致描述进行推测。然而,这里有一些猜测:

*strstr()扫描效率低

这个猜测是基于这样一个事实,即你使用一个字符串函数将你的HTTP头作为C字符串处理。因此,你必须nul终止(在头数据的末尾添加'\0'),然后在干草堆中搜索"\r\n\r\n"
你需要从头开始,因为HTTP管道可能会将多个请求填充到一个接收的缓冲区中。比较测试代码必须在它知道它完成之前扫描nul终止符,所以这使得循环条件稍微复杂一些。

  • 时间复杂度O(n2)

基于分析表明扫描是一个瓶颈的假设,一个原因可能是您正在将新接收的数据与先前接收的数据连接起来,以便您可以再次执行strstr()调用来查找头的结尾,因为您之前没有找到它。
如果您使用的是将旧缓冲区转换为字符串的strcat(),而将新缓冲区转换为字符串,则会导致对旧数据进行另一次扫描,以找到旧缓冲区的末尾,从而执行连接。
如果您在连接后重新开始strstr()调用,这将导致对旧数据的另一次扫描,以找到您已经发现不存在的"\r\n\r\n"

解决假设问题

因为我们没有代码,所以为编造的问题提供修复是有点愚蠢的。然而,为了子孙后代,即使它不适用于你的问题,给给予一个完整的答案仍然是有帮助的。

  • 你可以只扫描'\n',看看它后面是否有一个空行。

这将大量的扫描工作简化为一个简单的字符比较,并将具有良好的线性行为。

  • 不要使用strcat()连接。

你的头数据应该被复制到一个缓冲区,这个缓冲区应该在大部分时间里保存所有的头数据(可能是16 KB左右)。当连接时,你应该简单地跳到之前扫描的数据的末尾,然后从那个点复制新读取的数据。

  • 不要从头开始扫描标题的结尾。

相反,在连接完成后,从您停止的点开始扫描,这将是从新读取/复制的数据开始。

不要解析两次

如果您已经消除了上述问题,那么在分析之后,您应该可以从头解析中看到相当好的性能。
然而,它仍然可以更高效。因为上面的建议已经完成了识别每个标题行结尾的工作,所以你可以将你的标题解析器构建到扫描循环中。
在找到\n之后,前面的字符应该是\r,所以你知道刚刚扫描的标题行的长度。因为你现在只是在寻找\n,你可以使用memchr()代替strstr(),这样就不需要nul终止你的输入。
如果你隐藏了每一行末尾的位置,你也有下一个标题行的开始。
当你到达一个空的标题行时,你知道你已经完成了对标题的解析。
这允许您解析标头,并在输入的单个扫描中找到标头的结尾。

不复制数据

您可以直接分配一个大的缓冲区来表示header块,而不是执行数据拼接。然后将相同的大缓冲区用于recv()调用。这就避免了拼接的需要。相反,您可以直接调用recv(),并在未能找到header的末尾时,从上一个块读取的末尾开始将偏移量放入缓冲区。

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分析之外,系统也将进行内存分析,并且将仔细检查内存占用情况。
这就是为什么嵌入式软件通常会使用缓冲区结构,将较小的缓冲区链接在一起来表示流式消息,而不是连续的大缓冲区。这是为了更好地调整内存使用量,并可以避免与内存碎片相关的问题。

故事的寓意

优化应该首先通过测量来进行。在实际进行优化时,解决方案并不总是简单的。但是,解决性能瓶颈对开发人员来说是非常令人满意的。

sulc1iza

sulc1iza2#

说到内容,我建议读取字节到字节,并让用户定义最大大小,因为连接可以随时关闭,并读取固定大小,将禁用文件上传或更大的内容-len.如果你想看看我的web服务器,你可以复制你想要的任何部分在你的https://github.com/OUIsolutions/CWebStudio
部分解析http其request/request_parser CwebHttpRequest_parse_http_request

dbf7pr2w

dbf7pr2w3#

你可以两者兼得。读大块到你的缓冲区和读字节从这个缓冲区。你将有一个最小数量的系统调用,你将有一个单一的字符(或行)解析器的简单性。你不需要寻找子字符串(即调用strstr)。

相关问题