Цель состоит в том, чтобы получить канал со следующей сигнатурой типа
protobufConduit :: MonadResource m => (ByteString -> a) -> Conduit ByteString m a
Канал должен многократно анализировать буферы протокола (используя функцию ByteString -> a
), полученные через TCP / IP (используя пакет network-conduit
).
Формат телеграфного сообщения:
{length (32 bits big endian)}{protobuf 1}{length}{protobuf 2}...
(Фигурные скобки не являются частью протокола, они используются здесь только для разделения сущностей).
Первая идея заключалась в том, чтобы использовать sequenceSink
для многократного применения Sink
, который может анализировать один ProtoBuf:
[...]
import qualified Data.Binary as B
import qualified Data.Conduit.Binary as CB
import qualified Data.Conduit.Util as CU
protobufConduit :: MonadResource m => (ByteString -> a) -> Conduit ByteString m a
protobufConduit protobufDecode =
CU.sequenceSink () $ \() ->
do lenBytes <- CB.take 4 -- read protobuf length
let len :: Word32
len = B.decode lengthBytes -- decode ProtoBuf length
intLen = fromIntegral len
protobufBytes <- CB.take intLen -- read the ProtoBuf bytes
return $ CU.Emit () [ protobufDecode protobufBytes ] -- emit decoded ProtoBuf
Это не работает (работает только для первого буфера протокола), потому что, похоже, есть несколько «оставшихся» байтов, уже прочитанных из источника, но не использованных через CB.take
, которые отбрасываются.
И я не нашел способа засунуть «остальное обратно в источник».
Я совершенно неправильно понял концепцию?
PS: Даже если я использую здесь буферы протокола, проблема не связана с буферами протокола. Для отладки проблемы я всегда использую {length}{UTF8 encoded string}{length}{UTF8 encoded string}...
и канал, аналогичный приведенному выше (utf8StringConduit :: MonadResource m => Conduit ByteString m Text
).
Обновить:
Я просто попытался заменить состояние (без состояния ()
в приведенном выше примере) оставшимися байтами и заменил вызовы CB.take
вызовами функции, которая сначала потребляет уже прочитанные байты (из состояния) и вызывает await
только по мере необходимости (когда состояние недостаточно велико). К сожалению, это тоже не работает, потому что, как только в источнике не осталось байтов, sequenceSink
не выполняет код, но состояние по-прежнему содержит оставшиеся байты :-(.
Если вас должен заинтересовать код (он не оптимизирован или очень хорош, но его должно быть достаточно для тестирования):
utf8StringConduit :: forall m. MonadResource m => Conduit ByteString m Text
utf8StringConduit =
CU.sequenceSink [] $ \st ->
do (lengthBytes, st') <- takeWithState BS.empty st 4
let len :: Word32
len = B.decode $ BSL.fromChunks [lengthBytes]
intLength = fromIntegral len
(textBytes, st'') <- takeWithState BS.empty st' intLength
return $ CU.Emit st'' [ TE.decodeUtf8 $ textBytes ]
takeWithState :: Monad m
=> ByteString
-> [ByteString]
-> Int
-> Pipe l ByteString o u m (ByteString, [ByteString])
takeWithState acc state 0 = return (acc, state)
takeWithState acc state neededLen =
let stateLenSum = foldl' (+) 0 $ map BS.length state
in if stateLenSum >= neededLen
then do let (firstChunk:state') = state
(neededChunk, pushBack) = BS.splitAt neededLen firstChunk
acc' = acc `BS.append` neededChunk
neededLen' = neededLen - BS.length neededChunk
state'' = if BS.null pushBack
then state'
else pushBack:state'
takeWithState acc' state'' neededLen'
else do aM <- await
case aM of
Just a -> takeWithState acc (state ++ [a]) neededLen
Nothing -> error "to be fixed later"
1 ответ
Для синтаксического анализа и сериализации буфера протокола мы используем messageWithLengthPutM
и messageWithLengthGetM
(см. Ниже), но я предполагаю, что он использует кодировку varint для длины, которая вам не нужна. Я бы, вероятно, попытался адаптировать нашу реализацию ниже, заменив messageWithLength
Get / Put чем-то вроде
myMessageWithLengthGetM =
do size <- getWord32be
getMessageWithSize size
Но я понятия не имею, как реализовать getMessageWithSize
, используя доступные функции из пакета буфера протокола. С другой стороны, вы можете просто getByteString
, а затем "повторно проанализировать" строку байтов.
По поводу каналов: пробовали ли вы реализовать канал без Data.Conduit.Util
? Что-то вроде
protobufConduit protobufDecode = loop
where
loop =
do len <- liftM convertLen (CB.take 4)
bs <- CB.take len
yield (protobufDecode bs)
loop
Вот код, который мы используем:
pbufSerialize :: (ReflectDescriptor w, Wire w) => Conduit w IO ByteString
pbufSerialize = awaitForever f
where f pb = M.mapM_ yield $ BSL.toChunks $ runPut (messageWithLengthPutM pb)
pbufParse :: (ReflectDescriptor w, Wire w, Show w) => Conduit ByteString IO w
pbufParse = new
where
new = read (runGet messageWithLengthGetM . BSL.fromChunks . (:[]))
read parse =
do mbs <- await
case mbs of
Just bs -> checkResult (parse bs)
Nothing -> return ()
checkResult result =
case result of
Failed _ errmsg -> fail errmsg
Partial cont -> read (cont . Just . BSL.fromChunks . (:[]))
Finished rest _ msg ->
do yield msg
checkResult (runGet messageWithLengthGetM rest)
pbufSerialize
и pbufParse
:-). Идея не использовать CU.sequenceSink
была решающей. Спасибо!
Похожие вопросы
Новые вопросы
haskell
Haskell — это чисто функциональный язык программирования со строгой статической типизацией, отложенными вычислениями, обширной поддержкой параллелизма и параллелизма, а также уникальными возможностями абстракции.
protobufBytes <- CB.take intLen
.protobuf
предшествуют 4 байта, которые представляют длину (в байтах)protobuf
. Моя первоначальная теория заключалась в том, что эти 4 байта могут фактически представлять длинуprotobuf
ПЛЮС заголовок длиной 4 байта. Это приведет к тому, что ваш код прочитает 4 байта после фактического концаprotobuf
, ошибочно потребляя 4 байта, которые должны представлять длину следующегоprotobuf
.