テレビのクイズ番組のような催しを実現できるWebSocket + Node.jsのリアルタイム・クイズアプリを作ってみた


Node.js上で動くシンプルなクイズアプリケーションを作ってみました。WebSocketを使うことによってテレビのクイズ番組のような催しが手軽に実現できます。

実際のアプリケーションは下記サイトで試してみることができます。
<a href="https://qiita-quiz.herokuapp.com">デモ</a>

このクイズアプリの利用方法は下記の通りです。

- 出題者がクイズを出題するとそのクイズが回答者の画面に即座に飛び込んできます。
- 回答者は正解だと思う回答を選択します。すると出題者の画面では選択された回答がグラフとなってリアルタイムに更新されていきます。
- 出題者が正解を発表します。回答者の画面には正解・残念のいずれかが表示されます。

すべてのソースコードはGithubで公開しています。
https://github.com/nkjm/quiz

このアプリケーションのポイントは下記２点です。

- WebSocketを使って出題、回答、正解をリアルタイムに画面に反映させる。
- socket.ioの名前空間機能によって複数のクイズチャンネルが作成できる。

ではポイントとなるコードを見ていきましょう。

##（サーバー側）クイズチャンネルの作成

通常であれば下記のようにサーバー側でsocket.ioのインスタンスを作成後、connectionイベントにいくつかのハンドラーを登録していきます。

```javascript:/app.js
// Expressフレームワークのインスタンス作成
var express = require('express');
var app = express();

// HTTPサーバーインスタンス作成
var server = app.listen(process.env.PORT || 3000);

// socket.ioインスタンス作成
var io = require("socket.io")(server);

io.on("connection", function(socket){
	// ハンドラー登録
});
```

ただしこのソケットはすべてのユーザーで共有されることになります。今回はひとつのサーバー上で複数のクイズチャンネルを作成できるようにし、チャンネル毎にコミュニケーションを独立させる必要があります。

一つのサーバーで独立したコミュニケーションの空間を実現するにはsocket.ioが提供する名前空間機能が便利です。名前空間を利用するには下記のようにコードをカスタマイズします。

```javascript:/app.js
// socket.ioインスタンス作成
var io = require("socket.io")(server);

// 名前空間を作成
var quizChannel = io.of("/YOUR_NAMESPACE");

// 名前空間毎にハンドラーを登録
quizChannel.on("connection", function(socket){
	// ハンドラー登録
});
```
これでクイズの出題者は任意のクイズチャンネルを作成し、その中に閉じて回答者とクイズを楽しむことができます。

このクライアント側（出題者および回答者）のコードを作成していきます。クライアントはクイズの出題、そのクイズの受信、回答の提出、回答の受信、正解の発表、正解の受信といったコミュニケーションをおこなうことになります。

このクライアント同士のコミュニケーションを実現するにはサーバーがクライアントからのメッセージを他のクライアントに中継する処理が必要です。これを記述するのがハンドラーの部分になります。下記のようにイベントを受信したらそのまま同名前空間内のクライアントに中継して送信します。

```javascript:/app.js
quizChannel.on("connection", function(socket){
	// クイズの出題
	socket.on("quizPublished", function(quiz){
		quizChannel.emit("quizPublished", quiz);
	});
	
	// 回答の提出
	socket.on("answerSubmitted", function(answer){
		quizChannel.emit("answerSubmitted", answer);
	});
	
	// 正解の発表
	socket.on("correctAnswerGiven", function(option){
		quizChannel.emit("correctAnswerGiven", option);
	});
});
```

##（出題者・回答者側）クイズチャンネルへの接続

作成されたクイズチャンネル（名前空間）に参加するにはどうすればよいでしょうか？
通常だと下記のコードでソケットに接続することができます。

```javascript:/public/javascripts/quiz-host.js,quiz-guest.js
var connection = io("http://YOUR_HOSTNAME");
```

特定の名前空間に接続する場合はホスト名に続けて名前空間を指定すればOKです。

```javascript:/public/javascripts/quiz-host.js,quiz-guest.js
var connection = io("http://YOUR_HOSTNAME/YOUR_NAMESPACE");
```

クイズの出題者・回答者ともに上記のコードでクイズチャンネルに接続します。

##（出題者側）クイズの出題

ではクイズを出題するコードをみてみましょう。

```javascript:/public/javascripts/quiz-host.js
connection.emit('quizPublished', quiz);
```

とてもシンプル。クイズが出題されたよ、ということを知らせるquizPublishedイベントを指定し、送信するデータをquizオブジェクトにセットしてメッセージを送信します。quizオブジェクトは下記のようになっており、質問と選択肢が含まれています。

```javascript:quiz
{
	id: "YOUR_QUIZ_ID",
	question: "YOUR_QUESTION",
	optionList: [
		{id: "YOUR_OPTION_ID", option: "YOUR_OPTION"},
		{id: "YOUR_OPTION_ID", option: "YOUR_OPTION"}
	]
}
```


##（回答者側）クイズの受信

出題されたクイズはサーバーで中継されて全クライアントに送信されます。そのクイズを受信するのが下記のコードです。受信するメッセージとしてquizPublishedイベントを指定し、そのデータをコールバック関数の引数（quiz）にて受け取っています。

```javascript:/public/javascripts/quiz-guest.js
connection.on("quizPublished", function(quiz){
	// 受信したクイズをUIに表示
});
```

##（回答者側）回答の提出
回答の提出は、やり方はクイズ出題とまったく同じです。ただしイベントをanswerSubmittedにし、送信するデータにはanswerオブジェクトをセットしています。

```javascript:/public/javascripts/quiz-guest.js
connection.emit('answerSubmitted', answer);
```

answerオブジェクトには単に選択肢のIDが含まれています。

```javascript:answer
{
	optionId: "YOUR_OPTION_ID"
}
```

##（出題者側）回答の受信

回答の受信はクイズの受信とまったく同じです。もうsocket.ioを使ったメッセージの送受信は慣れてきましたね。

```javascript:/public/javascripts/quiz-host.js
connection.on("answerSubmitted", function(answer){
	// 受信した回答をグラフに反映
});
```

このように出題者と回答者はsocket.ioの構成の中ではどちらもクライアントであり、同じようにメッセージを送信し、受信することができます。

受信したいメッセージについてはあらかじめ上記のようにハンドラーを登録しておけばOKです。

##（出題者側）正解の発表

イベントにcorrectAnswerGivenを指定してメッセージを送信します。
送信するデータには正解となる選択肢のIDをセットしています。

```javascript:/public/javascripts/quiz-host.js
connection.emit('correctAnswerGiven', answer);
```

```javascript:answer
{
	optionId: "YOUR_CORRECT_OPTION_ID"
}
```

##（回答者側）正解の受信

回答者側ではcorrectAnswerGivenイベントのメッセージを受信して正解・不正解を表示します。

```javascript:/public/javascripts/quiz-guest.js
connection.on("correctAnswerGiven", function(answer){
	// 正解・不正解をUIに表示
});
```

これでsocket.ioを使ったWebSocketでのメッセージングについては主要な機能をカバーできました。あとはハンドラー部分でUIの制御を入れてあげればアプリとして動作してきます。

<a href="https://github.com/nkjm/quiz">サンプル</a>ではAngular.jsを使ってUIを構築しています。よければ参考にしてみてください。