メインコンテンツにスキップ
バージョン: 4.x

サーバー配信

再接続時にクライアントの状態を同期するには、2つの一般的な方法があります

  • サーバーが状態全体を送信する
  • または、クライアントが最後に処理したイベントを追跡し、サーバーが欠落している部分を送信する

どちらも完全に有効な解決策であり、どちらを選択するかはユースケースによって異なります。このチュートリアルでは、後者を使用します。

まず、チャットアプリケーションのメッセージを永続化しましょう。今日では、多くの優れたオプションがあります。ここでは SQLite を使用します。

ヒント

SQLite に詳しくない場合は、こちらのように、オンラインで多くのチュートリアルが利用可能です。

必要なパッケージをインストールしましょう

npm install sqlite sqlite3

各メッセージを SQL テーブルに保存するだけです

index.js
const express = require('express');
const { createServer } = require('node:http');
const { join } = require('node:path');
const { Server } = require('socket.io');
const sqlite3 = require('sqlite3');
const { open } = require('sqlite');

async function main() {
// open the database file
const db = await open({
filename: 'chat.db',
driver: sqlite3.Database
});

// create our 'messages' table (you can ignore the 'client_offset' column for now)
await db.exec(`
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_offset TEXT UNIQUE,
content TEXT
);
`);

const app = express();
const server = createServer(app);
const io = new Server(server, {
connectionStateRecovery: {}
});

app.get('/', (req, res) => {
res.sendFile(join(__dirname, 'index.html'));
});

io.on('connection', (socket) => {
socket.on('chat message', async (msg) => {
let result;
try {
// store the message in the database
result = await db.run('INSERT INTO messages (content) VALUES (?)', msg);
} catch (e) {
// TODO handle the failure
return;
}
// include the offset with the message
io.emit('chat message', msg, result.lastID);
});
});

server.listen(3000, () => {
console.log('server running at http://localhost:3000');
});
}

main();

クライアントはオフセットを追跡します

index.html
<script>
const socket = io({
auth: {
serverOffset: 0
}
});

const form = document.getElementById('form');
const input = document.getElementById('input');
const messages = document.getElementById('messages');

form.addEventListener('submit', (e) => {
e.preventDefault();
if (input.value) {
socket.emit('chat message', input.value);
input.value = '';
}
});

socket.on('chat message', (msg, serverOffset) => {
const item = document.createElement('li');
item.textContent = msg;
messages.appendChild(item);
window.scrollTo(0, document.body.scrollHeight);
socket.auth.serverOffset = serverOffset;
});
</script>

そして最後に、サーバーは(再)接続時に欠落しているメッセージを送信します

index.js
// [...]

io.on('connection', async (socket) => {
socket.on('chat message', async (msg) => {
let result;
try {
result = await db.run('INSERT INTO messages (content) VALUES (?)', msg);
} catch (e) {
// TODO handle the failure
return;
}
io.emit('chat message', msg, result.lastID);
});

if (!socket.recovered) {
// if the connection state recovery was not successful
try {
await db.each('SELECT id, content FROM messages WHERE id > ?',
[socket.handshake.auth.serverOffset || 0],
(_err, row) => {
socket.emit('chat message', row.content, row.id);
}
)
} catch (e) {
// something went wrong
}
}
});

// [...]

実際に見てみましょう

上のビデオでわかるように、一時的な切断とフルページリロードの両方で機能します。

ヒント

"接続状態の復元" 機能との違いは、正常な復元ではメインデータベースにアクセスする必要がない可能性があることです(たとえば、Redisストリームからメッセージをフェッチする場合があります)。

さて、クライアント配信について話しましょう。