通过网络实现高效的二进制I / O.

时间:2015-02-09 21:09:49

标签: haskell network-programming binary-data

我正在尝试编写一个讨论二进制网络协议的小型Haskell程序,而且我遇到了惊人的难度。

似乎很清楚,二进制数据应存储为ByteString

问题:我应该只是hGet / hPut单个多字节整数,还是构建整个事物的大ByteString并使用它更高效?

似乎binary包在这里应该有用。但是,binary仅处理 lazy ByteString值。

问题: lazy hGet上的ByteString是否实际上严格读取了指定的字节数?或者它是否尝试做某种懒惰的I / O? (我想要懒惰的I / O!)

问题:为什么文档没有指定这个?

代码看起来会包含很多"获取下一个整数,将其与此值进行比较,如果没有则抛出错误,否则继续执行下一步...& #34;如果不写意大利面条代码,我不确定如何干净地构建它。

总而言之,我尝试做的事情非常简单,但我似乎正在努力寻找使代码看起来简单的方法。也许我只是过度思考这个并且遗漏了一些明显的东西......

2 个答案:

答案 0 :(得分:3)

TCP要求应用程序提供自己的消息边界标记。标记消息边界的简单协议是发送一块数据的长度,数据块以及是否存在属于同一消息的剩余块。保存消息边界信息的标头的最佳大小取决于消息大小的分布。

开发我们自己的小消息协议,我们将为头文件使用两个字节。来自字节的最高有效位(被视为Word16)将保持消息中是否存在剩余的块。剩余的15位将保存消息的长度(以字节为单位)。这将允许块大小高达32k,比典型的TCP数据包大。如果消息通常非常小,则双字节头将不是最佳的,特别是如果它们小于127字节。

我们将network-simple用于代码的网络部分。我们会使用binary个数据包序列化或反序列化消息,encodedecode来往于懒惰ByteString s。

import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString as B

import Network.Simple.TCP 
import Data.Bits
import Data.Binary
import Data.Functor
import Control.Monad.IO.Class

我们需要的第一个实用程序是能够将Word16标头写入严格的ByteString并再次将其读回。我们将以big-endian顺序编写它们。或者,可以使用Binary的{​​{1}}实例来编写这些内容。

Word16

主要挑战是发送和接收二进制包强制使用的懒惰writeBE :: Word16 -> B.ByteString writeBE x = B.pack . map fromIntegral $ [(x .&. 0xFF00) `shiftR` 8, x .&. 0xFF] readBE :: B.ByteString -> Maybe Word16 readBE s = case map fromIntegral . B.unpack $ s of [w1, w0] -> Just $ w1 `shiftL` 8 .|. w0 _ -> Nothing 。由于我们一次只能发送多达32k字节,因此我们需要能够将ByteString一个惰性字节串转换为总长度不超过最大值的块。单个块可能已超过最大值;任何不适合我们新块的块都分成多个块。

rechunk

rechunk :: Int -> [B.ByteString] -> [(Int, [B.ByteString])] rechunk n = go [] 0 . filter (not . B.null) where go acc l [] = [(l, reverse acc)] go acc l (x:xs) = let lx = B.length x l' = lx + l in if l' <= n then go (x:acc) l' xs else let (x0, x1) = B.splitAt (n-l) x in (n, reverse (x0:acc)) : go [] 0 (x1:xs) 将循环,直到我们请求的所有字节都已收到。

recvExactly

发送一个懒惰的recvExactly :: MonadIO m => Socket -> Int -> m (Maybe [B.ByteString]) recvExactly s toRead = go [] toRead where go acc toRead = do body <- recv s toRead maybe (return Nothing) (go' acc toRead) body go' acc toRead body = if B.length body < toRead then go (body:acc) (toRead - B.length body) else return . Just . reverse $ acc 包括将它分成我们知道可以发送的大小的块,并发送每个块以及包含大小的标题以及是否还有更多的块。

ByteString

接收延迟sendLazyBS :: (MonadIO m) => Socket -> L.ByteString -> m () sendLazyBS s = go . rechunk maxChunk . L.toChunks where maxChunk = 0x7FFF go [] = return () go ((li, ss):xs) = do let l = fromIntegral li let h = writeBE $ if null xs then l else l .|. 0x8000 sendMany s (h:ss) go xs 包括读取两个字节的标题,读取标题指示的大小的块,并且只要标题指示有更多的块,就继续读取。

ByteString

发送或接收包含recvLazyBS :: (MonadIO m, Functor m) => Socket -> m (Maybe L.ByteString) recvLazyBS s = fmap L.fromChunks <$> go [] where go acc = do header <- recvExactly s 2 maybe (return Nothing) (go' acc) (header >>= readBE . B.concat) go' acc h = do body <- recvExactly s . fromIntegral $ h .&. 0x7FFF let next = if h .&. 0x8000 /= 0 then go else return . Just . concat . reverse maybe (return Nothing) (next . (:acc) ) body 个实例的邮件只是发送Binary d懒惰encode或接收惰性ByteStringByteString

decode

答案 1 :(得分:2)

重新提问1 ...

如果句柄配置了NoBuffering,则每个hPutStr调用将生成写入系统调用。这将导致大量小写入的巨大性能损失。例如,参见一些基准测试的SO答案:https://stackoverflow.com/a/28146677/866915

另一方面,如果句柄启用了缓冲,则需要显式刷新句柄以确保发送缓冲数据。

我假设您正在使用像TCP这样的流媒体协议。使用UDP,您显然必须将每条消息形成并发送为原子单元。

问题#2 ......

读取代码看起来,hGet对于延迟字节串将从defaultChunkSize的块中读取大约32k的句柄。

更新:在这种情况下,hGet似乎执行惰性IO。这是一些测试它的代码。 饲料:

#!/usr/bin/env perl
$| = 1;
my $c = 0;
my $k = "1" x 1024;
while (1) {
  syswrite(STDOUT, $k);
  $c++;
  print STDERR "wrote 1k count = $c\n";
}

Test.hs:

import qualified Data.ByteString.Lazy as LBS
import System.IO

main = do
  s <- LBS.hGet stdin 320000
  let s2 = LBS.take 10 s
  print $ ("Length s2 = ", s2)

正在运行perl feed | runhaskell Test.hs 很明显,Haskell程序要求perl程序中的所有320k,即使它只使用前10个字节。