概要
WebSocketは、クライアントとサーバー間にTCP上の全二重・双方向通信チャネルを確立するプロトコルです。2011年にRFC 6455として標準化され、現在すべての主要Webブラウザが対応しています。
従来のHTTPはクライアントがリクエストを送り、サーバーがレスポンスを返す一方向・都度接続のモデルでした。このモデルはリアルタイムデータのプッシュ通知に本質的に不向きです。WebSocketはHTTPのアップグレードメカニズムを使って接続を確立し、一度接続が確立されると双方向に任意のタイミングでメッセージを送信できます。
IoT・組み込み分野では以下の用途に使われます。
- センサーデータのブラウザへのリアルタイム表示ダッシュボード
- デバイスへのリモートコントロール(コマンド送信)
- ファームウェア書き込みの進捗表示
- 工場ラインの監視・可視化システム
歴史・背景
HTTP/1.1時代、リアルタイム通信の実現にはさまざまな回避策が使われていました。
| 技術 | 方法 | 問題点 |
|---|---|---|
| ポーリング | 一定間隔でGETリクエストを繰り返す | 無駄なリクエスト、遅延 |
| ロングポーリング | レスポンスを意図的に遅らせる | サーバーコネクション占有 |
| Server-Sent Events | サーバーからの一方向ストリーム | クライアントからサーバーには送れない |
| Flash/Silverlight | プラグインで実現 | プラグイン廃止で使用不可 |
2008年頃からIETFとW3Cが協調してWebSocketの標準化を進め、2011年のRFC 6455公開・HTML5との組み合わせで普及しました。主要ブラウザが対応した2012年以降は事実上のリアルタイムWeb標準となっています。
技術仕様
ハンドシェイク
WebSocket接続はHTTPのGETリクエストから開始します。
# クライアントからのアップグレードリクエスト
GET /ws HTTP/1.1
Host: dashboard.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: mqtt, chat
# サーバーの応答(101 Switching Protocols)
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: mqtt
Sec-WebSocket-Acceptはクライアントが送ったSec-WebSocket-KeyにGUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)を連結してSHA-1ハッシュし、Base64エンコードしたものです。これによりHTTPキャッシュサーバーが誤ってWebSocketフレームをキャッシュすることを防いでいます。
フレーム構造
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - -+
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - -+-------------------------------+
| | Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - -+
| Payload Data continued ... |
+---------------------------------------------------------------+
opcodeの種類
| opcode | 意味 |
|---|---|
| 0x0 | 継続フレーム |
| 0x1 | テキストフレーム(UTF-8) |
| 0x2 | バイナリフレーム |
| 0x8 | 接続クローズ |
| 0x9 | Ping |
| 0xA | Pong |
マスキング
クライアントからサーバーへ送るフレームは必ずマスキングされます(32ビットランダムマスクキーとXOR)。サーバーからクライアントへはマスキング不要です。これはキャッシュポイズニング攻撃を防ぐためです。
WebSocket over TLS(WSS)
ws://example.com/ws → 暗号化なし(HTTP相当)
wss://example.com/ws → TLS暗号化あり(HTTPS相当)
IoT機器がインターネット越しにWebSocket通信する場合は必ずWSSを使います。
動作原理
接続確立から通信終了まで
Client Server
| |
|-- HTTP GET /ws (Upgrade: ws) ----->| ← TCPコネクション上
|<-- 101 Switching Protocols --------|
| |
| ← TCP接続は維持されたまま WebSocketプロトコルに切り替わる →
| |
|-- TEXT: {"cmd":"subscribe","ch":1}>|
|<-- TEXT: {"event":"data","val":25} | サーバーからのプッシュ
|<-- TEXT: {"event":"data","val":26} |
|<-- BINARY: [0x01, 0x02, ...] ------| バイナリも送信可
| |
|-- CLOSE (code:1000) -------------->| ← 正常切断
|<-- CLOSE (code:1000) --------------|
| |
| ← TCP FIN/ACK → |
キープアライブ(Ping/Pong)
TCPのアイドル接続はNAT/ファイアウォールに切断される場合があります。定期的なPing/Pongでコネクションを維持します。
# サーバー側でPingを定期送信
import asyncio
import websockets
async def handler(websocket, path):
async for message in websocket:
# Pingは自動的にPongで応答される(websocketsライブラリ)
await websocket.send(process(message))
# Ping間隔の設定
async with websockets.serve(handler, "0.0.0.0", 8765,
ping_interval=20, # 20秒ごとにPing
ping_timeout=10): # 10秒でタイムアウト
await asyncio.Future()
サブプロトコル
Sec-WebSocket-Protocolヘッダーで、WebSocket上で動かす上位プロトコルをネゴシエートできます。MQTTをWebSocketの上で動かす「MQTT over WebSocket」はブローカーが多数サポートしており、ブラウザからMQTTブローカーに直接接続できます。
用途・ユースケース
IoTリアルタイムダッシュボード
センサーデータをブラウザにリアルタイム表示する最もポピュラーな構成。
// ブラウザ側のJavaScript
const ws = new WebSocket('wss://dashboard.example.com/ws/sensors');
ws.addEventListener('open', () => {
// 購読するセンサーを指定
ws.send(JSON.stringify({
action: 'subscribe',
devices: ['sensor_01', 'sensor_02']
}));
});
ws.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
// リアルタイムグラフを更新
updateChart(data.device_id, data.temperature, data.timestamp);
});
ws.addEventListener('close', (event) => {
console.log('切断:', event.code, event.reason);
// 自動再接続
setTimeout(() => reconnect(), 3000);
});
ESP32からのWebSocket接続
// Arduino/ESP32 での WebSocket クライアント実装
#include <WebSocketsClient.h>
#include <WiFi.h>
WebSocketsClient webSocket;
void webSocketEvent(WStype_t type, uint8_t *payload, size_t length) {
switch (type) {
case WStype_CONNECTED:
Serial.println("WebSocket接続完了");
// 認証トークンを送信
webSocket.sendTXT("{\"type\":\"auth\",\"token\":\"xxx\"}");
break;
case WStype_TEXT:
Serial.printf("受信: %s\n", payload);
handleCommand(payload, length);
break;
case WStype_DISCONNECTED:
Serial.println("切断 - 再接続待ち");
break;
}
}
void setup() {
WiFi.begin(SSID, PASSWORD);
while (WiFi.status() != WL_CONNECTED) delay(500);
webSocket.beginSSL("dashboard.example.com", 443, "/ws/device");
webSocket.onEvent(webSocketEvent);
webSocket.setReconnectInterval(5000);
}
void loop() {
webSocket.loop();
// センサーデータを定期送信
static unsigned long last_send = 0;
if (millis() - last_send > 5000) {
float temp = readTemperature();
char json[64];
snprintf(json, sizeof(json),
"{\"type\":\"data\",\"temp\":%.1f}", temp);
webSocket.sendTXT(json);
last_send = millis();
}
}
MQTT over WebSocket(ブラウザからMQTTブローカーへ)
// ブラウザからMosquittoブローカーに直接接続
import mqtt from 'mqtt';
const client = mqtt.connect('wss://broker.example.com:8083/mqtt', {
username: 'device_user',
password: 'secret',
clientId: 'browser_dashboard_01'
});
client.on('connect', () => {
client.subscribe('sensors/#');
});
client.on('message', (topic, message) => {
const data = JSON.parse(message.toString());
updateDashboard(topic, data);
});
ファームウェア書き込み進捗表示
# サーバー側:OTA進捗をWebSocketでプッシュ
import asyncio
import websockets
async def ota_progress_handler(websocket, path):
device_id = path.split('/')[-1]
try:
async for progress_event in flash_firmware(device_id):
await websocket.send(json.dumps({
"event": "progress",
"percent": progress_event.percent,
"bytes_written": progress_event.bytes_written,
"total_bytes": progress_event.total_bytes
}))
await websocket.send(json.dumps({"event": "complete"}))
except Exception as e:
await websocket.send(json.dumps({"event": "error", "message": str(e)}))
実装・開発のポイント
主要ライブラリ
| プラットフォーム | ライブラリ | 言語 |
|---|---|---|
| サーバー(Python) | websockets, FastAPI, aiohttp | Python |
| サーバー(Node.js) | ws, Socket.IO | JavaScript |
| ブラウザ | ネイティブ WebSocket API | JavaScript |
| ESP32/Arduino | WebSocketsClient(Markus Sattler) | C++ |
| Raspberry Pi(Python) | websockets, websocket-client | Python |
| Rust | tokio-tungstenite | Rust |
自動再接続の実装
IoT機器は電波状況や電源断で接続が切れることが多く、堅牢な再接続ロジックが必要です。
import asyncio
import websockets
async def connect_with_retry(uri, on_message):
delay = 1
max_delay = 60
while True:
try:
async with websockets.connect(uri) as ws:
delay = 1 # 接続成功でリセット
async for msg in ws:
await on_message(msg)
except (websockets.exceptions.ConnectionClosed,
OSError) as e:
print(f"切断: {e}. {delay}秒後に再接続")
await asyncio.sleep(delay)
delay = min(delay * 2, max_delay) # 指数バックオフ
バイナリとテキストの使い分け
センサーデータをバイナリパックすることで通信量を削減できます。
import struct
# テキスト(JSON): 44バイト
# {"temperature": 25.3, "humidity": 60.1}
# バイナリ(struct pack): 9バイト
data = struct.pack('>BhH', 0x01, int(25.3 * 10), int(60.1 * 10))
# 0x01=センサータイプ, 253=25.3℃×10, 601=60.1%×10
他技術との比較
| 項目 | WebSocket | MQTT | SSE | HTTP polling |
|---|---|---|---|---|
| 双方向通信 | ○ | ○ | × | △(疑似) |
| ブラウザネイティブ | ◎ | × | ◎ | ◎ |
| 接続維持 | ○(単一TCP) | ○ | ○(HTTP chunked) | ×(都度接続) |
| 中間ブローカー | 不要 | 必要 | 不要 | 不要 |
| 省電力 | △ | ○ | △ | × |
| 組み込み適合 | ○ | ◎ | △ | △ |
MQTTはPub/Subブローカーを介した非同期モデルで、多数デバイスのスケーリングに優れます。WebSocketはポイント・ツー・ポイントの直接接続で、ブラウザとの相性が抜群です。REST APIと組み合わせ、定期データはREST、リアルタイム通知はWebSocketという使い分けが一般的です。