D言語/vibe.dでWebSocketプログラミング

この記事は株式会社富士通システムズウェブテクノロジーが企画する[いのべこ夏休みアドベントカレンダー 2020](https://qiita.com/jugemsan/items/5761773308fb1df0c23a)の21日目の記事です。本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。

はじめに
===

趣味で使っているD言語でWebSocketを取り扱う話です。
D言語はマイナー（少なくとも私の周りで利用者を見かけない）なので、環境構築手順も載せました。

環境
===

利用するOSやツールのバージョンは以下の通りです。

||バージョン|
|---|---|
|OS|Windows 10 Pro 1909|
|コンパイラ|DMD 2.093.0|
|IDE|Visual Studio Community 2019 / Visual D 1.0.0|
|ブラウザ|Google Chrome 84.0.4147.135|


[D言語](https://dlang.org/)について
===

C/C++の流れを汲む言語で、ネイティブコードを生成します。詳細に興味があれば[公式サイト](https://dlang.org/)を見てみてください。
仕事でよく使うJava/.NET系に比べると標準ライブラリは少ないですが、機能がいろいろあって楽しい言語です。
この記事には文法の説明は含まれませんが、C++/Java/C#を知っていれば何となくサンプルコードが読めると思います。

[vibe.d](https://vibed.org/)について
===

D言語で実装されたウェブアプリケーションフレームワークです。
HTMLテンプレートやREST APIの機能もありますが、今回はWebSocketのみを扱います。
ThreadではなくFiberで処理を行うという特徴があります。


開発環境の構築
===

コーディングを行うための環境を構築します。
以下の順で進めます。

1. DMD(Digital Mars D compiler)のインストール
1. Visual Studio Community 2019と関連ツールのインストール
1. Visual D(Visual Studio向けD言語パッケージ)のインストール

## 1.DMDのインストール

[公式サイトのダウンロードページ](https://dlang.org/download.html)からDMDのインストーラー(exe版)をダウンロードして実行します。執筆時点では2.093.0が最新ですのでその前提で進めます。

以下、気を付けるポイントだけ明記します。

- Choose Componentsでは`Extras`の下の`Download Visual D`をチェックしないでください。DMD 2.093.0に付属のVisual Dは少し古くVisual Studio 2019に対応していませんので、後で最新版をインストールします。
<img width="499" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679067/c2597ca4-b2ca-95cc-3907-579d0da78697.png">

- Choose Visual Studio Installationでは`Download and install Visual Studio 2019`を選択してください。
<img width="499" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679067/3ebc56b5-6e16-fb3a-79f8-87dd447b1de8.png">

`Install`ボタンをクリックすると、DMDのインストールが始まるとともにVisual Studio Installerのダウンロードが始まります。

## 2.Visual Studio Community 2019と関連ツールのインストール

Visual Studio Installerのダウンロードが完了すると、インストール画面が表示されます。
そのままインストールするとツールが足りず、後々エラーが出るので、以下のように`C++ によるデスクトップ開発`を追加します。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679067/2e85765e-c055-da8d-5dd2-1e421e70fb84.png)

インストールが終わるとVisual Studio 2019が起動しますが、このままではD言語を扱うことはできませんので、終了させます。

## 3.Visual Dのインストール

[Visual Dのサイト](http://rainers.github.io/visuald/visuald/StartPage.html)からVisual Dのインストーラーをダウンロードします。`Download Visual D extension only`版をダウンロードして実行してください。

すでにVisual Studio 2019がインストールされているため、以下のように表示されるはずです。
<img width="499" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679067/fc8d67a3-f80c-01b7-331b-3b53714a69e4.png">

そのまま進めてインストールが完了すると、Visual Studio 2019で`Visual D`の機能を利用できるようになります。正常にインストールされていれば、Visual Studio 2019のメニューの`拡張機能`に`Visual D`が表示されます。
![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679067/952ba26b-0983-f37b-bc76-eecb836cf298.png)
これで環境構築は完了です。次はコーディングに向けてプロジェクトを作成します。

プロジェクトの作成
===

DMDに付属している[dub](https://dub.pm/getting_started)を利用してプロジェクトを作成します。`dub`はD言語の公式パッケージマネージャーです。

適当な場所にプロジェクト用のフォルダ(今回は`C:\myproject`)を作り、作業場所とします。
コマンドプロンプトを起動してフォルダに移動し、`dub init`を実行すると、対話モードになります。以下のように入力していきます。

|設定項目|入力|
|---|---|
|Package recipe format|sdl|
|Name|お好みで変更|
|Description|お好みで変更|
|Author name|お好みで変更|
|License|そのままEnter|
|Copyright string|お好みで変更|
|Add dependency|vibe-dを追加|

```
C:\myproject>dub init
Package recipe format (sdl/json) [json]: sdl
Name [myproject]:
Description [A minimal D application.]:
Author name [Nobuyuki]: godmyoh
License [proprietary]:
Copyright string [Copyright ﾂｩ 2020, godmyoh]:
Add dependency (leave empty to skip) []: vibe-d
Adding dependency vibe-d ~>0.9.0
Add dependency (leave empty to skip) []:
Successfully created an empty project in 'C:\myproject'.
Package successfully created in .

C:\myproject>
```
以下のように空のアプリケーションが生成されます。

```
myproject
│  .gitignore
│  dub.sdl
│
└─source
        app.d
```
`dub.sdl`には対話で入力した内容が設定値として保存されています。今後`dub`コマンドを実行すると`dub.sdl`の内容に沿って処理が行われます。

`app.d`の中を見ると、以下のように単純な`main`関数が生成されています。

```d:app.d
import std.stdio;

void main()
{
	writeln("Edit source/app.d to start your project.");
}
```

# 空のアプリケーションのビルドと実行

生成した空のアプリケーションを、コマンドラインとVisual Studioそれぞれからビルド、実行してみます。

## コマンドラインからのビルドと実行

`dub build`コマンドでビルドできます。

```
C:\myproject>dub build
Performing "debug" build using C:\D\dmd2\windows\bin\dmd.exe for x86_64.
taggedalgebraic 0.11.16: building configuration "library"...
eventcore 0.9.7: building configuration "winapi"...
stdx-allocator 2.77.5: building configuration "library"...
vibe-core 1.9.3: building configuration "winapi"...

(中略)

vibe-d 0.9.0: building configuration "vibe-core"...
myproject ~master: building configuration "application"...
Linking...
To force a rebuild of up-to-date targets, run again with --force.
Copying files for vibe-d:tls...

C:\myproject>
```

ビルドの過程で`vibe-d`及び`vibe-d`が依存するライブラリのソースコードがダウンロードされ、コンパイルされます。
ビルドの結果、`myproject.exe`が生成されていますので、実行してみます。

```
C:\myproject>myproject.exe
Edit source/app.d to start your project.

C:\myproject>
```

## Visual Studioからのビルドと実行

Visual Studioでプロジェクトを開くために、`.sln`等のファイルを生成します。

`dub generate visuald`コマンドで生成できます。

```
C:\myproject>dub generate visuald
Solution 'myproject.sln' generated.
Copying files for vibe-d:tls...

C:\myproject>
```

これでVisual Studioのソリューションファイル`myproject.sln`が生成されました。Visual Studio 2019で開くと以下のように表示されます。

<img width="387" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679067/64e897bc-1566-9554-f687-ace7a69b66c1.png">
`vibe-d`に依存するすべてのライブラリがプロジェクトとして表示されていますが、手を入れるのはmyprojectだけです。以下は、試しにソリューションエクスプローラーから`app.d`を開いたところです。
![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679067/bf0f4144-4900-bbb1-65a7-56e1600cb0cb.png)
ソリューションエクスプローラーからソリューションを右クリックして`ソリューションのビルド`を行うと、依存関係がすべてコンパイルされ、`myproject.exe`がビルドされます。
![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679067/cd13a806-38a4-fda9-1064-a2a7b90f7b42.png)
実行やデバッグも、一般的なVisual Studioの操作と同様に行えます。以下はブレークポイントを設定してデバッグ実行した場合です。
![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679067/32ea7f64-b6e9-4ea4-645a-c2205005cae0.png)

# WebSocketプログラミング

WebSocketを使ったプログラムを作成します。
実は[vibe-dの公式チュートリアル](https://vibed.org/blog/posts/a-scalable-chat-room-service-in-d)にも似たようなネタがあるのですが、気にせず書いていきます。

## WebSocketの受け口を作る

サーバ側でWebSocketの接続を待ち受けるための最低限の処理を実装します。
ポート`8080`のパス`/ws`でWebSocketの接続ができるよう、`vibe-d`の設定を行います。
WebSocketの処理の本体は`myWebSocketHandler`で、クライアントから送られてきたデータを単にログに出すだけです。

```d:app.d
module app;

import vibe.vibe;

void main()
{
	auto settings = new HTTPServerSettings;
	settings.port = 8080;

	auto router = new URLRouter;
	router.get("/ws", handleWebSockets(&myWebSocketHandler));

	listenHTTP(settings, router);
	runApplication();
}

void myWebSocketHandler(scope WebSocket socket)
{
	while (socket.waitForData)
	{
		auto received = socket.receiveText;

		logInfo("RECEIVED:" ~ received);
	}

	logInfo("DISCONNECTED");
}
```

これをビルドして実行します。`dub run`コマンドを使います。

```
C:\myproject>dub run
Performing "debug" build using C:\D\dmd2\windows\bin\dmd.exe for x86_64.

(中略)

Running .\myproject.exe
[main(----) INF] Listening for requests on http://[::]:8080/
[main(----) INF] Listening for requests on http://0.0.0.0:8080/
```

ちゃんと動いているはずですが、これだけでは動作確認ができないので、クライアントを実装します。

## WebSocketのクライアントを作る

クライアントはHTMLで簡単なものを作成します。
テキストボックスにメッセージを入力して送るだけです。

```html:client.html
<html>
<body bgcolor="#e0e0e0">
<script>
var ws = new WebSocket('ws://localhost:8080/ws');
</script>
<img src="https://dlang.org/images/d3.png">
<br><br>
<input type="text" id="message">
<br>
<button type="button" id="send" onclick="ws.send(document.getElementById('message').value)">Send</button>
<body>
</html>
```

見た目が寂しかったので、D言語の公式マスコットキャラクターである`D言語くん`/`D-man`の画像を埋め込みました。Chromeで表示すると以下のようになります。

<img width="181" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679067/5e6b4ab4-f874-e219-b3ae-5ec5f73202b8.png">
試しにhogehogeと入力してSendをクリックすると、期待通りコマンドプロンプト側にもhogehogeとログが表示されました。
<img width="181" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679067/bf9a336d-4d44-082d-dedf-ed8ef66c03fc.png">

```
[main(----) INF] Listening for requests on http://[::]:8080/
[main(----) INF] Listening for requests on http://0.0.0.0:8080/
[main(3oGb) INF] RECEIVED:hogehoge
```

## 双方向通信をする

WebSocketは双方向通信が可能なので、サーバ側からクライアント側に向けてメッセージを送ってみます。
ここでは簡単に、一定時間ごとにクライアントに対してメッセージを送ります。せっかく画面上に`D言語くん`がいますので、`D言語くん`がしゃべっているかのように見せかけて表示させます。

以下は`client.html`の修正版です。かなり雑ですが、`D言語くん`の右にテキストボックスを置いて、サーバからメッセージが来たらそこに表示するように修正します。

```html:client.html
<html>
<body bgcolor="#e0e0e0">
<script>
var ws = new WebSocket('ws://localhost:8080/ws');
// ↓追加:サーバからメッセージを受け取ったらテキストボックスに表示
ws.onmessage = (msg) => { document.getElementById('server_message').value = msg.data; };
</script>
<img src="https://dlang.org/images/d3.png">
<!-- ↓追加:D言語くんの右にテキストボックス(背景が透明＆枠無し)を置く -->
<input type="text" id="server_message" style="position:relative; top:-130px; background-color:transparent; border-style:none; width:300px; font-size:50px;">
<br><br>
<input type="text" id="message">
<br>
<button type="button" id="send" onclick="ws.send(document.getElementById('message').value)">Send</button>
<body>
</html>
```

サーバ側も処理を追加します。
クライアントからのメッセージと無関係なタイミングで処理を行うため、`vibe-d`の`Task`機能を利用します。

```d
module app;

import vibe.vibe;

void main()
{
	auto settings = new HTTPServerSettings;
	settings.port = 8080;

	auto router = new URLRouter;
	router.get("/ws", handleWebSockets(&myWebSocketHandler));

	// ↓追加:D言語くんをしゃべらせる処理をTaskとして実行
	runTask(&startDman);

	listenHTTP(settings, router);
	runApplication();
}

// ↓追加:接続済みのWebSocketの一覧を保持しておく配列
WebSocket[] sockets;

void myWebSocketHandler(scope WebSocket socket)
{
	// ↓追加:接続したWebSocketを配列に追加
	sockets ~= socket;

	while (socket.waitForData)
	{
		auto received = socket.receiveText;

		logInfo("RECEIVED:" ~ received);
	}

	logInfo("DISCONNECTED");

	// ↓追加:切断したWebSocketを配列から削除
	foreach (i, s; sockets)
	{
		if (s is socket)
		{
			sockets = sockets[0..i] ~ sockets[i+1..$];
			break;
		}
	}
}

// ↓追加:D言語くんをしゃべらせる処理
void startDman()
{
	// 接続中の全クライアントにメッセージを送る
	void send(string message)
	{
		foreach (s; sockets)
			s.send(message);
	}

	// 2秒間隔で "< Hello!" と "" を交互に送る
	while (true)
	{
		send("< Hello!");
		sleep(2.seconds);
		send("");
		sleep(2.seconds);
	}
}
```

`dub run`で最新版を実行し、`client.html`をF5でリフレッシュします。
<img width="352" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679067/148581e8-cb07-65b6-6810-b1c27feb0854.png">
画像だと分かりませんが、`< Hello!`というメッセージが2秒ごとに出たり消えたりしています。
これで、双方向通信ができました。

# 終わりに

ここまで読んでいただきありがとうございました。
記事の中ではほとんど紹介できませんでしたが、D言語は楽しい言語ですので興味があればぜひ調べてみてください。仲間募集中です。