ASP.NET Core에서 WebSocket 지원

이 문서는 ASP.NET Core에서 WebSocket을 시작하는 방법을 설명합니다. WebSocket(RFC 6455)은 TCP 연결을 통해 지속적인 양방향 통신 채널을 사용할 수 있도록 해주는 프로토콜입니다. 채팅, 대시보드 및 게임 앱 등 신속한 실시간 통신을 활용하는 앱에서 사용됩니다.

샘플 코드 보기 및 다운로드(다운로드 방법, 실행 방법).

HTTP/2 WebSockets 지원

WebSockets over HTTP/2를 사용하면 다음과 같은 새로운 기능을 활용할 수 있습니다.

  • 헤더 압축.
  • 서버에 대한 여러 요청을 수행할 때 필요한 시간과 리소스를 줄이는 멀티플렉싱.

이 지원되는 기능은 모든 HTTP/2 지원 플랫폼의 Kestrel에서 사용할 수 있습니다. 버전 협상은 브라우저 및 Kestrel에서 자동이므로 새 API가 필요하지 않습니다.

.NET 7에서는 Kestrel, SignalR JavaScript 클라이언트, Blazor WebAssembly를 사용한 SignalR 등에 대한 Websockets over HTTP/2 지원을 도입했습니다.

참고 항목

HTTP/2 WebSockets는 GET이 아닌 CONNECT 요청을 사용하므로 고유한 경로 및 컨트롤러를 업데이트해야 할 수 있습니다. 자세한 내용은 이 문서의 기존 컨트롤러에 대한 HTTP/2 WebSockets 지원 추가를 참조하세요.

Chrome 및 Edge에는 기본적으로 HTTP/2 WebSockets가 사용하도록 설정되며 FireFox의 about:config 페이지에서 network.http.spdy.websockets 플래그로 사용하도록 설정할 수 있습니다.

WebSockets는 원래 HTTP/1.1용으로 디자인되었지만 이후 HTTP/2를 통해 작동하도록 조정되었습니다. (RFC 8441)

SignalR

ASP.NET Core SignalR은 앱에 실시간 웹 기능을 추가하는 것을 간소화하는 라이브러리입니다. 가능하면 Websocket을 사용합니다.

대부분의 애플리케이션에서는 원시 WebSockets 대신 SignalR을 권장합니다. SignalR:

  • WebSockets를 사용할 수 없는 환경에 대한 전송 대체(fallback)를 제공합니다.
  • 기본 원격 프로시저 호출 앱 모델을 제공합니다.
  • 대부분의 시나리오에서 원시 WebSockets 사용과 비교할 때 큰 성능상의 단점이 없습니다.

WebSockets over HTTP/2는 다음에서 지원됩니다.

  • ASP.NET Core SignalR JavaScript 클라이언트
  • Blazor WebAssembly를 사용한 ASP.NET Core SignalR

일부 앱에 대해 .NET의 gRPC는 Websocket을 대신하는 방법을 제공합니다.

필수 조건

  • ASP.NET Core를 지원하는 모든 OS:
    • Windows 7/Windows Server 2008 이상
    • Linux
    • macOS
  • 앱이 IIS를 사용하여 Windows에서 실행되는 경우:
    • Windows 8 / Windows Server 2012 이상
    • IIS 8 / IIS 8 Express
    • Websocket을 사용하도록 설정해야 합니다. IIS/IIS Express 지원 섹션을 참조하세요.
  • 앱이 HTTP.sys 실행되는 경우:
    • Windows 8 / Windows Server 2012 이상
  • 지원되는 브라우저는 Can I use를 참조하세요.

미들웨어 구성하기

Program.cs에서 WebSocket 미들웨어 추가:

app.UseWebSockets();

이때 다음과 같은 설정을 구성할 수 있습니다.

  • KeepAliveInterval - 프록시가 연결을 유지할 수 있도록 클라이언트로 "핑(ping)" 프레임을 전송하는 빈도입니다. 기본값은 2분입니다.
  • AllowedOrigins - WebSocket 요청에 허용된 원본 헤더 값의 목록입니다. 기본적으로 모든 원본이 허용됩니다. 자세한 내용은 이 문서의 WebSocket 원본 제한을 참조하세요.
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);

WebSocket 요청 수락하기

요청 수명 주기의 뒷부분에서(예를 들어, Program.cs 또는 작업 메서드 뒷부분에서) WebSocket 요청 여부를 확인하고 WebSocket 요청을 수락합니다.

다음 예제는 Program.cs에서 가져온 것입니다.

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
    else
    {
        await next(context);
    }

});

WebSocket 요청은 모든 URL을 통해서 전달될 수 있지만, 이 예제 코드에서는 /ws 에 대한 요청만 수락합니다.

컨트롤러 메서드에서 유사한 방법을 사용할 수 있습니다.

public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

WebSocket을 사용하는 경우 연결 기간 동안 미들웨어 파이프라인이 계속 실행되도록 해야 합니다. 미들웨어 파이프라인 종료 후 WebSocket 메시지를 보내거나 받으려고 하면 다음과 같은 예외가 발생할 수 있습니다.

System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.

백그라운드 서비스를 사용하여 WebSocket에 데이터를 쓰는 경우 미들웨어 파이프라인이 계속 실행되도록 해야 합니다. TaskCompletionSource<TResult>을 사용하여 이 작업을 수행합니다. TaskCompletionSource를 백그라운드 서비스에 전달하고 WebSocket 사용을 완료하면 TrySetResult를 호출하도록 합니다. 그런 다음, await는 요청 중에 다음 예제와 같이 Task 속성을 만듭니다.

app.Run(async (context) =>
{
    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    var socketFinishedTcs = new TaskCompletionSource<object>();

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});

작업 메서드에서 너무 빨리 반환하면 WebSocket 닫힌 예외가 발생할 수도 있습니다. 작업 메서드에서 소켓을 수락하는 경우 작업 메서드에서 반환하기 전에 소켓을 사용하는 코드가 완료될 때까지 기다립니다.

소켓이 완료될 때까지 대기하려면 Task.Wait, Task.Result 또는 유사한 차단 호출을 사용하지 마세요. 심각한 스레드 문제가 발생할 수 있습니다. 항상 await를 사용합니다.

기존 컨트롤러에 대한 HTTP/2 WebSockets 지원 추가

.NET 7에서는 Kestrel, SignalR JavaScript 클라이언트, Blazor WebAssembly를 사용한 SignalR 등에 대한 Websockets over HTTP/2 지원을 도입했습니다. HTTP/2 WebSockets는 GET 대신 CONNECT 요청을 사용합니다. 이전에 Websocket 요청에 컨트롤러 작업 메서드에 [HttpGet("/path")]를 사용한 경우 대신 [Route("/path")]를 사용하도록 업데이트합니다.

public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

압축

Warning

암호화된 연결에서 압축을 사용할 수 있게 설정하면 앱이 CRIME/BREACH 공격을 받을 수 있습니다. 중요한 정보를 보내는 경우 압축을 사용하지 않도록 설정하거나 WebSocketMessageFlags.DisableCompression을 호출할 때 WebSocket.SendAsync를 사용합니다. 이는 WebSocket의 양쪽 모두에 적용됩니다. 브라우저의 WebSocket API에는 전송당 압축을 사용하지 않도록 설정할 수 있는 구성이 없습니다.

WebSocket을 통한 메시지 압축이 필요한 경우 수락 코드가 다음과 같이 압축을 허용하도록 지정해야 합니다.

using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(
    new WebSocketAcceptContext { DangerousEnableCompression = true }))
{

}

WebSocketAcceptContext.ServerMaxWindowBitsWebSocketAcceptContext.DisableServerContextTakeover는 압축 작동 방식을 제어하는 고급 옵션입니다.

처음 연결을 설정할 때 클라이언트와 서버 간에 압축이 협상됩니다. WebSocket RFC에 대한 압축 확장에서 협상에 대해 자세히 알아볼 수 있습니다.

참고 항목

서버 또는 클라이언트에서 압축 협상을 수락하지 않으면 연결이 설정된 상태가 계속 유지되지만 메시지를 주고 받을 때는 연결에서 압축을 사용하지 않습니다.

메시지 보내기 및 받기

AcceptWebSocketAsync 메서드는 TCP 연결을 WebSocket 연결로 업그레이드하고 WebSocket 개체를 제공합니다. WebSocket 개체를 사용하여 메시지를 보내고 받습니다.

앞에서 살펴본 WebSocket 요청을 수락하는 코드는 WebSocket 개체를 Echo 메서드로 전달합니다. 코드는 메시지를 수신하고 동일한 메시지를 즉시 보냅니다. 메시지는 클라이언트가 연결을 닫을 때까지 루프에서 보내고 받습니다.

private static async Task Echo(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    var receiveResult = await webSocket.ReceiveAsync(
        new ArraySegment<byte>(buffer), CancellationToken.None);

    while (!receiveResult.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(
            new ArraySegment<byte>(buffer, 0, receiveResult.Count),
            receiveResult.MessageType,
            receiveResult.EndOfMessage,
            CancellationToken.None);

        receiveResult = await webSocket.ReceiveAsync(
            new ArraySegment<byte>(buffer), CancellationToken.None);
    }

    await webSocket.CloseAsync(
        receiveResult.CloseStatus.Value,
        receiveResult.CloseStatusDescription,
        CancellationToken.None);
}

루프가 시작되기 전에 WebSocket 연결을 수락하면 미들웨어 파이프라인이 종료됩니다. 그리고 소켓을 닫으면 파이프라인이 풀립니다. 즉, 요청은 WebSocket이 수락될 때 파이프라인에서 앞으로 이동하지 않습니다. 루프가 완료되고 소켓이 닫히면 요청이 파이프라인 백업을 진행합니다.

클라이언트 연결 끊김 처리

연결이 끊어져서 클라이언트 연결이 해제된 경우 서버에 자동으로 알림이 전송되지 않습니다. 클라이언트에서 연결 끊김 메시지를 보내는 경우에만 서버에서 연결 끊김 메시지를 받습니다. 인터넷 연결이 끊긴 경우 메시지가 전송되지 않습니다. 그런 경우 조치를 취하려면 특정 시간 내에서 클라이언트에서 아무런 메시지도 받지 않은 후 시간 제한을 설정합니다.

클라이언트가 항상 메시지를 보내지 않고 연결이 유휴 상태가 되기 때문에 시간 제한을 원하지 않는 경우 클라이언트에서 타이머를 사용하여 X초마다 ping 메시지를 보내도록 합니다. 서버에서 메시지가 이전 메시지 후 2*X초 이내에 도착하지 않는 경우 연결을 종료하고 클라이언트 연결이 끊겼음을 보고합니다. ping 메시지를 지연시킬 수 있는 네트워크 지연에 대해 추가 시간을 두려면 예상 시간 간격의 2배 동안 기다립니다.

WebSocket 원본 제한

CORS에서 제공하는 보호 기능은 WebSocket에 적용되지 않습니다. 브라우저는 다음을 수행하지 않습니다.

  • CORS pre-flight 요청을 수행합니다.
  • WebSocket 요청을 생성할 때 Access-Control 헤더에 지정된 제한 사항을 준수합니다.

그러나 브라우저는 WebSocket 요청을 발급할 때 Origin 헤더를 보냅니다. 애플리케이션은 예상된 원본에서 제공하는 WebSocket만 허용되도록 이러한 헤더의 유효성을 검사하도록 구성되어야 합니다.

"https://server.com""에서 서버를 호스트하고 "https://client.com""에서 클라이언트를 호스트하는 경우 AllowedOrigins 목록에 "https://client.com""을 추가하여 WebSocket을 확인합니다.

var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

참고 항목

Origin 헤더는 클라이언트에 의해 제어되며 Referer 헤더와 마찬가지로 위조될 수 있습니다. 이러한 헤더를 인증 메커니즘으로 사용하지 마세요.

IIS/IIS Express 지원

IIS/IIS Express 8 이상이 있는 Windows Server 2012 이상 및 Windows 8 이상에서는 WebSocket 프로토콜을 지원하지만 WebSockets over HTTP/2를 지원하지는 않습니다.

참고 항목

IIS Express를 사용할 때 Websocket이 항상 활성화됩니다.

IIS에서 Websocket 사용

Windows Server 2012 이상에서 WebSocket 프로토콜을 지원하려면:

참고 항목

IIS Express를 사용할 때 이러한 단계가 필요하지 않습니다.

  1. 관리 메뉴 또는 서버 관리자의 링크를 통해 역할 및 기능 추가 마법사를 사용합니다.
  2. 역할 기반 또는 기능 기반 설치를 선택합니다. 다음을 선택합니다.
  3. 적절한 서버를 선택합니다(로컬 서버가 기본적으로 선택됨). 다음을 선택합니다.
  4. 역할 트리에서 Web Server(IIS)를 확장하고 Web Server를 확장한 다음, 애플리케이션 개발을 확장합니다.
  5. WebSocket 프로토콜을 선택합니다. 다음을 선택합니다.
  6. 추가 기능이 필요 없는 경우 다음을 선택합니다.
  7. 설치를 선택합니다.
  8. 설치가 완료되면 닫기를 선택하여 마법사를 종료합니다.

Windows 8 이상에서 WebSocket 프로토콜을 지원하려면:

참고 항목

IIS Express를 사용할 때 이러한 단계가 필요하지 않습니다.

  1. 제어판>프로그램>프로그램 및 기능>Windows 기능 사용/사용 안 함(화면 왼쪽)으로 이동합니다.
  2. 인터넷 정보 서비스>World Wide Web 서비스>애플리케이션 개발 기능 노드를 엽니다.
  3. WebSocket 프로토콜 기능을 선택합니다. 확인을 선택합니다.

Node.js에서 socket.io를 사용할 때 WebSocket 비활성화

Node.jssocket.io에서 WebSocket 지원을 사용하는 경우 web.config 또는 applicationHost.configwebSocket 요소를 사용하여 기본 IIS WebSocket 모듈을 비활성화합니다. 이 단계를 수행하지 않으면 IIS WebSocket 모듈이 Node.js 및 앱이 아닌 WebSocket 통신을 처리하려고 시도합니다.

<system.webServer>
  <webSocket enabled="false" />
</system.webServer>

샘플 앱

