메시지 게시 및 구독 자습서에서는 Azure Web PubSub를 통해 메시지를 게시하고 구독하는 기본 사항을 알아봅니다. 이 자습서에서는 Azure Web PubSub의 이벤트 시스템을 배우고 이를 사용하여 실시간 통신 기능을 갖춘 완전한 웹 애플리케이션을 빌드합니다.
CLI 참조 명령을 로컬에서 실행하려면 Azure CLI를 설치합니다. Windows 또는 macOS에서 실행 중인 경우 Docker 컨테이너에서 Azure CLI를 실행하는 것이 좋습니다. 자세한 내용은 Docker 컨테이너에서 Azure CLI를 실행하는 방법을 참조하세요.
로컬 설치를 사용하는 경우 az login 명령을 사용하여 Azure CLI에 로그인합니다. 인증 프로세스를 완료하려면 터미널에 표시되는 단계를 수행합니다. 다른 로그인 옵션은 Azure CLI를 사용하여 로그인을 참조하세요.
메시지가 표시되면 처음 사용할 때 Azure CLI 확장을 설치합니다. 확장에 대한 자세한 내용은 Azure CLI에서 확장 사용을 참조하세요.
이 명령의 출력에는 새로 만든 리소스의 속성이 표시됩니다. 아래에 나열된 두 개의 속성을 기록합니다.
리소스 이름: 위의 --name 매개 변수에 제공한 이름입니다.
hostName: 이 예제에서 호스트 이름은 <your-unique-resource-name>.webpubsub.azure.com/입니다.
이때 Azure 계정은 이 새 리소스에서 모든 작업을 수행할 권한이 있는 유일한 계정입니다.
나중에 사용할 수 있도록 ConnectionString 가져오기
Important
원시 연결 문자열 데모용으로만 이 문서에 표시됩니다.
연결 문자열에는 애플리케이션이 Azure Web PubSub 서비스에 액세스하는 데 필요한 권한 부여 정보가 포함됩니다. 연결 문자열 내의 액세스 키는 서비스의 루트 암호와 비슷합니다. 프로덕션 환경에서는 항상 액세스 키를 보호합니다. Azure Key Vault를 사용하여 키를 안전하게 관리 및 회전하고 연결을 WebPubSubServiceClient보호합니다.
액세스 키를 다른 사용자에게 배포하거나 하드 코딩하거나 다른 사용자가 액세스할 수 있는 일반 텍스트로 저장하지 않도록 합니다. 키가 손상되었다고 생각되면 키를 교체하세요.
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을 복사하여 나중에 자습서에서 읽을 환경 변수 WebPubSubConnectionString에 설정합니다. 아래의 <connection-string>을 가져온 ConnectionString으로 바꿉니다.
Azure Web PubSub에는 서버 역할과 클라이언트 역할이 있습니다. 이 개념은 웹 애플리케이션의 서버 역할 및 클라이언트 역할과 비슷합니다. 서버는 클라이언트를 관리하고, 클라이언트 메시지를 수신 대기하고, 응답해야 합니다. 클라이언트는 사용자 메시지를 보내고 서버에서 메시지를 받고 최종 사용자를 위해 메시지를 시각화해야 합니다.
이 자습서에서는 실시간 채팅 웹 애플리케이션을 빌드합니다. 실제 웹 애플리케이션에서는 클라이언트를 인증하고 애플리케이션 UI에 대한 정적 웹 페이지를 제공하는 것도 서버의 역할에 포함됩니다.
메시지 게시 및 구독 자습서에서 구독자는 연결 문자열을 직접 사용합니다. 실제 애플리케이션에서는 연결 문자열이 서비스에 대한 모든 작업을 수행할 수 있는 높은 권한을 갖기 때문에 클라이언트와 연결 문자열을 공유하는 것은 안전하지 않습니다. 이제 서버가 연결 문자열을 사용하고 클라이언트가 액세스 토큰이 포함된 전체 URL을 가져올 수 있도록 negotiate 엔드포인트를 노출하도록 하겠습니다. 이러한 방식으로 서버는 무단 액세스를 방지하기 위해 negotiate 엔드포인트 앞에 권한 부여 미들웨어를 추가할 수 있습니다.
이제 클라이언트가 토큰을 생성하기 위해 호출할 /negotiate 엔드포인트를 추가하겠습니다.
using Azure.Core;
using Microsoft.Azure.WebPubSub.AspNetCore;
using Microsoft.Azure.WebPubSub.Common;
using Microsoft.Extensions.Primitives;
// Read connection string from environment
var connectionString = Environment.GetEnvironmentVariable("WebPubSubConnectionString");
if (connectionString == null)
{
throw new ArgumentNullException(nameof(connectionString));
}
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddWebPubSub(o => o.ServiceEndpoint = new WebPubSubServiceEndpoint(connectionString))
.AddWebPubSubServiceClient<Sample_ChatApp>();
var app = builder.Build();
app.UseStaticFiles();
// return the Client Access URL with negotiate endpoint
app.MapGet("/negotiate", (WebPubSubServiceClient<Sample_ChatApp> service, HttpContext context) =>
{
var id = context.Request.Query["id"];
if (StringValues.IsNullOrEmpty(id))
{
context.Response.StatusCode = 400;
return null;
}
return new
{
url = service.GetClientAccessUri(userId: id).AbsoluteUri
};
});
app.Run();
sealed class Sample_ChatApp : WebPubSubHub
{
}
AddWebPubSubServiceClient<THub>()는 협상 단계에서 WebPubSubServiceClient<THub> 클라이언트 연결 토큰을 생성하고 허브 메서드에서 허브 이벤트가 트리거될 때 서비스 REST API를 호출하는 데 사용할 수 있는 서비스 클라이언트 를 삽입하는 데 사용됩니다. 이 토큰 생성 코드는 토큰을 생성할 때 인수(userId)를 하나 더 전달하는 것을 제외하고 메시지 게시 및 구독 자습서에서 사용한 코드와 비슷합니다. 사용자 ID를 사용하여 클라이언트 ID를 식별할 수 있으므로 메시지를 받을 때 어디서 오는 메시지인지 알 수 있습니다.
이 코드는 이전 단계에서 설정한 환경 변수 WebPubSubConnectionString에서 연결 문자열을 읽습니다.
dotnet run --urls http://localhost:8080을 사용하여 서버를 다시 실행합니다.
먼저 Azure Web PubSub SDK를 설치합니다.
npm install --save @azure/web-pubsub
이제 토큰을 생성하기 위해 /negotiate API를 추가하겠습니다.
const express = require('express');
const { WebPubSubServiceClient } = require('@azure/web-pubsub');
const app = express();
const hubName = 'Sample_ChatApp';
let serviceClient = new WebPubSubServiceClient(process.env.WebPubSubConnectionString, hubName);
app.get('/negotiate', async (req, res) => {
let id = req.query.id;
if (!id) {
res.status(400).send('missing user id');
return;
}
let token = await serviceClient.getClientAccessToken({ userId: id });
res.json({
url: token.url
});
});
app.use(express.static('public'));
app.listen(8080, () => console.log('server started'));
이 토큰 생성 코드는 토큰을 생성할 때 인수(userId)를 하나 더 전달하는 것을 제외하고 메시지 게시 및 구독 자습서에서 사용한 코드와 비슷합니다. 사용자 ID를 사용하여 클라이언트 ID를 식별할 수 있으므로 메시지를 받을 때 어디서 오는 메시지인지 알 수 있습니다.
이 코드는 이전 단계에서 설정한 환경 변수 WebPubSubConnectionString에서 연결 문자열을 읽습니다.
node server를 실행하여 서버를 다시 실행합니다.
먼저 Azure Web PubSub SDK 종속성과 gson을 pom.xml의 dependencies 노드에 추가합니다.
이제 토큰을 생성하기 위해 App.java 파일에 /negotiate API를 추가하겠습니다.
package com.webpubsub.tutorial;
import com.azure.messaging.webpubsub.WebPubSubServiceClient;
import com.azure.messaging.webpubsub.WebPubSubServiceClientBuilder;
import com.azure.messaging.webpubsub.models.GetClientAccessTokenOptions;
import com.azure.messaging.webpubsub.models.WebPubSubClientAccessToken;
import com.azure.messaging.webpubsub.models.WebPubSubContentType;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import io.javalin.Javalin;
public class App {
public static void main(String[] args) {
String connectionString = System.getenv("WebPubSubConnectionString");
if (connectionString == null) {
System.out.println("Please set the environment variable WebPubSubConnectionString");
return;
}
// create the service client
WebPubSubServiceClient service = new WebPubSubServiceClientBuilder()
.connectionString(connectionString)
.hub("Sample_ChatApp")
.buildClient();
// start a server
Javalin app = Javalin.create(config -> {
config.staticFiles.add("public");
}).start(8080);
// Handle the negotiate request and return the token to the client
app.get("/negotiate", ctx -> {
String id = ctx.queryParam("id");
if (id == null) {
ctx.status(400);
ctx.result("missing user id");
return;
}
GetClientAccessTokenOptions option = new GetClientAccessTokenOptions();
option.setUserId(id);
WebPubSubClientAccessToken token = service.getClientAccessToken(option);
ctx.contentType("application/json");
Gson gson = new Gson();
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("url", token.getUrl());
String response = gson.toJson(jsonObject);
ctx.result(response);
return;
});
}
}
이 토큰 생성 코드는 토큰을 생성할 때 setUserId 메서드를 호출하여 사용자 ID를 설정하는 것을 제외하고 메시지 게시 및 구독 자습서에서 사용한 코드와 비슷합니다. 사용자 ID를 사용하여 클라이언트 ID를 식별할 수 있으므로 메시지를 받을 때 어디서 오는 메시지인지 알 수 있습니다.
이 코드는 이전 단계에서 설정한 환경 변수 WebPubSubConnectionString에서 연결 문자열을 읽습니다.
import os
from flask import (
Flask,
request,
send_from_directory,
)
from azure.messaging.webpubsubservice import (
WebPubSubServiceClient
)
hub_name = 'Sample_ChatApp'
connection_string = os.environ.get('WebPubSubConnectionString')
app = Flask(__name__)
service = WebPubSubServiceClient.from_connection_string(connection_string, hub=hub_name)
@app.route('/<path:filename>')
def index(filename):
return send_from_directory('public', filename)
@app.route('/negotiate')
def negotiate():
id = request.args.get('id')
if not id:
return 'missing user id', 400
token = service.get_client_access_token(user_id=id)
return {
'url': token['url']
}, 200
if __name__ == '__main__':
app.run(port=8080)
이 토큰 생성 코드는 토큰을 생성할 때 인수(user_id)를 하나 더 전달하는 것을 제외하고 메시지 게시 및 구독 자습서에서 사용한 코드와 비슷합니다. 사용자 ID를 사용하여 클라이언트 ID를 식별할 수 있으므로 메시지를 받을 때 어디서 오는 메시지인지 알 수 있습니다.
이 코드는 이전 단계에서 설정한 환경 변수 WebPubSubConnectionString에서 연결 문자열을 읽습니다.
python server.py을 사용하여 서버를 다시 실행합니다.
http://localhost:8080/negotiate?id=user1에 액세스하여 이 API를 테스트할 수 있으며 액세스 토큰과 함께 Azure Web PubSub의 전체 URL을 제공합니다.
이벤트 처리
Azure Web PubSub에서는 클라이언트 쪽에서 특정 작업이 발생하는 경우(예: 클라이언트 연결, 연결, 연결 끊김 또는 클라이언트가 메시지 보내기) 서비스가 서버에 알림을 보내 이러한 이벤트에 반응할 수 있도록 합니다.
이벤트는 Webhook 형식으로 서버에 전달됩니다. Webhook는 애플리케이션 서버를 통해 제공 및 노출되며 Azure Web PubSub 서비스 쪽에서 등록됩니다. 이 서비스는 이벤트가 발생할 때마다 webhook를 호출합니다.
이제 이전 단계에서 만든 Sample_ChatApp 클래스 내에 Web PubSub 서비스를 호출하는 데 사용하는 WebPubSubServiceClient<Sample_ChatApp>와 작동하는 생성자를 추가합니다. 그리고 connected 이벤트가 트리거될 때 응답하려면 OnConnectedAsync()을, 클라이언트의 메시지를 처리하려면 OnMessageReceivedAsync()를 사용합니다.
sealed class Sample_ChatApp : WebPubSubHub
{
private readonly WebPubSubServiceClient<Sample_ChatApp> _serviceClient;
public Sample_ChatApp(WebPubSubServiceClient<Sample_ChatApp> serviceClient)
{
_serviceClient = serviceClient;
}
public override async Task OnConnectedAsync(ConnectedEventRequest request)
{
Console.WriteLine($"[SYSTEM] {request.ConnectionContext.UserId} joined.");
}
public override async ValueTask<UserEventResponse> OnMessageReceivedAsync(UserEventRequest request, CancellationToken cancellationToken)
{
await _serviceClient.SendToAllAsync(RequestContent.Create(
new
{
from = request.ConnectionContext.UserId,
message = request.Data.ToString()
}),
ContentType.ApplicationJson);
return new UserEventResponse();
}
}
위 코드에서는 서비스 클라이언트를 사용하여 SendToAllAsync에 참여하는 모든 사용자에게 JSON 형식의 알림 메시지를 브로드캐스트합니다.
Express @azure/web-pubsub-express용 Web PubSub SDK는 CloudEvents 요청을 구문 분석하고 처리하는 데 도움이 됩니다.
npm install --save @azure/web-pubsub-express
클라이언트 연결 이벤트를 처리하기 위해 /eventhandler(Web PubSub SDK에서 제공하는 express 미들웨어로 수행됨)에서 REST API를 노출하려면 다음 코드로 server.js를 업데이트합니다.
const express = require("express");
const { WebPubSubServiceClient } = require("@azure/web-pubsub");
const { WebPubSubEventHandler } = require("@azure/web-pubsub-express");
const app = express();
const hubName = "Sample_ChatApp";
let serviceClient = new WebPubSubServiceClient(process.env.WebPubSubConnectionString, hubName);
let handler = new WebPubSubEventHandler(hubName, {
path: "/eventhandler",
onConnected: async (req) => {
console.log(`${req.context.userId} connected`);
},
handleUserEvent: async (req, res) => {
if (req.context.eventName === "message")
await serviceClient.sendToAll({
from: req.context.userId,
message: req.data,
});
res.success();
},
});
app.get("/negotiate", async (req, res) => {
let id = req.query.id;
if (!id) {
res.status(400).send("missing user id");
return;
}
let token = await serviceClient.getClientAccessToken({ userId: id });
res.json({
url: token.url,
});
});
app.use(express.static("public"));
app.use(handler.getMiddleware());
app.listen(8080, () => console.log("server started"));
위 코드에서 onConnected는 클라이언트가 연결될 때 콘솔에 메시지를 출력합니다. 연결된 클라이언트의 ID를 볼 수 있도록 req.context.userId를 사용하고 있습니다. 그리고 클라이언트가 메시지를 보낼 때 handleUserEvent가 호출됩니다. WebPubSubServiceClient.sendToAll()을 사용하여 JSON 개체의 메시지를 모든 클라이언트에 브로드캐스트합니다. handleUserEvent에도 이벤트 보낸 사람에게 메시지를 다시 보낼 수 있는 res 개체가 포함되어 있습니다. 여기서는 간단하게 res.success()를 호출하여 WebHook가 200을 반환하도록 하고 있습니다. 클라이언트에 아무것도 반환하지 않으려는 경우에도 이 호출이 필요합니다. 이 호출이 없으면 WebHook가 절대 반환되지 않고 클라이언트 연결이 닫힙니다.
지금은 Java로 직접 이벤트 처리기를 구현해야 합니다. 단계는 프로토콜 사양에 따라 진행되며 아래 목록에 설명되어 있습니다.
이벤트 처리기 경로에 대한 HTTP 처리기를 추가합니다. 여기서는 /eventhandler라고 하겠습니다.
먼저 남용 방지 OPTIONS 요청을 처리하고, 헤더에 WebHook-Request-Origin 헤더가 포함되어 있는지 확인하고, WebHook-Allowed-Origin 헤더를 반환하겠습니다. 간단한 데모 수행을 위해 여기서는 *를 반환하여 모든 원본을 허용하겠습니다.
그런 다음, 들어오는 요청이 우리가 예상하는 이벤트인지 확인하겠습니다. 이 데모에서는 connected 이벤트가 중요하다고 가정하겠습니다. 이 이벤트에는 ce-type 헤더가 azure.webpubsub.sys.connected로 포함됩니다. 조인된 이벤트를 모든 클라이언트에게 브로드캐스팅하여 누가 채팅방에 참여했는지 확인할 수 있도록 남용 보호 논리를 추가했습니다.
위의 코드에서는 클라이언트가 연결될 때 간단하게 콘솔에 메시지를 출력합니다. 연결된 클라이언트의 ID를 볼 수 있도록 ctx.header("ce-userId")를 사용하고 있습니다.
message 이벤트의 ce-type은 항상 azure.webpubsub.user.message입니다. 자세한 내용은 이벤트 메시지를 참조하세요. 메시지가 들어올 때 JSON 형식의 메시지를 연결된 모든 클라이언트에 브로드캐스트하는 메시지를 처리하도록 논리를 업데이트합니다.
// handle events: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#events
app.post("/eventhandler", ctx -> {
String event = ctx.header("ce-type");
if ("azure.webpubsub.sys.connected".equals(event)) {
String id = ctx.header("ce-userId");
System.out.println(id + " connected.");
} else if ("azure.webpubsub.user.message".equals(event)) {
String id = ctx.header("ce-userId");
String message = ctx.body();
Gson gson = new Gson();
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("from", id);
jsonObject.addProperty("message", message);
String messageToSend = gson.toJson(jsonObject);
service.sendToAll(messageToSend, WebPubSubContentType.APPLICATION_JSON);
}
ctx.status(200);
});
지금은 Python에서 직접 이벤트 처리기를 구현해야 합니다. 단계는 프로토콜 사양에 따라 진행되며 아래 목록에 설명되어 있습니다.
이벤트 처리기 경로에 대한 HTTP 처리기를 추가합니다. 여기서는 /eventhandler라고 하겠습니다.
먼저 남용 방지 OPTIONS 요청을 처리하고, 헤더에 WebHook-Request-Origin 헤더가 포함되어 있는지 확인하고, WebHook-Allowed-Origin 헤더를 반환하겠습니다. 간단한 데모 수행을 위해 여기서는 *를 반환하여 모든 원본을 허용하겠습니다.
# validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
@app.route('/eventhandler', methods=['OPTIONS'])
def handle_event():
if request.method == 'OPTIONS':
if request.headers.get('WebHook-Request-Origin'):
res = Response()
res.headers['WebHook-Allowed-Origin'] = '*'
res.status_code = 200
return res
그런 다음, 들어오는 요청이 우리가 예상하는 이벤트인지 확인하겠습니다. 이 데모에서는 connected 이벤트가 중요하다고 가정하겠습니다. 이 이벤트에는 ce-type 헤더가 azure.webpubsub.sys.connected로 포함됩니다. 남용 방지 뒤에 논리를 추가합니다.
위의 코드에서는 클라이언트가 연결될 때 간단하게 콘솔에 메시지를 출력합니다. 연결된 클라이언트의 ID를 볼 수 있도록 request.headers.get('ce-userid')를 사용하고 있습니다.
message 이벤트의 ce-type은 항상 azure.webpubsub.user.message입니다. 자세한 내용은 이벤트 메시지를 참조하세요. 메시지가 들어오면 연결된 모든 클라이언트에게 메시지를 브로드캐스트하는 메시지를 처리하도록 논리를 업데이트합니다.
@app.route('/eventhandler', methods=['POST', 'OPTIONS'])
def handle_event():
if request.method == 'OPTIONS':
if request.headers.get('WebHook-Request-Origin'):
res = Response()
res.headers['WebHook-Allowed-Origin'] = '*'
res.status_code = 200
return res
elif request.method == 'POST':
user_id = request.headers.get('ce-userid')
type = request.headers.get('ce-type')
if type == 'azure.webpubsub.sys.connected':
print(f"{user_id} connected")
return '', 204
elif type == 'azure.webpubsub.user.message':
# default uses JSON
service.send_to_all(message={
'from': user_id,
'message': request.data.decode('UTF-8')
})
# returned message is also received by the client
return {
'from': "system",
'message': "message handled by server"
}, 200
else:
return 'Bad Request', 400
웹 페이지 업데이트
이제 연결하고, 메시지를 보내고, 받은 메시지를 페이지에 표시하는 논리를 추가하도록 index.html을 업데이트하겠습니다.
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
<input id="message" placeholder="Type to chat...">
<div id="messages"></div>
<script>
(async function () {
let id = prompt('Please input your user name');
let res = await fetch(`/negotiate?id=${id}`);
let data = await res.json();
let ws = new WebSocket(data.url);
ws.onopen = () => console.log('connected');
let messages = document.querySelector('#messages');
ws.onmessage = event => {
let m = document.createElement('p');
let data = JSON.parse(event.data);
m.innerText = `[${data.type || ''}${data.from || ''}] ${data.message}`;
messages.appendChild(m);
};
let message = document.querySelector('#message');
message.addEventListener('keypress', e => {
if (e.charCode !== 13) return;
ws.send(message.value);
message.value = '';
});
})();
</script>
</body>
</html>
위 코드에서 브라우저의 네이티브 WebSocket API를 사용하여 연결하고 WebSocket.send()를 사용하여 메시지를 보내고 WebSocket.onmessage를 사용하여 수신된 메시지를 수신 대기하는 것을 볼 수 있습니다.
또한 클라이언트 SDK를 사용하여 서비스에 연결할 수도 있으며, 이를 통해 자동 다시 연결, 오류 처리 등의 기능을 활용할 수 있습니다.
이제 채팅이 작동하려면 한 단계만 남았습니다. Web PubSub 서비스에서 관심 있는 이벤트와 이벤트를 보낼 위치를 구성하겠습니다.
이벤트 처리기 설정
Web PubSub 서비스에 이벤트 처리기를 설정하여 서비스에 이벤트를 보낼 위치를 알려 줍니다.
웹 서버가 로컬로 실행될 때 인터넷에 액세스할 수 있는 엔드포인트가 없는 경우 Web PubSub 서비스가 localhost를 호출하려면 어떻게 해야 하나요? 일반적으로 두 가지 방법이 있습니다. 하나는 일반 터널 도구를 사용하여 localhost를 공개적으로 노출하는 것이고, 다른 하나는 awps-tunnel을 사용하여 도구를 통해 Web PubSub 서비스의 트래픽을 로컬 서버로 터널링하는 것입니다.
이 섹션에서는 Azure CLI를 사용하여 이벤트 처리기를 설정하고 awps-tunnel을 사용하여 트래픽을 localhost로 라우팅합니다.
허브 설정 구성
Web PubSub가 awps-tunnel의 터널 연결을 통해 메시지를 라우팅하도록 tunnel 체계를 사용하도록 URL 템플릿을 설정합니다. 이 문서에 설명된 대로 포털 또는 CLI에서 이벤트 처리기를 설정할 수 있습니다. 여기서는 CLI를 통해 설정합니다. 이전 단계에서 설정한 대로 /eventhandler 경로의 이벤트를 수신 대기하므로 URL 템플릿을 tunnel:///eventhandler로 설정합니다.
http://localhost:8080/index.html을(를) 여십시오. 사용자 이름을 입력하고 채팅을 시작할 수 있습니다.
connect 이벤트 처리기를 사용한 지연 인증
이전 섹션에서는 협상 엔드포인트를 사용하여 클라이언트가 Web PubSub 서비스에 연결하기 위한 Web PubSub 서비스 URL과 JWT 액세스 토큰을 반환하는 방법을 보여 주었습니다. 예를 들어, 리소스가 제한된 에지 디바이스와 같은 경우 클라이언트는 Web PubSub 리소스에 직접 연결하는 것을 선호할 수 있습니다. 이러한 경우 클라이언트 인증을 지연하도록 connect 이벤트 처리기를 구성하고, 클라이언트에 사용자 ID를 할당하고, 클라이언트가 조인되면 클라이언트가 조인하는 그룹을 할당하고, 클라이언트가 갖는 권한과 클라이언트에 대한 WebSocket 응답으로 WebSocket 하위 프로토콜을 구성하는 등의 작업을 수행할 수 있습니다. 자세한 내용은 연결 이벤트 처리기 사양을 참조하세요.
이제 이벤트 처리기를 사용하여 connect 협상 섹션이 수행하는 것과 유사한 작업을 수행해 보겠습니다.
허브 설정 업데이트
먼저 connect 이벤트 처리기도 포함하도록 허브 설정을 업데이트하겠습니다. JWT 액세스 토큰이 없는 클라이언트가 서비스에 연결할 수 있도록 익명 연결도 허용해야 합니다.
이제 연결 이벤트 azure.webpubsub.sys.connect를 처리하는 논리를 추가하겠습니다.
// validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
app.options("/eventhandler", ctx -> {
ctx.header("WebHook-Allowed-Origin", "*");
});
// handle events: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#connect
app.post("/eventhandler", ctx -> {
String event = ctx.header("ce-type");
if ("azure.webpubsub.sys.connect".equals(event)) {
String body = ctx.body();
System.out.println("Reading from request body...");
Gson gson = new Gson();
JsonObject requestBody = gson.fromJson(body, JsonObject.class); // Parse JSON request body
JsonObject query = requestBody.getAsJsonObject("query");
if (query != null) {
System.out.println("Reading from request body query:" + query.toString());
JsonElement idElement = query.get("id");
if (idElement != null) {
JsonArray idInQuery = query.get("id").getAsJsonArray();
if (idInQuery != null && idInQuery.size() > 0) {
String id = idInQuery.get(0).getAsString();
ctx.contentType("application/json");
Gson response = new Gson();
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("userId", id);
ctx.result(response.toJson(jsonObject));
return;
}
}
} else {
System.out.println("No query found from request body.");
}
ctx.status(401).result("missing user id");
} else if ("azure.webpubsub.sys.connected".equals(event)) {
String id = ctx.header("ce-userId");
System.out.println(id + " connected.");
ctx.status(200);
} else if ("azure.webpubsub.user.message".equals(event)) {
String id = ctx.header("ce-userId");
String message = ctx.body();
service.sendToAll(String.format("{\"from\":\"%s\",\"message\":\"%s\"}", id, message), WebPubSubContentType.APPLICATION_JSON);
ctx.status(200);
}
});
이제 ce-type 헤더를 azure.webpubsub.sys.connect로 포함해야 하는 시스템 connect 이벤트를 처리하겠습니다. 남용 방지 뒤에 논리를 추가합니다.
@app.route('/eventhandler', methods=['POST', 'OPTIONS'])
def handle_event():
if request.method == 'OPTIONS' or request.method == 'GET':
if request.headers.get('WebHook-Request-Origin'):
res = Response()
res.headers['WebHook-Allowed-Origin'] = '*'
res.status_code = 200
return res
elif request.method == 'POST':
user_id = request.headers.get('ce-userid')
type = request.headers.get('ce-type')
print("Received event of type:", type)
# Sample connect logic if connect event handler is configured
if type == 'azure.webpubsub.sys.connect':
body = request.data.decode('utf-8')
print("Reading from connect request body...")
query = json.loads(body)['query']
print("Reading from request body query:", query)
id_element = query.get('id')
user_id = id_element[0] if id_element else None
if user_id:
return {'userId': user_id}, 200
return 'missing user id', 401
elif type == 'azure.webpubsub.sys.connected':
return user_id + ' connected', 200
elif type == 'azure.webpubsub.user.message':
service.send_to_all(content_type="application/json", message={
'from': user_id,
'message': request.data.decode('UTF-8')
})
return Response(status=204, content_type='text/plain')
else:
return 'Bad Request', 400
직접 연결하려면 index.html을 업데이트합니다.
이제 Web PubSub 서비스에 직접 연결하도록 웹 페이지를 업데이트하겠습니다. 한 가지 언급할 점은 이제 데모 목적으로 Web PubSub 서비스 엔드포인트가 클라이언트 코드에 하드 코딩되어 있다는 것입니다. 아래 HTML의 서비스 호스트 이름 <the host name of your service>를 자체 서비스의 값으로 업데이트하세요. 서버에서 Web PubSub 서비스 엔드포인트 값을 가져오는 것이 여전히 유용할 수 있으며, 클라이언트가 연결되는 위치에 더 많은 유연성과 제어 가능성을 제공합니다.
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
<input id="message" placeholder="Type to chat...">
<div id="messages"></div>
<script>
(async function () {
// sample host: mock.webpubsub.azure.com
let hostname = "<the host name of your service>";
let id = prompt('Please input your user name');
let ws = new WebSocket(`wss://${hostname}/client/hubs/Sample_ChatApp?id=${id}`);
ws.onopen = () => console.log('connected');
let messages = document.querySelector('#messages');
ws.onmessage = event => {
let m = document.createElement('p');
let data = JSON.parse(event.data);
m.innerText = `[${data.type || ''}${data.from || ''}] ${data.message}`;
messages.appendChild(m);
};
let message = document.querySelector('#message');
message.addEventListener('keypress', e => {
if (e.charCode !== 13) return;
ws.send(message.value);
message.value = '';
});
})();
</script>
</body>
</html>