PHPで作ったサイトでSocket.ioを使いたい

2022年12月14日

既に存在している一人用の WEB アプリに「リアルタイムな双方向通信を搭載したい」と言うのが、Node.js に手を出した理由である管理人。

この WEB アプリの仕様は

  • 事前処理は PHP で済ませて HTML を出力している。
  • レイアウトは CSS で整えている。
  • アプリの動きは Javascript で制御している。
  • シーンで必要になるデータは Javascript で非同期に PHP を呼び出して取得している。

こんな感じ。

フロントエンドは当然 Javascript で、バックエンドは PHP を使用している。(※PHP から MySQL に繋いでデータ取得とかしてる。)

ここに、双方向通信機能が使えるらしい Socket.io とやらを組み込みこみたい!と言うお話。

 

WEB サーバの処理は Apache がやれば良い

当然っちゃ当然だが、Node.js や Socket.io 絡みの事を調べると「WEB サーバを実装する記事」が大量にヒットする。

static なディレクトリを指定してリソースを置き、リクエストをルーティングして表示する HTML を振り分けて…みたいな。

しかし管理人には WEB サーバを作る知識が殆ど無いし、そう言うのは信頼と実績の Apache くんに任せる方向で行きたいと思っている。

どうあがいても HTTP サーバは立てる事になるが、リクエストに応じたイベントをルーティングするだけのサーバを待機させて、HTML(PHP)からそれを呼び出せばイメージしているものが出来るに違いない。

右往左往しまくったが、結果的に既存のサイトをそのまま再利用しつつ、Node.js を読み込んで Socket.io でのリアルタイム双方向通信が可能になった。

 

ファイル構成

/root/node_modules/
     /client.js
     /index.php
     /package-lock.json
     /package.json
     /server.js

ここは人によって違うと思うので、参考程度に。

 

Node.js 側

Node.js も PHP もサーバサイドなので「Node.js 側」ってのは間違った表現かも知れないが、まだ良く分かっていないので線引きしておく。

事前準備

インストールしておく必要があるモジュールは2つ。

$ npm install express
$ npm install socket.io

socket.io は必須として、色々試しまくっている過程で express は入れといた方が良いと判断した。(※セッションとか MySQL とかを Node.js から使う時に楽だった。)

server.js のソース

import express from "express";
import { createServer } from "http";
import { Server } from "socket.io";

class testServer {
	constructor() {
		//=============================================
		// express
		//---------------------------------------------
		const app = express();
		app.disable("x-powered-by");
		app.options('*', (req, res) => res.sendStatus(200));
		
		//=============================================
		// HTTP Server
		//---------------------------------------------
		const http = createServer(app);
		
		//=============================================
		// Socket.IO
		//---------------------------------------------
		const io = new Server(http, {
			cors: {
				origin: "http://<yourdomain>",
				methods: ["POST", "GET", "PUT", "DELETE", "OPTIONS"],
				allowedHeaders: ["Origin", "X-Requested-With", "Content-Type", "Accept"],
				credentials: true
			}
		});
		io.of("/").on("connection", socket => {
			console.log("conn:" + socket.id); // server.js のコンソール
			io.to(socket.id).emit("test1", socket.id); // 繋いだ本人だけに送信
			socket.broadcast.emit("test2", socket.id); // 繋いだ本人以外に送信
			// 待機させておきたいリスナを書く
		});
		
		//=============================================
		// Server start
		//---------------------------------------------
		http.listen(<yourport>, () => console.log("listen:<yourport>"));
	}
}

new testServer();

自己紹介をやめさせる(Line: 11)

app.disable("x-powered-by");

Express はデフォルトでヘッダ値に「I am Express!!」と判別できる値を返してしまうので、悪意のある人にサーバが「Node.js + Express で実装している」と言う事を教えてしまう。

HTML のソース上で Socket.io とか見えるからバレバレな気がするが、一応この値は返さないようにしておく。

プリフライト・リクエストに返答(Line: 12)

app.options('*', (req, res) => res.sendStatus(200));

仕様上、この Node.js を呼び出す時は Cross-Origin な状態となるため、実際のリクエスト前にプリフライト・リクエストが飛んでくる。

取り合えず笑顔(200)を返しておかないと、実際のリクエストが処理出来ないため置き笑顔を仕込んでおく。

Socket.io で CORS 設定(Line: 23-28)

cors: {
	origin: "http://<yourdomain>",
	methods: ["POST", "GET", "PUT", "DELETE", "OPTIONS"],
	allowedHeaders: ["Origin", "X-Requested-With", "Content-Type", "Accept"],
	credentials: true
}

ここでめちゃくちゃハマった、Socket.io の v3 からは CORS の設定を明示しないとダメになったらしい。

npm info socket.io version
4.5.4

Socket.io 絡みの検索をしていても、中々これにたどり着く事が出来なくて、延々と Express の use で CORS を設定する方法を調べてしまっていた。

Line: 2 で呼び出し元のドメインを許可する形になるため、通常はポート番号は省略されていると思うが、XAMPP などの開発環境を使っている場合はポート番号も書く必要がある。

$ node server.js
listen: <yourport>

で、実行(待機)させておく。

 

PHP 側

この時点では特に PHP のスクリプトは書いていないので HTML でも問題無いが、最終的にはこのページがリクエストされた時点で、ログイン情報やら各種情報を動的に取得するので PHP で書いている。

index.php のソース

<?php
// 最終的にはここでなんかする
?><!doctype html>
<html>
<head>
<meta charset="utf-8" />
<script src="http://<yourdomain>:<yourport>/socket.io/socket.io.js"></script>
<title>Test</title>
</head>

<body>
<h1>Test</h1>
<script type="module" src="client.js"></script>
</body>
</html>

Node.js を読み込む(Line: 7)

Socket.io を使ったサーバを起動すると、この JS が呼べるようになるらしい。

たどり着くまで時間がかかったが、これがやりたかったと言っても過言ではない。

いつものフロントエンド用(Line: 13)

socket.io.js の後に読み込むものだと思うが、デベロッパーツール(ネットワーク)のウォーターフォールを見る限り client.js のダウンロードが先に終わっていた。

ちょっと怪しい気がするが、取り合えずは動いている。

client.js のソース

class Client {
	constructor() {
		const socket = io.connect("http://<yourdomain>:<yourport>", {
			//secure: true,
			withCredentials: true,
			reconnectionAttempts: 10	// サーバへの再接続リクエスト上限
		});
		socket.on("test1", id => console.log("your:" + id));
		socket.on("test2", id => console.log("other:" + id));
	}
}

new Client();

いよいよ接続(Line: 3)

socket.io.js を読み込んでいると、io と言うオブジェクトが使えるようになる。

これこそが求めていたリアルタイム双方向通信を実現してくれる、神のようなオブジェクト。

有難く接続したい訳ですが、ここで Node.js の URL を指定する必要がある。

通常かどうかは分からないが、Node.js はポート 80 や 443 を使わないため、URL に何らかのポート番号を含む事にな。

つまり Cross-Origin な状態となるので、server.js でこちょこちょ設定する必要があった訳だ。

client.js ではオプションで「withCredentials: true」だけ指定すれば繋がるが、セキュリティ的に他にも色々設定した方が良さそうな気がする。

なお、サーバへのリクエスト上限についてはトライ&エラーしてると、デベロッパーツールのコンソールがエラーを吐き続けて真っ赤になるのが嫌で設定している。

自分の Socket ID を表示(Line: 8)

デベロッパーツールを開いた状態で、普通に index.php にアクセスする。

想定では server.js(Line: 32)が反応して、client.js(Line: 8)が実行されるハズ。

your:HnpnvFI5ilUzjcBPAAAF

いいね。

この状態でリロードを連打してみたら、たまに一瞬だけなんらかのエラーが出ている事が確認出来た。

やはり読み込み順序が関係しているのではないかと思われるので、client.js のクラスをインスタンス化するタイミングを制御する実験もしてみようと思う。

他の人の Socket ID を表示(Line: 9)

新しいタブや、全く別のブラウザを開いて、普通に index.php にアクセスする。

想定では server.js(Line: 33)が反応して、client.js(Line: 9)が実行されるハズ。

your:HnpnvFI5ilUzjcBPAAAF
other:2iI2OCQY29Q8SZnzAABj

これはブロードバンドと言って、アクセスした本人以外全てのユーザーにイベントが発生している。

なので、別で開いた方のページをリロードしまくったり、別タブで開きまくると、

your:HnpnvFI5ilUzjcBPAAAF
other:2iI2OCQY29Q8SZnzAABj
other:3YvklON2BiX-tUOFAABl
other:NJn_vpyCm5sUX26bAABn
other:njLYfPQ60H7SXGw6AABp
other:AW_8k_yfNtnt_FJqAABr
other:EfhaEZ7a5-A2M-i-AABt
other:E7930MnDt25YiJ_EAABv
other:VPJO1wdamEAYHnMkAABx
other:R-7YD-y8PEY5sdJ2AABz
other:c5cth8aDofXenAMiAAB1

こうなる。

 

まとめ

以上で「既存のサイトはそのまま使い回して Socket.io の美味しい所だけ使う」が実現できたので、必要最低限の学習コストだけで様々な事が可能になっていくハズだ。

実際のサービスで運用する場合は当然 SSL 化が必須になるが、当サイトで使っているローカル開発環境でオレオレ証明書を使うと、ブラウザによって動く場合と動かない場合が発生したので、やむなく HTTP でテストを行っている。

普通に困っているので、何か良い案があれば是非ご教授願いたく思います。