Quantcast
Channel: かずきのBlog@hatena
Viewing all articles
Browse latest Browse all 1387

TypeScript を使ってサーバーレスで WebSocket サーバーを作ってみた

$
0
0

先日 Microsoft Azure のサーバーレスプラットフォームの Azure Functions に SignalR Service bindings の一般提供開始のアナウンスがありました!

azure.microsoft.com

リポジトリはこちら。

github.com

ということで、これもまた先日 Azure Functions Core Tools でサポートされた TypeScript のプロジェクトテンプレートを使ってやってみたいと思います。

やる前に

Azure Functions の SignalR Service bindings にはクライアントに接続のための情報を返すための機能と、繋いでいるクライアントへメッセージを送信するための出力バインディングがあります。

つまり、Azure Functions 側から繋ぎに来ているクライアントにメッセージをブロードキャストすることは出来ますが、クライアントからの SignalR での通信を受けることは出来ないっぽいようです。 クライアントから Azure Functions へは HttpTrigger あたりを使った普通の REST API 呼び出しを使う感じですね。

プロジェクト作成

Azure Functions の拡張機能を入れた Visual Studio Code をベースに作業をしていきたいと思います。 ということでさくっと Create New Project...で Azure Functions のプロジェクトを作成します。

f:id:okazuki:20190309111307p:plain

言語は TypeScript を選ぼう

f:id:okazuki:20190309111249p:plain

そして以下のコマンドを実行して SignalR 用の拡張をインストールします。

func extensions install --package Microsoft.Azure.WebJobs.Extensions.SignalRService --version 1.0.0

SignalR 対応をしていこう

SignalR 接続情報入力バインドというものを作ってクライアントに Azure の SignalR Service に接続するための情報を返してあげます。とりあえず HttpTrigger の関数を作って SignalR の接続情報を返す処理を追加します。名前は negotiateじゃないとダメみたいです。

func newコマンドか Visual Studio Code の関数を作成するボタン(プロジェクトを作った時に押したボタンの横にある)で HttpTrigger の関数を作ります。

作成した関数の function.jsonsignalRConnctionInfoのバインドを追加します。

{"bindings": [{"authLevel": "anonymous",
      "type": "httpTrigger",
      "direction": "in",
      "name": "req",
      "methods": ["get",
        "post"
      ]},
    {"type": "signalRConnectionInfo",
      "name": "connectionInfo",
      "hubName": "chat",
      "connectionStringSetting": "SignalRConnection",
      "direction": "in"
    },
    {"type": "http",
      "direction": "out",
      "name": "res"
    }],
  "scriptFile": "..\\dist\\negotiate\\index.js"
}

関数の中では入力として受け取った接続情報を単純に HTTP のボディにセットして返します。

import{ AzureFunction, Context, HttpRequest }from"@azure/functions"const httpTrigger: AzureFunction =asyncfunction(context: Context, req: HttpRequest, connectionInfo: any): Promise<void>{
    context.res ={ body: connectionInfo };};exportdefault httpTrigger;

これでクライアントから繋ぐことができるようになったので、続けて繋いできたクライアントにメッセージを投げようと思います。それ用の関数として PostMessage という名前の HttpTrigger 関数を作成します。

接続してきているクライアントにメッセージを投げるには SignalR 用の出力バインディングを関数に追加してやれば良さそう。 ということで PostMessage 関数の function.json に signalr の出力バインディングの定義を追加します。

{"bindings": [{"authLevel": "anonymous","type": "httpTrigger","direction": "in","name": "req","methods": ["get","post"]},{"type": "signalR","name": "signalRMessages","hubName": "chat","connectionStringSetting": "SignalRConnection","direction": "out"},{"type": "http","direction": "out","name": "res"}],"scriptFile": "..\\dist\\PostMessage\\index.js"}

出力バインディングに渡す C# のオブジェクトを見てみると userId, groupName, target, argumentsを渡してやればよさそう。 targetarugmentsが必須での折はオプションみたいです。ということで、このようなインターフェースを TypeScript で定義しました。

exportinterface SignalRMessage {
    userId?: string
    groupName?: string
    target: stringarguments: {[key:string]: any}}

そして PostMessage 関数で適当にリクエストの body をそのままクライアントに渡すように作成しました。

import{ AzureFunction, Context, HttpRequest }from"@azure/functions"import{ SignalRMessage }from"../signalr/signalrmessage"const httpTrigger: AzureFunction =asyncfunction(context: Context, 
    req: HttpRequest): Promise<void>{
    context.log(JSON.stringify(req.body));
    context.bindings.signalRMessages =newArray<SignalRMessage>({
        target: "receiveMessage",arguments: [
            req.body,],});};exportdefault httpTrigger;

テスト

ここまで出来たらテストしてみます。Azure に SignalR Service を作成しましょう。 大量のデータはさばけないけど開発用には Free プランで作っておけばよさそうです。エミュレーターとかあればもうちょっと捗りそうだけど、まぁ仕方ないのかな。

作成した SignalR Service の Keys を見ると接続文字列があるのでコピーします。

f:id:okazuki:20190309134201p:plain

ローカル用のアプリケーション設定を書く local.settings.jsonを開いて function.jsonで使ってた SignalRConnectionという名前で先ほどの接続文字列を追加します。

{"IsEncrypted": false,
  "Values": {"AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "SignalRConnection": "ここに接続文字列を張り付け"
  }}

