19

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

因此,为此,我希望它高效。由于当前的 http 库是基于字符串的,而且我将处理大文件,而且我真的只需要解析 http 响应,因此我决定在 attoparsec 中推出我自己的库。

当我完成我的程序时,我发现当我解析一个包含 wav 文件的 9 meg http 响应时,当我分析它时,它在尝试解析 http 响应的主体时分配了一个内存. 当我查看 HTTP.prof 时,我看到了一些行:

httpBody Main 362 1 0.0 0.0 93.8 99.3

 取 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
      需求输入数据.Attoparsec.Internal 375 293 0.0 0.0 93.8 99.2
       提示 Data.Attoparsec.Internal 378 293 0.0 0.0 93.8 99.2
        +++ 数据.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数据结构的字节串,然后在集合之间,我检查我刚刚得到的结构的内容长度,从剩余的字节串中提取适当的数量,然后如果还有任何字节串就继续。

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

4

1 回答 1

5

组合的家伙在这里:)

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

  • 消息很小:从套接字读取最多 4k 并一次吃一点字节串(字节串切片很快,我们在用完 4k 后将其丢弃)。

  • 消息是“大”的(这里的大意味着在 bittorrent 中大约 16 Kilobyte):我们计算我们有多少 4k 块可以完成,然后我们简单地请求底层网络套接字来填充东西。我们现在有两个字节串, 4k 块和大块的剩余部分。它们拥有所有数据,因此将它们连接起来并解析它们就是我们所做的。

    您也许可以优化串联步骤。

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

相关的combinatorrent commit是fc131fe24,见

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

详情。

于 2010-12-05T21:46:06.873 回答