메시지 게시 및 구독 자습서에서는 Azure Web PubSub를 통해 메시지를 게시하고 구독하는 기본 사항을 알아봅니다. 이 자습서에서는 Azure Web PubSub의 이벤트 시스템을 배우고 이를 사용하여 실시간 통신 기능을 갖춘 완전한 웹 애플리케이션을 빌드합니다.
이 자습서에서는 다음을 하는 방법을 알아볼 수 있습니다.
- Web PubSub 서비스 인스턴스 만들기
- Azure Web PubSub에 대한 이벤트 처리기 설정 구성
- 앱 서버에서 이벤트 처리 및 실시간 채팅 앱 빌드
Azure 계정이 없는 경우 시작하기 전에 체험 계정을 만듭니다.
필수 조건
- 이렇게 설정하려면 버전 2.22.0 이상의 Azure CLI가 필요합니다. Azure Cloud Shell을 사용하는 경우 최신 버전이 이미 설치되어 있습니다.
Azure Web PubSub 인스턴스 만들기
리소스 그룹 만들기
리소스 그룹은 Azure 리소스가 배포 및 관리되는 논리적 컨테이너입니다.
az group create 명령을 사용하여 myResourceGroup
위치에서 eastus
이라는 리소스 그룹을 만듭니다.
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 리소스를 만듭니다.
중요합니다
각 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 가져오기
중요합니다
원시 연결 문자열 데모용으로만 이 문서에 표시됩니다.
연결 문자열에는 애플리케이션이 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으로 바꿉니다.
export WebPubSubConnectionString="<connection-string>"
SET WebPubSubConnectionString=<connection-string>
프로젝트 설정
필수 조건
애플리케이션 만들기
Azure Web PubSub에는 서버 역할과 클라이언트 역할이 있습니다. 이 개념은 웹 애플리케이션의 서버 역할 및 클라이언트 역할과 비슷합니다. 서버는 클라이언트를 관리하고, 클라이언트 메시지를 수신 대기하고, 응답해야 합니다. 클라이언트는 사용자 메시지를 보내고 서버에서 메시지를 받고 최종 사용자를 위해 메시지를 시각화해야 합니다.
이 자습서에서는 실시간 채팅 웹 애플리케이션을 빌드합니다. 실제 웹 애플리케이션에서는 클라이언트를 인증하고 애플리케이션 UI에 대한 정적 웹 페이지를 제공하는 것도 서버의 역할에 포함됩니다.
ASP.NET Core 8을 사용하여 웹 페이지를 호스트하고 들어오는 요청을 처리합니다.
먼저 chatapp
폴더에 ASP.NET Core 웹앱을 만들겠습니다.
새 웹앱을 만듭니다.
mkdir chatapp
cd chatapp
dotnet new web
정적 웹 페이지 호스팅을 지원하려면 app.UseStaticFiles()
Program.cs를 추가합니다.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.UseStaticFiles();
app.Run();
HTML 파일을 만들고 wwwroot/index.html
로 저장합니다. 나중에 채팅 앱의 UI에 사용할 것입니다.
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
</body>
</html>
dotnet run --urls http://localhost:8080
을 실행하여 서버를 테스트하고 브라우저에서 http://localhost:8080/index.html
에 액세스할 수 있습니다.
널리 사용되는 Node.js용 웹 프레임워크인 express.js를 사용하여 웹 페이지를 호스팅하고 수신 요청을 처리합니다.
먼저 chatapp
폴더에 Express 웹앱을 만들겠습니다.
express.js를 설치합니다.
mkdir chatapp
cd chatapp
npm init -y
npm install --save express
그런 다음, express 서버를 만들고 server.js
로 저장합니다.
const express = require('express');
const app = express();
app.use(express.static('public'));
app.listen(8080, () => console.log('server started'));
또한 HTML 파일을 만들고 public/index.html
로 저장합니다. 나중에 채팅 앱의 UI에 사용할 것입니다.
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
</body>
</html>
node server
을 실행하여 서버를 테스트하고 브라우저에서 http://localhost:8080
에 액세스할 수 있습니다.
Javalin 웹 프레임워크를 사용하여 웹 페이지를 호스트하고 들어오는 요청을 처리합니다.
Maven을 사용하여 새 앱 webpubsub-tutorial-chat
를 만들고 webpubsub-tutorial-chat 폴더로 전환합니다.
mvn archetype:generate --define interactiveMode=n --define groupId=com.webpubsub.tutorial --define artifactId=webpubsub-tutorial-chat --define archetypeArtifactId=maven-archetype-quickstart --define archetypeVersion=1.4
cd webpubsub-tutorial-chat
javalin
의 dependencies
노드에 pom.xml
웹 프레임워크 종속성을 추가합니다.
-
javalin
: 간단한 Java용 간단한 웹 프레임워크
-
slf4j-simple
: Java용 로거
<!-- https://mvnrepository.com/artifact/io.javalin/javalin -->
<dependency>
<groupId>io.javalin</groupId>
<artifactId>javalin</artifactId>
<version>6.1.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-simple -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.12</version>
</dependency>
/src/main/java/com/webpubsub/tutorial 디렉터리로 이동합니다. 편집기에서 App.java 파일을 엽니다. 정적 파일을 제공하려면 Javalin.create
를 사용합니다.
package com.webpubsub.tutorial;
import io.javalin.Javalin;
public class App {
public static void main(String[] args) {
// start a server
Javalin app = Javalin.create(config -> {
config.staticFiles.add("public");
}).start(8080);
}
}
설정에 따라 언어 수준을 Java 8로 명시적으로 설정해야 할 수도 있습니다. 이 단계는 pom.xml에서 수행할 수 있습니다. 다음 코드 조각을 추가합니다.
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
HTML 파일을 만들고 /src/main/resources/public/index.html에 저장합니다. 나중에 채팅 앱의 UI에 사용됩니다.
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
</body>
</html>
pom.xml 파일이 들어 있는 디렉터리에서 다음 명령을 실행하고, 브라우저에서 http://localhost:8080
에 액세스하여 서버를 테스트할 수 있습니다.
mvn compile & mvn package & mvn exec:java -Dexec.mainClass="com.webpubsub.tutorial.App" -Dexec.cleanupDaemonThreads=false
이 작업을 수행하기 위해 널리 사용되는 Python용 웹 프레임워크인 Flask를 사용합니다.
먼저 Flask가 준비된 프로젝트 폴더 chatapp
를 만듭니다.
환경 만들기 및 활성화
mkdir chatapp
cd chatapp
python3 -m venv .venv
. .venv/bin/activate
활성화된 환경 내에서 Flask를 설치합니다.
pip install Flask
그런 다음 Flask 서버를 만들고 이를 server.py
로 저장합니다.
from flask import (
Flask,
send_from_directory,
)
app = Flask(__name__)
@app.route('/<path:filename>')
def index(filename):
return send_from_directory('public', filename)
if __name__ == '__main__':
app.run(port=8080)
또한 HTML 파일을 만들고 public/index.html
로 저장합니다. 나중에 채팅 앱의 UI에 사용할 것입니다.
<html>
<body>
<h1>Azure Web PubSub Chat</h1>
</body>
</html>
브라우저에서 python server.py
를 실행하고 http://127.0.0.1:8080/index.html에 액세스하여 서버를 테스트할 수 있습니다.
협상 엔드포인트 추가
메시지 게시 및 구독 자습서에서 구독자는 연결 문자열을 직접 사용합니다. 실제 애플리케이션에서는 연결 문자열이 서비스에 대한 모든 작업을 수행할 수 있는 높은 권한을 갖기 때문에 클라이언트와 연결 문자열을 공유하는 것은 안전하지 않습니다. 이제 서버가 연결 문자열을 사용하고 클라이언트가 액세스 토큰이 포함된 전체 URL을 가져올 수 있도록 negotiate
엔드포인트를 노출하도록 하겠습니다. 이러한 방식으로 서버는 무단 액세스를 방지하기 위해 negotiate
엔드포인트 앞에 권한 부여 미들웨어를 추가할 수 있습니다.
먼저 종속성을 설치합니다.
dotnet add package Microsoft.Azure.WebPubSub.AspNetCore
이제 클라이언트가 토큰을 생성하기 위해 호출할 /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을 dependencies
의 pom.xml
노드에 추가합니다.
<!-- https://mvnrepository.com/artifact/com.azure/azure-messaging-webpubsub -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-messaging-webpubsub</artifactId>
<version>1.2.12</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
이제 토큰을 생성하기 위해 /negotiate
파일에 App.java
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;
});
}
}
이 토큰 생성 코드는 토큰을 생성할 때 메서드를 호출하여 사용자 ID를 설정하는 것을 제외하고 setUserId
에서 사용한 코드와 비슷합니다. 사용자 ID를 사용하여 클라이언트 ID를 식별할 수 있으므로 메시지를 받을 때 어디서 오는 메시지인지 알 수 있습니다.
이 코드는 WebPubSubConnectionString
에서 설정한 환경 변수 에서 연결 문자열을 읽습니다.
다음 명령을 사용하여 서버를 다시 실행합니다.
mvn compile & mvn package & mvn exec:java -Dexec.mainClass="com.webpubsub.tutorial.App" -Dexec.cleanupDaemonThreads=false
먼저 Azure Web PubSub SDK를 설치합니다.
pip install azure-messaging-webpubsubservice
이제 토큰을 생성하기 위해 서버에 /negotiate
API를 추가하겠습니다.
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를 호출합니다.
Azure Web PubSub는 CloudEvents에 따라 이벤트 데이터를 설명합니다.
아래에서는 클라이언트가 연결될 때 connected
시스템 이벤트를 처리하고 클라이언트가 채팅 앱을 빌드하기 위해 메시지를 보낼 때 message
사용자 이벤트를 처리합니다.
이전 단계에서 설치한 AspNetCore용 Web PubSub SDK Microsoft.Azure.WebPubSub.AspNetCore
도 CloudEvents 요청을 구문 분석하고 처리하는 데 도움이 될 수 있습니다.
먼저, app.Run()
앞에 이벤트 처리기를 추가합니다. 이벤트에 대한 엔드포인트 경로를 지정합니다. 여기서는 /eventhandler
로 지정하겠습니다.
app.MapWebPubSubHub<Sample_ChatApp>("/eventhandler/{*path}");
app.Run();
이제 이전 단계에서 만든 Sample_ChatApp
클래스 내에 Web PubSub 서비스를 호출하는 데 사용하는 WebPubSubServiceClient<Sample_ChatApp>
와 작동하는 생성자를 추가합니다. 그리고 OnConnectedAsync()
이벤트가 트리거될 때 응답하려면 connected
을, 클라이언트의 메시지를 처리하려면 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
헤더를 반환하겠습니다. 간단한 데모 수행을 위해 여기서는 *
를 반환하여 모든 원본을 허용하겠습니다.
// validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
app.options("/eventhandler", ctx -> {
ctx.header("WebHook-Allowed-Origin", "*");
});
그런 다음, 들어오는 요청이 우리가 예상하는 이벤트인지 확인하겠습니다. 이 데모에서는 connected
이벤트가 중요하다고 가정하겠습니다. 이 이벤트에는 ce-type
헤더가 azure.webpubsub.sys.connected
로 포함됩니다. 조인된 이벤트를 모든 클라이언트에게 브로드캐스팅하여 누가 채팅방에 참여했는지 확인할 수 있도록 남용 보호 논리를 추가했습니다.
// 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#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.");
}
ctx.status(200);
});
위의 코드에서는 클라이언트가 연결될 때 간단하게 콘솔에 메시지를 출력합니다. 연결된 클라이언트의 ID를 볼 수 있도록 ctx.header("ce-userId")
를 사용하고 있습니다.
ce-type
이벤트의 message
은 항상 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
로 포함됩니다. 남용 방지 뒤에 논리를 추가합니다.
# validation: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#protection
# handle events: https://learn.microsoft.com/azure/azure-web-pubsub/reference-cloud-events#events
@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')
if request.headers.get('ce-type') == 'azure.webpubsub.sys.connected':
return user_id + ' connected', 200
else:
return 'Not found', 404
위의 코드에서는 클라이언트가 연결될 때 간단하게 콘솔에 메시지를 출력합니다. 연결된 클라이언트의 ID를 볼 수 있도록 request.headers.get('ce-userid')
를 사용하고 있습니다.
ce-type
이벤트의 message
은 항상 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가 tunnel
의 터널 연결을 통해 메시지를 라우팅하도록 awps-tunnel
체계를 사용하도록 URL 템플릿을 설정합니다.
이 문서에 설명된 대로 포털 또는 CLI에서 이벤트 처리기를 설정할 수 있습니다. 여기서는 CLI를 통해 설정합니다. 이전 단계에서 설정한 대로 /eventhandler
경로의 이벤트를 수신 대기하므로 URL 템플릿을 tunnel:///eventhandler
로 설정합니다.
Azure CLI az webpubsub hub create 명령을 사용하여 Sample_ChatApp
허브에 대한 이벤트 처리기 설정을 만듭니다.
중요합니다
<your-unique-resource-name>을 이전 단계에서 만든 Web PubSub 리소스의 이름으로 바꿉니다.
az webpubsub hub create -n "<your-unique-resource-name>" -g "myResourceGroup" --hub-name "Sample_ChatApp" --event-handler url-template="tunnel:///eventhandler" user-event-pattern="*" system-event="connected"
awps-tunnel을 로컬에서 실행
awps-tunnel 다운로드 및 설치
이 도구는 Node.js 버전 16 이상에서 실행됩니다.
npm install -g @azure/web-pubsub-tunnel-tool
서비스 연결 문자열 사용 및 실행
export WebPubSubConnectionString="<your connection string>"
awps-tunnel run --hub Sample_ChatApp --upstream http://localhost:8080
웹 서버 실행
이제 모든 것이 설정되었습니다. 웹 서버를 실행하고 실제로 채팅 앱을 사용하겠습니다.
이제 dotnet run --urls http://localhost:8080
을 사용하여 서버를 실행합니다.
이 자습서의 전체 코드 샘플은 여기서 찾을 수 있습니다.
이제 node server
을 사용하여 서버를 실행합니다.
이 자습서의 전체 코드 샘플은 여기서 찾을 수 있습니다.
이제 아래 명령을 사용하여 서버를 실행합니다.
mvn compile & mvn package & mvn exec:java -Dexec.mainClass="com.webpubsub.tutorial.App" -Dexec.cleanupDaemonThreads=false
이 자습서의 전체 코드 샘플은 여기서 찾을 수 있습니다.
이제 python server.py
을 사용하여 서버를 실행합니다.
이 자습서의 전체 코드 샘플은 여기서 찾을 수 있습니다.
http://localhost:8080/index.html
을(를) 여십시오. 사용자 이름을 입력하고 채팅을 시작할 수 있습니다.
connect
이벤트 처리기를 사용한 지연 인증
이전 섹션에서는 협상 엔드포인트를 사용하여 클라이언트가 Web PubSub 서비스에 연결하기 위한 Web PubSub 서비스 URL과 JWT 액세스 토큰을 반환하는 방법을 보여 주었습니다. 예를 들어, 리소스가 제한된 에지 디바이스와 같은 경우 클라이언트는 Web PubSub 리소스에 직접 연결하는 것을 선호할 수 있습니다. 이러한 경우 클라이언트 인증을 지연하도록 connect
이벤트 처리기를 구성하고, 클라이언트에 사용자 ID를 할당하고, 클라이언트가 조인되면 클라이언트가 조인하는 그룹을 할당하고, 클라이언트가 갖는 권한과 클라이언트에 대한 WebSocket 응답으로 WebSocket 하위 프로토콜을 구성하는 등의 작업을 수행할 수 있습니다. 자세한 내용은 연결 이벤트 처리기 사양을 참조하세요.
이제 이벤트 처리기를 사용하여 connect
협상 섹션이 수행하는 것과 유사한 작업을 수행해 보겠습니다.
허브 설정 업데이트
먼저 connect
이벤트 처리기도 포함하도록 허브 설정을 업데이트하겠습니다. JWT 액세스 토큰이 없는 클라이언트가 서비스에 연결할 수 있도록 익명 연결도 허용해야 합니다.
Azure CLI az webpubsubhub update 명령을 사용하여 Sample_ChatApp
허브에 대한 이벤트 처리기 설정을 만듭니다.
중요합니다
<your-unique-resource-name>을 이전 단계에서 만든 Web PubSub 리소스의 이름으로 바꿉니다.
az webpubsub hub update -n "<your-unique-resource-name>" -g "myResourceGroup" --hub-name "Sample_ChatApp" --allow-anonymous true --event-handler url-template="tunnel:///eventhandler" user-event-pattern="*" system-event="connected" system-event="connect"
연결 이벤트를 처리하도록 업스트림 논리 업데이트
이제 연결 이벤트를 처리하도록 업스트림 논리를 업데이트하겠습니다. 협상 엔드포인트를 제거할 수도 있습니다.
데모 목적으로 엔드포인트 협상에서 수행하는 작업과 유사하게 쿼리 매개 변수에서 ID도 읽습니다. 연결 이벤트에서 원래 클라이언트 쿼리는 연결 이벤트 요청 본문에 유지됩니다.
Sample_ChatApp
클래스 내에서 OnConnectAsync()
를 재정의하여 connect
이벤트를 처리합니다.
sealed class Sample_ChatApp : WebPubSubHub
{
private readonly WebPubSubServiceClient<Sample_ChatApp> _serviceClient;
public Sample_ChatApp(WebPubSubServiceClient<Sample_ChatApp> serviceClient)
{
_serviceClient = serviceClient;
}
public override ValueTask<ConnectEventResponse> OnConnectAsync(ConnectEventRequest request, CancellationToken cancellationToken)
{
if (request.Query.TryGetValue("id", out var id))
{
return new ValueTask<ConnectEventResponse>(request.CreateResponse(userId: id.FirstOrDefault(), null, null, null));
}
// The SDK catches this exception and returns 401 to the caller
throw new UnauthorizedAccessException("Request missing id");
}
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();
}
}
클라이언트 연결 이벤트를 처리하도록 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",
handleConnect: async (req, res) => {
if (req.context.query.id){
res.success({ userId: req.context.query.id });
} else {
res.fail(401, "missing user id");
}
},
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.use(express.static("public"));
app.use(handler.getMiddleware());
app.listen(8080, () => console.log("server started"));
이제 연결 이벤트 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);
}
});
이제 connect
헤더를 ce-type
로 포함해야 하는 시스템 azure.webpubsub.sys.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>
서버를 다시 실행
이제 서버를 다시 실행하고 이전 지침에 따라 웹 페이지를 참조하세요.
awps-tunnel
을 중지한 경우 터널 도구를 다시 실행하시기 바랍니다.
다음 단계
이 자습서에서는 Azure Web PubSub 서비스에서 이벤트 시스템이 작동하는 방식에 대한 기본 개념을 제공합니다.
다른 자습서를 확인하여 서비스 사용 방법을 자세히 알아보세요.