クライアント配信
クライアントから送信されたメッセージが常にサーバーに届くようにする方法を見ていきましょう。
デフォルトでは、Socket.IO は「最大1回」の配信保証(「発射して忘れる」とも呼ばれる)を提供します。これは、メッセージがサーバーに到達しなかった場合に再試行が行われないことを意味します。
バッファリングされたイベント
クライアントが切断されると、socket.emit() の呼び出しは、再接続されるまでバッファリングされます。
上記の動画では、「リアルタイム」メッセージは接続が再確立されるまでバッファリングされます。
この動作は、アプリケーションにとって完全に十分な場合があります。ただし、メッセージが失われる可能性のあるケースがいくつかあります。
- イベントの送信中に接続が切断された
- イベントの処理中にサーバーがクラッシュまたは再起動された
- データベースが一時的に利用できない
少なくとも1回
「少なくとも1回」の保証を実装できます。
- 確認応答を使用して手動で
function emit(socket, event, arg) {
  socket.timeout(5000).emit(event, arg, (err) => {
    if (err) {
      // no ack from the server, let's retry
      emit(socket, event, arg);
    }
  });
}
emit(socket, 'hello', 'world');
- または retriesオプションを使用
const socket = io({
  ackTimeout: 10000,
  retries: 3
});
socket.emit('hello', 'world');
どちらの場合も、クライアントはサーバーから確認応答を受信するまでメッセージの送信を再試行します。
io.on('connection', (socket) => {
  socket.on('hello', (value, callback) => {
    // once the event is successfully handled
    callback();
  });
})
retries オプションを使用すると、メッセージはキューに入れられ、1つずつ送信されるため、メッセージの順序が保証されます。これは、最初のオプションの場合ではありません。
正確に1回
再試行の問題は、サーバーが同じメッセージを複数回受信する可能性があるため、各メッセージを一意に識別し、データベースに1回だけ保存する方法が必要になることです。
チャットアプリケーションで「正確に1回」の保証を実装する方法を見ていきましょう。
まず、クライアント側で各メッセージに一意の識別子を割り当てることから始めます。
- ES6
- ES5
<script>
  let counter = 0;
  const socket = io({
    auth: {
      serverOffset: 0
    },
    // enable retries
    ackTimeout: 10000,
    retries: 3,
  });
  const form = document.getElementById('form');
  const input = document.getElementById('input');
  const messages = document.getElementById('messages');
  form.addEventListener('submit', (e) => {
    e.preventDefault();
    if (input.value) {
      // compute a unique offset
      const clientOffset = `${socket.id}-${counter++}`;
      socket.emit('chat message', input.value, clientOffset);
      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>
<script>
  var counter = 0;
  var socket = io({
    auth: {
      serverOffset: 0
    },
    // enable retries
    ackTimeout: 10000,
    retries: 3,
  });
  var form = document.getElementById('form');
  var input = document.getElementById('input');
  var messages = document.getElementById('messages');
  form.addEventListener('submit', function(e) {
    e.preventDefault();
    if (input.value) {
      // compute a unique offset
      var clientOffset = `${socket.id}-${counter++}`;
      socket.emit('chat message', input.value, clientOffset);
      input.value = '';
    }
  });
  socket.on('chat message', function(msg, serverOffset) {
    var item = document.createElement('li');
    item.textContent = msg;
    messages.appendChild(item);
    window.scrollTo(0, document.body.scrollHeight);
    socket.auth.serverOffset = serverOffset;
  });
</script>
socket.id 属性は、各接続に割り当てられるランダムな 20 文字の識別子です。
また、getRandomValues() を使用して一意のオフセットを生成することもできました。
そして、このオフセットをサーバー側のメッセージと一緒に保存します。
// [...]
io.on('connection', async (socket) => {
  socket.on('chat message', async (msg, clientOffset, callback) => {
    let result;
    try {
      result = await db.run('INSERT INTO messages (content, client_offset) VALUES (?, ?)', msg, clientOffset);
    } catch (e) {
      if (e.errno === 19 /* SQLITE_CONSTRAINT */ ) {
        // the message was already inserted, so we notify the client
        callback();
      } else {
        // nothing to do, just let the client retry
      }
      return;
    }
    io.emit('chat message', msg, result.lastID);
    // acknowledge the event
    callback();
  });
  if (!socket.recovered) {
    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
    }
  }
});
// [...]
このようにして、client_offset 列の UNIQUE 制約により、メッセージの重複が防止されます。
イベントを確認応答することを忘れないでください。そうしないと、クライアントは(retries 回まで)再試行し続けます。
socket.on('chat message', async (msg, clientOffset, callback) => {
  // ... and finally
  callback();
});
繰り返しますが、デフォルトの保証(「最大1回」)はアプリケーションにとって十分な場合がありますが、これでより信頼性が高くなる方法がわかりました。
次のステップでは、アプリケーションを水平方向にスケーリングする方法を見ていきます。
- CommonJS
- ES modules
この例は、ブラウザで直接実行できます。
この例は、ブラウザで直接実行できます。