你的问题是关于优化的
您询问有关解析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 来表示流消息,而不是连续的大缓冲区.这是为了更好地调整内存使用大小,并可以避免与内存碎片相关的问题.
这个故事的寓意
首先应该通过测量来实现优化.在实际进行优化时,解决方案并不总是简单的.然而,对于开发人员来说,解决性能瓶颈可能会非常令人满意.