TCP требует, чтобы приложение предоставляло свои собственные маркеры границы сообщений. Простым протоколом отмечать границы сообщений является отправка длины фрагмента данных, фрагмента данных и оставшихся фрагментов, которые являются частью одного и того же сообщения. Оптимальный размер заголовка, который содержит информацию о границе сообщения, зависит от распределения размеров сообщений.
Разработка собственного протокола сообщений, мы будем использовать два байта для наших заголовков. Самый старший бит из байтов (обработанный как Word16
) будет содержать то, остались или нет оставшиеся фрагменты в сообщении. Остальные 15 бит будут содержать длину сообщения в байтах. Это позволит размер блоков до 32 тыс., Что больше, чем типичные пакеты TCP. Заголовок из двух байтов будет менее оптимальным, если сообщения обычно очень малы, особенно если они меньше 127 байт.
Мы собираемся использовать network-simple для сетевой части нашего кода. Мы будем сериализовать или десериализовать сообщения с пакетом binary, который encode
s и decode
s от и от ленивого 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
с и читать их обратно. Мы напишем их по-крупному. В качестве альтернативы они могут быть записаны в виде экземпляра Binary
для 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
Основной задачей будет получать и отправлять ленивые ByteString
сек навязанных нам бинарного пакета. Поскольку мы можем отправлять только до 32 тыс. Байт за один раз, мы должны иметь возможность 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
состоит из разбить его на куски размера, мы знаем, что мы можем посылать и посылать каждый кусок вместе с заголовком, держащего размером и есть ли еще глыбы ли.
Получение ленивым 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
экземпляр просто послать encode
д ленивого ByteString
или получение отложенной ByteString
и decode
ING его.
sendBinary :: (MonadIO m, Binary a) => Socket -> a -> m()
sendBinary s = sendLazyBS s . encode
recvBinary :: (MonadIO m, Binary a, Functor m) => Socket -> m (Maybe a)
recvBinary s = d . fmap decodeOrFail <$> recvLazyBS s
where
d (Just (Right (_, _, x))) = Just x
d _ = Nothing
Я не могу ответить на все ваши вопросы, но я считаю, трубой и труба поможет вам избежать ленивого I/O: проверить [Data.Conduit.Binary] (https://hackage.haskell.org/ package/conduit-0.4.0/docs/Data-Conduit-Binary.html) и [Pipes.ByteString] (https://hackage.haskell.org/package/pipes-bytestring-2.1.1/docs/Pipes-ByteString ,html) –
У меня нет моих старых тестов, но я помню, что было более эффективно напрямую «hPut» и «hGet» ваши номера прямо в сокет, чем создавать большую «ByteString» и отправлять их. Разница в скорости может быть в 5 раз быстрее для 'hPut' /' hGet'. Вот как, например, как все пакеты 'blaze- *' получают свои улучшения скорости. –
@GabrielGonzalez, это потому, что 'hPut' и' hGet' используют функции, которые уже выполняют свою собственную буферизацию? – dfeuer