webソケットについて
WebSocketの基本について
WebSocketとは
Webブラウザ(クライアント)とサーバー間でリアルタイムかつ双方向の通信を可能にするHTTPプロトコル(通信規格) ws://またはwss://で始まる
WebページではHTTP接続を経てアクションの度にサーバーにリクエストを行いサーバーが応答(データ転送)を行うが、 WebSocketではサーバとの接続を一度確立しさえすれば、接続しっぱなしで双方向通信ができる。つまり、サーバ側からもデータを自由に配信できる。
チャットアプリ、オンラインゲーム、株価更新など、リアルタイムで更新が必要なアプリケーションで活用されている
HTTPとの違い
HTTP は基本的に「リクエストした時だけ返ってくる」
WebSocket 系は接続を張ったまま、サーバーからもクライアントへ送れるため、チャット、通知、リアルタイム更新に向いている。
1. サンプル実装
ネイティブWebSocketだと自前で面倒を見る範囲が増えることや、 一般的なWebアプリではSocket.IOや類似ライブラリを使うことの方が多いこともあり、 今回はSocket.ioを使ったサンプル実装を行う。
Socket.io
Socket.io:WebSocket(プロトコル)を含む「リアルタイム通信ライブラリ」であり、WebSocketを「使うこともある」便利ラッパー。
まずWebSocketで接続を試し、WebSocketが利用可能でない場合にHTTPロングポーリングを用いた自動フォールバック対応してくれるため、幅広い環境で安定して動作するし開発者は違いを意識しなくていい
Socket.IOでも「接続・切断・イベント」はちゃんと理解できるが、“それを意識しなくても動くように設計されている”
サンプル実装手順
- 作業フォルダ作成
mkdir websocket-practice
cd websocket-practice
npm init -y
npm install express socket.io- ファイル構成
websocket-practice/
├─ app.js
└─ public/
├─ index.html
└─ script.js- app.js
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const app = express();
const server = http.createServer(app);
const io = new Server(server);
app.use(express.static("public"));
io.on("connection", (socket) => { // 接続
console.log("ユーザー接続:", socket.id);
socket.on("chat message", (msg) => { // イベント受信
console.log("受信:", msg);
io.emit("chat message", msg); // イベント送信 全員に配信
});
socket.on("disconnect", () => { // 切断
console.log("ユーザー切断:", socket.id);
});
});
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});- public/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Socket.IO Chat</title>
<style>
body {
font-family: sans-serif;
max-width: 600px;
margin: 40px auto;
padding: 0 16px;
}
.row {
display: flex;
gap: 8px;
}
input {
flex: 1;
padding: 8px;
}
button {
padding: 8px 16px;
}
ul {
margin-top: 16px;
padding-left: 20px;
}
</style>
</head>
<body>
<h1>簡易チャット</h1>
<div class="row">
<input id="message" placeholder="メッセージを入力" />
<button id="sendButton">送信</button>
</div>
<ul id="messages"></ul>
<script src="/socket.io/socket.io.js"></script>
<script src="/script.js"></script>
</body>
</html>- public/script.js
const socket = io();
const messageInput = document.getElementById("message");
const sendButton = document.getElementById("sendButton");
const messages = document.getElementById("messages");
socket.on("connect", () => {
console.log("接続完了:", socket.id);
});
socket.on("chat message", (msg) => {
const li = document.createElement("li");
li.textContent = msg;
messages.appendChild(li);
});
function sendMessage() {
const message = messageInput.value.trim();
if (!message) return;
socket.emit("chat message", message);
messageInput.value = "";
}
sendButton.addEventListener("click", sendMessage);
messageInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
sendMessage();
}
});- 実行
node app.jsブラウザで開く。
http://localhost:3000
同じURLをブラウザ2枚で開いて、片方で送ったメッセージがもう片方にも出れば成功
このサンプルで何をしているか
- ブラウザが io() でサーバーに接続
- クライアントが socket.emit("chat message", message) で送信
- サーバーが socket.on("chat message", ...) で受信
- サーバーが io.emit("chat message", msg) で全員に配信
Socket.IO と WebSocket は同じではない
今回は、WebSocketを試したくて、まずSocket.IOで双方向通信の最小構成を触った。
2. 翻訳APIを使ったサンプルアプリの実装
Socket.ioを使ったサンプルコードを実装して、リアルタイム通信とSocket.ioの基本的な使い方は押さえたので、
次は実務に寄せたサンプルの実装として、翻訳APIを使って「翻訳進捗リアルタイム表示」を実装する
サンプル実装手順
- プロジェクト作成
mkdir deepl-socket-translate
cd deepl-socket-translate
npm init -y
npm install express socket.io axios dotenv- ファイル構成
deepl-socket-translate/
├─ app.js
├─ .env
└─ public/
├─ index.html
└─ script.js- .env
DEEPL_API_KEY=DeepL_APIキー
DEEPL_API_URL=https://api-free.deepl.com/v2/translate- app.js
require("dotenv").config();
const express = require("express");
const http = require("http");
const axios = require("axios");
const { Server } = require("socket.io");
const app = express();
const server = http.createServer(app);
const io = new Server(server);
app.use(express.static("public"));
const DEEPL_API_KEY = process.env.DEEPL_API_KEY;
const DEEPL_API_URL =
process.env.DEEPL_API_URL || "https://api-free.deepl.com/v2/translate";
if (!DEEPL_API_KEY) {
console.error("DEEPL_API_KEY が未設定です");
process.exit(1);
}
io.on("connection", (socket) => {
console.log("接続:", socket.id);
socket.on("translate:start", async ({ text, targetLang = "EN-US" }) => {
try {
if (!text || typeof text !== "string") {
socket.emit("translate:error", {
message: "翻訳するテキストを入力してください",
});
return;
}
socket.emit("translate:progress", {
progress: 10,
message: "翻訳リクエストを受け付けました",
});
await sleep(300);
socket.emit("translate:progress", {
progress: 30,
message: "DeepL APIへ送信しています",
});
const translatedText = await translateWithDeepL(text, targetLang);
socket.emit("translate:progress", {
progress: 90,
message: "翻訳結果を整形しています",
});
await sleep(300);
socket.emit("translate:progress", {
progress: 100,
message: "翻訳が完了しました",
});
socket.emit("translate:done", {
originalText: text,
translatedText,
targetLang,
});
} catch (error) {
console.error(error);
socket.emit("translate:error", {
message: "翻訳に失敗しました",
});
}
});
socket.on("disconnect", () => {
console.log("切断:", socket.id);
});
});
async function translateWithDeepL(text, targetLang) {
const params = new URLSearchParams();
params.append("auth_key", DEEPL_API_KEY);
params.append("text", text);
params.append("target_lang", targetLang);
const response = await axios.post(DEEPL_API_URL, params, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
return response.data.translations[0].text;
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
server.listen(3000, () => {
console.log("http://localhost:3000 で起動しました");
});- public/index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>DeepL + Socket.IO 翻訳進捗デモ</title>
<style>
body {
font-family: sans-serif;
max-width: 720px;
margin: 40px auto;
padding: 0 16px;
}
textarea {
width: 100%;
height: 120px;
padding: 8px;
box-sizing: border-box;
}
button {
margin-top: 12px;
padding: 8px 16px;
cursor: pointer;
}
progress {
width: 100%;
height: 24px;
margin-top: 16px;
}
.box {
margin-top: 16px;
padding: 12px;
border: 1px solid #ddd;
border-radius: 8px;
white-space: pre-wrap;
}
.error {
color: #c00;
}
</style>
</head>
<body>
<h1>DeepL + Socket.IO 翻訳進捗デモ</h1>
<textarea id="textInput" placeholder="翻訳したい日本語を入力してください"></textarea>
<br />
<button id="translateButton">英訳する</button>
<progress id="progressBar" value="0" max="100"></progress>
<div id="status" class="box">待機中</div>
<h2>翻訳結果</h2>
<div id="result" class="box"></div>
<script src="/socket.io/socket.io.js"></script>
<script src="/script.js"></script>
</body>
</html>- public/script.js
const socket = io();
const textInput = document.getElementById("textInput");
const translateButton = document.getElementById("translateButton");
const progressBar = document.getElementById("progressBar");
const statusBox = document.getElementById("status");
const resultBox = document.getElementById("result");
socket.on("connect", () => {
statusBox.textContent = `接続しました: ${socket.id}`;
});
socket.on("disconnect", () => {
statusBox.textContent = "サーバーとの接続が切断されました";
});
socket.on("translate:progress", (data) => {
progressBar.value = data.progress;
statusBox.textContent = `${data.progress}%: ${data.message}`;
});
socket.on("translate:done", (data) => {
progressBar.value = 100;
statusBox.textContent = "翻訳完了";
resultBox.textContent = data.translatedText;
translateButton.disabled = false;
});
socket.on("translate:error", (data) => {
statusBox.textContent = data.message;
statusBox.classList.add("error");
translateButton.disabled = false;
});
translateButton.addEventListener("click", () => {
const text = textInput.value.trim();
if (!text) {
statusBox.textContent = "テキストを入力してください";
return;
}
statusBox.classList.remove("error");
resultBox.textContent = "";
progressBar.value = 0;
translateButton.disabled = true;
socket.emit("translate:start", {
text,
targetLang: "EN-US",
});
});- 実行
node app.jsブラウザで開く。
http://localhost:3000
この実装で押さえるべき流れ
ブラウザ
socket.emit("translate:start")
↓
Node.jsサーバー
socket.on("translate:start")
↓
DeepL API呼び出し
↓
Node.jsサーバー
socket.emit("translate:progress")
socket.emit("translate:done")
↓
ブラウザ
socket.on("translate:progress")
socket.on("translate:done")Socket.ioはemitでイベントを送り、onでイベントを受ける。公式ドキュメントでも、Node.jsのEventEmitterに近い形でイベントを送受信すると説明されている。
注意点
このサンプルの「進捗」は、DeepL API自体のリアルな進捗ではない。
DeepLのテキスト翻訳APIは、基本的にはリクエストを投げて結果が返るタイプ。なので今回の進捗は、
- 受付
- API送信中
- 結果整形中
- 完了
という処理段階の進捗表示
3. ネイティブWebSocketで簡易エコー/チャットの実装
ネイティブWebSocketだと自前で面倒を見る範囲が増える
- 再接続
- 部屋/ルーム
- イベント名管理
- ブロードキャスト
- 認証
- エラー処理
- 接続維持
例えば下記のようなケースでネイティブWebSocketが使われる場面もあり、必ずしも常にSocket.ioで実装されるとは限らない
- 依存ライブラリを増やしたくない
- 通信を極力軽くしたい
- 独自プロトコルを扱う
- AWS API Gateway WebSocket APIなどを使う
- ゲーム、金融、IoTなど低レイヤー制御が重要
Socket.ioでは低レベルの通信処理は抽象化されているため、補足としてネイティブWebSocketでも簡単な実装を行い、違いを確認する
サンプル実装手順
参考:WebSocket 入門
参考:WebSocketを完全に理解できるコードを書いたので解説します
参考:Node.jsサーバーで使うSocket.IO入門