在C#的Socket长连接开发中,网络波动、客户端异常退出等情况都可能导致连接无声断开,此时如果没有心跳检测机制,服务端和客户端都无法及时感知连接失效,会造成资源浪费和业务异常。心跳检测通过定时发送小数据包并等待响应,来确认连接是否存活,结合断线自动重连机制,能大幅提升长连接应用的可靠性。

TCP心跳检测的核心逻辑
TCP心跳检测的本质是通信双方约定一个固定的时间间隔,由一方或双方定时发送特定格式的数据包(即心跳包),接收方收到后返回对应的响应包。如果发送方在设定的超时时间内没有收到响应,就判定当前连接已经断开。
心跳包的设计不需要携带复杂的业务数据,通常只需要一个固定的标识即可,比如约定心跳包的内容为Heartbeat,响应包内容为Heartbeat_ACK,这样可以减少网络传输开销。
客户端实现心跳检测的步骤
- 建立Socket连接后,启动一个独立的线程或定时器,按照设定的间隔发送心跳包
- 发送心跳包后记录发送时间,同时启动超时计时
- 收到服务端的响应包后,重置超时计时器,更新最后收到响应的时间
- 如果超时时间内没有收到响应,触发断线处理逻辑
服务端实现心跳检测的步骤
- 接收客户端连接后,记录客户端的最后活跃时间
- 每次收到客户端的任意数据包(包括心跳包和业务包),都更新该客户端的最后活跃时间
- 启动一个定时扫描线程,遍历所有连接的客户端,判断当前时间与最后活跃时间的差值是否超过设定的超时阈值
- 如果超过阈值,判定客户端连接失效,主动关闭对应的Socket并释放资源
断线自动重连的实现思路
断线自动重连需要在检测到连接断开后,按照设定的策略重新尝试建立连接,避免频繁重试对系统造成压力。常见的重试策略是指数退避,即每次重试的间隔时间逐步增加,比如第一次重试间隔1秒,第二次2秒,第三次4秒,直到达到最大重试间隔后保持固定间隔重试。
重连成功后,需要重新启动心跳检测线程,恢复正常的通信逻辑,同时可以根据业务需求同步之前未完成的操作。
完整示例代码
客户端实现代码
以下是C#客户端实现心跳检测和断线自动重连的完整代码:
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;
public class TcpClientWithHeartbeat
{
// 服务端IP和端口
private string serverIp = "127.0.0.1";
private int serverPort = 9000;
// 心跳发送间隔,单位毫秒
private int heartbeatInterval = 3000;
// 心跳超时时间,单位毫秒
private int heartbeatTimeout = 5000;
// 最大重连间隔,单位毫秒
private int maxReconnectInterval = 30000;
// 当前重连间隔
private int currentReconnectInterval = 1000;
private Socket clientSocket;
private Thread heartbeatThread;
private Thread receiveThread;
private bool isConnected = false;
private DateTime lastHeartbeatResponseTime;
private ManualResetEvent reconnectEvent = new ManualResetEvent(false);
// 启动客户端
public void Start()
{
ConnectToServer();
// 启动重连监控线程
Thread reconnectThread = new Thread(ReconnectLoop);
reconnectThread.IsBackground = true;
reconnectThread.Start();
}
// 连接服务端
private void ConnectToServer()
{
try
{
clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
clientSocket.Connect(serverIp, serverPort);
isConnected = true;
lastHeartbeatResponseTime = DateTime.Now;
Console.WriteLine("连接服务端成功");
// 启动心跳发送线程
heartbeatThread = new Thread(SendHeartbeat);
heartbeatThread.IsBackground = true;
heartbeatThread.Start();
// 启动接收线程
receiveThread = new Thread(ReceiveData);
receiveThread.IsBackground = true;
receiveThread.Start();
// 重置重连间隔
currentReconnectInterval = 1000;
}
catch (Exception ex)
{
Console.WriteLine($"连接服务端失败:{ex.Message}");
isConnected = false;
}
}
// 发送心跳包
private void SendHeartbeat()
{
while (isConnected)
{
try
{
// 发送心跳包
byte[] heartbeatData = Encoding.UTF8.GetBytes("Heartbeat");
clientSocket.Send(heartbeatData);
Console.WriteLine("发送心跳包");
// 检查是否超时
TimeSpan timeSinceLastResponse = DateTime.Now - lastHeartbeatResponseTime;
if (timeSinceLastResponse.TotalMilliseconds > heartbeatTimeout)
{
Console.WriteLine("心跳超时,判定连接断开");
HandleDisconnect();
}
Thread.Sleep(heartbeatInterval);
}
catch (Exception ex)
{
Console.WriteLine($"发送心跳包异常:{ex.Message}");
HandleDisconnect();
}
}
}
// 接收数据
private void ReceiveData()
{
byte[] buffer = new byte[1024];
while (isConnected)
{
try
{
int length = clientSocket.Receive(buffer);
if (length == 0)
{
Console.WriteLine("接收到空数据,判定连接断开");
HandleDisconnect();
break;
}
string receiveStr = Encoding.UTF8.GetString(buffer, 0, length);
// 判断是否为心跳响应
if (receiveStr == "Heartbeat_ACK")
{
lastHeartbeatResponseTime = DateTime.Now;
Console.WriteLine("收到心跳响应");
}
else
{
// 处理业务数据
Console.WriteLine($"收到业务数据:{receiveStr}");
}
}
catch (Exception ex)
{
Console.WriteLine($"接收数据异常:{ex.Message}");
HandleDisconnect();
break;
}
}
}
// 处理断线
private void HandleDisconnect()
{
if (!isConnected) return;
isConnected = false;
try
{
clientSocket.Close();
}
catch { }
// 通知重连线程进行重连
reconnectEvent.Set();
}
// 重连循环
private void ReconnectLoop()
{
while (true)
{
reconnectEvent.WaitOne();
reconnectEvent.Reset();
Console.WriteLine($"开始重连,当前重连间隔:{currentReconnectInterval}毫秒");
Thread.Sleep(currentReconnectInterval);
ConnectToServer();
if (isConnected)
{
// 重连成功,重置重连间隔
currentReconnectInterval = 1000;
}
else
{
// 重连失败,增加重连间隔,不超过最大值
currentReconnectInterval = Math.Min(currentReconnectInterval * 2, maxReconnectInterval);
}
}
}
// 发送业务数据
public void SendBusinessData(string data)
{
if (!isConnected) return;
try
{
byte[] sendData = Encoding.UTF8.GetBytes(data);
clientSocket.Send(sendData);
}
catch (Exception ex)
{
Console.WriteLine($"发送业务数据异常:{ex.Message}");
HandleDisconnect();
}
}
}
服务端实现代码
以下是C#服务端实现心跳检测的代码:
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
public class TcpServerWithHeartbeat
{
// 监听端口
private int listenPort = 9000;
// 心跳超时时间,单位毫秒
private int heartbeatTimeout = 10000;
// 客户端最后活跃时间字典
private Dictionary<Socket, DateTime> clientLastActiveTime = new Dictionary<Socket, DateTime>();
private Socket serverSocket;
private bool isRunning = false;
// 启动服务端
public void Start()
{
serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
serverSocket.Bind(new IPEndPoint(IPAddress.Any, listenPort));
serverSocket.Listen(100);
isRunning = true;
Console.WriteLine($"服务端启动,监听端口:{listenPort}");
// 启动客户端连接接收线程
Thread acceptThread = new Thread(AcceptClient);
acceptThread.IsBackground = true;
acceptThread.Start();
// 启动心跳检测线程
Thread heartbeatCheckThread = new Thread(CheckClientHeartbeat);
heartbeatCheckThread.IsBackground = true;
heartbeatCheckThread.Start();
}
// 接收客户端连接
private void AcceptClient()
{
while (isRunning)
{
try
{
Socket clientSocket = serverSocket.Accept();
Console.WriteLine($"客户端连接成功:{clientSocket.RemoteEndPoint}");
// 记录客户端最后活跃时间
clientLastActiveTime[clientSocket] = DateTime.Now;
// 启动客户端数据接收线程
Thread receiveThread = new Thread(ReceiveClientData);
receiveThread.IsBackground = true;
receiveThread.Start(clientSocket);
}
catch (Exception ex)
{
Console.WriteLine($"接收客户端连接异常:{ex.Message}");
}
}
}
// 接收客户端数据
private void ReceiveClientData(object obj)
{
Socket clientSocket = obj as Socket;
byte[] buffer = new byte[1024];
while (true)
{
try
{
int length = clientSocket.Receive(buffer);
if (length == 0)
{
Console.WriteLine($"客户端断开连接:{clientSocket.RemoteEndPoint}");
RemoveClient(clientSocket);
break;
}
// 更新客户端最后活跃时间
clientLastActiveTime[clientSocket] = DateTime.Now;
string receiveStr = Encoding.UTF8.GetString(buffer, 0, length);
// 判断是否为心跳包
if (receiveStr == "Heartbeat")
{
// 返回心跳响应
byte[] responseData = Encoding.UTF8.GetBytes("Heartbeat_ACK");
clientSocket.Send(responseData);
Console.WriteLine($"收到客户端心跳包,已返回响应:{clientSocket.RemoteEndPoint}");
}
else
{
// 处理业务数据
Console.WriteLine($"收到客户端业务数据:{receiveStr},来自:{clientSocket.RemoteEndPoint}");
}
}
catch (Exception ex)
{
Console.WriteLine($"接收客户端数据异常:{ex.Message},客户端:{clientSocket.RemoteEndPoint}");
RemoveClient(clientSocket);
break;
}
}
}
// 检测客户端心跳
private void CheckClientHeartbeat()
{
while (isRunning)
{
Thread.Sleep(5000);
List<Socket> needRemoveClients = new List<Socket>();
DateTime now = DateTime.Now;
foreach (var item in clientLastActiveTime)
{
TimeSpan timeSinceLastActive = now - item.Value;
if (timeSinceLastActive.TotalMilliseconds > heartbeatTimeout)
{
Console.WriteLine($"客户端心跳超时,断开连接:{item.Key.RemoteEndPoint}");
needRemoveClients.Add(item.Key);
}
}
foreach (var client in needRemoveClients)
{
RemoveClient(client);
}
}
}
// 移除客户端
private void RemoveClient(Socket clientSocket)
{
if (clientLastActiveTime.ContainsKey(clientSocket))
{
clientLastActiveTime.Remove(clientSocket);
}
try
{
clientSocket.Close();
}
catch { }
}
}
注意事项
- 心跳间隔和超时时间需要根据实际业务场景调整,间隔过短会增加网络开销,过长会导致断线感知不及时
- 重连策略要避免无限快速重试,否则会在网络异常时占用大量系统资源
- 多线程操作共享资源时需要注意线程安全,比如客户端字典的读写需要加锁
- Socket操作需要做好异常处理,避免异常导致程序崩溃
实际开发中可以根据需求扩展心跳包的内容,比如加入客户端标识、时间戳等信息,方便排查问题。如果使用的是异步Socket,可以将心跳逻辑和接收逻辑整合到异步回调中,减少线程开销。