이 아티클과 함께 제공되는 샘플 앱은 에코 앱입니다. WebSocket 연결을 생성하는 웹 페이지가 제공되며 서버는 수신한 메시지를 클라이언트로 재전송합니다. 샘플 앱은 .NET 7 이상의 대상 프레임워크를 사용하는 경우 WebSockets over HTTP/2를 지원합니다.

앱을 실행합니다.

  • Visual Studio에서 앱을 실행하려면: Visual Studio에서 샘플 프로젝트를 열고 Ctrl+F5를 눌러 디버거 없이 실행합니다.
  • 명령 셸에서 앱을 실행하려면: dotnet run 명령을 실행하고 브라우저에서 http://localhost:<port>로 이동합니다.

웹 페이지에 연결 상태가 표시됩니다.

WebSocket 연결 전 웹 페이지 초기 상태

Connect 를 선택해서 지정한 URL로 WebSocket 요청을 전송합니다. 그리고 테스트 메시지를 입력한 다음 Send 를 선택합니다. 테스트를 모두 마쳤으면 Close Socket 을 선택하십시오. Communication Log 섹션에는 각각의 열기, 전송 및 닫기 동작이 발생한 순서대로 나타납니다.

WebSocket 연결 및 테스트 메시지를 보내고 받은 후 웹 페이지 최종 상태

이 문서는 ASP.NET Core에서 WebSocket을 시작하는 방법을 설명합니다. WebSocket(RFC 6455)은 TCP 연결을 통해 지속적인 양방향 통신 채널을 사용할 수 있도록 해주는 프로토콜입니다. 채팅, 대시보드 및 게임 앱 등 신속한 실시간 통신을 활용하는 앱에서 사용됩니다.

샘플 코드 보기 및 다운로드(다운로드 방법, 실행 방법).

HTTP/2 WebSockets 지원

WebSockets over HTTP/2를 사용하면 다음과 같은 새로운 기능을 활용할 수 있습니다.

  • 헤더 압축.
  • 서버에 대한 여러 요청을 수행할 때 필요한 시간과 리소스를 줄이는 멀티플렉싱.

이 지원되는 기능은 모든 HTTP/2 지원 플랫폼의 Kestrel에서 사용할 수 있습니다. 버전 협상은 브라우저 및 Kestrel에서 자동이므로 새 API가 필요하지 않습니다.

.NET 7에서는 Kestrel, SignalR JavaScript 클라이언트, Blazor WebAssembly를 사용한 SignalR 등에 대한 Websockets over HTTP/2 지원을 도입했습니다.

참고 항목

HTTP/2 WebSockets는 GET이 아닌 CONNECT 요청을 사용하므로 고유한 경로 및 컨트롤러를 업데이트해야 할 수 있습니다. 자세한 내용은 이 문서의 기존 컨트롤러에 대한 HTTP/2 WebSockets 지원 추가를 참조하세요.

Chrome 및 Edge에는 기본적으로 HTTP/2 WebSockets가 사용하도록 설정되며 FireFox의 about:config 페이지에서 network.http.spdy.websockets 플래그로 사용하도록 설정할 수 있습니다.

WebSockets는 원래 HTTP/1.1용으로 디자인되었지만 이후 HTTP/2를 통해 작동하도록 조정되었습니다. (RFC 8441)

SignalR

ASP.NET Core SignalR은 앱에 실시간 웹 기능을 추가하는 것을 간소화하는 라이브러리입니다. 가능하면 Websocket을 사용합니다.

대부분의 애플리케이션에서는 원시 WebSockets 대신 SignalR을 권장합니다. SignalR:

  • WebSockets를 사용할 수 없는 환경에 대한 전송 대체(fallback)를 제공합니다.
  • 기본 원격 프로시저 호출 앱 모델을 제공합니다.
  • 대부분의 시나리오에서 원시 WebSockets 사용과 비교할 때 큰 성능상의 단점이 없습니다.

WebSockets over HTTP/2는 다음에서 지원됩니다.

  • ASP.NET Core SignalR JavaScript 클라이언트
  • Blazor WebAssembly를 사용한 ASP.NET Core SignalR

일부 앱에 대해 .NET의 gRPC는 Websocket을 대신하는 방법을 제공합니다.

필수 조건

  • ASP.NET Core를 지원하는 모든 OS:
    • Windows 7/Windows Server 2008 이상
    • Linux
    • macOS
  • 앱이 IIS를 사용하여 Windows에서 실행되는 경우:
    • Windows 8 / Windows Server 2012 이상
    • IIS 8 / IIS 8 Express
    • Websocket을 사용하도록 설정해야 합니다. IIS/IIS Express 지원 섹션을 참조하세요.
  • 앱이 HTTP.sys 실행되는 경우:
    • Windows 8 / Windows Server 2012 이상
  • 지원되는 브라우저는 Can I use를 참조하세요.

미들웨어 구성하기

Program.cs에서 WebSocket 미들웨어 추가:

app.UseWebSockets();

이때 다음과 같은 설정을 구성할 수 있습니다.

  • KeepAliveInterval - 프록시가 연결을 유지할 수 있도록 클라이언트로 "핑(ping)" 프레임을 전송하는 빈도입니다. 기본값은 2분입니다.
  • AllowedOrigins - WebSocket 요청에 허용된 원본 헤더 값의 목록입니다. 기본적으로 모든 원본이 허용됩니다. 자세한 내용은 이 문서의 WebSocket 원본 제한을 참조하세요.
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);

WebSocket 요청 수락하기

요청 수명 주기의 뒷부분에서(예를 들어, Program.cs 또는 작업 메서드 뒷부분에서) WebSocket 요청 여부를 확인하고 WebSocket 요청을 수락합니다.

다음 예제는 Program.cs에서 가져온 것입니다.

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
    else
    {
        await next(context);
    }

});

WebSocket 요청은 모든 URL을 통해서 전달될 수 있지만, 이 예제 코드에서는 /ws 에 대한 요청만 수락합니다.

컨트롤러 메서드에서 유사한 방법을 사용할 수 있습니다.

public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

WebSocket을 사용하는 경우 연결 기간 동안 미들웨어 파이프라인이 계속 실행되도록 해야 합니다. 미들웨어 파이프라인 종료 후 WebSocket 메시지를 보내거나 받으려고 하면 다음과 같은 예외가 발생할 수 있습니다.

System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.

백그라운드 서비스를 사용하여 WebSocket에 데이터를 쓰는 경우 미들웨어 파이프라인이 계속 실행되도록 해야 합니다. TaskCompletionSource<TResult>을 사용하여 이 작업을 수행합니다. TaskCompletionSource를 백그라운드 서비스에 전달하고 WebSocket 사용을 완료하면 TrySetResult를 호출하도록 합니다. 그런 다음, await는 요청 중에 다음 예제와 같이 Task 속성을 만듭니다.

app.Run(async (context) =>
{
    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    var socketFinishedTcs = new TaskCompletionSource<object>();

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});

작업 메서드에서 너무 빨리 반환하면 WebSocket 닫힌 예외가 발생할 수도 있습니다. 작업 메서드에서 소켓을 수락하는 경우 작업 메서드에서 반환하기 전에 소켓을 사용하는 코드가 완료될 때까지 기다립니다.

소켓이 완료될 때까지 대기하려면 Task.Wait, Task.Result 또는 유사한 차단 호출을 사용하지 마세요. 심각한 스레드 문제가 발생할 수 있습니다. 항상 await를 사용합니다.

기존 컨트롤러에 대한 HTTP/2 WebSockets 지원 추가

.NET 7에서는 Kestrel, SignalR JavaScript 클라이언트, Blazor WebAssembly를 사용한 SignalR 등에 대한 Websockets over HTTP/2 지원을 도입했습니다. HTTP/2 WebSockets는 GET 대신 CONNECT 요청을 사용합니다. 이전에 Websocket 요청에 컨트롤러 작업 메서드에 [HttpGet("/path")]를 사용한 경우 대신 [Route("/path")]를 사용하도록 업데이트합니다.

public class WebSocketController : ControllerBase
{
    [Route("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

압축

Warning

암호화된 연결에서 압축을 사용할 수 있게 설정하면 앱이 CRIME/BREACH 공격을 받을 수 있습니다. 중요한 정보를 보내는 경우 압축을 사용하지 않도록 설정하거나 WebSocketMessageFlags.DisableCompression을 호출할 때 WebSocket.SendAsync를 사용합니다. 이는 WebSocket의 양쪽 모두에 적용됩니다. 브라우저의 WebSocket API에는 전송당 압축을 사용하지 않도록 설정할 수 있는 구성이 없습니다.

WebSocket을 통한 메시지 압축이 필요한 경우 수락 코드가 다음과 같이 압축을 허용하도록 지정해야 합니다.

using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(
    new WebSocketAcceptContext { DangerousEnableCompression = true }))
{

}

WebSocketAcceptContext.ServerMaxWindowBitsWebSocketAcceptContext.DisableServerContextTakeover는 압축 작동 방식을 제어하는 고급 옵션입니다.

처음 연결을 설정할 때 클라이언트와 서버 간에 압축이 협상됩니다. WebSocket RFC에 대한 압축 확장에서 협상에 대해 자세히 알아볼 수 있습니다.

참고 항목

서버 또는 클라이언트에서 압축 협상을 수락하지 않으면 연결이 설정된 상태가 계속 유지되지만 메시지를 주고 받을 때는 연결에서 압축을 사용하지 않습니다.

메시지 보내기 및 받기

AcceptWebSocketAsync 메서드는 TCP 연결을 WebSocket 연결로 업그레이드하고 WebSocket 개체를 제공합니다. WebSocket 개체를 사용하여 메시지를 보내고 받습니다.

앞에서 살펴본 WebSocket 요청을 수락하는 코드는 WebSocket 개체를 Echo 메서드로 전달합니다. 코드는 메시지를 수신하고 동일한 메시지를 즉시 보냅니다. 메시지는 클라이언트가 연결을 닫을 때까지 루프에서 보내고 받습니다.

private static async Task Echo(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    var receiveResult = await webSocket.ReceiveAsync(
        new ArraySegment<byte>(buffer), CancellationToken.None);

    while (!receiveResult.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(
            new ArraySegment<byte>(buffer, 0, receiveResult.Count),
            receiveResult.MessageType,
            receiveResult.EndOfMessage,
            CancellationToken.None);

        receiveResult = await webSocket.ReceiveAsync(
            new ArraySegment<byte>(buffer), CancellationToken.None);
    }

    await webSocket.CloseAsync(
        receiveResult.CloseStatus.Value,
        receiveResult.CloseStatusDescription,
        CancellationToken.None);
}

루프가 시작되기 전에 WebSocket 연결을 수락하면 미들웨어 파이프라인이 종료됩니다. 그리고 소켓을 닫으면 파이프라인이 풀립니다. 즉, 요청은 WebSocket이 수락될 때 파이프라인에서 앞으로 이동하지 않습니다. 루프가 완료되고 소켓이 닫히면 요청이 파이프라인 백업을 진행합니다.

클라이언트 연결 끊김 처리

연결이 끊어져서 클라이언트 연결이 해제된 경우 서버에 자동으로 알림이 전송되지 않습니다. 클라이언트에서 연결 끊김 메시지를 보내는 경우에만 서버에서 연결 끊김 메시지를 받습니다. 인터넷 연결이 끊긴 경우 메시지가 전송되지 않습니다. 그런 경우 조치를 취하려면 특정 시간 내에서 클라이언트에서 아무런 메시지도 받지 않은 후 시간 제한을 설정합니다.

클라이언트가 항상 메시지를 보내지 않고 연결이 유휴 상태가 되기 때문에 시간 제한을 원하지 않는 경우 클라이언트에서 타이머를 사용하여 X초마다 ping 메시지를 보내도록 합니다. 서버에서 메시지가 이전 메시지 후 2*X초 이내에 도착하지 않는 경우 연결을 종료하고 클라이언트 연결이 끊겼음을 보고합니다. ping 메시지를 지연시킬 수 있는 네트워크 지연에 대해 추가 시간을 두려면 예상 시간 간격의 2배 동안 기다립니다.

WebSocket 원본 제한

CORS에서 제공하는 보호 기능은 WebSocket에 적용되지 않습니다. 브라우저는 다음을 수행하지 않습니다.

  • CORS pre-flight 요청을 수행합니다.
  • WebSocket 요청을 생성할 때 Access-Control 헤더에 지정된 제한 사항을 준수합니다.

그러나 브라우저는 WebSocket 요청을 발급할 때 Origin 헤더를 보냅니다. 애플리케이션은 예상된 원본에서 제공하는 WebSocket만 허용되도록 이러한 헤더의 유효성을 검사하도록 구성되어야 합니다.

"https://server.com""에서 서버를 호스트하고 "https://client.com""에서 클라이언트를 호스트하는 경우 AllowedOrigins 목록에 "https://client.com""을 추가하여 WebSocket을 확인합니다.

var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

참고 항목

Origin 헤더는 클라이언트에 의해 제어되며 Referer 헤더와 마찬가지로 위조될 수 있습니다. 이러한 헤더를 인증 메커니즘으로 사용하지 마세요.

IIS/IIS Express 지원

IIS/IIS Express 8 이상이 있는 Windows Server 2012 이상 및 Windows 8 이상에서는 WebSocket 프로토콜을 지원하지만 WebSockets over HTTP/2를 지원하지는 않습니다.

참고 항목

IIS Express를 사용할 때 Websocket이 항상 활성화됩니다.

IIS에서 Websocket 사용

Windows Server 2012 이상에서 WebSocket 프로토콜을 지원하려면:

참고 항목

IIS Express를 사용할 때 이러한 단계가 필요하지 않습니다.

  1. 관리 메뉴 또는 서버 관리자의 링크를 통해 역할 및 기능 추가 마법사를 사용합니다.
  2. 역할 기반 또는 기능 기반 설치를 선택합니다. 다음을 선택합니다.
  3. 적절한 서버를 선택합니다(로컬 서버가 기본적으로 선택됨). 다음을 선택합니다.
  4. 역할 트리에서 Web Server(IIS)를 확장하고 Web Server를 확장한 다음, 애플리케이션 개발을 확장합니다.
  5. WebSocket 프로토콜을 선택합니다. 다음을 선택합니다.
  6. 추가 기능이 필요 없는 경우 다음을 선택합니다.
  7. 설치를 선택합니다.
  8. 설치가 완료되면 닫기를 선택하여 마법사를 종료합니다.

Windows 8 이상에서 WebSocket 프로토콜을 지원하려면:

참고 항목

IIS Express를 사용할 때 이러한 단계가 필요하지 않습니다.

  1. 제어판>프로그램>프로그램 및 기능>Windows 기능 사용/사용 안 함(화면 왼쪽)으로 이동합니다.
  2. 인터넷 정보 서비스>World Wide Web 서비스>애플리케이션 개발 기능 노드를 엽니다.
  3. WebSocket 프로토콜 기능을 선택합니다. 확인을 선택합니다.

Node.js에서 socket.io를 사용할 때 WebSocket 비활성화

Node.jssocket.io에서 WebSocket 지원을 사용하는 경우 web.config 또는 applicationHost.configwebSocket 요소를 사용하여 기본 IIS WebSocket 모듈을 비활성화합니다. 이 단계를 수행하지 않으면 IIS WebSocket 모듈이 Node.js 및 앱이 아닌 WebSocket 통신을 처리하려고 시도합니다.

<system.webServer>
  <webSocket enabled="false" />
</system.webServer>

샘플 앱

이 아티클과 함께 제공되는 샘플 앱은 에코 앱입니다. WebSocket 연결을 생성하는 웹 페이지가 제공되며 서버는 수신한 메시지를 클라이언트로 재전송합니다. 샘플 앱은 .NET 7 이상의 대상 프레임워크를 사용하는 경우 WebSockets over HTTP/2를 지원합니다.

앱을 실행합니다.

  • Visual Studio에서 앱을 실행하려면: Visual Studio에서 샘플 프로젝트를 열고 Ctrl+F5를 눌러 디버거 없이 실행합니다.
  • 명령 셸에서 앱을 실행하려면: dotnet run 명령을 실행하고 브라우저에서 http://localhost:<port>로 이동합니다.

웹 페이지에 연결 상태가 표시됩니다.

WebSocket 연결 전 웹 페이지 초기 상태

Connect 를 선택해서 지정한 URL로 WebSocket 요청을 전송합니다. 그리고 테스트 메시지를 입력한 다음 Send 를 선택합니다. 테스트를 모두 마쳤으면 Close Socket 을 선택하십시오. Communication Log 섹션에는 각각의 열기, 전송 및 닫기 동작이 발생한 순서대로 나타납니다.

WebSocket 연결 및 테스트 메시지를 보내고 받은 후 웹 페이지 최종 상태

이 문서는 ASP.NET Core에서 WebSocket을 시작하는 방법을 설명합니다. WebSocket(RFC 6455)은 TCP 연결을 통해 지속적인 양방향 통신 채널을 사용할 수 있도록 해주는 프로토콜입니다. 채팅, 대시보드 및 게임 앱 등 신속한 실시간 통신을 활용하는 앱에서 사용됩니다.

샘플 코드 보기 및 다운로드(다운로드 방법, 실행 방법).

SignalR

ASP.NET Core SignalR은 앱에 실시간 웹 기능을 추가하는 것을 간소화하는 라이브러리입니다. 가능하면 Websocket을 사용합니다.

대부분의 애플리케이션의 경우 원시 WebSockets보다 SignalR을 권장합니다. SignalR은 WebSockets를 사용할 수 없는 환경에 대한 전송 대체(fallback)를 제공합니다. 기본 원격 프로시저 호출 앱 모델도 제공합니다. 그리고 대부분의 시나리오에서 SignalR은 원시 WebSockets 사용과 비교할 때 큰 성능상의 단점이 없습니다.

일부 앱에 대해 .NET의 gRPC는 Websocket을 대신하는 방법을 제공합니다.

필수 조건

  • ASP.NET Core를 지원하는 모든 OS:
    • Windows 7/Windows Server 2008 이상
    • Linux
    • macOS
  • 앱이 IIS를 사용하여 Windows에서 실행되는 경우:
    • Windows 8 / Windows Server 2012 이상
    • IIS 8 / IIS 8 Express
    • Websocket을 사용하도록 설정해야 합니다. IIS/IIS Express 지원 섹션을 참조하세요.
  • 앱이 HTTP.sys 실행되는 경우:
    • Windows 8 / Windows Server 2012 이상
  • 지원되는 브라우저는 Can I use를 참조하세요.

미들웨어 구성하기

Program.cs에서 WebSocket 미들웨어 추가:

app.UseWebSockets();

이때 다음과 같은 설정을 구성할 수 있습니다.

  • KeepAliveInterval - 프록시가 연결을 유지할 수 있도록 클라이언트로 "핑(ping)" 프레임을 전송하는 빈도입니다. 기본값은 2분입니다.
  • AllowedOrigins - WebSocket 요청에 허용된 원본 헤더 값의 목록입니다. 기본적으로 모든 원본이 허용됩니다. 자세한 내용은 이 문서의 WebSocket 원본 제한을 참조하세요.
var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

app.UseWebSockets(webSocketOptions);

WebSocket 요청 수락하기

요청 수명 주기의 뒷부분에서(예를 들어, Program.cs 또는 작업 메서드 뒷부분에서) WebSocket 요청 여부를 확인하고 WebSocket 요청을 수락합니다.

다음 예제는 Program.cs에서 가져온 것입니다.

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            context.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }
    else
    {
        await next(context);
    }

});

WebSocket 요청은 모든 URL을 통해서 전달될 수 있지만, 이 예제 코드에서는 /ws 에 대한 요청만 수락합니다.

컨트롤러 메서드에서 유사한 방법을 사용할 수 있습니다.

public class WebSocketController : ControllerBase
{
    [HttpGet("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

WebSocket을 사용하는 경우 연결 기간 동안 미들웨어 파이프라인이 계속 실행되도록 해야 합니다. 미들웨어 파이프라인 종료 후 WebSocket 메시지를 보내거나 받으려고 하면 다음과 같은 예외가 발생할 수 있습니다.

System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.

백그라운드 서비스를 사용하여 WebSocket에 데이터를 쓰는 경우 미들웨어 파이프라인이 계속 실행되도록 해야 합니다. TaskCompletionSource<TResult>을 사용하여 이 작업을 수행합니다. TaskCompletionSource를 백그라운드 서비스에 전달하고 WebSocket 사용을 완료하면 TrySetResult를 호출하도록 합니다. 그런 다음, await는 요청 중에 다음 예제와 같이 Task 속성을 만듭니다.

app.Run(async (context) =>
{
    using var webSocket = await context.WebSockets.AcceptWebSocketAsync();
    var socketFinishedTcs = new TaskCompletionSource<object>();

    BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

    await socketFinishedTcs.Task;
});

작업 메서드에서 너무 빨리 반환하면 WebSocket 닫힌 예외가 발생할 수도 있습니다. 작업 메서드에서 소켓을 수락하는 경우 작업 메서드에서 반환하기 전에 소켓을 사용하는 코드가 완료될 때까지 기다립니다.

소켓이 완료될 때까지 대기하려면 Task.Wait, Task.Result 또는 유사한 차단 호출을 사용하지 마세요. 심각한 스레드 문제가 발생할 수 있습니다. 항상 await를 사용합니다.

압축

Warning

암호화된 연결에서 압축을 사용할 수 있게 설정하면 앱이 CRIME/BREACH 공격을 받을 수 있습니다. 중요한 정보를 보내는 경우 압축을 사용하지 않도록 설정하거나 WebSocketMessageFlags.DisableCompression을 호출할 때 WebSocket.SendAsync를 사용합니다. 이는 WebSocket의 양쪽 모두에 적용됩니다. 브라우저의 WebSocket API에는 전송당 압축을 사용하지 않도록 설정할 수 있는 구성이 없습니다.

WebSocket을 통한 메시지 압축이 필요한 경우 수락 코드가 다음과 같이 압축을 허용하도록 지정해야 합니다.

using (var webSocket = await context.WebSockets.AcceptWebSocketAsync(
    new WebSocketAcceptContext { DangerousEnableCompression = true }))
{

}

WebSocketAcceptContext.ServerMaxWindowBitsWebSocketAcceptContext.DisableServerContextTakeover는 압축 작동 방식을 제어하는 고급 옵션입니다.

처음 연결을 설정할 때 클라이언트와 서버 간에 압축이 협상됩니다. WebSocket RFC에 대한 압축 확장에서 협상에 대해 자세히 알아볼 수 있습니다.

참고 항목

서버 또는 클라이언트에서 압축 협상을 수락하지 않으면 연결이 설정된 상태가 계속 유지되지만 메시지를 주고 받을 때는 연결에서 압축을 사용하지 않습니다.

메시지 보내기 및 받기

AcceptWebSocketAsync 메서드는 TCP 연결을 WebSocket 연결로 업그레이드하고 WebSocket 개체를 제공합니다. WebSocket 개체를 사용하여 메시지를 보내고 받습니다.

앞에서 살펴본 WebSocket 요청을 수락하는 코드는 WebSocket 개체를 Echo 메서드로 전달합니다. 코드는 메시지를 수신하고 동일한 메시지를 즉시 보냅니다. 메시지는 클라이언트가 연결을 닫을 때까지 루프에서 보내고 받습니다.

private static async Task Echo(WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    var receiveResult = await webSocket.ReceiveAsync(
        new ArraySegment<byte>(buffer), CancellationToken.None);

    while (!receiveResult.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(
            new ArraySegment<byte>(buffer, 0, receiveResult.Count),
            receiveResult.MessageType,
            receiveResult.EndOfMessage,
            CancellationToken.None);

        receiveResult = await webSocket.ReceiveAsync(
            new ArraySegment<byte>(buffer), CancellationToken.None);
    }

    await webSocket.CloseAsync(
        receiveResult.CloseStatus.Value,
        receiveResult.CloseStatusDescription,
        CancellationToken.None);
}

루프가 시작되기 전에 WebSocket 연결을 수락하면 미들웨어 파이프라인이 종료됩니다. 그리고 소켓을 닫으면 파이프라인이 풀립니다. 즉, 요청은 WebSocket이 수락될 때 파이프라인에서 앞으로 이동하지 않습니다. 루프가 완료되고 소켓이 닫히면 요청이 파이프라인 백업을 진행합니다.

클라이언트 연결 끊김 처리

연결이 끊어져서 클라이언트 연결이 해제된 경우 서버에 자동으로 알림이 전송되지 않습니다. 클라이언트에서 연결 끊김 메시지를 보내는 경우에만 서버에서 연결 끊김 메시지를 받습니다. 인터넷 연결이 끊긴 경우 메시지가 전송되지 않습니다. 그런 경우 조치를 취하려면 특정 시간 내에서 클라이언트에서 아무런 메시지도 받지 않은 후 시간 제한을 설정합니다.

클라이언트가 항상 메시지를 보내지 않고 연결이 유휴 상태가 되기 때문에 시간 제한을 원하지 않는 경우 클라이언트에서 타이머를 사용하여 X초마다 ping 메시지를 보내도록 합니다. 서버에서 메시지가 이전 메시지 후 2*X초 이내에 도착하지 않는 경우 연결을 종료하고 클라이언트 연결이 끊겼음을 보고합니다. ping 메시지를 지연시킬 수 있는 네트워크 지연에 대해 추가 시간을 두려면 예상 시간 간격의 2배 동안 기다립니다.

WebSocket 원본 제한

CORS에서 제공하는 보호 기능은 WebSocket에 적용되지 않습니다. 브라우저는 다음을 수행하지 않습니다.

  • CORS pre-flight 요청을 수행합니다.
  • WebSocket 요청을 생성할 때 Access-Control 헤더에 지정된 제한 사항을 준수합니다.

그러나 브라우저는 WebSocket 요청을 발급할 때 Origin 헤더를 보냅니다. 애플리케이션은 예상된 원본에서 제공하는 WebSocket만 허용되도록 이러한 헤더의 유효성을 검사하도록 구성되어야 합니다.

"https://server.com""에서 서버를 호스트하고 "https://client.com""에서 클라이언트를 호스트하는 경우 AllowedOrigins 목록에 "https://client.com""을 추가하여 WebSocket을 확인합니다.

var webSocketOptions = new WebSocketOptions
{
    KeepAliveInterval = TimeSpan.FromMinutes(2)
};

webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

참고 항목

Origin 헤더는 클라이언트에 의해 제어되며 Referer 헤더와 마찬가지로 위조될 수 있습니다. 이러한 헤더를 인증 메커니즘으로 사용하지 마세요.

IIS/IIS Express 지원

IIS/IIS Express 8 이상이 있는 Windows Server 2012 이상 및 Windows 8 이상에서는 WebSocket 프로토콜을 지원합니다.

참고 항목

IIS Express를 사용할 때 Websocket이 항상 활성화됩니다.

IIS에서 Websocket 사용

Windows Server 2012 이상에서 WebSocket 프로토콜을 지원하려면:

참고 항목

IIS Express를 사용할 때 이러한 단계가 필요하지 않습니다.

  1. 관리 메뉴 또는 서버 관리자의 링크를 통해 역할 및 기능 추가 마법사를 사용합니다.
  2. 역할 기반 또는 기능 기반 설치를 선택합니다. 다음을 선택합니다.
  3. 적절한 서버를 선택합니다(로컬 서버가 기본적으로 선택됨). 다음을 선택합니다.
  4. 역할 트리에서 Web Server(IIS)를 확장하고 Web Server를 확장한 다음, 애플리케이션 개발을 확장합니다.
  5. WebSocket 프로토콜을 선택합니다. 다음을 선택합니다.
  6. 추가 기능이 필요 없는 경우 다음을 선택합니다.
  7. 설치를 선택합니다.
  8. 설치가 완료되면 닫기를 선택하여 마법사를 종료합니다.

Windows 8 이상에서 WebSocket 프로토콜을 지원하려면:

참고 항목

IIS Express를 사용할 때 이러한 단계가 필요하지 않습니다.

  1. 제어판>프로그램>프로그램 및 기능>Windows 기능 사용/사용 안 함(화면 왼쪽)으로 이동합니다.
  2. 인터넷 정보 서비스>World Wide Web 서비스>애플리케이션 개발 기능 노드를 엽니다.
  3. WebSocket 프로토콜 기능을 선택합니다. 확인을 선택합니다.

Node.js에서 socket.io를 사용할 때 WebSocket 비활성화

Node.jssocket.io에서 WebSocket 지원을 사용하는 경우 web.config 또는 applicationHost.configwebSocket 요소를 사용하여 기본 IIS WebSocket 모듈을 비활성화합니다. 이 단계를 수행하지 않으면 IIS WebSocket 모듈이 Node.js 및 앱이 아닌 WebSocket 통신을 처리하려고 시도합니다.

<system.webServer>
  <webSocket enabled="false" />
</system.webServer>

샘플 앱

이 아티클과 함께 제공되는 샘플 앱은 에코 앱입니다. WebSocket 연결을 생성하는 웹 페이지가 제공되며 서버는 수신한 메시지를 클라이언트로 재전송합니다. 샘플 앱은 IIS Express를 사용하여 Visual Studio에서 실행되도록 구성되지 않으므로, dotnet run을 사용하여 명령 셸에서 앱을 실행하고 브라우저에서 http://localhost:<port>으로 이동합니다. 웹 페이지에 연결 상태가 표시됩니다.

WebSocket 연결 전 웹 페이지 초기 상태

Connect 를 선택해서 지정한 URL로 WebSocket 요청을 전송합니다. 그리고 테스트 메시지를 입력한 다음 Send 를 선택합니다. 테스트를 모두 마쳤으면 Close Socket 을 선택하십시오. Communication Log 섹션에는 각각의 열기, 전송 및 닫기 동작이 발생한 순서대로 나타납니다.

WebSocket 연결 및 테스트 메시지를 보내고 받은 후 웹 페이지 최종 상태

이 문서는 ASP.NET Core에서 WebSocket을 시작하는 방법을 설명합니다. WebSocket(RFC 6455)은 TCP 연결을 통해 지속적인 양방향 통신 채널을 사용할 수 있도록 해주는 프로토콜입니다. 채팅, 대시보드 및 게임 앱 등 신속한 실시간 통신을 활용하는 앱에서 사용됩니다.

예제 코드 살펴보기 및 다운로드 (다운로드 방법). 다운로드 예제는 영역을 테스트하기 위한 기초적인 앱을 제공합니다. 실행 방법.

SignalR

ASP.NET Core SignalR은 앱에 실시간 웹 기능을 추가하는 것을 간소화하는 라이브러리입니다. 가능하면 Websocket을 사용합니다.

대부분의 애플리케이션의 경우 원시 WebSockets보다 SignalR을 권장합니다. SignalR은 WebSockets를 사용할 수 없는 환경에 대한 전송 대체(fallback)를 제공합니다. 기본 원격 프로시저 호출 앱 모델도 제공합니다. 그리고 대부분의 시나리오에서 SignalR은 원시 WebSockets 사용과 비교할 때 큰 성능상의 단점이 없습니다.

일부 앱에 대해 .NET의 gRPC는 Websocket을 대신하는 방법을 제공합니다.

필수 조건

  • ASP.NET Core를 지원하는 모든 OS:
    • Windows 7/Windows Server 2008 이상
    • Linux
    • macOS
  • 앱이 IIS를 사용하여 Windows에서 실행되는 경우:
    • Windows 8 / Windows Server 2012 이상
    • IIS 8 / IIS 8 Express
    • Websocket을 사용하도록 설정해야 합니다. IIS/IIS Express 지원 섹션을 참조하세요.
  • 앱이 HTTP.sys 실행되는 경우:
    • Windows 8 / Windows Server 2012 이상
  • 지원되는 브라우저는 Can I use를 참조하세요.

미들웨어 구성하기

Startup 클래스의 Configure 메서드에 WebSockets 미들웨어를 추가합니다.

app.UseWebSockets();

참고 항목

컨트롤러에서 WebSocket 요청을 허용하려면 app.UseWebSockets에 대한 호출이 app.UseEndpoints 이전에 발생해야 합니다.

이때 다음과 같은 설정을 구성할 수 있습니다.

  • KeepAliveInterval - 프록시가 연결을 유지할 수 있도록 클라이언트로 "핑(ping)" 프레임을 전송하는 빈도입니다. 기본값은 2분입니다.
  • AllowedOrigins - WebSocket 요청에 허용된 원본 헤더 값의 목록입니다. 기본적으로 모든 원본이 허용됩니다. 자세한 내용은 아래의 "WebSocket 원본 제한"을 참조하세요.
var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
};

app.UseWebSockets(webSocketOptions);

WebSocket 요청 수락하기

요청 수명 주기의 뒷부분에서(예를 들어, Configure 메서드나 작업 메서드 뒷부분에서) WebSocket 요청 여부를 확인하고 WebSocket 요청을 수락합니다.

다음 예제는 Configure 메서드의 뒷부분에서 나옵니다.

app.Use(async (context, next) =>
{
    if (context.Request.Path == "/ws")
    {
        if (context.WebSockets.IsWebSocketRequest)
        {
            using (WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync())
            {
                await Echo(context, webSocket);
            }
        }
        else
        {
            context.Response.StatusCode = (int) HttpStatusCode.BadRequest;
        }
    }
    else
    {
        await next();
    }

});

WebSocket 요청은 모든 URL을 통해서 전달될 수 있지만, 이 예제 코드에서는 /ws 에 대한 요청만 수락합니다.

컨트롤러 메서드에서 유사한 방법을 사용할 수 있습니다.

public class WebSocketController : ControllerBase
{
    [HttpGet("/ws")]
    public async Task Get()
    {
        if (HttpContext.WebSockets.IsWebSocketRequest)
        {
            using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
            await Echo(webSocket);
        }
        else
        {
            HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
        }
    }

WebSocket을 사용하는 경우 연결 기간 동안 미들웨어 파이프라인이 계속 실행되도록 해야 합니다. 미들웨어 파이프라인 종료 후 WebSocket 메시지를 보내거나 받으려고 하면 다음과 같은 예외가 발생할 수 있습니다.

System.Net.WebSockets.WebSocketException (0x80004005): The remote party closed the WebSocket connection without completing the close handshake. ---> System.ObjectDisposedException: Cannot write to the response body, the response has completed.
Object name: 'HttpResponseStream'.

백그라운드 서비스를 사용하여 WebSocket에 데이터를 쓰는 경우 미들웨어 파이프라인이 계속 실행되도록 해야 합니다. TaskCompletionSource<TResult>을 사용하여 이 작업을 수행합니다. TaskCompletionSource를 백그라운드 서비스에 전달하고 WebSocket 사용을 완료하면 TrySetResult를 호출하도록 합니다. 그런 다음, await는 요청 중에 다음 예제와 같이 Task 속성을 만듭니다.

app.Use(async (context, next) =>
{
    using (WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync())
    {
        var socketFinishedTcs = new TaskCompletionSource<object>();

        BackgroundSocketProcessor.AddSocket(webSocket, socketFinishedTcs);

        await socketFinishedTcs.Task;
    }
});

작업 메서드에서 너무 빨리 반환하면 WebSocket 닫힌 예외가 발생할 수도 있습니다. 작업 메서드에서 소켓을 수락하는 경우 작업 메서드에서 반환하기 전에 소켓을 사용하는 코드가 완료될 때까지 기다립니다.

소켓이 완료될 때까지 대기하려면 Task.Wait, Task.Result 또는 유사한 차단 호출을 사용하지 마세요. 심각한 스레드 문제가 발생할 수 있습니다. 항상 await를 사용합니다.

메시지 보내기 및 받기

AcceptWebSocketAsync 메서드는 TCP 연결을 WebSocket 연결로 업그레이드하고 WebSocket 개체를 제공합니다. WebSocket 개체를 사용하여 메시지를 보내고 받습니다.

앞에서 살펴본 WebSocket 요청을 수락하는 코드는 WebSocket 개체를 Echo 메서드로 전달합니다. 코드는 메시지를 수신하고 동일한 메시지를 즉시 보냅니다. 메시지는 클라이언트가 연결을 닫을 때까지 루프에서 보내고 받습니다.

private async Task Echo(HttpContext context, WebSocket webSocket)
{
    var buffer = new byte[1024 * 4];
    WebSocketReceiveResult result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    while (!result.CloseStatus.HasValue)
    {
        await webSocket.SendAsync(new ArraySegment<byte>(buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None);

        result = await webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
    }
    await webSocket.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);
}

루프가 시작되기 전에 WebSocket 연결을 수락하면 미들웨어 파이프라인이 종료됩니다. 그리고 소켓을 닫으면 파이프라인이 풀립니다. 즉, 요청은 WebSocket이 수락될 때 파이프라인에서 앞으로 이동하지 않습니다. 루프가 완료되고 소켓이 닫히면 요청이 파이프라인 백업을 진행합니다.

클라이언트 연결 끊김 처리

연결이 끊어져서 클라이언트 연결이 해제된 경우 서버에 자동으로 알림이 전송되지 않습니다. 클라이언트에서 연결 끊김 메시지를 보내는 경우에만 서버에서 연결 끊김 메시지를 받습니다. 인터넷 연결이 끊긴 경우 메시지가 전송되지 않습니다. 그런 경우 조치를 취하려면 특정 시간 내에서 클라이언트에서 아무런 메시지도 받지 않은 후 시간 제한을 설정합니다.

클라이언트가 항상 메시지를 보내지 않고 연결이 유휴 상태가 되기 때문에 시간 제한을 원하지 않는 경우 클라이언트에서 타이머를 사용하여 X초마다 ping 메시지를 보내도록 합니다. 서버에서 메시지가 이전 메시지 후 2*X초 이내에 도착하지 않는 경우 연결을 종료하고 클라이언트 연결이 끊겼음을 보고합니다. ping 메시지를 지연시킬 수 있는 네트워크 지연에 대해 추가 시간을 두려면 예상 시간 간격의 2배 동안 기다립니다.

참고 항목

내부 ManagedWebSocket 는 Ping/Pong 프레임을 암시적으로 처리하여 옵션이 0보다 큰 경우 KeepAliveInterval 연결을 활성 상태로 유지하며 기본값은 30초(TimeSpan.FromSeconds(30))입니다.

WebSocket 원본 제한

CORS에서 제공하는 보호 기능은 WebSocket에 적용되지 않습니다. 브라우저는 다음을 수행하지 않습니다.

  • CORS pre-flight 요청을 수행합니다.
  • WebSocket 요청을 생성할 때 Access-Control 헤더에 지정된 제한 사항을 준수합니다.

그러나 브라우저는 WebSocket 요청을 발급할 때 Origin 헤더를 보냅니다. 애플리케이션은 예상된 원본에서 제공하는 WebSocket만 허용되도록 이러한 헤더의 유효성을 검사하도록 구성되어야 합니다.

"https://server.com""에서 서버를 호스트하고 "https://client.com""에서 클라이언트를 호스트하는 경우 AllowedOrigins 목록에 "https://client.com""을 추가하여 WebSocket을 확인합니다.

var webSocketOptions = new WebSocketOptions()
{
    KeepAliveInterval = TimeSpan.FromSeconds(120),
};
webSocketOptions.AllowedOrigins.Add("https://client.com");
webSocketOptions.AllowedOrigins.Add("https://www.client.com");

app.UseWebSockets(webSocketOptions);

참고 항목

Origin 헤더는 클라이언트에 의해 제어되며 Referer 헤더와 마찬가지로 위조될 수 있습니다. 이러한 헤더를 인증 메커니즘으로 사용하지 마세요.

IIS/IIS Express 지원

IIS/IIS Express 8 이상이 있는 Windows Server 2012 이상 및 Windows 8 이상에서는 WebSocket 프로토콜을 지원합니다.

참고 항목

IIS Express를 사용할 때 Websocket이 항상 활성화됩니다.

IIS에서 Websocket 사용

Windows Server 2012 이상에서 WebSocket 프로토콜을 지원하려면:

참고 항목

IIS Express를 사용할 때 이러한 단계가 필요하지 않습니다.

  1. 관리 메뉴 또는 서버 관리자의 링크를 통해 역할 및 기능 추가 마법사를 사용합니다.
  2. 역할 기반 또는 기능 기반 설치를 선택합니다. 다음을 선택합니다.
  3. 적절한 서버를 선택합니다(로컬 서버가 기본적으로 선택됨). 다음을 선택합니다.
  4. 역할 트리에서 Web Server(IIS)를 확장하고 Web Server를 확장한 다음, 애플리케이션 개발을 확장합니다.
  5. WebSocket 프로토콜을 선택합니다. 다음을 선택합니다.
  6. 추가 기능이 필요 없는 경우 다음을 선택합니다.
  7. 설치를 선택합니다.
  8. 설치가 완료되면 닫기를 선택하여 마법사를 종료합니다.

Windows 8 이상에서 WebSocket 프로토콜을 지원하려면:

참고 항목

IIS Express를 사용할 때 이러한 단계가 필요하지 않습니다.

  1. 제어판>프로그램>프로그램 및 기능>Windows 기능 사용/사용 안 함(화면 왼쪽)으로 이동합니다.
  2. 인터넷 정보 서비스>World Wide Web 서비스>애플리케이션 개발 기능 노드를 엽니다.
  3. WebSocket 프로토콜 기능을 선택합니다. 확인을 선택합니다.

Node.js에서 socket.io를 사용할 때 WebSocket 비활성화

Node.jssocket.io에서 WebSocket 지원을 사용하는 경우 web.config 또는 applicationHost.configwebSocket 요소를 사용하여 기본 IIS WebSocket 모듈을 비활성화합니다. 이 단계를 수행하지 않으면 IIS WebSocket 모듈이 Node.js 및 앱이 아닌 WebSocket 통신을 처리하려고 시도합니다.

<system.webServer>
  <webSocket enabled="false" />
</system.webServer>

샘플 앱

이 아티클과 함께 제공되는 샘플 앱은 에코 앱입니다. WebSocket 연결을 생성하는 웹 페이지가 제공되며 서버는 수신한 메시지를 클라이언트로 재전송합니다. 샘플 앱은 IIS Express를 사용하여 Visual Studio에서 실행되도록 구성되지 않으므로, dotnet run을 사용하여 명령 셸에서 앱을 실행하고 브라우저에서 http://localhost:5000으로 이동합니다. 웹 페이지에 연결 상태가 표시됩니다.

WebSocket 연결 전 웹 페이지 초기 상태

Connect 를 선택해서 지정한 URL로 WebSocket 요청을 전송합니다. 그리고 테스트 메시지를 입력한 다음 Send 를 선택합니다. 테스트를 모두 마쳤으면 Close Socket 을 선택하십시오. Communication Log 섹션에는 각각의 열기, 전송 및 닫기 동작이 발생한 순서대로 나타납니다.

WebSocket 연결 및 테스트 메시지를 보내고 받은 후 웹 페이지 최종 상태