Este pacote é uma camada de abstração construída em cima das implementações-padrão do System.Net.WebSockets, para lidar com o ciclo de vida dos WebSockets e para parsear e converter suas mensagens de byte arrays. Estas abstrações são tanto para cliente como para servidor (ASP.NET).
Ele é compatível com compilação NativeAOT e trimming.
Adicionar pacote NuGet ao arquivo do projeto:
<ItemGroup>
<PackageReference Include="AlexandreHtrb.WebSocketExtensions" Version="1.2.0" />
</ItemGroup>A utilização é bem simples. As classes WebSocketServerSideConnector e WebSocketClientSideConnector recebem objetos WebSocket nativos do .NET e cuidam de todo o ciclo de vida dos WebSockets, conexão e desconexão, recebimento e envio de mensagens, e conversão de/para bytes.
Dentro de cada conector há um ExchangedMessagesCollector, que coleta as mensagens enviadas e recebidas, e as disponibiliza em um IAsyncEnumerable. Assim, a conversa entre cliente e servidor entra em um loop await foreach (assíncrono). Quando uma das partes se desconecta, o loop é encerrado.
Antes de entrar no loop da conversa, uma das partes deve ter a iniciativa de mandar uma mensagem.
WebSocketServerSideConnector wsc = new(ws, collectOnlyClientSideMessages: true);
await foreach (var msg in wsc.ExchangedMessagesCollector!.ReadAllAsync())
{
await wsc.SendMessageAsync(WebSocketMessageType.Text, msg.ReadAsUtf8Text() switch
{
"Olá!" => "Oi!",
"Que horas são?" => "Agora são " + DateTime.Now.TimeOfDay,
"Obrigado!" => "De nada!",
_ => "Não entendi sua mensagem!"
}, false);
}using var cws = MakeClientWebSocket();
using var hc = MakeHttpClient(disableSslVerification: true);
Uri uri = new("ws://localhost:5000/test/http1websocket");
WebSocketClientSideConnector wsc = new(collectOnlyServerSideMessages: true);
// Conectando
await wsc.ConnectAsync(cws, hc, uri, cancellationToken);
// Envio da primeira mensagem
await wsc.SendMessageAsync(WebSocketMessageType.Text, "Olá!", false);
// Loop da conversa
await foreach (var msg in wsc.ExchangedMessagesCollector!.ReadAllAsync())
{
string? replyTxt = msg.ReadAsUtf8Text() switch
{
"Oi!" => "Que horas são?",
string s when s.StartsWith("Agora são") => "Obrigado!",
_ => null
};
if (replyTxt != null)
await wsc.SendMessageAsync(WebSocketMessageType.Text, replyTxt, false);
}Com o código acima, a conversa se desenrolará assim:
Cliente: Olá!
Servidor: Oi!
Cliente: Que horas são?
Servidor: Agora são 11:54:53
Cliente: Obrigado!
Servidor: De nada!
- Habilitar
app.UseWebSockets():
private static IApplicationBuilder ConfigureApp(this WebApplication app) =>
app.MapTestEndpoints()
.UseWebSockets(new()
{
KeepAliveInterval = TimeSpan.FromMinutes(2)
});- Mapear o endpoint para comunicação WebSocket:
public static WebApplication MapTestEndpoints(this WebApplication app)
{
app.MapGet("test/http1websocket", TestHttp1WebSocket);
return app;
}
private static async Task TestHttp1WebSocket(HttpContext httpCtx, ILogger<BackgroundWebSocketsProcessor> logger)
{
if (!httpCtx.WebSockets.IsWebSocketRequest)
{
byte[] txtBytes = Encoding.UTF8.GetBytes("Only WebSockets requests are accepted here!");
httpCtx.Response.StatusCode = (int)HttpStatusCode.BadRequest;
await httpCtx.Response.BodyWriter.WriteAsync(txtBytes);
}
else
{
using var webSocket = await httpCtx.WebSockets.AcceptWebSocketAsync();
TaskCompletionSource<object> socketFinishedTcs = new();
await BackgroundWebSocketsProcessor.RegisterAndProcessAsync(logger, webSocket, socketFinishedTcs);
await socketFinishedTcs.Task;
}
}- Criar um processador para o WebSocket:
using AlexandreHtrb.WebSocketExtensions;
using System.Net.WebSockets;
public static class BackgroundWebSocketsProcessor
{
public static async Task RegisterAndProcessAsync(ILogger<BackgroundWebSocketsProcessor> logger, WebSocket ws, TaskCompletionSource<object> socketFinishedTcs)
{
WebSocketServerSideConnector wsc = new(ws, collectOnlyClientSideMessages: true);
int msgCount = 0;
await foreach (var msg in wsc.ExchangedMessagesCollector!.ReadAllAsync())
{
logger.LogInformation("Message {msgCount}, {direction}: {msgText}", ++msgCount, msg.Direction, msg.FormatForLogging());
// tratamento das mensagens aqui
}
socketFinishedTcs.SetResult(true); // fim da conversa
}
}wsc.OnConnectionChanged = (state, exception) =>
{
logger.LogInformation("Connection state: {state}", state);
logger.LogError(exception, "Connection exception");
};Aqui é possível colocar tentativas de reconexão.
Keep-Alive é o mecanismo para manter a conexão WebSocket viva, sem que proxies ou outros elementos no meio do caminho a cortem.
Em .NET, podemos configurar um intervalo de freqüência, em que a parte envia um frame ping, e um período de timeout, que após o envio do ping, a parte espera o recebimento de um frame pong. Se não receber o pong, então entende-se que a conexão morreu.
Esse tópico é abordado com mais detalhes nesta página do WebSocket.org.
ClientWebSocket cws = new();
cws.Options.KeepAliveInterval = TimeSpan.FromSeconds(30);
#if NET10_0_OR_GREATER
cws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(45);
#endif
await wsc.ConnectAsync(cws, hc, uri, default);private static IApplicationBuilder ConfigureApp(this WebApplication app) =>
app.MapTestEndpoints()
.UseWebSockets(new()
{
KeepAliveInterval = TimeSpan.FromSeconds(30),
#if NET10_0_OR_GREATER
KeepAliveTimeout = TimeSpan.FromSeconds(45)
#endif
});WebSocketServerSideConnector wsc = new(ws, collectOnlyClientSideMessages: false);
WebSocketClientSideConnector wsc = new(collectOnlyServerSideMessages: false);Os booleanos controlam se apenas as mensagens vindas do lado oposto serão coletadas. Coletar mensagens do próprio lado pode ser interessante para logs.
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken);
await wsc.SendMessageAsync(WebSocketMessageType.Text, "Alô", false);
}_ = Task.Run(async () =>
{
await Task.Delay(maximumLifetimePeriod, cancellationToken);
await wsc.DisconnectAsync();
});// não use 'using'
FileStream fs = new("C:\\Files\my_image.jpg", FileMode.Open);
await wsc.SendMessageAsync(WebSocketMessageType.Binary, fs, false);Ao usar Streams para enviar mensagens, não usar a palavra-chave using. O Stream será descartado (.Dispose()) dentro do conector.
Além disso, ao enviar arquivos, pode ser interessante ter um tamanho maior de buffer no WebSocketConnector. Quando buffers maiores são usados, menos idas-e-voltas são necessárias para ler um Stream, o que pode acarretar em transmissões mais rápidas.
WebSocketServerSideConnector wsc = new(ws, true, bufferSize: 65_536);
WebSocketClientSideConnector wsc = new(bufferSize: 65_536);ClientWebSocket cws = new();
cws.Options.CollectHttpResponseDetails = true;
await wsc.ConnectAsync(cws, hc, uri, cancellationToken);
var wsHttpStatusCode = wsc.ConnectionHttpStatusCode;
var wsResponseHeaders = wsc.ConnectionHttpHeaders;ClientWebSocket cws = new();
cws.Options.SetRequestHeader("Authorization", "Bearer my_token");
cws.Options.SetRequestHeader("Header1", "Value1");
await wsc.ConnectAsync(cws, hc, uri, cancellationToken);ClientWebSocket cws = new();
cws.Options.AddSubProtocol("subprotocol1");
await wsc.ConnectAsync(cws, hc, uri, cancellationToken);private static async Task TestHttp1WebSocket(HttpContext httpCtx, ILogger<BackgroundWebSocketsProcessor> logger)
{
if (!httpCtx.WebSockets.IsWebSocketRequest)
{
// ...
}
else
{
using var webSocket = await httpCtx.WebSockets.AcceptWebSocketAsync();
TaskCompletionSource<object> socketFinishedTcs = new();
+ string? subprotocol = webSocket.SubProtocol ?? httpCtx.WebSockets.WebSocketRequestedProtocols.FirstOrDefault();
await BackgroundWebSocketsProcessor.RegisterAndProcessAsync(logger, webSocket, subprotocol, socketFinishedTcs);
await socketFinishedTcs.Task;
}
}ClientWebSocket cws = new();
cws.Options.DangerousDeflateOptions = new()
{
ClientContextTakeover = true,
ClientMaxWindowBits = 14,
ServerContextTakeover = true,
ServerMaxWindowBits = 14
};
await wsc.ConnectAsync(cws, hc, uri, cancellationToken);Importante: Não se deve passar segredos e mensagens criptografadas ao mesmo tempo em que se usa compressão, pois há risco de ataques BREACH e CRIME. Nesses casos, deve-se desabilitar a compressão nessas mensagens:
await wsc.SendMessageAsync(
WebSocketMessageType.Text,
$"Token criptografado {token}",
disableCompression: true);ClientWebSocket cws = new();
cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact;
cws.Options.HttpVersion = new(2,0);
await wsc.ConnectAsync(cws, hc, uri, cancellationToken);Em WebSockets HTTP/2, o método HTTP CONNECT é usado, ao invés de GET.
public static WebApplication MapTestEndpoints(this WebApplication app)
{
app.MapMethods("test/http2websocket", new[] { HttpMethods.Connect }, TestHttp2WebSocket);
return app;
}