监控面板上的“连接断开”提示,突然发现游戏服务器的玩家都在“消失”——原来TCP连接在深夜的网络波动中“猝死”了!
核心概念:
{"type": "heartbeat"}
)HeartbeatTimeout
秒未收到心跳响应,触发断开2^retry * baseDelay
)重启连接步骤1:实现基础TCP连接
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
public class TcpClientWithHeartbeat
{
private TcpClient _client;
private NetworkStream _stream;
private bool _isConnected = false;
private readonly object _lock = new object(); // 线程安全锁
// 连接服务器
public async Task ConnectAsync(string host, int port)
{
try
{
_client = new TcpClient();
await _client.ConnectAsync(host, port);
_stream = _client.GetStream();
_isConnected = true;
Console.WriteLine("Connected to server!");
}
catch (Exception ex)
{
Console.WriteLine($"Connection failed: {ex.Message}");
_isConnected = false;
}
}
// 断开连接
public void Disconnect()
{
lock (_lock)
{
if (_client != null)
{
_client.Close();
_stream = null;
_isConnected = false;
}
}
}
}
步骤2:心跳包的发送与接收
// 实现心跳包发送
public async Task StartHeartbeatAsync(CancellationToken cancellationToken)
{
const int heartbeatInterval = 5000; // 5秒发送一次
while (!_cancellationToken.IsCancellationRequested)
{
try
{
if (_isConnected)
{
// 发送心跳包(示例:JSON格式)
var heartbeat = "{\"type\": \"heartbeat\"}";
await SendDataAsync(heartbeat);
// 等待响应(超时检测)
var response = await ReceiveDataAsync(heartbeatTimeout: 3000);
if (response != null && response.Contains("heartbeat"))
Console.WriteLine("Heartbeat acknowledged!");
else
throw new Exception("Heartbeat failed!");
}
await Task.Delay(heartbeatInterval, cancellationToken);
}
catch (Exception ex)
{
HandleConnectionFailure(ex); // 触发重连
break;
}
}
}
// 发送数据
private async Task SendDataAsync(string data)
{
if (_stream == null || !_stream.CanWrite) return;
var buffer = System.Text.Encoding.UTF8.GetBytes(data);
await _stream.WriteAsync(buffer, 0, buffer.Length);
}
// 接收数据
private async Task<string> ReceiveDataAsync(int heartbeatTimeout)
{
if (_stream == null || !_stream.CanRead) return null;
var buffer = new byte[1024];
var bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None);
if (bytesRead == 0) return null;
return System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead);
}
步骤3:自动重连的“指数退避算法”
// 自动重连逻辑
private async void HandleConnectionFailure(Exception ex)
{
Console.WriteLine($"Connection failed: {ex.Message}. Retrying...");
// 指数退避:第n次重试等待时间 = 2^n * baseDelay
int retryCount = 0;
const int baseDelay = 1000; // 基础延迟1秒
while (true)
{
try
{
await ConnectAsync("server.com", 8080);
if (_isConnected)
{
Console.WriteLine("Reconnected successfully!");
// 重启心跳
await StartHeartbeatAsync(CancellationToken.None);
return;
}
}
catch { }
// 等待下一次重试
var delay = (int)(Math.Pow(2, retryCount) * baseDelay);
await Task.Delay(delay);
retryCount++;
}
}
技巧1:动态调整心跳间隔
// 根据网络延迟动态调整心跳间隔
private async Task AdaptiveHeartbeat()
{
int measuredLatency = 0; // 通过Ping获取延迟
int baseInterval = 5000;
while (true)
{
try
{
await SendDataAsync("{\"type\": \"ping\"}");
await Task.Delay(1000); // 等待响应
measuredLatency = // 通过响应计算延迟
var newInterval = baseInterval + measuredLatency * 2;
await Task.Delay(newInterval);
}
catch { }
}
}
技巧2:业务数据“心跳化”
// 在业务数据中嵌入心跳标记
public async Task SendBusinessDataAsync(string data)
{
var packet = new
{
type = "business",
payload = data,
heartbeat = DateTime.UtcNow.Ticks // 添加心跳时间戳
};
await SendDataAsync(JsonConvert.SerializeObject(packet));
}
// 服务端解析时同时检测心跳
public void HandlePacket(Packet packet)
{
if (packet.type == "business")
ProcessBusiness(packet.payload);
else if (packet.type == "heartbeat")
RecordHeartbeat(packet.heartbeat); // 记录心跳时间戳
}
技巧3:多线程安全
// 使用Monitor保证线程安全
private async Task SendDataThreadSafe(string data)
{
lock (_lock)
{
if (!_isConnected) return;
var buffer = System.Text.Encoding.UTF8.GetBytes(data);
await _stream.WriteAsync(buffer, 0, buffer.Length);
}
}
问题1:心跳包被防火墙拦截?
// 加密心跳包或伪装成业务数据
private string EncryptHeartbeat(string data)
{
// 使用AES加密
using var aes = Aes.Create();
var encryptor = aes.CreateEncryptor();
using var ms = new MemoryStream();
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
{
cs.Write(Encoding.UTF8.GetBytes(data));
}
return Convert.ToBase64String(ms.ToArray());
}
问题2:重连时数据丢失?
// 使用消息队列暂存未发送的数据
private readonly ConcurrentQueue<string> _pendingMessages = new();
public async Task SendDataAsync(string data)
{
if (_isConnected)
{
await SendDataThreadSafe(data);
}
else
{
_pendingMessages.Enqueue(data); // 暂存未发送的消息
}
}
// 重连成功后重发
private async void OnReconnected()
{
while (_pendingMessages.TryDequeue(out var msg))
{
await SendDataThreadSafe(msg);
}
}
通过本文的方案,你的TCP客户端可以:
未来展望: