已关闭。此问题为opinion-based。目前不接受回答。
**要改进此问题吗?**更新此问题,以便editing this post可以使用事实和引文来回答。
6天前关闭
Improve this question的
我正在用C写一个HTTP服务器。注意,我知道如何解析 * HTTP请求格式的字符串 *。这意味着我可以在收到它们之后轻松地解析出头部。
我的奋斗目标如下:
HTTP协议是建立在TCP套接字之上的。因此,客户端发送的请求不能保证在一个read()
操作之后就能完整地传递。因此,我需要读取请求的头部,获取Content-Length
,然后继续到read()
主体,知道我应该读取多少数据。
我使用nonblocking IO,以防对某些读者有影响。
对此,我有两个想法,每一个都有其严重的缺点。
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()
,因此效率也很低。
有什么建议可以更有效地做到这一点吗?
**重要!**我知道第二种方法更好。我正在寻找一些比我更好的新建议。
3条答案
按热度按时间ql3eal8s1#
您的问题是关于优化的
你问了解析HTTP头的问题,并描述了你的方法,总结起来就是:
然后你解释了你找到所有头的结尾的技术。然后你以这个问题结束:
有什么建议可以更有效地做到这一点吗?
没有具体限制如何更有效地做到这一点。
优化前需要进行profiling
你需要测量软件的性能,并识别代码中的热点。例如,代码花费的时间比你预期的要长。
为了便于讨论,我们假设您正在高效地阅读套接字数据块,以创建一个潜在的HTTP消息头块,并且头解析是软件中的一个热点。
方法的潜在问题
如果你的头解析被证明是一个瓶颈,并且你认为它与扫描头的结尾有关,那么你需要一个假设来解释为什么它是一个瓶颈。
由于您没有发布任何代码,我们只能根据您的大致描述进行推测。然而,这里有一些猜测:
*
strstr()
扫描效率低这个猜测是基于这样一个事实,即你使用一个字符串函数将你的HTTP头作为C字符串处理。因此,你必须nul终止(在头数据的末尾添加'\0'),然后在干草堆中搜索
"\r\n\r\n"
。你需要从头开始,因为HTTP管道可能会将多个请求填充到一个接收的缓冲区中。比较测试代码必须在它知道它完成之前扫描nul终止符,所以这使得循环条件稍微复杂一些。
基于分析表明扫描是一个瓶颈的假设,一个原因可能是您正在将新接收的数据与先前接收的数据连接起来,以便您可以再次执行
strstr()
调用来查找头的结尾,因为您之前没有找到它。如果您使用的是将旧缓冲区转换为字符串的
strcat()
,而将新缓冲区转换为字符串,则会导致对旧数据进行另一次扫描,以找到旧缓冲区的末尾,从而执行连接。如果您在连接后重新开始
strstr()
调用,这将导致对旧数据的另一次扫描,以找到您已经发现不存在的"\r\n\r\n"
。解决假设问题
因为我们没有代码,所以为编造的问题提供修复是有点愚蠢的。然而,为了子孙后代,即使它不适用于你的问题,给给予一个完整的答案仍然是有帮助的。
'\n'
,看看它后面是否有一个空行。这将大量的扫描工作简化为一个简单的字符比较,并将具有良好的线性行为。
strcat()
连接。你的头数据应该被复制到一个缓冲区,这个缓冲区应该在大部分时间里保存所有的头数据(可能是16 KB左右)。当连接时,你应该简单地跳到之前扫描的数据的末尾,然后从那个点复制新读取的数据。
相反,在连接完成后,从您停止的点开始扫描,这将是从新读取/复制的数据开始。
不要解析两次
如果您已经消除了上述问题,那么在分析之后,您应该可以从头解析中看到相当好的性能。
然而,它仍然可以更高效。因为上面的建议已经完成了识别每个标题行结尾的工作,所以你可以将你的标题解析器构建到扫描循环中。
在找到
\n
之后,前面的字符应该是\r
,所以你知道刚刚扫描的标题行的长度。因为你现在只是在寻找\n
,你可以使用memchr()
代替strstr()
,这样就不需要nul终止你的输入。如果你隐藏了每一行末尾的位置,你也有下一个标题行的开始。
当你到达一个空的标题行时,你知道你已经完成了对标题的解析。
这允许您解析标头,并在输入的单个扫描中找到标头的结尾。
不复制数据
您可以直接分配一个大的缓冲区来表示header块,而不是执行数据拼接。然后将相同的大缓冲区用于
recv()
调用。这就避免了拼接的需要。相反,您可以直接调用recv()
,并在未能找到header的末尾时,从上一个块读取的末尾开始将偏移量放入缓冲区。字符串
不要浪费内存
常规的应用程序通常不太担心占用太多内存,它们是相对较短的程序,因此使用的内存会相对较快地交还给系统。
然而,在嵌入式系统上长时间运行的程序通常需要更加吝啬。因此,除了CPU分析之外,系统也将进行内存分析,并且将仔细检查内存占用情况。
这就是为什么嵌入式软件通常会使用缓冲区结构,将较小的缓冲区链接在一起来表示流式消息,而不是连续的大缓冲区。这是为了更好地调整内存使用量,并可以避免与内存碎片相关的问题。
故事的寓意
优化应该首先通过测量来进行。在实际进行优化时,解决方案并不总是简单的。但是,解决性能瓶颈对开发人员来说是非常令人满意的。
sulc1iza2#
说到内容,我建议读取字节到字节,并让用户定义最大大小,因为连接可以随时关闭,并读取固定大小,将禁用文件上传或更大的内容-len.如果你想看看我的web服务器,你可以复制你想要的任何部分在你的https://github.com/OUIsolutions/CWebStudio
部分解析http其request/request_parser CwebHttpRequest_parse_http_request
dbf7pr2w3#
你可以两者兼得。读大块到你的缓冲区和读字节从这个缓冲区。你将有一个最小数量的系统调用,你将有一个单一的字符(或行)解析器的简单性。你不需要寻找子字符串(即调用
strstr
)。