客戶端傳遞
讓我們看看如何確保伺服器始終收到客戶端傳送的訊息。
資訊
預設情況下,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
選項時,訊息順序有保證,因為訊息會排隊並逐一傳送。第一個選項並非如此。
完全一次
重試的問題在於伺服器現在可能會多次收到同一則訊息,因此需要一種方式來唯一識別每則訊息,並只將它儲存在資料庫中一次。
讓我們看看如何在聊天應用程式中實作「完全一次」保證。
我們會從在用戶端為每則訊息指定唯一識別碼開始
- ES6
- ES5
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>
index.html
<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()
來產生唯一的偏移量。
然後我們將此偏移量與伺服器端的訊息一起儲存
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();
});
資訊
再次強調,預設保證(「最多一次」)可能足以應付您的應用程式,但現在您知道如何讓它更可靠了。
在下一步中,我們將了解如何水平擴充我們的應用程式。
資訊
- CommonJS
- ES 模組
您可以在下列位置的瀏覽器中直接執行此範例:
您可以在下列位置的瀏覽器中直接執行此範例: