我正在尝试编写一个讨论二进制网络协议的小型Haskell程序,而且我遇到了惊人的难度。
似乎很清楚,二进制数据应存储为ByteString
。
问题:我应该只是hGet
/ hPut
单个多字节整数,还是构建整个事物的大ByteString
并使用它更高效?
似乎binary
包在这里应该有用。但是,binary
仅处理 lazy ByteString
值。
问题: lazy hGet
上的ByteString
是否实际上严格读取了指定的字节数?或者它是否尝试做某种懒惰的I / O? (我不想要懒惰的I / O!)
问题:为什么文档没有指定这个?
代码看起来会包含很多"获取下一个整数,将其与此值进行比较,如果没有则抛出错误,否则继续执行下一步...& #34;如果不写意大利面条代码,我不确定如何干净地构建它。
总而言之,我尝试做的事情非常简单,但我似乎正在努力寻找使代码看起来简单的方法。也许我只是过度思考这个并且遗漏了一些明显的东西......
答案 0 :(得分:3)
TCP要求应用程序提供自己的消息边界标记。标记消息边界的简单协议是发送一块数据的长度,数据块以及是否存在属于同一消息的剩余块。保存消息边界信息的标头的最佳大小取决于消息大小的分布。
开发我们自己的小消息协议,我们将为头文件使用两个字节。来自字节的最高有效位(被视为Word16
)将保持消息中是否存在剩余的块。剩余的15位将保存消息的长度(以字节为单位)。这将允许块大小高达32k,比典型的TCP数据包大。如果消息通常非常小,则双字节头将不是最佳的,特别是如果它们小于127字节。
我们将network-simple用于代码的网络部分。我们会使用binary个数据包序列化或反序列化消息,encode
和decode
来往于懒惰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
或接收惰性ByteString
和ByteString
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个字节。