Socket.ioとReact

Socket.ioとReact

Reactで作ったオセロゲームをWebで対戦(Socket.ioの双方向通信により)できるようにしようと思います
「ブロントエンドはReact」「バックエンドはNode.js」でRender(Herokuが有料化されるらしいので😂)にデプロイしました

「Socket.io」はWebSockeプロトコルを扱えるNode.jsのライブラリです
WebSockeプロトコルとはブラウザ上での双方向通信を可能にした通信規格です
一度コネクションを確立した後は、サーバーとクライアントのどちらからも通信が可能です

目次
  1. socket.io
    1. 初期設定
    2. イベント
      1. イベント発行とリスナー登録
      2. room
  2. フロントをReactで
  3. Renderにデプロイ

socket.io

初期設定

インストールと初期設定(socket.ioクライアントとsocket.ioサーバーを接続する)
ブラウザで「http://localhost:3000/」にアクセルするとターミナルに「接続されました」を表示する
*expressもインストール(ルーティングのため)

npm init -y
//expressインストール
npm install express
//nodemonはサーバーアプリケーションが自動で再起するようにします
npm install nodemon --save-dev
//バックエンド側 socket.ioサーバー
npm install socket.io

package.jsonのscriptsに追加
これで「npm start 」でサーバー起動して変更も監視してくれます

"scripts": {
    "start": "nodemon index.js"
  },
const path = require('path');
const express = require('express');
const app = express();
//httpモジュールを使いサーバーの処理はexpress
const http = require('http');
const server = http.createServer(app)

//環境変数が取得できない場合は3000ポートを使う
const port = process.env.PORT || 3000

//ioインスタンス作成(サーバーを渡す)
const { Server } = require("socket.io");
const io = new Server(server);

//ここから編集
io.on('connection', () => {
  console.log('接続されました');
});
//ここまで

// ホーム(/)にアクセス時に返却するHTMLファイル
app.get('/', (req, res) => {
  res.sendFile(path.join(__dirname,'./public/index.html'));
});

//expressのサーバーapp.lestenでないことに注意
server.listen(port)

プロジェクトフォルダ直下にpublicフォルダを作って
index.htmlファイルを置く
「socket.ioクライアント」を読み込む

<!DOCTYPE html>
<html lang="ja">
<head>
 <!--省略 -->
</head>
<body>
    <h1>Hello Socet</h1>
</body>
<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io();
</script>
</html>

「socket.ioクライアント」でバンドルツールを使う場合

npm install socket.io-client
//モジュールの読み込み
import io from "socket.io-client";
const socket = io();

イベント

サーバーとクライアント間のデータのやり取り

イベント発行とリスナー登録

一方でイベントを発行し、他方でリスナーを登録します

  • サーバー側でクライアント側の接続を受け付けると「connectionイベント」が発生
  • このとき引数としてsocketオブジェクト(新しい接続の情報)が発生
  • socket.emitメソッドを使用してイベントをクライアントに送信

接続するクライアントの数だけ「connectionイベント」が発生します

イベントを発行
socketオブジェクトの「emitメソッド」

 socket.emit(イベント名, イベントリスナーに渡す値);

イベントリスナーを登録
socketオブジェクトの「onメソッド」

socket.on(イベント名, コールバック関数);

サーバーでイベントを発行
クライアントでイベントリスナーを登録

//サーバ
io.on("connection", (socket) => {
  socket.emit("hello", "world");
});

//クライアント
const socket = io();
socket.on("hello", (arg) => {
  console.log(arg); // world
});

クライアントでイベントを発行
サーバーでイベントリスナーを登録

//サーバー
io.on("connection", (socket) => {
  socket.on("hello", (arg) => {
    console.log(arg); // world
  });
});

//クライアント
const socket = io();
socket.emit("hello", "world");

任意の数の引数を送信できます
emit()の引数にコールバックを追加できます
このコールバックは、相手側がイベントを確認すると呼び出されます
コールバックには引数を渡すことができます
*ドキュメントより

