자습서: 하위 프로토콜을 사용하여 WebSocket 클라이언트 간에 메시지 게시 및 구독

채팅 앱 빌드 자습서에서는 WebSocket API를 사용하여 Azure Web PubSub와 데이터를 보내고 받는 방법을 알아보았습니다. 클라이언트가 서비스와 통신할 때는 프로토콜이 필요하지 않습니다. 예를 들어 WebSocket.send()를 사용하여 모든 형식의 데이터를 보낼 수 있으며 서버는 데이터를 있는 그대로 수신합니다. WebSocket API 프로세스는 사용하기 쉽지만 기능이 제한적입니다. 예를 들어 서버에 이벤트를 보낼 때 이벤트 이름을 지정하거나, 서버에 메시지를 전송하는 대신 다른 클라이언트에 메시지를 게시할 수 없습니다. 이 자습서에서는 하위 프로토콜을 사용하여 클라이언트의 기능을 확장하는 방법을 배웁니다.

이 자습서에서는 다음을 하는 방법을 알아볼 수 있습니다.

  • Web PubSub 서비스 인스턴스 만들기
  • WebSocket 연결을 설정하기 위한 전체 URL 생성
  • 하위 프로토콜을 사용하여 WebSocket 클라이언트 간에 메시지 게시

Azure를 구독하고 있지 않다면 시작하기 전에 Azure 체험 계정을 만듭니다.

사전 요구 사항

  • 이렇게 설정하려면 버전 2.22.0 이상의 Azure CLI가 필요합니다. Azure Cloud Shell을 사용하는 경우 최신 버전이 이미 설치되어 있습니다.

Azure Web PubSub 인스턴스 만들기

리소스 그룹 만들기

리소스 그룹은 Azure 리소스가 배포 및 관리되는 논리적 컨테이너입니다. az group create 명령을 사용하여 eastus 위치에서 myResourceGroup이라는 리소스 그룹을 만듭니다.

az group create --name myResourceGroup --location EastUS

Web PubSub 인스턴스 만들기

az extension add를 실행하여 webpubsub 확장을 현재 버전으로 설치하거나 업그레이드합니다.

az extension add --upgrade --name webpubsub

Azure CLI az webpubsub create 명령을 사용하여 만든 리소스 그룹에 Web PubSub를 만듭니다. 다음 명령은 EastUS의 리소스 그룹 myResourceGroup 아래에 무료 Web PubSub 리소스를 만듭니다.

Important

각 Web PubSub 리소스에는 고유한 이름이 있어야 합니다. 다음 예제에서 <your-unique-resource-name>을 Web PubSub의 이름으로 바꿉니다.

az webpubsub create --name "<your-unique-resource-name>" --resource-group "myResourceGroup" --location "EastUS" --sku Free_F1

이 명령의 출력에는 새로 만든 리소스의 속성이 표시됩니다. 아래에 나열된 두 개의 속성을 기록합니다.

  • 리소스 이름: 위의 --name 매개 변수에 제공한 이름입니다.
  • hostName: 이 예제에서 호스트 이름은 <your-unique-resource-name>.webpubsub.azure.com/입니다.

이때 Azure 계정은 이 새 리소스에서 모든 작업을 수행할 권한이 있는 유일한 계정입니다.

나중에 사용할 수 있도록 ConnectionString 가져오기

Important

연결 문자열에는 애플리케이션이 Azure Web PubSub 서비스에 액세스하는 데 필요한 권한 부여 정보가 포함됩니다. 연결 문자열 내의 액세스 키는 서비스의 루트 암호와 비슷합니다. 프로덕션 환경에서는 항상 액세스 키를 보호해야 합니다. Azure Key Vault를 사용하여 키를 안전하게 관리하고 회전합니다. 액세스 키를 다른 사용자에게 배포하거나 하드 코딩하거나 다른 사용자가 액세스할 수 있는 일반 텍스트로 저장하지 않도록 합니다. 키가 손상되었다고 생각되면 키를 교체하세요.

Azure CLI az webpubsub key 명령을 사용하여 서비스의 ConnectionString을 가져옵니다. <your-unique-resource-name> 자리 표시자를 Azure Web PubSub 인스턴스 이름으로 바꿉니다.

az webpubsub key show --resource-group myResourceGroup --name <your-unique-resource-name> --query primaryConnectionString --output tsv

나중에 사용할 수 있게 연결 문자열을 복사합니다.

가져온 ConnectionString을 복사합니다. 이 자습서의 뒷부분에서 <connection_string> 값으로 사용됩니다.

프로젝트 설정

필수 조건

하위 프로토콜 사용

클라이언트는 특정 하위 프로토콜을 사용하여 WebSocket 연결을 시작할 수 있습니다. Azure Web PubSub 서비스는 클라이언트가 업스트리밍 서버에 왕복하는 대신 Web PubSub 서비스를 통해 직접 게시/구독할 수 있도록 json.webpubsub.azure.v1이라는 하위 프로토콜을 지원합니다. 하위 프로토콜에 대한 자세한 내용은 Azure Web PubSub 지원 JSON WebSocket 하위 프로토콜을 참조하세요.

다른 프로토콜 이름을 사용하는 경우 해당 이름은 서비스에서 무시되고 연결 이벤트 처리기의 서버에 전달되므로 사용자 고유의 프로토콜을 만들 수 있습니다.

이제 json.webpubsub.azure.v1 하위 프로토콜을 사용하여 웹 애플리케이션을 만들어 보겠습니다.

  1. 종속성 설치

    mkdir logstream
    cd logstream
    dotnet new web
    dotnet add package Microsoft.Extensions.Azure
    dotnet add package Azure.Messaging.WebPubSub
    
  2. /negotiate API 및 웹 페이지를 호스트하는 서버 쪽을 만듭니다.

    Program.cs를 다음 코드로 업데이트합니다.

    • AddAzureClients를 업데이트하여 서비스 클라이언트를 추가하고 구성에서 연결 문자열을 읽습니다.
    • app.Run(); 앞에 app.UseStaticFiles();를 추가하여 정적 파일을 지원합니다.
    • /negotiate 요청을 통해 클라이언트 액세스 토큰을 생성하도록 app.MapGet를 업데이트합니다.
    using Azure.Messaging.WebPubSub;
    using Microsoft.Extensions.Azure;
    
    var builder = WebApplication.CreateBuilder(args);
    builder.Services.AddAzureClients(s =>
    {
        s.AddWebPubSubServiceClient(builder.Configuration["Azure:WebPubSub:ConnectionString"], "stream");
    });
    
    var app = builder.Build();
    app.UseStaticFiles();
    app.MapGet("/negotiate", async context =>
    {
        var service = context.RequestServices.GetRequiredService<WebPubSubServiceClient>();
        var response = new
        {
            url = service.GetClientAccessUri(roles: new string[] { "webpubsub.sendToGroup.stream", "webpubsub.joinLeaveGroup.stream" }).AbsoluteUri
        };
        await context.Response.WriteAsJsonAsync(response);
    });
    
    app.Run();
    
  3. 웹 페이지 만들기

    아래 내용으로 HTML 페이지를 만들고 wwwroot/index.html로 저장합니다.

    <html>
      <body>
        <div id="output"></div>
        <script>
          (async function () {
            let res = await fetch('/negotiate')
            let data = await res.json();
            let ws = new WebSocket(data.url, 'json.webpubsub.azure.v1');
            ws.onopen = () => {
              console.log('connected');
            };
    
            let output = document.querySelector('#output');
            ws.onmessage = event => {
              let d = document.createElement('p');
              d.innerText = event.data;
              output.appendChild(d);
            };
          })();
        </script>
      </body>
    </html>                                                                
    

    위 코드는 서비스에 연결하고 페이지에 수신된 메시지를 인쇄하기만 합니다. 주요 변경 내용은 WebSocket 연결을 만들 때 하위 프로토콜을 지정하는 것입니다.

  4. 서버 실행

    .NET Core용 Secret Manager 도구를 사용하여 연결 문자열을 설정합니다. 아래 명령을 실행하고, <connection_string>이전 단계에서 가져온 연결 문자열로 바꾸고, 브라우저에서 http://localhost:5000/index.html을 엽니다.

    dotnet user-secrets init
    dotnet user-secrets set Azure:WebPubSub:ConnectionString "<connection-string>"
    dotnet run
    

    Chrome을 사용하는 경우 F12 키를 누르거나 마우스 오른쪽 단추 ->검사 ->개발자 도구를 클릭하고 네트워크 탭을 선택하면 됩니다. 웹 페이지를 로드하면 WebSocket 연결이 설정된 것을 볼 수 있습니다. WebSocket 연결을 검사하기 위해 선택하면 아래 connected 이벤트 메시지가 클라이언트에 수신되는 것을 확인할 수 있습니다. 이 클라이언트에 대해 생성된 connectionId를 가져올 수 있습니다.

    {"type":"system","event":"connected","userId":null,"connectionId":"<the_connection_id>"}
    

하위 프로토콜의 도움을 받으면 연결이 connected일 때 연결의 일부 메타데이터를 가져올 수 있습니다.

이제 클라이언트는 일반 텍스트 대신 JSON 메시지를 받습니다. JSON 메시지에는 메시지의 유형 및 원본과 같은 추가 정보가 포함되어 있습니다. 따라서 이 정보를 사용하여 메시지에 대한 추가 처리를 수행할 수 있습니다. 예를 들어 다른 원본에서 온 메시지인 경우 다른 스타일로 표시할 수 있습니다. 이에 대한 내용은 이후 섹션에서 찾을 수 있습니다.

클라이언트에서 메시지 게시

채팅 앱 빌드 자습서에서 클라이언트가 WebSocket 연결을 통해 Web PubSub 서비스에 메시지를 보내면 서비스는 서버 쪽에서 사용자 이벤트를 트리거합니다. 하위 프로토콜을 사용하면 클라이언트가 JSON 메시지를 전송하여 더 많은 기능을 갖게 됩니다. 예를 들어 Web PubSub 서비스를 통해 클라이언트에서 다른 클라이언트로 직접 메시지를 게시할 수 있습니다.

이 기능은 대량의 데이터를 다른 클라이언트에 실시간으로 스트리밍하려는 경우에 유용합니다. 이 기능을 사용하여 콘솔 로그를 실시간으로 브라우저에 스트리밍할 수 있는 로그 스트리밍 애플리케이션을 빌드해 보겠습니다.

  1. 스트리밍 프로그램 만들기

    다음과 같이 stream 프로그램을 만듭니다.

    mkdir stream
    cd stream
    dotnet new console
    

    Program.cs를 다음 내용으로 업데이트합니다.

    using System;
    using System.Net.Http;
    using System.Net.WebSockets;
    using System.Text;
    using System.Text.Json;
    using System.Threading.Tasks;
    
    namespace stream
    {
        class Program
        {
            private static readonly HttpClient http = new HttpClient();
            static async Task Main(string[] args)
            {
                // Get client url from remote
                var stream = await http.GetStreamAsync("http://localhost:5000/negotiate");
                var url = (await JsonSerializer.DeserializeAsync<ClientToken>(stream)).url;
                var client = new ClientWebSocket();
                client.Options.AddSubProtocol("json.webpubsub.azure.v1");
    
                await client.ConnectAsync(new Uri(url), default);
    
                Console.WriteLine("Connected.");
                var streaming = Console.ReadLine();
                while (streaming != null)
                {
                    if (!string.IsNullOrEmpty(streaming))
                    {
                        var message = JsonSerializer.Serialize(new
                        {
                            type = "sendToGroup",
                            group = "stream",
                            data = streaming + Environment.NewLine,
                        });
                        Console.WriteLine("Sending " + message);
                        await client.SendAsync(Encoding.UTF8.GetBytes(message), WebSocketMessageType.Text, true, default);
                    }
    
                    streaming = Console.ReadLine();
                }
    
                await client.CloseAsync(WebSocketCloseStatus.NormalClosure, null, default);
            }
    
            private sealed class ClientToken
            {
                public string url { get; set; }
            }
        }
    }
    
    

    여기서는 "그룹"이라는 새로운 개념을 볼 수 있습니다. 그룹은 연결 그룹에 메시지를 게시할 수 있는 허브의 논리적 개념입니다. 허브에 여러 그룹을 포함할 수 있으며 한 클라이언트가 동시에 여러 그룹을 구독할 수 있습니다. 하위 프로토콜을 사용하는 경우 전체 허브에 브로드캐스트하는 대신 한 그룹에만 게시할 수 있습니다. 용어에 대한 자세한 내용은 기본 개념을 참조하세요.

  2. 여기서는 그룹을 사용하기 때문에 index.html 콜백 내에서 WebSocket 연결이 설정되면 그룹에 조인하도록 ws.onopen 웹 페이지를 업데이트해야 합니다.

    let ackId = 0;
    ws.onopen = () => {
      console.log('connected');
      ws.send(JSON.stringify({
        type: 'joinGroup',
        group: 'stream',
        ackId: ++ackId
      }));
    };
    

    클라이언트가 메시지를 joinGroup 형식으로 전송하여 그룹에 조인하는 것을 볼 수 있습니다.

  3. 또한 JSON 응답을 구문 분석하고 ws.onmessage 그룹의 메시지만 인쇄하도록 stream 콜백 논리를 살짝 업데이트합니다. 그러면 라이브 스트림 프린터로 작동합니다.

    ws.onmessage = event => {
      let message = JSON.parse(event.data);
      if (message.type === 'message' && message.group === 'stream') {
        let d = document.createElement('span');
        d.innerText = message.data;
        output.appendChild(d);
        window.scrollTo(0, document.body.scrollHeight);
      }
    };
    
  4. 보안상의 이유로 클라이언트는 기본적으로 그룹 자체를 게시하거나 구독할 수 없습니다. 이러한 이유로 토큰을 생성할 때 roles를 클라이언트로 설정했습니다.

    Startup.csGenerateClientAccessUri가 아래와 같으면 roles를 설정합니다.

    service.GenerateClientAccessUri(roles: new string[] { "webpubsub.sendToGroup.stream", "webpubsub.joinLeaveGroup.stream" })
    
  5. 마지막으로, 보기 좋게 표시되도록 index.html에 스타일을 적용합니다.

    <html>
    
      <head>
        <style>
          #output {
            white-space: pre;
            font-family: monospace;
          }
        </style>
      </head>
    

이제 아래 코드를 실행하고 텍스트를 입력하면 실시간으로 브라우저에 표시됩니다.

ls -R | dotnet run

# Or call `dir /s /b | dotnet run` when you are using CMD under Windows

또는 데이터가 실시간으로 브라우저에 스트리밍되는 것을 볼 수 있도록 속도를 낮출 수도 있습니다.

for i in $(ls -R); do echo $i; sleep 0.1; done | dotnet run

이 자습서의 전체 코드 샘플은 여기서 찾을 수 있습니다.

다음 단계

이 자습서에서는 하위 프로토콜을 허용하여 Web PubSub 서비스에 연결하는 방법과 연결된 클라이언트에 메시지를 게시하는 방법에 대한 기본 개념을 알아보았습니다.

다른 자습서를 확인하여 서비스 사용 방법을 자세히 알아보세요.