在Haskell中混合使用ByteString解析和网络IO

Chr*_*ice 6 haskell

背景

我正在尝试为二进制网络协议编写客户端.所有网络操作都通过单个TCP连接执行,因此从这个意义上说,服务器的输入是连续的字节流.然而,在应用层,服务器在概念上在流上发送分组,并且在发送其自己的响应之前,客户端继续读取直到它知道已完全接收到分组.

完成这项工作所需的大量工作包括解析和生成二进制数据,我正在使用Data.Serialize模块.

问题

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

我希望能够用这些属性编写一些代码:

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

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

我试过的

我试图将lazy 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...
Run Code Online (Sandbox Code Playgroud)

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

Yur*_*ras 7

你的解析器有问题,它太急切了.由于某种原因,它很可能需要消息之后的下一个字节.hGetContentsbytestring不会阻塞等待整个块.它在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
Run Code Online (Sandbox Code Playgroud)

客户端懒洋洋地读取整个内容:

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
Run Code Online (Sandbox Code Playgroud)

如果您尝试同时运行两者,您将看到客户端每秒打印"hello".所以,网络子系统还可以,问题出在其他地方 - 最有可能是你的解析器.