2015-02-09 2 views
10

Я пытаюсь написать небольшую программу Haskell, которая говорит о бинарном сетевом протоколе, и у меня возникает удивительная сложность.Эффективный двоичный ввод-вывод по сети

Кажется очевидным, что двоичные данные должны храниться как ByteString.

Вопрос: Должен ли я просто hGet/hPut отдельные многобайтовые целые числа, или это более производительным, чтобы построить большой ByteString всей вещи и использовать это?

Похоже, что пакет binary должен быть полезен здесь. Однако binary касается только ленивыйByteString значения.

Вопрос: Есть ли hGet на ленивыйByteString фактически читать указанное число байтов строго? Или он пытается сделать какой-то ленивый ввод-вывод? (Я делаю не хочу ленивый I/O!)

Вопрос: Почему в документации не указано это?

Код выглядит так, будто он будет содержать много «получить следующее целое число, сравнить его с этим значением, если нет, то выбросить ошибку, в противном случае перейти к следующему шагу ...» Я не уверен, как чтобы четко структурировать это без написания кода спагетти.

В общем, то, что я пытаюсь сделать, довольно просто, но я, кажется, борюсь за способ сделать код посмотреть просто. Может быть, я просто слишком думая, что это и не хватает что-то очевидное ...

+2

Я не могу ответить на все ваши вопросы, но я считаю, трубой и труба поможет вам избежать ленивого 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) –

+0

У меня нет моих старых тестов, но я помню, что было более эффективно напрямую «hPut» и «hGet» ваши номера прямо в сокет, чем создавать большую «ByteString» и отправлять их. Разница в скорости может быть в 5 раз быстрее для 'hPut' /' hGet'. Вот как, например, как все пакеты 'blaze- *' получают свои улучшения скорости. –

+0

@GabrielGonzalez, это потому, что 'hPut' и' hGet' используют функции, которые уже выполняют свою собственную буферизацию? – dfeuer

ответ

2

Re вопрос 1 ...

Если ручка настраивается с NoBuffering каждого hPutStr вызова будет генерировать системы записи вызова. Это приведет к огромному штрафу за производительность для большого количества небольших записей. См., Например, этот ответ SO для некоторого бенчмаркинга: https://stackoverflow.com/a/28146677/866915

С другой стороны, если ручка включена с буферизацией, вам необходимо явно очистить дескриптор, чтобы обеспечить отправку буферизованных данных.

Я предполагаю, что вы используете потоковый протокол, такой как TCP. С UDP вы, очевидно, должны формировать и отправлять каждое сообщение как атомную единицу.

Re вопрос # 2 ...

Чтение кода он появляется, что hGet для ленивых байтовых строк будет читать из ручки в куски defaultChunkSize которая составляет около 32к.

Обновление: Похоже, что hGet не выполнить ленивый ввод в этом случае. Вот какой код проверить это. корма:

#!/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 требует все 320K из программы на языке Perl, даже если он использует только первые 10 байт.

3

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