.NET의 WebSockets 지원

WebSocket 프로토콜을 사용하면 클라이언트와 원격 호스트 간의 양방향 통신이 가능합니다. System.Net.WebSockets.ClientWebSocket은 여는 핸드셰이크를 통해 WebSocket 연결을 설정할 수 있는 기능을 제공하며, ConnectAsync 메서드에 의해 생성되고 전송됩니다.

HTTP/1.1 및 HTTP/2 WebSocket의 차이점

HTTP/1.1을 통한 WebSockets는 단일 TCP 연결을 사용하므로 연결 전체 헤더를 통해 관리됩니다. 자세한 내용은 RFC 6455 참조하세요. HTTP/1.1을 통해 WebSocket을 설정하는 방법의 다음 예제를 고려하세요.

Uri uri = new("ws://corefx-net-http11.azurewebsites.net/WebSocket/EchoWebSocket.ashx");

using ClientWebSocket ws = new();
await ws.ConnectAsync(uri, default);

var bytes = new byte[1024];
var result = await ws.ReceiveAsync(bytes, default);
string res = Encoding.UTF8.GetString(bytes, 0, result.Count);

await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Client closed", default);

멀티플렉싱 특성으로 인해 HTTP/2에서 다른 방법을 사용해야 합니다. WebSocket은 스트림별로 설정됩니다. 자세한 내용은 rfC 8441 참조하세요. HTTP/2를 사용하면 일반 HTTP 스트림과 함께 여러 웹 소켓 스트림에 대해 하나의 연결을 사용하고 HTTP/2의 보다 효율적인 네트워크 사용을 WebSockets로 확장할 수 있습니다. 기존 풀링된 연결을 재사용할 수 있도록 ConnectAsync(Uri, HttpMessageInvoker, CancellationToken)을 허용하는 특별한 HttpMessageInvoker 오버로드가 있습니다.

using SocketsHttpHandler handler = new();
using ClientWebSocket ws = new();
await ws.ConnectAsync(uri, new HttpMessageInvoker(handler), cancellationToken);

HTTP 버전 및 정책 설정

기본적으로 ClientWebSocket HTTP/1.1을 사용하여 여는 핸드셰이크를 보내고 다운그레이드를 허용합니다. .NET 7에서는 HTTP/2를 통한 웹 소켓을 사용할 수 있습니다. ConnectAsync호출하기 전에 변경할 수 있습니다.

using SocketsHttpHandler handler = new();
using ClientWebSocket ws = new();

ws.Options.HttpVersion = HttpVersion.Version20;
ws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;

await ws.ConnectAsync(uri, new HttpMessageInvoker(handler), cancellationToken);

호환되지 않는 옵션

ClientWebSocket 연결이 설정되기 전에 사용자가 설정할 수 System.Net.WebSockets.ClientWebSocketOptions 속성이 있습니다. 그러나 HttpMessageInvoker 제공되면 이러한 속성도 있습니다. 모호성을 방지하려면 이 경우 속성은 HttpMessageInvoker설정해야 하며 ClientWebSocketOptions 기본값이 있어야 합니다. 그렇지 않으면, ClientWebSocketOptions이 변경되었을 때 ConnectAsync 오버로드가 ArgumentException를 발생시킵니다.

using HttpClientHandler handler = new()
{
    CookieContainer = cookies;
    UseCookies = cookies != null;
    ServerCertificateCustomValidationCallback = remoteCertificateValidationCallback;
    Credentials = useDefaultCredentials
        ? CredentialCache.DefaultCredentials
        : credentials;
};
if (proxy is null)
{
    handler.UseProxy = false;
}
else
{
    handler.Proxy = proxy;
}
if (clientCertificates?.Count > 0)
{
    handler.ClientCertificates.AddRange(clientCertificates);
}
HttpMessageInvoker invoker = new(handler);
using ClientWebSocket cws = new();
await cws.ConnectAsync(uri, invoker, cancellationToken);

압축

WebSocket 프로토콜은 RFC 7692정의된 대로 메시지별 수축을 지원합니다. System.Net.WebSockets.ClientWebSocketOptions.DangerousDeflateOptions의해 제어됩니다. 있는 경우 옵션은 핸드셰이크 단계 중에 서버로 전송됩니다. 서버에서 메시지별 deflate를 지원하고 옵션이 허용되는 경우 모든 메시지에 대해 기본적으로 압축을 사용하도록 설정된 ClientWebSocket 인스턴스가 만들어집니다.

using ClientWebSocket ws = new()
{
    Options =
    {
        DangerousDeflateOptions = new WebSocketDeflateOptions()
        {
            ClientMaxWindowBits = 10,
            ServerMaxWindowBits = 10
        }
    }
};

중요하다

압축을 사용하기 전에 애플리케이션을 사용하도록 설정하면 범죄/위반 유형의 공격이 적용된다는 점에 유의하세요. 자세한 내용은 CRIME위반참조하세요. 이러한 메시지에 대한 DisableCompression 플래그를 지정하여 비밀이 포함된 데이터를 보낼 때 압축을 해제하는 것이 좋습니다.

Keep-Alive 전략