テスト用クライアントを作成します。クライアントは何でもいいのですが、今回は Vue.js で試してみようと思います。因みに現時点では JavaScript, Java, C# あたりに対応しています。Swift とかはプレビューみたいです。

docs.microsoft.com

vue の cli を入れてる状態で vue create vue-signalr-clientといった感じで任意の場所に TypeScript のプロジェクトを作ります。 プロジェクトを作ったら npm i @aspnet/signalrで SignalR を追加します。

src/components/HelloWorld.vueをいじっていきます。

<template><div><div><input type="text" v-model="message" /><button @click="onPostMessageClick">PostMessage</button></div><div><div :key="index" v-for="(chatMessage, index) in chatMessages">{{ chatMessage }}</div></div></div></template><script lang="ts">import{ Component, Prop, Vue }from'vue-property-decorator';import{ HubConnection, HubConnectionBuilder, LogLevel, JsonHubProtocol }from'@aspnet/signalr';@Componentexportdefaultclass HelloWorld extends Vue {public message: string='';public chatMessages: string[]=[];private connection!: HubConnection;publicasync onPostMessageClick(): Promise<void>{if(this.connection ==null){return;}await fetch('http://localhost:7071/api/PostMessage',{
      method: 'POST',
      body: JSON.stringify({ text: this.message }),});this.message ='';}publicasync created(): Promise<void>{
    console.log('created called');this.connection =new HubConnectionBuilder()
      .withUrl('http://localhost:7071/api')
      .configureLogging(LogLevel.Information)
      .build();this.connection.on('receiveMessage',(message)=>{
      console.log(JSON.stringify(message));this.chatMessages.push(message.text);});awaitthis.connection.start();
    console.log('created finished: ' + this.connection);}}</script><!-- Add "scoped" attribute to limit CSS to this component only --><style scoped></style>

created 関数で SignalR の接続を作成しています。HubConnectionBuilderwithUrlメソッドで Azure Functions の URL の api の部分までを渡します。そうすると勝手に negotiate関数を探しに行って接続を確立してくれます。

本番では、これだけじゃなくて再接続処理も入れないといけないっぽいですね。

ASP.NET Core SignalR JavaScript クライアント | Microsoft Docs

CORS の設定

さて、Vue のプロジェクトを実行すると http://localhost:8080で起動するので Azure Functions のローカルのランタイムに CORS の設定をしてやります。 local.settings.jsonを開いて以下のように CORS の設定を追加します。

{"IsEncrypted": false,
  "Values": {"AzureWebJobsStorage": "",
    "FUNCTIONS_WORKER_RUNTIME": "node",
    "SignalRConnection": "Endpoint=https://okazukisignalr.service.signalr.net;AccessKey=bH4HAffm8395oxBUyA2D3kBPYSOyhv0it1B5teCXkdA=;Version=1.0;"
  },
  "Host": {"CORS": "http://localhost:8080",
    "CORSCredentials": true}}

動かす

Azure Functions の方のプロジェクトを実行して、Vue のクライアント側を実行します。2つのブラウザーで Vue のクライアントのページを開いて動かしてみると…

動いた!

f:id:okazuki:20190309154432p:plain

クラウドにデプロイ

では Azure SignalR Service を作成したところに Function App を従量課金プランで作成します。 ランタイムスタックは今回の場合は JavaScript ですね。

Function App が作成されたら、アプリケーション設定を開いて SignalRConnection というキーで SignalR Service への接続文字列を追加しておきます。

追加したら func azure functionapp publish FunctionAppの名前と打ち込んでデプロイしましょう。

テスト用の Vue のアプリもデプロイしましょう。デプロイ先は Azure のほうに Storage Account を作って静的 Web サイトのオプションをオンにします。インデックスドキュメントは index.htmlにしておきましょう。

f:id:okazuki:20190309160039p:plain

HelloWorld.vueで SignalR の接続先が http://localhost:8080となっている部分を https://FunctionApp名.azurewebsites.netに変えます。そして、Vue のプロジェクトで npm run buildしてプロダクション用にビルドします。 dist フォルダーの中身を先ほど作ったストレージアカウントの BLOB の $web というコンテナにアップロードします。 Azure Storage Explorer 使うと楽です。

そして Function App の CORS の設定でストレージアカウントのプライマリエンドポイントの名前を設定します。

f:id:okazuki:20190309160817p:plain

ここまで完了すると以下のように Azure 上でも動くはずです。

f:id:okazuki:20190309160946p:plain

おまけ

.NET クライアントライブラリや Java クライアントライブラリもあるので…

docs.microsoft.com

docs.microsoft.com

こんな感じで Web ページと C#, Java あたりで作ったアプリで同じデータを受信するようにもできます。

f:id:okazuki:20190309174018p:plain

参考

docs.microsoft.com

ソースコード

GitHub にあげてます。因みに Azure のリソースは全て削除してあるので動かすときはソースコード内にハードコーディングされてる URL は書き換えてください。

github.com

最後に

今回は気分で TypeScript を使いましたが C#, Java とかでもサーバーサイドは書けます。

Durable Functions あたりと組み合わせるとオーケストレーター関数の進捗状況のプッシュ通知的なものに SignalR Service 使って通知とかできそうで面白そうですね。


Viewing all articles
Browse latest Browse all 1387

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>