//サーバ
io.on("connection", (socket) => {
  socket.on("update item", (arg1, arg2, callback) => {
    console.log(arg1); // 1
    console.log(arg2); // { name: "updated" }
    callback({status: "ok"});
  });
});
//クライアント
socket.emit("update item", "1", { name: "updated" }, (response) => {
  console.log('届きました', response.status); //届きました ok
});

クリックすると1プラスした値をコンソールに出力する

//サーバー側
let count = 0
io.on('connection', (socket) => {
//countUpイベント
  socket.emit('countUp', count);
//incrementイベントのリスナー
socket.on('increment', ()=>{
   count ++
//io: 全ての接続に対してcountUpイベント
io.emit('countUp', count)
  })
});

//クライアントからサーバーへデータを送る
<button>+1</button>
//省略
<script src="/socket.io/socket.io.js"></script>
<script>
    const socket = io();
    //countUpイベントのリスナー
    socket.on('countUp', (count)=>{
    console.log(count)
    })
    //クリック
    document.querySelector('button').addEventListener('click', ()=>{
    //incrementイベント
    socket.emit('increment')
 })
</script>

接続されているすべてのクライアントにイベントを送信

io.emit("hoge", "world");

送信者以外のの接続済みクライアント全員へイベントを送信

io.on("connection", (socket) => {
  socket.broadcast.emit("hoge", "world");
});

「socket.broadcast」:接続中のユーザー(新しい接続したユーザ以外)に新しい接続があることを知らせる
「’disconnect’イベント(切断された時のイベント)」:接続が切断されたことを接続中のユーザーに知らせる
クライアント側で「sendMessageイベント」を登録、「ファームのsubmit」で送信したメッセージもコンソールに出力します

//サーバー側
io.on('connection', (socket) => {
    console.log('新しい接続')
    socket.emit('message', 'Welcome!')
    socket.broadcast.emit('message', '新しい接続があります!')
  //クライアントからのsendMessageイベントのリスナー
    socket.on('sendMessage', (message) => {
        io.emit('message', message)
    })
    socket.on('disconnect', () => {
        io.emit('message', '切断されました!')
    })
})
//クライアント側
<form id="form">
    <input name="message" />
    <button>Send</button>
</form>
//省略
const socket = io()
socket.on('message', (message) => {
    console.log(message)
})
document.querySelector('#form').addEventListener('submit', (e) => {
    e.preventDefault()
    const message = e.target.elements.message.value
    socket.emit('sendMessage', message)
})

XSS対策にxss-filtersというライブラリがあるようです

npm install xss-filters

const xssFilters = require('xss-filters');

socket.emit('sendMessage', xssFilters.inHTMLData(message))

room

ルームの概念はサーバーのみで、クライアントからは参加しているルームのリストにアクセスできません

特定のチャネル(some room)だけがリスナーを呼び出せるようにする
socket.join

io.on("connection", socket => {
  socket.join("some room");
});

特定のチャネル(some room)にイベント(some event)を登録する
some room一致するすべてのソケットにパケットを送信
注意: toの引数は文字列です

io.to("some room").emit("some event");

Socketは一意の識別子(socket.id)で識別され、IDで識別されるルームに自動的に参加します

io.on("connection", socket => {
  socket.on("private message", (anotherSocketId, msg) => {
    socket.to(anotherSocketId).emit("private message", socket.id, msg);
  });
});

特定の部屋のメンバーとコミュニケーションする

io.to.emit
socket.broadcast.emit

フロントをReactで

フォルダの構成
frontendフォルダはプロジェクトフォルダ直下でnpx create-react-app frontendで作成

プロジェクト
|_ frontendフォルダ
|   |_build(本番用) 
|   |_src(開発用) 
|   |_public(開発index.htmlなど) 
|   |_package.json(フロント用)
|   
|_ index.js(サーバー側ファイル)
|   
|_package.json(サーバー用)
    