.NET 8 및 그 이전 버전에서는 유일하게 사용 가능한 Keep-Alive 전략이 원치 않는 PONG 입니다. 이 전략은 기본 TCP 연결이 유휴 상태가 되지 않도록 하기에 충분합니다. 그러나 원격 호스트가 응답하지 않는 경우(예: 원격 서버 크래시) 원치 않는 PONG에서 이러한 상황을 감지하는 유일한 방법은 TCP 시간 제한에 의존하는 것입니다.

.NET 9 기존 설정을 새 KeepAliveInterval 설정으로 보완하여 오랫동안 원하는 KeepAliveTimeout Keep-Alive 전략을 도입했습니다. .NET 9부터 Keep-Alive 전략이 다음과 같이 선택됩니다.

  1. Keep-Alive는 OFF의 경우
    • KeepAliveInterval TimeSpan.Zero 또는 Timeout.InfiniteTimeSpan
  2. 요청되지 않은 PONG,
    • KeepAliveInterval은 양수이자 유한한 TimeSpan이며, -AND-
    • KeepAliveTimeout TimeSpan.Zero 또는 Timeout.InfiniteTimeSpan
  3. PING/PONG(있는 경우)
    • KeepAliveInterval은 양수이자 유한한 TimeSpan이며, -AND-
    • KeepAliveTimeout는 양의 유한 TimeSpan입니다.

기본 KeepAliveTimeout 값은 Timeout.InfiniteTimeSpan. 따라서 기본 Keep-Alive 동작은 .NET 버전 간에 일관성을 유지합니다.

ClientWebSocket사용하는 경우 기본 ClientWebSocketOptions.KeepAliveInterval 값은 WebSocket.DefaultKeepAliveInterval(일반적으로 30초)입니다. 즉, ClientWebSocket은 기본적으로 Keep-Alive이 ON 상태이며, 기본 전략은 요청되지 않은 PONG입니다.

PING/PONG 전략으로 전환하려면 ClientWebSocketOptions.KeepAliveTimeout을 재정의하면 충분합니다.

var ws = new ClientWebSocket();
ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(20);
await ws.ConnectAsync(uri, cts.Token);

기본 WebSocket경우 Keep-Alive 기본적으로 OFF입니다. PING/PONG 전략을 사용하려면 WebSocketCreationOptions.KeepAliveIntervalWebSocketCreationOptions.KeepAliveTimeout 모두 설정해야 합니다.

var options = new WebSocketCreationOptions()
{
    KeepAliveInterval = WebSocket.DefaultKeepAliveInterval,
    KeepAliveTimeout = TimeSpan.FromSeconds(20)
};
var ws = WebSocket.CreateFromStream(stream, options);

원치 않는 PONG 전략을 사용하는 경우 PONG 프레임은 단방향 하트비트로 사용됩니다. 원격 엔드포인트가 통신하는지 여부에 관계없이 KeepAliveInterval 간격으로 정기적으로 전송됩니다.

PING/PONG 전략이 활성화된 경우 KeepAliveInterval 이후 경과된 시간 후에 PING 프레임이 전송됩니다. 각 PING 프레임에는 예상된 PONG 응답과 쌍을 이루는 정수 토큰이 포함되어 있습니다. KeepAliveTimeout 경과한 후 PONG 응답이 도착하지 않으면 원격 엔드포인트가 응답하지 않는 것으로 간주되고 WebSocket 연결이 자동으로 중단됩니다.

var ws = new ClientWebSocket();
ws.Options.KeepAliveInterval = TimeSpan.FromSeconds(10);
ws.Options.KeepAliveTimeout = TimeSpan.FromSeconds(10);
await ws.ConnectAsync(uri, cts.Token);

// NOTE: There must be an outstanding read at all times to ensure
// incoming PONGs are processed
var result = await _webSocket.ReceiveAsync(buffer, cts.Token);

시간 제한이 경과하면 미해결인 ReceiveAsync이(가) OperationCanceledException예외를 발생시킵니다.

System.OperationCanceledException: Aborted
 ---> System.AggregateException: One or more errors occurred. (The WebSocket didn't receive a Pong frame in response to a Ping frame within the configured KeepAliveTimeout.) (Unable to read data from the transport connection: The I/O operation has been aborted because of either a thread exit or an application request..)
 ---> System.Net.WebSockets.WebSocketException (0x80004005): The WebSocket didn't receive a Pong frame in response to a Ping frame within the configured KeepAliveTimeout.
   at System.Net.WebSockets.ManagedWebSocket.KeepAlivePingHeartBeat()
...

PONG 처리를 위해 계속 읽기

메모

현재 WebSocketReceiveAsync이 보류 중인 경우에만 들어오는 프레임을 처리합니다.

중요하다

Keep-Alive 제한 시간을 사용하려면 PONG 응답이 중요하게즉시 처리되어야 합니다. 원격 엔드포인트가 활성 상태이며 PONG 응답을 올바르게 전송함에도 불구하고, WebSocket이 들어오는 프레임을 처리하지 않을 경우, Keep-Alive 메커니즘은 "오탐" 중단을 실행할 수 있습니다. 이 문제는 시간 제한이 경과하기 전에 전송 스트림에서 PONG 프레임을 선택하지 않는 경우에 발생할 수 있습니다.

양호한 연결이 끊어지지 않도록 하기 위해, Keep-Alive 타임아웃이 구성된 모든 WebSocket에서 읽기 작업을 보류 상태로 유지하는 것이 좋습니다.