6

背景

我正在尝试为二进制网络协议编写客户端。所有网络操作都通过单个 TCP 连接执行,因此从这个意义上说,来自服务器的输入是连续的字节流。然而,在应用层,从概念上讲,服务器在流上发送一个数据包,客户端继续读取,直到它知道该数据包已完整接收,然后才发送自己的响应。

完成这项工作所需的大量工作涉及解析和生成二进制数据,为此我使用了 Data.Serialize 模块。

问题

服务器在 TCP 流上向我发送一个“数据包”。数据包不一定由换行符终止,也不是预定大小。它确实由预定数量的字段组成,并且字段通常以描述该字段长度的 4 字节数字开头。在 Data.Serialize 的帮助下,我已经有了将这个数据包的 ByteString 版本解析为更易于管理的类型的代码。

我希望能够编写一些具有这些属性的代码:

  1. 解析只定义一次,最好在我的 Serialize 实例中。我宁愿不在 IO monad 中进行额外的解析来读取正确的字节数。
  2. 当我尝试解析一个给定的数据包并且还没有所有字节都到达时,惰性 IO 只会等待额外的字节到达。
  3. 相反,当我尝试解析给定数据包并且其所有字节都已到达时,IO 不再阻塞。也就是说,我想从服务器读取足够的流来解析我的类型并形成响应以发回。如果在足够的字节到达解析我的类型之后 IO 阻塞,那么客户端和服务器将陷入死锁,每个都在等待来自另一个的更多数据。
  4. 在我发送我自己的响应之后,我可以通过解析我期望来自服务器的下一个类型的数据包来重复这个过程。

简而言之, 是否可以利用我当前的 ByteString 解析代码与惰性 IO 结合来从网络中读取正确数量的字节?

我试过的

我尝试将惰性 ByteStreams 与我的 Data.Serialize 实例结合使用,如下所示:

import Network
import System.IO
import qualified Data.ByteString.Lazy as L
import Data.Serialize

data MyType

instance Serialize MyType

main = withSocketsDo $ do
  h <- connectTo server port
  hSetBuffering h NoBuffering
  inputStream <- L.hGetContents h
  let Right parsed = decodeLazy inputStream :: Either String MyType
  -- Then use parsed to form my own response, then wait for the server reply...

这似乎主要在上面的第 3 点上失败:即使在有足够数量的字节到达解析 MyType 之后,它仍然保持阻塞。我强烈怀疑这是因为 ByteStrings 一次以给定的块大小读取,并且L.hGetContents正在等待该块的其余部分到达。虽然读取有效块大小的这种属性有助于从磁盘进行有效读取,但它似乎妨碍了我读取足够的字节来解析我的数据。

4

1 回答 1

7

你的解析器出了点问题,它太急切了。由于某种原因,它很可能需要消息之后的下一个字节。hGetContentsfrombytestring不会阻止等待整个块。它在hGetSome内部使用。

我创建了简单的测试用例。服务器每秒发送一次“hello”:

import Control.Concurrent
import System.IO
import Network

port :: Int
port = 1234

main :: IO ()
main = withSocketsDo $ do
  s <- listenOn $ PortNumber $ fromIntegral port
  (h, _, _) <- accept s

  let loop :: Int -> IO ()
      loop 0 = return ()
      loop i = do
        hPutStr h "hello"
        threadDelay 1000000
        loop $ i - 1
  loop 5

  sClose s

客户端懒洋洋地读取全部内容:

import qualified Data.ByteString.Lazy as BSL
import System.IO
import Network

port :: Int
port = 1234

main :: IO ()
main = withSocketsDo $ do
  h <- connectTo "localhost" $ PortNumber $ fromIntegral port
  bs <- BSL.hGetContents h
  BSL.putStrLn bs
  hClose h

如果您尝试同时运行两者,您将看到客户端每秒钟打印一次“hello”。所以,网络子系统没问题,问题出在其他地方——很可能在你的解析器中。

于 2013-03-12T08:29:10.047 回答