用 C 语言解析 HTTP 请求的有效方法 [已关闭]

Efficient ways to parse an HTTP request in C [closed]

提问人:Sgg8 提问时间:11/8/2023 最后编辑:Sgg8 更新时间:11/9/2023 访问量:209

问:


想改进这个问题吗?更新问题,以便可以通过编辑这篇文章用事实和引文来回答。

13天前关闭。

我正在用 C 编写一个 HTTP 服务器。 请注意,我确实知道如何解析 HTTP 请求格式的字符串。这意味着我可以在收到标头后轻松解析它们。

我的挣扎如下:

HTTP 协议建立在 TCP 套接字之上。因此,不能保证客户端发送的请求在一次操作后就完好无损地交付。因此,我需要读取请求直到标头的末尾,get ,然后继续到正文,知道我应该读取多少数据。read()Content-Lengthread()

我正在使用非阻塞 IO,以防这对某些读者很重要。

为此,我有两个想法,每个想法都有其严重的缺点。

  1. read()一次一个字节,检查缓冲区的末尾是否是每次之后的 。然后获取并阅读正文。由于系统调用次数过多,效率非常低。"\r\n\r\n"read()Content-Lengthread()

  2. 以较大的块读入缓冲区,每次检查是否读取请求的末尾,用于查找子字符串。找到子字符串后,将它后面的字符数保存在变量中,get .继续读取字符。由于每次都必须打电话,效率也很低。strstr()"\r\n\r\n""\r\n\r\n"nContent-LengthContent-Length - nstrstr()read()

关于如何更有效地做到这一点有什么建议吗?

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

C HTTP 套接字 解析 HttpRequest

评论

3赞 dbush 11/8/2023
选择后者。更少的系统调用,无论如何你都必须检查你读取的每个字节。
1赞 KamilCuk 11/8/2023
做后者。通常,ppl 使用的读取缓冲区大小等于页面大小 4k。
0赞 Steffen Ullrich 11/8/2023
执行后者,但在读取新部分之前,从缓冲区末尾的某处的 strstr 开始,而不是每次都从开头开始(len-3 以匹配 4 个字节,假设至少需要 1 个新字节)。
1赞 Arun 11/8/2023
这是否回答了您的问题 stackoverflow.com/questions/41286260/...
0赞 Cubic 11/8/2023
后者,但删减部分,不能保证你会在一次读取调用中获得标头末尾标记;您可能只读取一个块中的内容。strstr\r\n

答:

0赞 Mateus Moutinho 11/9/2023 #1

谈到内容,我建议逐字节读取,并让用户定义最大大小,因为连接可以随时关闭,并且读取固定大小会禁用文件上传或更大的内容。 如果你想看看我的网络服务器,你可以复制你想要的任何部分 https://github.com/OUIsolutions/CWebStudio

我解析 http 其请求/request_parser CwebHttpRequest_parse_http_request的部分

评论

0赞 Sgg8 11/9/2023
这种方法的问题在于,这是一个昂贵的系统调用。据我了解,您提到的任何其他积极影响都可以通过非阻塞 IO 轻松处理,因为操作系统可能不会让读取操作在阻塞之前花费几分钟read()
0赞 Sgg8 11/9/2023
虽然经过多次验证,但我不能 100% 确定我的后一种说法。从未在手册中看到过这一点
0赞 user207421 11/9/2023
读取固定大小的行为并不像您声称的那样。它会阻止,直到某些数据可用,然后返回该数据无论它有多少。
0赞 user207421 11/9/2023
@Sgg8 我就是这么说的。这就是为什么这个答案是不正确的。
0赞 Sgg8 11/10/2023
@user207421我以为你在谈论“行为不像你声称的那样”时是在对我说话。这是一个误会
2赞 jxh 11/9/2023 #2

您的问题是关于优化的

你问关于解析 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\nmemchr()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 分析之外,还将对系统进行内存分析,并仔细检查内存占用。

这就是为什么嵌入式软件通常使用缓冲区结构,将较小的缓冲区链接在一起来表示流式消息,而不是连续的大缓冲区。这是为了更好地调整内存使用的大小,并可以避免与内存碎片相关的问题。

故事的寓意

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

评论

0赞 Sgg8 11/9/2023
我可以轻松地一次解析出所有标头。我只需要全部阅读,这是一个问题
0赞 jxh 11/9/2023
既然需要解析标头,为什么要扫描输入两次?
0赞 Sgg8 11/9/2023
因为分块进行解析要困难得多。我还没有设法为此设计出一种有效的算法。例如,如果我得到的只是 ,我不能把它放在我的哈希图中。我必须将其存储在某个地方,并在一段时间后收到足够的数据时插入到另一个调用中。至少我不能这样做,除非我将我的哈希图修改到它看起来不再像哈希图的程度。这种结构的性能会下降。但是,如果您对如何分块执行解析有一些想法,我很高兴听到它们read()Content-Len
0赞 jxh 11/9/2023
然后,您有一个关于如何有效地将数据块解析为流的新问题。但是,这与我描述的存储位置并没有太大区别。位置的定义成为指向块和偏移量的指针。如果您有一些部分解析的输入,请在部分的开头将部分拆分为两个,将部分放入新块的开头。
0赞 jxh 11/9/2023
你试图从提供的答案中调整你的问题,但我认为你已经得到了所有可能的帮助,因为你是如何提出你的问题的。
0赞 0___________ 11/9/2023 #3

你可以两者兼而有之。以大块形式读取缓冲区,并从此缓冲区逐字节读取。您将拥有最少数量的系统调用,并且您将拥有单个字符(或行)解析器的简单性。您无需查找子字符串(即调用)。strstr

评论

0赞 Sgg8 11/9/2023
我怎么知道何时停止使用这种方法阅读?
0赞 0___________ 11/9/2023
@Sgg8 保留读取的字节数。很简单。但我不太明白你的问题。
0赞 Sgg8 11/9/2023
我事先不知道请求的大小
0赞 0___________ 11/9/2023
@Sgg8 你不需要知道。你需要它做什么?这种FIFO缓冲区的实现非常简单,在尝试编写Web服务器之前,您应该将其作为练习
0赞 Sgg8 11/9/2023
我需要知道大小,以便知道何时停止阅读并继续编写响应