On this page we describe the TCP Transport as an example for developers who wish to write their own instantiations of the Transport layer. The purpose of any such instantiation is to provide a function
createTransport :: <transport specific arguments> -> IO (Either <transport specific error> Transport)
For instance, the TCP transport offers
createTransport :: N.HostName -> N.ServiceName -> IO (Either IOException Transport)
This could be the only function that Network.Transport.TCP
exports (the only reason it exports more is to provide an API for unit tests for the TCP transport, some of which work at a lower level). Your implementation will now be guided by the Network.Transport
API. In particular, you will need to implement newEndPoint
, which in turn will require you to implement receive
, connect
, etc.
The following picture is a schematic overview of how the Network.Transport concepts map down to their TCP counterparts.
Transport
instances. At the top we have two Transport instances on the same TCP node 198.51.100.1. These could be as part of the same Haskell process or, perhaps more typically, in separate processes. Different Transport instances on the same host must get a different port number.EndPoint
s. A Transport can have multiple EndPoints (or none). EndPointAddresses in the TCP transport are triplets TCP host:TCP port:endpoint ID
.Chan
on which all endpoint Event
s are posted.We briefly discuss the implementation of the most important functions in the Transport API.
createTransport
)When a TCP transport is created at host 192.51.100.1, port 1080, createTransport
sets up a socket, binds it to 192.51.100.1:1080, and then spawns a thread to listen for incoming requests. This thread will handle the connection requests for all endpoints on this TCP node: individual endpoints do not set up their own listening sockets.
This is the only set up that Network.Transport.TCP.createTransport
needs to do.
newEndPoint
)In the TCP transport the set up of new endpoints is straightforward. We only need to create a new Chan
on which we will output all the events and allocate a new ID for the endpoint. Now receive
is just readChan
and address
is the triplet host:port:ID
connect
)Consider the situation shown in the diagram above, and suppose that endpoint 198.51.100.1:1081:0 (lets call it A) wants to connect to 198.51.100.2:1080:1 (_B_). Since there is no TCP connection between these two endpoints yet we must set one up.
connect
to A we know that a TCP connection to A is already available.ConnectionRequestAccepted
and spawn a thread to listen for incoming messages on the newly created TCP connection.At this point there is a TCP connection between A and B but not yet a Network.Transport connection; at this point, however, the procedure is the same for all connection requests from A to B (as well as as from B to A):
RequestConnectionId
message to B across the existing TCP connection.ConnectionOpened
on B’s endpoint.A complication arises when A and B simultaneously attempt to connect each other and no TCP connection has yet been set up. In this case two TCP connections will temporarily be created; B will accept the connection request from A, keeping the first TCP connection, but A will reply with
ConnectionRequestCrossed
, denying the connection request from B, and then close the socket.
Note that connection IDs are locally unique. When A and B both connect to C, then C will receive two ConnectionOpened
events with IDs (say) 1024 and 1025. However, when A connects to B and C, then it is entirely possible that the connection ID that A receives from both B and C is identical. Connection IDs for outgoing connections are however not visible to application code.
send
)To send a message from A to B the payload is given a Transport header consisting of the message length and the connection ID. When B receives the message it posts a Received
event.
To close a connection, A just sends a CloseConnection
request to B, and B will post a ConnectionClosed
event.
When there are no more Transport connections between two endpoints the TCP connection between them is torn down.
Actually, it’s not quite that simple, because A and B need to agree that the TCP connection is no longer required. A might think that the connection can be torn down, but there might be a
RequestConnectionId
message from B to A in transit in the network. A and B both do reference counting on the TCP connection. When A’s reference count for its connection to B reaches zero it will send aCloseSocket
request to B. When B receives it, and its refcount for its connection to A is also zero, it will close its socket and reply with a reciprocalCloseSocket
to A. If however B had already sent aRequestConnectionId
to A it will simply ignore theCloseSocket
request, and when A receives theRequestConnectionId
it simply forgets it ever sent theCloseSocket
request.
In the TCP transport createTransport
needs to do some setup, newEndPoint
barely needs to do any at all, and connect
needs to set up a TCP connection when none yet exists to the destination endpoint. In Transport instances for connectionless protocols this balance of work will be different. For instance, for a UDP transport createTransport
may barely need to do any setup, newEndPoint
may set up a UDP socket for the endpoint, and connect
may only have to setup some internal datastructures and send a UDP message.
Network.Transport API functions should not throw any exceptions, but declare explicitly in their types what errors can be returned. This means that we are very explicit about which errors can occur, and moreover map Transport-specific errors (“socket unavailable”) to generic Transport errors (“insufficient resources”). A typical example is connect
with type:
connect :: EndPoint -- ^ Local endpoint
-> EndPointAddress -- ^ Remote endpoint
-> Reliability -- ^ Desired reliability of the connection
-> IO (Either (TransportError ConnectErrorCode) Connection)
TransportError
is defined as
data TransportError error = TransportError error String
deriving Typeable
and has Show
and Exception
instances so that application code has the option of throw
ing returned errors. Here is a typical example of error handling in the TCP transport; it is an internal function that does the initial part of the TCP connection setup: create a new socket, and the remote endpoint ID we’re interested in and our own address, and then wait for and return the response:
socketToEndPoint :: EndPointAddress -- ^ Our address
-> EndPointAddress -- ^ Their address
-> IO (Either (TransportError ConnectErrorCode) (N.Socket, ConnectionRequestResponse))
socketToEndPoint (EndPointAddress ourAddress) theirAddress = try $ do
(host, port, theirEndPointId) <- case decodeEndPointAddress theirAddress of
Nothing -> throw (failed . userError $ "Could not parse")
Just dec -> return dec
addr:_ <- mapExceptionIO invalidAddress $ N.getAddrInfo Nothing (Just host) (Just port)
bracketOnError (createSocket addr) N.sClose $ \sock -> do
mapExceptionIO failed $ N.setSocketOption sock N.ReuseAddr 1
mapExceptionIO invalidAddress $ N.connect sock (N.addrAddress addr)
response <- mapExceptionIO failed $ do
sendMany sock (encodeInt32 theirEndPointId : prependLength [ourAddress])
recvInt32 sock
case tryToEnum response of
Nothing -> throw (failed . userError $ "Unexpected response")
Just r -> return (sock, r)
where
createSocket :: N.AddrInfo -> IO N.Socket
createSocket addr = mapExceptionIO insufficientResources $
N.socket (N.addrFamily addr) N.Stream N.defaultProtocol
invalidAddress, insufficientResources, failed :: IOException -> TransportError ConnectErrorCode
invalidAddress = TransportError ConnectNotFound . show
insufficientResources = TransportError ConnectInsufficientResources . show
failed = TransportError ConnectFailed . show
Note how exceptions get mapped to TransportErrors
using mapExceptionID
, which is defined in Network.Transport.Internal
as
mapExceptionIO :: (Exception e1, Exception e2) => (e1 -> e2) -> IO a -> IO a
mapExceptionIO f p = catch p (throw . f)
Moreover, the original exception is used as the String
part of the TransportError
. This means that application developers get transport-specific feedback, which is useful for debugging, not cannot take use this transport-specific information in their code, which would couple applications to tightly with one specific transport implementation.