跳到主要內容
版本:4.x

客戶端傳遞

讓我們看看如何確保伺服器始終收到客戶端傳送的訊息。

資訊

預設情況下,Socket.IO 提供「至多一次」的傳遞保證(也稱為「發射後遺忘」),這表示如果訊息未傳遞到伺服器,將不會重試。

緩衝事件

當客戶端斷線時,對 socket.emit() 的任何呼叫都會緩衝,直到重新連線

在上面的影片中,「即時」訊息會緩衝,直到重新建立連線為止。

這種行為可能完全足以滿足您的應用程式。不過,在以下幾種情況下,訊息可能會遺失

  • 傳送事件時,連線中斷
  • 處理事件時,伺服器發生故障或重新啟動
  • 資料庫暫時無法使用

至少一次

我們可以實作「至少一次」保證

  • 手動確認
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 選項時,訊息順序有保證,因為訊息會排隊並逐一傳送。第一個選項並非如此。

完全一次

重試的問題在於伺服器現在可能會多次收到同一則訊息,因此需要一種方式來唯一識別每則訊息,並只將它儲存在資料庫中一次。

讓我們看看如何在聊天應用程式中實作「完全一次」保證。

我們會從在用戶端為每則訊息指定唯一識別碼開始

index.html
<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>
注意

socket.id 屬性是隨機的 20 個字元識別碼,會指定給每個連線。

我們也可以使用 getRandomValues() 來產生唯一的偏移量。

然後我們將此偏移量與伺服器端的訊息一起儲存

index.js
// [...]

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();
});
資訊

再次強調,預設保證(「最多一次」)可能足以應付您的應用程式,但現在您知道如何讓它更可靠了。

在下一步中,我們將了解如何水平擴充我們的應用程式。

資訊

您可以在下列位置的瀏覽器中直接執行此範例: