はじめに
リアルタイム相互通信する Cordova アプリを作ったことがあります。フロントエンドに HTML+JavaScript で、Socket.IO 通信を使いました。通信を中継するのに Node.js のサーバアプリを用意しました。
サーバアプリを安価に稼動させるにはどうしたらいいでしょうか。確認していると、自分がウェブサイト公開するために契約しているレンタルサーバで PHP が使えることに気づきました。上記の Node.js サーバアプリを PHP に移植できないでしょうか。
Node.js で Socket.IO を使ってみる
Node.js
で Socket.IO
を使うのをおさらいします。
Socket.IO を触ってみた #JavaScript - Qiita
やさしいsocket.io #Node.js - Qiita
Socket.IO
は、Node.js サーバとクライアントの間のリアルタイム通信を提供するライブラリです。Node.js 用とブラウザ用の JavaScript ライブラリがあります。基本的には WebSocket を使用します。
(Socket.IOとは #Socket.io - Qiita)
Node.js で Socket.IO を使ってみる①
サーバのプログラム server.js
とクライアントのプログラム index.html
を用意します。
var app = require('express')();
var server = require('http').Server(app);
var socketio = require('socket.io')(server);
app.get('/client', function(req, res){
res.sendFile(__dirname + '/client.html');
});
// クライアントから接続された
socketio.on('connection', function(socket){
console.log("client connected");
// クライアントから切断された
socket.on('disconnect', function(){
console.log("client disconnected.");
});
// クライアントからデータを受信した
socket.on('message', function(data){
console.log(`data received: ${data}`);
// クライアントにデータを送信する
socketio.emit('message', data);
});
});
server.listen(3000, function(){
console.log("server started.");
});
サーバのプログラムは Node.js
で書きます。ライブラリ socket.io
を使います。
クライアントからアクションされると socketio
オブジェクトでイベント発火します。接続されると on('connection')
、切断されると on('disconnect')
、データが送られると on('message')
ですね。
on('message')
は、クライアントで emit('message')
しているのを受けています。受取したデータを他のクライアントに転送しています。↑
<script src="/socket.io/socket.io.js"></script>
</head>
<body>
<p>Socket.IO Client</p>
<p id="status"></p>
<p><input type="text" id="text" value="Hello!">
<input type="button" id="send" value="送信">
</body>
window.addEventListener('load', function(e){
// サーバに接続
document.querySelector('#status').textContent = "接続中...";
var socketio = io("http://localhost:3000", { autoConnect: true });
// 接続成功
socketio.on('connect', function(e){
document.querySelector('#status').textContent = "接続成功";
});
// 接続切断
socketio.on('disconnect', function(e){
document.querySelector('#status').textContent = "接続切断";
});
// サーバからデータを受取
socketio.on('message', function(data){
document.querySelector('#status').textContent = "データ受信: " + data;
});
// サーバにデータを送信
document.querySelector('#send').addEventListener('click', function(e){
var text = document.querySelector('#text').value;
socketio.emit('message', text);
});
});
クライアントのプログラムは HTML
と JavaScript
で書いてウェブブラウザで動作します。ライブラリ socket.io
を参照します。
サーバから送られるイベントを受取しています。接続成功したとき 'connect'
、接続切断したとき 'disconnect'
、データを受取すると 'message'
ですね。
'message'
イベントは、サーバで emit('message')
しているのを受けています。↑
Node.js で Socket.IO を使ってみる②
上記のコードでは、サーバアプリに接続した全てのクライアントアプリ同士でデータの送受信し合います。チャットサービスを実装したとき、チャットルームごとに別々のサーバアプリを稼動させないといけなくなります。そうしなくていいように Socket.IO
に「ルーム機能」が用意されています。
// クライアントから接続された
socketio.on('connection', (socket) => {
(中略)
// ルームに参加する要求を受信した
socket.on('join', (room) => {
socket.join(room);
console.log(`client joined: ${room}`);
socket.emit('message', `client joined: ${room}`);
});
// ルームから退出する要求を受信した
socket.on('leave', (room) => {
socket.leave(room);
console.log(`client left: ${room}`);
socket.emit('message', `client left: ${room}`);
});
// クライアントからデータを受信した
socket.on('message', (data) => {
console.log(`data received: ${data}`);
// クライアントにデータを送信する
socketio.to(data.room).emit('message', data.text);
});
ルームに参加する処理を加えます。'join'
イベントで socket.join()
、'leave'
イベントで socket.leave()
します。
'message'
イベントで受取する(クライアントから送られる)データは room
と text
を持つオブジェクトにします。クライアントに送信するのに socketio.emit()
すると接続されている全てのクライアントが対象になりますが、socketio.to().emit()
にすることで指定したルームに参加しているクライアントだけに送信されます。↑
クライアントのプログラムで emit()
するとき、イベント 'join'
、leave'
を指定します。
emit('message')
でサーバに送るデータは room
と text
を持つオブジェクトにします。↓
<body>
<p>Socket.IO Client</p>
<p><input type="text" id="room" value="room1">
<input type="button" id="join" value="参加">
<input type="button" id="leave" value="退出"></p>
(中略)
window.addEventListener('load', function(e){
(中略)
// ルームに参加
document.querySelector('#join').addEventListener('click', function(e){
var room = document.querySelector('#room').value;
socketio.emit('join', room);
});
// サーバにデータを送信
document.querySelector('#send').addEventListener('click', function(e){
var room = document.querySelector('#room').value;
var text = document.querySelector('#text').value;
socketio.emit('message', { room: room, text: text });
});
// ルームから退出
document.querySelector('#leave').addEventListener('click', function(e){
var room = document.querySelector('#room').value;
socketio.emit('leave', room);
});
PHP で WebSocket を使ってみる
Socket.IO
は定番ですが、基本は Node.js
のライブラリであり、PHP
に移植されたものはあるものの完全な互換ありません。調べていくと、WebSocket
を使って実装すればいいようです。
PHPでWebSocketとリアルタイム通信を実現するアーキテクチャ設計の方法と実装ガイド | IT trip
WebSocket
は、クライアントとサーバ間でリアルタイムなデータ通信を行うための通信規格(プロトコル)です。接続を確立するとクライアントとサーバが相互にデータを送受信できるため、チャットアプリやオンラインゲーム、ライブ配信などのリアルタイム性が求められるアプリケーションで利用されます。
(WebSocketとは何か?背景、HTTPとの違い、多彩な用途を解説!)
PHP で WebSocket を使ってみる①
サーバのプログラム server.php
とクライアントのプログラム index.html
を用意します。
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
require 'vendor/autoload.php';
class WebSocketServer implements MessageComponentInterface {
protected $clients;
public function __construct() {
$this->clients = new \SplObjectStorage;
}
// クライアントから接続された
public function onOpen(ConnectionInterface $conn) {
echo "client connected.\n";
$this->clients->attach($conn);
}
// クライアントから切断された
public function onClose(ConnectionInterface $conn) {
echo "client disconnected.\n";
$this->clients->detach($conn);
}
// クライアントからデータを受信した
public function onMessage(ConnectionInterface $from, $data) {
echo "data received: {$data}\n";
// クライアントにデータを送信する
foreach ($this->clients as $client) {
$client->send($data);
}
}
// エラーが発生した
public function onError(ConnectionInterface $conn, \Exception $e) {
echo "error occurred.\n";
$conn->close();
}
}
use Ratchet\App;
$server = new App('127.0.0.1', 3000);
$server->route('/', new WebSocketServer, ['*']);
$server->run();
サーバのプログラムを PHP
で書きます。ライブラリ Ratchet
を使います。
クライアントからアクションされると WebSocketServer
クラスでイベント発火します。接続されると onOpen()
、切断されると onClose()
、データが送られると onMessage()
ですね。
onMessage()
は、クライアントで connection.send()
しているのを受けています。このとき、受取したデータを他のクライアントに転送しています。
接続されているクライアントの管理は、Socket.IO
ではライブラリの内部で処理されるのですが、Ratchet
を使うとアプリで実装しないといけません。$clients
変数を用意して、onOpen()
で attach()
、onClose()
で detach()
します。↑
<body>
<p>WebSocket Client</p>
<p id="status"></p>
<p><input type="text" id="text" value="Hello!">
<input type="button" id="send" value="送信">
</body>
window.addEventListener('load', function(e){
// サーバに接続
document.querySelector('#status').textContent = "接続中...";
var connection = new WebSocket('ws://127.0.0.1:3000/');
// 接続成功
connection.addEventListener('open', function(e){
document.querySelector('#status').textContent = "接続成功";
});
// 接続切断
connection.addEventListener('close', function(e){
document.querySelector('#status').textContent = "接続切断";
});
// サーバからデータを受取
connection.addEventListener('message', function(e){
document.querySelector('#status').textContent = "データ受信: " + e.data;
});
// サーバにデータを送信
document.querySelector('#send').addEventListener('click', function(e){
var text = document.querySelector('#text').value;
connection.send(text);
});
});
クライアントのプログラムは HTML
と JavaScript
で書いてウェブブラウザで動作します。ブラウザのエンジンに実装されている WebSocket
オブジェクトを使います。
サーバから送られるイベントを受取しています。接続成功したとき 'open'
、接続切断したとき 'close'
、データを受取すると 'message'
ですね。
'message'
イベントは、サーバで send()
しているのを受けています。↑
PHP で WebSocket を使ってみる②
こちらでも「ルーム機能」を実装しましょう。以下のコードは大半を GitHub Copilot に書いて貰いました。
class WebSocketServer implements MessageComponentInterface {
protected $clients;
protected $rooms = []; // 追加
public function __construct() {
$this->clients = new \SplObjectStorage;
$this->rooms = []; // 追加
}
(中略)
// クライアントからデータを受信した
public function onMessage(ConnectionInterface $from, $buff) {
echo "data received: {$buff}\n";
$data = json_decode($buff, true);
switch ($data['action']) {
// ルームに参加する要求を受信した
case 'join':
$room = $data['room'];
if (!isset($this->rooms[$room])) {
$this->rooms[$room] = new \SplObjectStorage;
}
$this->rooms[$room]->attach($from);
echo "client joined: {$room}\n";
$from->send("client joined: {$room}");
break;
// ルームから退出する要求を受信した
case 'leave':
$room = $data['room'];
if (!isset($this->rooms[$room]) || !$this->rooms[$room]->contains($from)) {
return;
}
$this->rooms[$room]->detach($from);
echo "client left: {$room}\n";
$from->send("client left: {$room}");
break;
// クライアントからデータを受信した
case 'message':
$room = $data['room'];
$text = $data['text'];
echo "message received: {$text}\n";
if (!isset($this->rooms[$room])) {
return;
}
// クライアントにデータを送信する
foreach ($this->rooms[$room] as $client) {
$client->send($text);
}
break;
}
}
(中略)
ルームに参加する処理を加えます。
Socket.IO
では、クライアントで emit()
するときイベント 'join'
、'leave'
を指定して、サーバでそれぞれのハンドラで socket.join()
、socket.leave()
しました。
Ratchet
では、クライアントで send()
してサーバの onMessage()
ハンドラで処理するしかないので、send()
するとき action
を持つオブジェクトを渡して、onMessage()
で処理するとき action
を見て処理を分岐するようにします。
参加するルームの管理は、Socket.IO
ではライブラリの内部で処理されるのですが、Ratchet
を使うとアプリで実装しないといけません。$rooms
変数を用意して、'join'
の処理で attach()
、'leave'
の処理で detach()
します。↑
クライアントで send()
するとき、サーバに送るデータは action
を持つオブジェクトにします。↓
<body>
<p>WebSocket Client</p>
<p><input type="text" id="room" value="room1">
<input type="button" id="join" value="参加">
<input type="button" id="leave" value="退出"></p>
(中略)
window.addEventListener('load', function(e){
(中略)
// ルームに参加
document.querySelector('#join').addEventListener('click', function(e){
var room = document.querySelector('#room').value;
connection.send(JSON.stringify({ action: 'join', room: room }));
});
// ルームから退出
document.querySelector('#leave').addEventListener('click', function(e){
var room = document.querySelector('#room').value;
connection.send(JSON.stringify({ action: 'leave', room: room }));
});
// サーバにデータを送信
document.querySelector('#send').addEventListener('click', function(e){
var room = document.querySelector('#room').value;
var text = document.querySelector('#text').value;
connection.send(JSON.stringify({ action: 'message', room: room, text: text }));
});
});
Node.js で WebSocket を使ってみる
Node.js のサーバアプリを Socket.IO
でなく WebSocket
を使って書けるでしょうか。
5分で動かせるwebsocketのサンプル3つ #JavaScript - Qiita
Node.js で WebSocket を使ってみる①
サーバのプログラム server.js
を用意します。
var server = require('http').Server();
var websocket = new (require('ws').Server)({ server });
// クライアントから接続された
websocket.on('connection', function(socket){
console.log("client connected.");
// クライアントから切断された
socket.on('close', function(){
console.log("client disconnected.");
});
// クライアントからデータを受信した
socket.on('message', function(data){
console.log(`Received: ${data}`);
// クライアントにデータを送信する
socket.send(`${data}`);
});
});
server.listen(3000, function(){
console.log("server started.");
});
PHP の Ratchet
と違って、接続されたクライアントの管理はライブラリ ws
がしてくれるようです。↑
クライアントのプログラムは、PHP でサーバを書いたときと同じものが使えます。
Node.js で WebSocket を使ってみる②
こちらでも「ルーム機能」を実装しましょう。以下のコードは大半を GitHub Copilot に書いて貰いました。
var rooms = {} // 追加
(中略)
// クライアントからデータを受信した
socket.on('message', (buff) => {
console.log(`data received: ${buff}`);
var data = JSON.parse(buff);
switch(data.action) {
// ルームに参加する要求を受信した
case 'join':
if (!rooms[data.room]) {
rooms[data.room] = [];
}
rooms[data.room].push(socket);
console.log(`client joined: ${data.room}`);
socket.send(`client joined: ${data.room}`);
break;
// ルームから退出する要求を受信した
case 'leave':
if (!rooms[data.room]) {
return;
}
rooms[data.room] = rooms[data.room].filter((element) => (element !== socket));
console.log(`client left: ${data.room}`);
socket.send(`client left: ${data.room}`);
break;
// クライアントからデータを受信した
case 'message':
if (!rooms[data.room]) {
return;
}
// クライアントにデータを送信する
rooms[data.room].forEach(client => {
client.send(data.text);
});
break;
}
});
ルームに参加する処理を加えます。
参加するルームの管理は、PHP で実装したときと同様、アプリで実装しないといけません。rooms
変数を用意して、'join'
の処理で push()
、'leave'
の処理で filter()
します。↑
クライアントのプログラムは、PHP でサーバを書いたときと同じものが使えます。
おわりに
上記の PHP のプログラムを、自分が契約しているレンタルサーバに配置してみましたが、ウェブサイト用のサービスなので CGI 実行できるだけで、サーバプログラムは実行できませんでした。予め分かってもよさそうでしたが残念です。