我有一个充当中间人的HTTP代理服务器。它基本上执行以下操作:
因此,基本上在客户端浏览器和代理之间存在一个NetworkStream
,或更常见的是SslStream
,在代理与服务器之间存在另一个{1>}。
提出了在客户端和服务器之间转发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 请求。
答案 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)