代码之家  ›  专栏  ›  技术社区  ›  David McHealy

在大的“take”调用上,atParsc分配了大量内存

  •  19
  • David McHealy  · 技术社区  · 15 年前

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

    因此,为了达到这个目的,我希望它是有效率的。由于当前的HTTP库是基于字符串的,我将处理大型文件,而且我只需要解析HTTP响应,所以我决定在Attoparsec中使用自己的库。

    当我完成程序时,我发现当我用一个wav文件解析一个9meg的HTTP响应时,当我分析它时,它正在分配一个gig内存,当它试图解析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
    
    

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

    这是密码。n只是HTTP响应的内容长度(如果有)。如果没有一个,它只会试图拿走一切。

    我希望它返回1000个左右字符字节的懒惰字节串,但是即使我把它改为只接受n并返回严格的字节串,它仍然有这些分配(它使用14千兆内存)。

    
    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)
    

    我在读一篇由Combinator的人写的博客,他也有同样的问题,但我从来没有听说过解决方案。以前有没有人遇到过这个问题或找到了解决方案?

    编辑:好吧,我一整天都把这个忘了,什么也没得到。在研究了这个问题之后,我认为没有一种方法可以做到这一点,而不需要在ATTOParsec中添加一个懒惰的bytesting访问器。我还查看了所有其他的图书馆,它们要么缺少索引,要么缺少其他东西。

    所以我找到了一个解决办法。如果您考虑一个HTTP请求,它会出现headers、newline、newline和body。因为主体是最后一个,解析返回一个包含您解析的内容和字节字符串剩余内容的元组,所以我可以跳过在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 .

    1 回复  |  直到 15 年前
        1
  •  5
  •   I GIVE CRAP ANSWERS    15 年前

    我是Combinatorrent先生:)

    如果内存可用,那么Attoparsec的问题是一次需要输入一点,从而构建一个最终连接起来的懒惰字节串。我的“解决方案”是自己滚动输入函数。也就是说,我从一个网络套接字中获取ATOAParsec的输入流,我知道消息中需要多少字节。基本上,我分为两种情况:

    • 消息很小:从插座上读到4K,然后一点一点地把它吃掉(字节块很快,用完后我们就把4K扔掉了)。

    • 消息是“大的”(在BitTorrent语言中,大的意思是大约16kbyte):我们计算我们可以完成的4K块的数量,然后简单地请求底层网络套接字来填充这些内容。我们现在有两个字节,4K块的剩余部分和大块。它们拥有所有的数据,所以连接这些数据并在其中解析它们是我们要做的。

      您可能能够在第一步优化连接。

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

    相关的Combinator提交是fc131fe24,请参见

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

    详细信息。