The Bittorrent Wire protocol is a protocol used for communication between peers in a bittorrent cloud. The protocol is explicitly defined to balance between conserving bandwidth and being easy to parse. Hence, it is a binary protocol, but it is fairly easy to parse.
When I wrote etorrent as an experiment, I was faced with the problem of parsing this protocol. In Erlang the task is not hard however, since the language can pattern match on bit-patterns. I began by defining a set of constant values, each corresponding to a message type:
%% Packet types
-define(CHOKE, 0:8).
-define(UNCHOKE, 1:8).
-define(INTERESTED, 2:8).
-define(NOT_INTERESTED, 3:8).
-define(HAVE, 4:8).
-define(BITFIELD, 5:8).
-define(REQUEST, 6:8).
-define(PIECE, 7:8).
-define(CANCEL, 8:8).
-define(PORT, 9:8).
The designation 1:8 means that we need to represent the integer value of 1 in 8 bits of information (i.e., in a byte). With these values down, defining a decoder in Erlang is pretty straightforward:
%%--------------------------------------------------------------------
%% Function: recv_message(Message) -> keep_alive | choke | unchoke |
%% interested | not_interested | {have, integer()} | ...
%% Description: Receive a message from a peer and decode it
%%--------------------------------------------------------------------
recv_message(Rate, Message) ->
MSize = size(Message),
Decoded =
case Message of
<<>> ->
keep_alive;
<<?CHOKE>> ->
choke;
<<?UNCHOKE>> ->
unchoke;
<<?INTERESTED>> ->
interested;
<<?NOT_INTERESTED>> ->
not_interested;
<<?HAVE, PieceNum:32/big>> ->
{have, PieceNum};
<<?BITFIELD, BitField/binary>> ->
{bitfield, BitField};
<<?REQUEST, Index:32/big, Begin:32/big, Len:32/big>> ->
{request, Index, Begin, Len};
<<?PIECE, Index:32/big, Begin:32/big, Data/binary>> ->
{piece, Index, Begin, Data};
<<?CANCEL, Index:32/big, Begin:32/big, Len:32/big>> ->
{cancel, Index, Begin, Len};
<<?PORT, Port:16/big>> ->
{port, Port};
end,
{Decoded, etorrent_rate:update(Rate, MSize), MSize}.
This is pretty straightforward idiomatic Erlang as one would writeit. We convert the numbers into tuples or atoms. The idea is that the atom or the first element of the tuple identifies the type of the message that was sent. Note that in idiomatic Erlang code, the process will crash if the pattern match fails. In that case, we have to handle that problem in another janitorial process.
I toyed with the idea of doing the same parsing in Haskell. Initially, my thought was that it would be rather hard to beat the Erlang code in size. Looking at the above code, it is almost the specification of the protocol and we can't get rid of the specification.
In Haskell, I started with the default construction of an algebraic datatype. It simply reflects the tuple/atom construction of Erlang:
type BitField = B.ByteString
type PieceNum = Integer
type PieceOffset = Integer
type PieceLength = Integer
data Message = KeepAlive
| Choke
| Unchoke
| Interested
| NotInterested
| Have PieceNum
| BitField BitField
| Request PieceNum PieceOffset PieceLength
| Piece PieceNum PieceOffset B.ByteString
| Cancel PieceNum PieceOffset PieceLength
| Port Integer
deriving (Eq, Show)
If it seems more verbose than the initial Erlang counterpart, then it fools you. In the above, we also encode the types of the messages - but this is the only place where we would have to designate the types in the whole program. Type reconstruction by inference will figure out the details for us.
I expect my data to be presented to me via lazy bytestrings (see Data.ByteString.Lazy). It also turns out that George Giorgidze built the HCodecs package. This package provides decoders for MIDI, Wave and SoundFont2 files. But the real gem of the package is the development of binary parsers for ByteStrings. This turns out to be exactly what we would like to process.
The parser is a monad. So we can use do-notation to decode the messages. Here is an example of decoding the PIECE-message:
7 -> do pn <- fromIntegral <$> getWord32be
os <- fromIntegral <$> getWord32be
c <- getRemainingLazyByteString return $ Piece pn p c This looks nice, but the <$> from Control.Applicative hints
somewhat at where this will go. Note that we are not using the full
power of the monad. In particular, we don't use earlier results from
the monad to decide what to do next. Since there is no dependency
chain, it means we can just utilize that every monad is also an
applicative functor. In particular we can define
7 -> Piece <$> gw32 <*> gw32 <*> gw32 <*> getRemainingLazyByteString
...
where gw32 = fromIntegral <$> getWord32be
using the applicative functor to solve the game. With this down, the rest of the parser is easy:
decodeMsg :: Parser Message
decodeMsg =
do m <- getWord8 case m of 0 -> return Choke
1 -> return Unchoke
2 -> return Interested
3 -> return NotInterested
4 -> Have <$> gw32
5 -> BitField <$> getRemainingLazyByteString
6 -> Request <$> gw32 <*> gw32 <*> gw32
7 -> Piece <$> gw32 <*> gw32 <*> getRemainingLazyByteString
8 -> Cancel <$> gw32 <*> gw32 <*> gw32
9 -> Port <$> (fromIntegral <$> getWord16be)
_ -> fail "Incorrect message parse"
where gw32 = fromIntegral <$> getWord32be
And there you have it: A Haskell version which is more succinct than the Erlang version while being type safe in the process. The reason it works is due to the clever type class hierachy in Haskell: The parser is a monad and all monads are applicative functors. This lets us combine parsers cleverly into an effective decoder for wire protocol messages.
I do hope it is fairly efficient. Protocol decoding is probably going to be a hot place in the program.