index.jsとpackage.jsonの変更箇所について
import(ESモジュール)を使います(ReactのESLintの設定でrequireがエラーとなったため)
そうすると__dirname(現在のディレクトリのパス)がそのままでは使えない
Reactがnpm start(開発用サーバー)で3000ポートを使用しているなどの理由で
index.jsファイルを編集します
package.json(サーバー用)には"type": "module"を追加します

"type": "module",
  "scripts": {
    "start": "nodemon index.js"
  },
import express from 'express'
const app = express()
import path from 'path'

import { fileURLToPath } from 'url'
const port = process.env.PORT || 3001
import http from 'http'
const server = http.createServer(app);
import { Server } from 'socket.io'
const io = new Server(server);
const __dirname = path.dirname(fileURLToPath(import.meta.url));

//本番用ではnpm run build
//本番用:buildしたときに読み込むJSやCSSなど静的ファイル
app.use(express.static(path.join(__dirname, './frontend/build')));

//本番用:ホーム(/)にアクセス時に返却するHTMLファイル
// 「/」を「*」にするとすべてのパス
app.get('/', (req, res) => {
 res.sendFile(path.join(__dirname,'./frontend/index.html'));
});

開発サーバーはhttp://localhost:3000にアクセス
buildした場合はhttp://localhost:3001にアクセス

Reactのpackage.jsonファイルにproxyを設定
これで「localhost:3000」を「localhost:3001」に置き換えるのでCORSポリシーによってブロックされません

 {
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "proxy": "http://localhost:3001",
//略

Socketのイベント:React Hooksを使った例
接続と切断

const [isConnected, setIsConnected] = useState(socket.connected);
useEffect(() => {
    socket.on('connect', () => {
      setIsConnected(true);
    });
    socket.on('disconnect', () => {
      setIsConnected(false);
    });
//クリーンアップする
    return () => {
      socket.off('connect');
      socket.off('disconnect');
  };
}, []);

 return (
    <div>
      {isConnected ?<p>接続中</p> :<p>切断されました!</p>}
    </div>
  );

サーバー側:接続と切断

io.on('connection', (socket) => {
   
    socket.on('disconnect', () => {
     
    })
});

Renderにデプロイ

Herokuが有料化されるらしく😂
Renderを試してみます

GitHubにアップロードして、Renderのダッシュボードからリポジトリを指定するだけで簡単にデプロイできました

*Renderの無料プランのWebサービスは1か月あたり750時間の実行が可能です
無料使用制限を超えると月末に無料使用がリセットされか有料プランにアップグレードされるまで自動的に停止されます
無料プランのWebサービスは、非アクティブ状態が15分間続くと自動的にスリープするので30秒の応答遅延が発生します

余談:スリープ対策
UptimeRobot」を利用すると定期的に自動でサイトにアクセスしてくれます
50までのネットワークを無料で監視できます
監視のタイミングは、5分〜120分の任意間隔で設定できます

GitHubでプロジェクトのリポジトリ作成します
パブリックリポジトリとプライベートリポジトリの両方がサポートされています
プロジェクト直下に.gitignoreファイル作成

node_modules/
.DS_Store

重要:Reactプロジェクトの.gitignoreからは/buildをコメントアウトして、「buidlフォルダ」もリポジトリにアップします

git init
git add .
git commit -m'first commit'
git branch -M main
git remote add origin https://github.com/.....git
git push -u origin main

Renderダッシュボードから新しいWebサービスを作成します

プロジェクトのGitHubリポジトリを選択してリポジトリへのアクセス許可します
必要な内容を入力してWebサービスを作成します(コードに基づいていくつかの値を自動入力されています)

  • Name: Webサービスの名前
  • Environment: Node(プログラミング言語)
  • Region:デプロイ先の地理的リージョン (アジアはシンガポールでした)
  • Branch: main(ビルドするgitブランチ)
  • Build Command: yarn (npm installのこと)
  • Start Command: node index.js(Webサービスを開始するコマンド)
  • Plan: freeプラン(利用可能なプランから選択)

ステータスが「Deploy succeeded」になれば成功です

「Webサービスの名前 .onrender.com 」でアプリケーションが公開されます

GitHubリポジトリURL

公開サイト