在两个流之间代理WebSocket消息

时间:2019-05-01 16:05:31

标签: c# websocket tcpclient

我有一个充当中间人的HTTP代理服务器。它基本上执行以下操作:

  • 收听客户端浏览器请求
  • 将请求转发到服务器
  • 解析服务器响应
  • 将响应转发回客户端浏览器

因此,基本上在客户端浏览器和代理之间存在一个NetworkStream,或更常见的是SslStream,在代理与服务器之间存在另一个{}。

提出了在客户端和服务器之间转发WebSocket通信的要求。

因此,现在,当客户端浏览器请求将连接升级到websocket,并且远程服务器以HTTP代码101进行响应时,代理服务器会维护这些连接,以便将更多消息从客户端转发到服务器,反之亦然。

因此,代理从远程服务器接收到一条消息,说它已准备好交换协议后,它需要进入一个循环,在该循环中,将轮询客户端和服务器流中的数据,并将接收到的任何数据转发给另一方

问题

WebSocket允许双方随时发送消息。对于诸如ping / pong之类的控制消息,这尤其是个问题,在该消息中,任何一方都可以随时发送 ping ,而另一端则希望在其中发送 pong 及时。现在考虑有两个SslStream实例,它们没有DataAvailable属性,其中读取数据的唯一方法是调用Read / ReadAsync,直到某些实例返回数据可用。考虑以下伪代码:

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);
}

上面的伪代码从一个流读取并写入另一个流。问题是上述过程需要同时为两个流运行,因为我们不知道哪一方会在任何给定的时间点向另一方发送消息。但是,在激活读取操作的同时,不可能执行写入操作。而且由于我们没有必要的方法来轮询传入的数据,因此读取操作必须处于阻塞状态。这意味着,如果我们同时开始对两个流进行读取操作,则可以忘记写入它们。一个流最终将返回一些数据,但是我们将无法将该数据发送到另一流,因为它将仍然忙于尝试读取。至少要等到拥有该流的一方发送 ping 请求。

1 个答案:

答案 0 :(得分:0)

通过@MarcGravell的评论,我们了解到网络流支持独立的读/写操作,即NetworkStream充当两个独立管道-一个读,一个写-它是全双工的。

因此,代理WebSocket消息就像启动两个独立任务一样容易,一个任务是从客户端流读取并写入服务器流,而另一个任务是从服务器流读取并写入客户端流。

如果它对搜索它的人有帮助,这是我的实现方式:

public class WebSocketRequestHandler
{
    private const int MaxMessageLength = 0x7FFFFFFF;

    private const byte LengthBitMask = 0x7F;

    private const byte MaskBitMask = 0x80;

    private delegate Task WriteStreamAsyncDelegate(byte[] buffer, int offset, int count, CancellationToken cancellationToken);

    private delegate Task<byte[]> BufferStreamAsyncDelegate(int count, CancellationToken cancellationToken);

    public async Task HandleWebSocketMessagesAsync(CancellationToken cancellationToken = default(CancellationToken))
    {
        var clientListener = ListenForClientMessages(cancellationToken);
        var serverListener = ListenForServerMessages(cancellationToken);
        await Task.WhenAll(clientListener, serverListener);
    }

    private async Task ListenForClientMessages(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            cancellationToken.ThrowIfCancellationRequested();
            await ListenForMessages(YOUR_CLIENT_STREAM_BUFFER_METHOD_DELEGATE, YOUR_SERVER_STREAM_WRITE_METHOD_DELEGATE, cancellationToken);
        }
    }

    private async Task ListenForServerMessages(CancellationToken cancellationToken)
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            cancellationToken.ThrowIfCancellationRequested();
            await ListenForMessages(YOUR_SERVER_STREAM_BUFFER_METHOD_DELEGATE, YOUR_CLIENT_STREAM_WRITE_METHOD_DELEGATE, cancellationToken);
        }
    }

    private static async Task ListenForMessages(BufferStreamAsyncDelegate sourceStreamReader,
        WriteStreamAsyncDelegate destinationStreamWriter,
        CancellationToken cancellationToken)
    {
        var messageBuilder = new List<byte>();
        var firstByte = await sourceStreamReader(1, cancellationToken);
        messageBuilder.AddRange(firstByte);
        var lengthBytes = await GetLengthBytes(sourceStreamReader, cancellationToken);
        messageBuilder.AddRange(lengthBytes);
        var isMaskBitSet = (lengthBytes[0] & MaskBitMask) != 0;
        var length = GetMessageLength(lengthBytes);
        if (isMaskBitSet)
        {
            var maskBytes = await sourceStreamReader(4, cancellationToken);
            messageBuilder.AddRange(maskBytes);
        }

        var messagePayloadBytes = await sourceStreamReader(length, cancellationToken);
        messageBuilder.AddRange(messagePayloadBytes);
        await destinationStreamWriter(messageBuilder.ToArray(), 0, messageBuilder.Count, cancellationToken);
    }

    private static async Task<byte[]> GetLengthBytes(BufferStreamAsyncDelegate sourceStreamReader, CancellationToken cancellationToken)
    {
        var lengthBytes = new List<byte>();
        var firstLengthByte = await sourceStreamReader(1, cancellationToken);
        lengthBytes.AddRange(firstLengthByte);
        var lengthByteValue = firstLengthByte[0] & LengthBitMask;
        if (lengthByteValue <= 125)
        {
            return lengthBytes.ToArray();
        }

        switch (lengthByteValue)
        {
            case 126:
            {
                var secondLengthBytes = await sourceStreamReader(2, cancellationToken);
                lengthBytes.AddRange(secondLengthBytes);
                return lengthBytes.ToArray();
            }
            case 127:
            {
                var secondLengthBytes = await sourceStreamReader(8, cancellationToken);
                lengthBytes.AddRange(secondLengthBytes);
                return lengthBytes.ToArray();
            }
            default:
                throw new Exception($"Unexpected first length byte value: {lengthByteValue}");
        }
    }

    private static int GetMessageLength(byte[] lengthBytes)
    {
        byte[] subArray;
        switch (lengthBytes.Length)
        {
            case 1:
                return lengthBytes[0] & LengthBitMask;

            case 3:
                if (!BitConverter.IsLittleEndian)
                {
                    return BitConverter.ToUInt16(lengthBytes, 1);
                }

                subArray = lengthBytes.SubArray(1, 2);
                Array.Reverse(subArray);
                return BitConverter.ToUInt16(subArray, 0);

            case 9:
                subArray = lengthBytes.SubArray(1, 8);
                Array.Reverse(subArray);
                var retVal = BitConverter.ToUInt64(subArray, 0);
                if (retVal > MaxMessageLength)
                {
                    throw new Exception($"Unexpected payload length: {retVal}");
                }

                return (int) retVal;

            default:
                throw new Exception($"Impossibru!!1 The length of lengthBytes array was: '{lengthBytes.Length}'");
        }
    }
}

可以通过在执行初始握手后仅调用await handler.HandleWebSocketMessagesAsync(cancellationToken)来使用它。

SubArray方法是从这里获取的:https://stackoverflow.com/a/943650/828023(也来自@Marc haha​​)