Attoparsec在大型“接听”呼叫上分配了大量内存

时间:2010-11-11 04:02:00

标签: parsing haskell attoparsec

所以我正在编写一个数据包嗅探应用程序。基本上我想让它嗅探tcp会话,然后解析它们是否是http,如果它们是,如果它们有正确的内容类型等,请将它们保存为我的硬盘上的文件。

所以,为此,我希望它有效率。由于当前的http库是基于字符串的,我将处理大文件,我只需要解析http响应,我决定在attoparsec中自己编写。

当我完成我的程序时,我发现当我使用其中的wav文件解析一个9 meg的http响应时,当我对它进行分析时,它正在尝试解析一个内存时分配一个内存。 http响应。当我看到HTTP.prof时,我看到了一些内容:

httpBody              Main                                                 362           1   0.0    0.0    93.8   99.3

 take                 Data.Attoparsec.Internal                             366        1201   0.0    0.0    93.8   99.3
     takeWith            Data.Attoparsec.Internal                             367        3603   0.0    0.0    93.8   99.3
      demandInput        Data.Attoparsec.Internal                             375         293   0.0    0.0    93.8   99.2
       prompt            Data.Attoparsec.Internal                             378         293   0.0    0.0    93.8   99.2
        +++              Data.Attoparsec.Internal                             380         586  93.8   99.2    93.8   99.2

正如你所看到的,在httpbody中的某个地方,take被称为1201次,导致500+(+++)串行的字节串联,这导致了大量的内存分配。

这是代码。 N只是http响应的内容长度(如果有的话)。如果没有,它只会尝试采取一切。

我希望它返回一个1000左右的字符字节串的惰性字节串,但即使我将其更改为只取n并返回一个严格的字节串,它仍然在其中有这些分配(它使用14 gig的内存)


httpBody n = do
  x <- if n > 0
    then AC.take n
    else AC.takeWhile (\_ -> True)
  if B.length x == 0
    then return Nothing
    else return (Just x)

我正在阅读那个做组合的人的博客,他也有同样的问题,但我从来没有听说过解决方案。有没有人曾经遇到过这个问题或者找到了解决方案?

编辑:好吧,好吧,我把这一整天都放了,什么都没有。在研究了这个问题之后,我认为没有一种方法可以在没有为attoparsec添加一个惰性字节串访问器的情况下做到这一点。我还查看了所有其他库,它们或者缺少字节串或其他东西。

所以我找到了一个解决方法。如果您考虑一个http请求,它会转到标题,换行符,换行符,正文。由于正文是最后一个,并且解析返回一个元组,包含你解析的内容和剩余的字节串,我可以跳过解析attoparsec中的正文,而是直接从剩下的字节串中拔出正文。


parseHTTPs bs = if P.length results == 0
  then Nothing
  else Just results
  where results = foldParse(bs, [])

foldParse (bs,rs) = case ACL.parse httpResponse bs of
  ACL.Done rest r -> addBody (rest,rs) r
  otherwise ->  rs

addBody (rest,rs) http = foldParse (rest', rs')
  where
    contentlength = ((read . BU.toString) (maybe "0" id (hdrContentLength (rspHeaders http))))
    rest' = BL.drop contentlength rest
    rs' = rs ++ [http { rspBody = body' }]
    body'
      | contentlength == 0  = Just rest
      | BL.length rest == 0 = Nothing
      | otherwise           = Just (BL.take contentlength rest)
httpResponse = do
  (code, desc) <- statusLine
  hdrs <- many header
  endOfLine
--  body <- httpBody ((read . BU.toString) (maybe "0" id (hdrContentLength parsedHeaders)))

  return Response { rspCode = code, rspReason = desc, rspHeaders = parseHeaders hdrs,  rspBody = undefined }

它有点乱,但最终它的工作速度很快,并且只能分配我想要的东西。所以基本上你折叠收集http数据结构的字节串,然后在集合之间,我检查我刚刚得到的结构的内容长度,从剩余的字节串中提取适当的数量,然后继续,如果有任何字节串。< / p>

编辑:我实际完成了这个项目。奇迹般有效。我没有正确地阴谋,但如果有人想要查看整个来源,您可以在https://github.com/onmach/Audio-Sniffer找到它。

1 个答案:

答案 0 :(得分:5)

这里的组合家伙:)

如果内存服务,attoparsec的问题是需要一次输入一点点,建立一个最终连接的惰性字节串。我的“解决方案”是自己滚动输入功能。也就是说,我从网络套接字获取attoparsec的输入流,我知道消息中需要多少字节。基本上,我分为两种情况:

  • 消息很小:从套接字读取最多4k并一次吃掉一点字节串(字节串很快,我们在耗尽后扔掉4k)。

  • 消息是“大”(这里大的意思是在bittorrent中大约16千字节说话):我们计算我们拥有的4k块可以实现多少,然后我们只是请求底层网络套接字填充内容。我们现在有两个字节串,4k块的剩余部分和大块。他们拥有所有数据,因此将它们连接起来并解析它们就是我们所做的。

    您可以优化连接步骤。

TL; DR版本:我在attoparsec之外处理它并手动循环以避免问题。

相关的组合提交是fc131fe24,参见

https://github.com/jlouis/combinatorrent/commit/fc131fe24207909dd980c674aae6aaba27b966d4

了解详情。