我正在用C语言编写一个HTTP服务器.注意I DO KNOW如何解析a string in an HTTP request format.这意味着在我收到报头后,我可以轻松地解析出它们.

我的奋斗是这样的:

HTTP协议构建在TCP套接字之上.因此,不能保证在仅仅一次read()次操作之后,客户端发送的请求被完整地递送.因此,我需要将请求读到Header的末尾,获取Content-Length,然后继续到正文read(),知道我应该读取多少数据.

我用了nonblocking IO,以防有些读者觉得这很重要.

对于这一点,我有两个 idea ,每个 idea 都有严重的缺陷.

  1. 每次read()个字节,每次在read()之后判断缓冲区的结尾是否为"\r\n\r\n".然后拿到Content-Length,然后看身体.由于read()个系统调用的数量,效率非常低.

  2. 在更大的块中读入缓冲区,每次判断是否使用strstr()读取请求的结尾以找到"\r\n\r\n"个子字符串.当找到子字符串"\r\n\r\n"时,将其后面读取的字符数量保存在变量n中,即GET Content-Length.继续阅读Content-Length - n个字符.效率也很低,因为每隔read()次就必须拨打strstr().

对于如何更有效地完成这项工作,有什么建议吗?

IMPORTANT!个 我理解第二种方法更好.我正在寻找一些比我的更好的新建议.

推荐答案

你的问题是关于优化的

您询问有关解析HTTP头的问题,并描述了您的方法,概括起来是:

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

然后解释查找所有头的末尾的技术.然后你以这个问题结束:

对于如何更有效地完成这项工作,有什么建议吗?

对如何更有效地完成这项工作没有具体的限制.

您需要在优化之前执行性能分析

您需要测量软件的性能并确定代码中的热点.例如,代码花费的时间比您预期的要长.

为了便于讨论,我们假设您正在高效地读入块中的套接字数据以创建潜在的HTTP消息头块,并且头解析被证明是软件中的一个热点.

您的方法存在潜在问题

如果您的头解析被证明是一个瓶颈,并且您认为它与扫描头的结尾有关,那么您需要一个假设来解释为什么它是一个瓶颈.

由于你没有发布任何代码,我们被迫根据你的一般描述进行推测.然而,这里有一些猜测:

  • strstr()扫描效率低下

    这种猜测是基于这样一个事实,即您通过使用字符串函数将您的HTTP头视为C字符串.因此,您必须空终止(将‘\0’添加到标题数据的末尾),然后在大海捞针中搜索"\r\n\r\n".

    您在某种程度上需要从头开始,因为HTTP管道可能会将多个请求填充到单个已接收缓冲区中.比较测试代码必须在知道已完成之前扫描NUL终止符,因此这会使循环条件稍微复杂一些.

  • 可能的O(n2)行为

    基于分析已显示扫描是瓶颈的假设,一个原因可能是您正在将新接收的数据与以前接收的数据连接在一起,以便您可以再次执行strstr()调用以找到标头的末尾,因为您以前没有找到它.

    如果您正在使用strcat()个转换为字符串的旧缓冲区,同时将新缓冲区转换为字符串,则这会导致对旧数据进行另一次扫描以查找旧缓冲区的末尾,以便执行连接.

    如果您在连接后再次从头开始您的strstr()呼叫,这会导致对旧数据的另一次扫描,以发现您已经计算出不存在的"\r\n\r\n".

解决假设的问题

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

  • 您可以只扫描'\n',看看后面是否有空行.

    这将大部分扫描工作简化为简单的字符比较,并将具有良好的线性行为.

  • 请勿使用strcat()进行连接.

    您的头数据应该被复制到一个缓冲区中,该缓冲区应该在大多数时间(可能大约16KB)中保存所有头数据.连接时,您只需跳到以前扫描的内容的末尾,然后从该点复制新读取的数据.

  • 不从开头扫描标头的结尾.

    相反,在完成串联后,请从您离开的点开始扫描,该点将从新读取/复制的数据的开始处开始.

不要解析两次

如果您已经消除了上述问题,那么在分析之后,您应该可以从头解析中看到相当好的性能.

然而,仍有可能提高效率.由于上面的建议已经完成了识别每个标题行的末尾的工作,您只需将标题解析器构建到扫描循环中即可.

在找到\n之后,前一个字符应该是\r,所以您知道刚刚扫描的标题行的长度.由于您现在只是在寻找\n,您可以使用memchr()来代替strstr(),这样就不需要NUL终止您的输入.

如果您存储行的每个结尾的位置,则还可以获得下一个标题行的开始.

当您到达空的标题行时,您知道您已经完成了对标题的解析.

这允许您在一次输入扫描中解析标头并找到标头的结尾.

不复制数据

您可以只分配一个较大的缓冲区来表示头块,并为您的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分析之外,还将对系统进行内存分析,并仔细判断内存占用情况.

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

这个故事的寓意

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

C++相关问答推荐

Zig将std.os.argv转换为C类型argv

常数函数指针优化

增加getaddrinfo返回的IP地址数量

GCC引发不明确的诊断消息

将整数的.csv文件解析为C语言中的二维数组

为什么内核使用扩展到前后相同的宏定义?

C中的指针增量和减量(*--*++p)

Char变量如何在不使用方括号或花括号的情况下存储字符串,以及它如何迭代到下一个字符?

Setenv在c编程中的用法?

如何使用C for Linux和Windows的标准输入与gdb/mi进行通信?

如何识别Linux中USB集线器(根)和连接到集线器(根设备)的设备(子设备)?

S和查尔有什么不同[1]?

使用ld将目标文件链接到C标准库

正在try 理解C++中的`正在释放的指针未被分配‘错误

Makefile无法将代码刷新到ATmega328p

使用mmap为N整数分配内存

将char*数组深度复制到 struct 中?

如何使用 raylib 显示数组中的图像

Zig 中 C 的system函数的惯用替代方案

我们可以在不违反标准的情况下向标准函数声明添加属性吗?