Safe Haskell | None |
---|---|
Language | Haskell98 |
TCP implementation of the transport layer.
The TCP implementation guarantees that only a single TCP connection (socket)
will be used between endpoints, provided that the addresses specified are
canonical. If A connects to B and reports its address as
192.168.0.1:8080
and B subsequently connects tries to connect to A as
client1.local:http-alt
then the transport layer will not realize that the
TCP connection can be reused.
Applications that use the TCP transport should use
withSocketsDo
in their main function for Windows
compatibility (see Network.Socket).
- createTransport :: HostName -> ServiceName -> TCPParameters -> IO (Either IOException Transport)
- data TCPParameters = TCPParameters {}
- defaultTCPParameters :: TCPParameters
- createTransportExposeInternals :: HostName -> ServiceName -> TCPParameters -> IO (Either IOException (Transport, TransportInternals))
- data TransportInternals = TransportInternals {}
- type EndPointId = Word32
- encodeEndPointAddress :: HostName -> ServiceName -> EndPointId -> EndPointAddress
- decodeEndPointAddress :: EndPointAddress -> Maybe (HostName, ServiceName, EndPointId)
- data ControlHeader
- data ConnectionRequestResponse
- firstNonReservedLightweightConnectionId :: LightweightConnectionId
- firstNonReservedHeavyweightConnectionId :: HeavyweightConnectionId
- socketToEndPoint :: EndPointAddress -> EndPointAddress -> Bool -> Maybe Int -> IO (Either (TransportError ConnectErrorCode) (Socket, ConnectionRequestResponse))
- type LightweightConnectionId = Word32
Main API
createTransport :: HostName -> ServiceName -> TCPParameters -> IO (Either IOException Transport) Source
Create a TCP transport
data TCPParameters Source
Parameters for setting up the TCP transport
TCPParameters | |
|
defaultTCPParameters :: TCPParameters Source
Default TCP parameters
Internals (exposed for unit tests)
createTransportExposeInternals :: HostName -> ServiceName -> TCPParameters -> IO (Either IOException (Transport, TransportInternals)) Source
You should probably not use this function (used for unit testing only)
data TransportInternals Source
Internal functionality we expose for unit testing
TransportInternals | |
|
type EndPointId = Word32 Source
Local identifier for an endpoint within this transport
encodeEndPointAddress :: HostName -> ServiceName -> EndPointId -> EndPointAddress Source
Encode end point address
decodeEndPointAddress :: EndPointAddress -> Maybe (HostName, ServiceName, EndPointId) Source
Decode end point address
data ControlHeader Source
Control headers
CreatedNewConnection | Tell the remote endpoint that we created a new connection |
CloseConnection | Tell the remote endpoint we will no longer be using a connection |
CloseSocket | Request to close the connection (see module description) |
data ConnectionRequestResponse Source
Response sent by B to A when A tries to connect
ConnectionRequestAccepted | B accepts the connection |
ConnectionRequestInvalid | A requested an invalid endpoint |
ConnectionRequestCrossed | As request crossed with a request from B (see protocols) |
firstNonReservedLightweightConnectionId :: LightweightConnectionId Source
We reserve a bunch of connection IDs for control messages
firstNonReservedHeavyweightConnectionId :: HeavyweightConnectionId Source
We reserve some connection IDs for special heavyweight connections
:: EndPointAddress | Our address |
-> EndPointAddress | Their address |
-> Bool | Use SO_REUSEADDR? |
-> Maybe Int | Timeout for connect |
-> IO (Either (TransportError ConnectErrorCode) (Socket, ConnectionRequestResponse)) |
Establish a connection to a remote endpoint
Maybe throw a TransportError
type LightweightConnectionId = Word32 Source
Lightweight connection ID (sender allocated)
A ConnectionId is the concentation of a HeavyweightConnectionId
and a
LightweightConnectionId
.
Design notes
- Goals
The TCP transport maps multiple logical connections between A and B (in either direction) to a single TCP connection:
+-------+ +-------+ | A |==========================| B | | |>~~~~~~~~~~~~~~~~~~~~~~~~~|~~~\ | | Q |>~~~~~~~~~~~~~~~~~~~~~~~~~|~~~Q | | \~~~|~~~~~~~~~~~~~~~~~~~~~~~~~<| | | |==========================| | +-------+ +-------+
Ignoring the complications detailed below, the TCP connection is set up is when the first lightweight connection is created (in either direction), and torn down when the last lightweight connection (in either direction) is closed.
- Connecting
Let A, B be two endpoints without any connections. When A wants to connect to B, it locally records that it is trying to connect to B and sends a request to B. As part of the request A sends its own endpoint address to B (so that B can reuse the connection in the other direction).
When B receives the connection request it first checks if it did not
already initiate a connection request to A. If not it will acknowledge the
connection request by sending ConnectionRequestAccepted
to A and record
that it has a TCP connection to A.
The tricky case arises when A sends a connection request to B and B
finds that it had already sent a connection request to A. In this case B
will accept the connection request from A if As endpoint address is
smaller (lexicographically) than Bs, and reject it otherwise. If it rejects
it, it sends a ConnectionRequestCrossed
message to A. (The
lexicographical ordering is an arbitrary but convenient way to break the
tie.)
When it receives a ConnectionRequestCrossed
message the A thread that
initiated the request just needs to wait until the A thread that is dealing
with B's connection request completes.
- Disconnecting
The TCP connection is created as soon as the first logical connection from
A to B (or B to A) is established. At this point a thread (#
) is
spawned that listens for incoming connections from B:
+-------+ +-------+ | A |==========================| B | | |>~~~~~~~~~~~~~~~~~~~~~~~~~|~~~\ | | | | Q | | #| | | | |==========================| | +-------+ +-------+
The question is when the TCP connection can be closed again. Conceptually, we want to do reference counting: when there are no logical connections left between A and B we want to close the socket (possibly after some timeout).
However, A and B need to agree that the refcount has reached zero. It might happen that B sends a connection request over the existing socket at the same time that A closes its logical connection to B and closes the socket. This will cause a failure in B (which will have to retry) which is not caused by a network failure, which is unfortunate. (Note that the connection request from B might succeed even if A closes the socket.)
Instead, when A is ready to close the socket it sends a CloseSocket
request to B and records that its connection to B is closing. If A
receives a new connection request from B after having sent the
CloseSocket
request it simply forgets that it sent a CloseSocket
request
and increments the reference count of the connection again.
When B receives a CloseSocket
message and it too is ready to close the
connection, it will respond with a reciprocal CloseSocket
request to A
and then actually close the socket. A meanwhile will not send any more
requests to B after having sent a CloseSocket
request, and will actually
close its end of the socket only when receiving the CloseSocket
message
from B. (Since A recorded that its connection to B is in closing state
after sending a CloseSocket
request to B, it knows not to reciprocate B
reciprocal CloseSocket
message.)
If there is a concurrent thread in A waiting to connect to B after A
has sent a CloseSocket
request then this thread will block until A knows
whether to reuse the old socket (if B sends a new connection request
instead of acknowledging the CloseSocket
) or to set up a new socket.