概述
在实时通信系统中,确保消息的可靠传输是非常重要的。超时重传协议是一种常见的机制,用于在网络不稳定或消息丢失的情况下保证消息的最终送达。本文将详细介绍超时重传协议的工作原理,并提供一种基于SignalR的实现方案。此外,还将讨论如何优化该协议以降低服务器负载。
工作原理
超时重传协议的基本思想是在发送消息后设置一个定时器。如果在指定的时间内(例如10秒)没有收到确认(ACK),则认为消息未成功送达,并重新发送该消息。这种机制可以有效地应对网络延迟和丢包问题。
[Hub]
服务端使用SignalR库来处理客户端的连接和消息传递。以下是关键部分的代码实现:
using Microsoft.AspNetCore.SignalR;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
public class WebsocketLib : Hub
{
// 存储待处理消息的计时器
private readonly Dictionary<string, Timer> _pendingMessages = new Dictionary<string, Timer>();
// 设置最大重试次数
private readonly int maxRetries = 3;
// 发送单条消息的方法
public async Task EchoSingleMessage(string sender, string receiver, string message)
{
// 生成唯一的消息ID
var messageId = Guid.NewGuid().ToString();
int retryCount = 0;
// 启动计时器以检查ACK
var timer = new Timer(async state =>
{
// 如果计时器不在待处理消息字典中,则直接返回
if (!_pendingMessages.ContainsKey(messageId))
{
return;
}
// 如果在10秒内没有收到ACK,重新发送消息
if (retryCount < maxRetries)
{
//发送信息
await WebsocketLib.SendMessage(sender, receiver, message,messageId);
retryCount++;
}
else
{ // 重传失败需要记录到数据库
WebsocketLib.logFailMessgae(sender, receiver, messagemessageId);
// 取消计时器
_pendingMessages[messageId].Change(-1, 0);
// 从待处理消息字典中移除计时器
_pendingMessages.Remove(messageId);
}
}, null, TimeSpan.FromSeconds(10), TimeSpan.Zero);
// 将计时器添加到待处理消息字典中
_pendingMessages[messageId] = timer;
}
// 接收ACK消息的方法
public async Task AcknowledgeMessage(string ack)
{
// 从ACK消息中提取消息ID
var parts = ackMessage.Split(':');
var messageId = parts[0];
// 如果消息ID存在于待处理消息字典中
if (_pendingMessages.ContainsKey(messageId))
{
// 取消计时器
_pendingMessages[messageId].Change(-1, 0);
// 从待处理消息字典中移除计时器
_pendingMessages.Remove(messageId);
Console.WriteLine($"Received ACK for message: {ackMessage}");
}
}
}
Clients
客户端使用SignalR库连接到服务端,并处理接收到的消息。以下是关键部分的代码实现:
<script>
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chat")
.build();
connection.on("receiveMessage", (message) => {
console.log(`Received Message: ${message}`);
if(message.messageId){
//回传messageId
setTimeout(() => {
connection.invoke("AcknowledgeMessage", `${messageId}:ACK`).catch(err => console.error(err));
}, 5000);
}
});
async function start() {
try {
await connection.start();
console.log("SignalR Connected.");
} catch (err) {
console.error(err.toString());
}
}
connection.onclose(async () => {
await start();
});
start();
</script>
希望对大家有所帮助。