Node.jsのsokect.ioを使ってのPUSH通知を作ってみた

こんにちは、wattak です。

50の手習いで Web アプリを作ってみよう、と色々と調べておりまして、自分の備忘録もかねて作ってみた実装をこちらに投稿しようと思います。
## 今回のプログラム
任意のタイミングでサーバーからクライアントへ PUSH する仕組みを持つ環境を Node.js を使って構築してみました。

なお PUSH の仕組みは記事のタグにも記載しましたが socket.io の仕組みを使っております。
## 処理概要
以下のようなイメージで構築しようと考えております。

1.クライアントから接続要求
2.サーバーにてクライアント情報を追加
3.クライアントから受付要求
4.サーバーにて登録処理して応答
5.以後、任意のタイミングでサーバーからクライアントへ PUSH 通知

「任意のタイミング」については PUSH 用に RestAPI を用意し API コールのタイミングで通知を行うことにしました。
## 処理イメージ
ポンチ画レベルのシーケンスで恐縮ですが1.～5.の処理イメージを描いてみました。
![qiita_image.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/514729/a70f7bb1-6297-5a1b-c3a8-fe0b7590c661.jpeg)


## ソース
### サーバー側
サーバー側のソースです。
いずれも 55555 ポートにて接続できるようにしました。
※処理イメージには記載しませんでしたが、一覧メンバーを取得する「get_member」というAPIも実装しています。

```javascript:server.js
const express = require('express') ;
const { createServer } = require('http') ;
const { Server } = require('socket.io') ;

const app = express() ;
const httpServer = createServer(app) ;
const io = new Server(httpServer) ;
var count = 0 ;

// JSON を受け取れる仕掛け構築
app.use(express.urlencoded({extended: true})) ;
app.use(express.json()) ;

const MEMBER = {} ;
// 以下を想定
// {
//	"socket.id" : {name:"user1", count:"1"},
//	"socket.id" : {name:"user2", count:"2"}
// }

// do_push（Internal RestAPI）
app.post('/do_push', (req, res) => {
	if (Number(req.body.count) === 0) {
		// 全メンバーへPUSH
		io.emit('push', req.body.message) ;
	} else {
		socketid = getIdFromCount(req.body.count) ;
		if (socketid === null) {
			console.log('not found : ' + req.body.count) ;
			console.log(MEMBER) ;
		} else {
			// 見つけたメンバーへPUSH通知
			io.to(socketid).emit('push', req.body.message) ;
		}
	}
	res.sendStatus(200) ;
}) ;
// メンバー一覧取得
app.get('/get_member', (req, res) => {
	// MEMBERをJSONで応答
	res.status(200).json(MEMBER) ;
}) ;

// socket.io 接続イベント
io.on('connection', (socket) => {
	// socket.id でメンバー登録
	MEMBER[socket.id] = {
		name:null, count:0
	} ;
	// クライアントからの切断イベント
	socket.on('disconnect', (reason) => {
		// 当該メンバーの削除
		delete MEMBER[socket.id] ;
	}) ;
	// クライアントからの受付イベント
	socket.on('reception', (data) => {
		// TODO: 既に接続済みであれば切断する
		// TODO: 同時アクセス時の count の排他制御の検討が必要
		// 登録名を更新
		count = count + 1 ;
		MEMBER[socket.id].name = data.name ;
		MEMBER[socket.id].count = count ;
		// 受付結果をPUSH通知
		io.to(socket.id).emit('recept_result', { result : true }) ;
	}) ;
}) ;

function getIdFromCount(count) {
	for( let key in MEMBER ) {
		if (Number(MEMBER[key].count) === Number(count)) {
			return key ;
		}
	}
	return null ;
}

// listen
httpServer.listen(55555, () => {
	console.log('Start. port on 55555.') ;
}) ;
```

### クライアント側ソース
クライアント側のソースになります。
```javascript:client.js
var client = require('socket.io-client') ;
const URL = 'http://サーバーIPアドレス:55555' ;
const username = process.argv[2] ;

console.log('try to connect as ' + username + '.') ;

// 接続
var socket = client.connect(URL) ;
socket.on('connect', () => {
	// 接続時に受付へ
	socket.emit('reception', {name : username}) ;
}) ;
// 受付結果受信イベント
socket.on('recept_result', (data) => {
	console.log('Reception result : ' + data.result) ;
}) ;
// PUSH通知
socket.on('push', (msg) => {
	console.log('Receive : ' + msg) ;
}) ;
// サーバーから切断で終了
socket.on('disconnect', (reason) => {
	process.exit(0) ;
}) ;
```

### do_pushサンプル
do_push を叩くサンプルの JavaScript は下記です。
```javascript:push.js
var request = require('request') ;
var options = {
	uri: "http://サーバーIPアドレス:55555/do_push",
	headers: {
		"Content-type": "application/json"
	},
	json:{
		"count": null,
		"message": null
	}
} ;
options.json.count = process.argv[2] ;
options.json.message = process.argv[3] ;
request.post(options, (error, res, body) => {
}) ;
```

### 動作確認
サーバーは下記のようにして起動するとメッセージが出ます。
```
$ node server.js
Start. port on 55555.
```
その後、クライアント接続を下記のようにして起動するとメッセージが出て接続に行きます。
```
$ node client.js UserName1
try to connect as UserName1
```
いくつかクライアントを接続しておいた状態で、do_push を行いますが、サンプルは
```
$ node push.js 1 'Test.'
```
とすると、最初に接続したクライアントに
```
Receive : Test.
```
と出るはずです。
すべてのクライアントへの通知を行いたい場合は
```
$ node push.js 0 'Message to ALL.'
```
とやるとすべてのクライアントにて、
```
Receive : Message to ALL.
```
と出るはずです。
### 終了
終了はサーバーにて Ctrl+C するとすべての接続しているクライアントも終了します。
クライアント側の単独の終了はクライアント側にで Ctrl+C すると終了します。
その際、サーバー側は MEMBER 配列から当該データを delete するようにしております。

## おわりに
ユーザのなりすまし対策等の TODO がありますが、基本的な動きは構築出来ました。
また TIPS 的なものが出来次第、投稿しようと思います。

## 参考文献
[socket.io Documentation](https://socket.io/docs/v4/)
[はじめてのSocket.io #3 チャット編「ユーザー間でのなりすましを防ぐ」](https://blog.katsubemakito.net/nodejs/socketio/realtime-chat3)