I have a HTTP proxy server which acts as a middle-man. It basically does the following:
- Listen for client-browser request
- Forward the request to the server
- Parse the server response
- Forward the response back to client-browser
So basically there is one NetworkStream
, or even more often a SslStream
between a client-browser and the proxy, and another one between the proxy and a server.
A requirement has arisen to also forward WebSocket traffic between a client and a server.
So now when a client-browser requests a connection upgrade to websocket, and the remote server responds with HTTP code 101, the proxy server maintains these connections in order to forward further messages from client to server and vice versa.
So after the proxy has received a message from the remote server saying it's ready to switch protocols, it needs to enter a loop where both client and server streams are polled for data, and where any received data is forwarded to the other party.
The problem
WebSocket allows both sides to send messages at any time. This is especially a problem with control messages such as ping/pong, where any side could send a ping at any time and the other side is expected to reply with a pong in a timely manner. Now consider having two instances of SslStream
which don't have DataAvailable
property, where the only way to read data is to call Read
/ReadAsync
which might not return until some data is available. Consider the following pseudo-code:
public async Task GetMessage()
{
// All these methods that we await read from the source stream
byte[] firstByte = await GetFirstByte(); // 1-byte buffer
byte[] messageLengthBytes = await GetMessageLengthBytes();
uint messageLength = GetMessageLength(messageLengthBytes);
bool isMessageMasked = DetermineIfMessageMasked(messageLengthBytes);
byte[] maskBytes;
if (isMessageMasked)
{
maskBytes = await GetMaskBytes();
}
byte[] messagePayload = await GetMessagePayload(messageLength);
// This method writes to the destination stream
await ComposeAndForwardMessageToOtherParty(firstByte, messageLengthBytes, maskBytes, messagePayload);
}
The above pseudo code reads from one stream and writes to the other. The problem is that the above procedure needs to be run for both streams simultaneously, because we don't know which side would send a message to the other at any given point in time. However, it is impossible to perform a write operation while there is a read operation active. And because we don't have the means necessary to poll for incoming data, read operations have to be blocking. That means if we start read operations for both streams at the same time, we can forget about writing to them. One stream will eventually return some data, but we won't be able to send that data to the other stream as it will still be busy trying to read. And that might take a while, at least until the side that owns that stream sends a ping request.