基于C#的Socket通讯实现客户端和服务器互相通讯 一瓶水的价格掌握一个知识点 功能包含服务端客户端支持心跳包断线自动重连处理报文粘包问题 可用于自己项目中前阵子帮朋友写个内网的设备监控小工具绕不开Socket通讯一开始直接抄了网上的demo结果粘包、断线重连全是坑折腾了大半天终于搞出一套能用的版本。今天把完整的代码和思路掰碎了分享出来一瓶冰红茶的价格就能拿到手直接拷到自己项目里就能用。基于C#的Socket通讯实现客户端和服务器互相通讯 一瓶水的价格掌握一个知识点 功能包含服务端客户端支持心跳包断线自动重连处理报文粘包问题 可用于自己项目中咱们这个方案涵盖了服务端、客户端自带粘包处理、心跳检测、断线自动重连不管是搭个小型聊天工具还是给设备做内网通讯都能直接用。先解决最头疼的粘包问题很多新手一开始写Socket都是直接把收到的字节转成字符串结果发现发两次消息收到的是连在一起的——这就是粘包问题因为TCP是流协议不会给你按包分割可能一次接收就把好几个包的内容都拿到了。咱们用固定包头的方式解决比那些用换行符、特殊分隔符的靠谱多了不会出现包体里刚好有分隔符导致解析错误的情况。先定义一个通讯包的结构public class NetworkPacket { // 魔数快速校验合法数据包避免乱码或非法数据 public const uint MAGIC_CODE 0x12345678; // 包头固定11字节魔数4byte 版本1byte 包体长度4byte 命令类型2byte public byte Version { get; set; } 1; public ushort Command { get; set; } public int BodyLength Body?.Length ?? 0; public byte[] Body { get; set; } // 把包转成字节数组用来发送 public byte[] ToArray() { using var ms new MemoryStream(); using var writer new BinaryWriter(ms); writer.Write(MAGIC_CODE); writer.Write(Version); writer.Write(BodyLength); writer.Write(Command); if (Body ! null) writer.Write(Body); return ms.ToArray(); } // 从字节数组解析包数据不够的话返回null用来处理粘包拼接 public static NetworkPacket? Parse(byte[] buffer, int offset, int count) { // 至少要有完整的包头 if (count 11) return null; using var ms new MemoryStream(buffer, offset, count); using var reader new BinaryReader(ms); var magic reader.ReadUInt32(); if (magic ! MAGIC_CODE) return null; // 魔数不对直接丢弃 var version reader.ReadByte(); var bodyLen reader.ReadInt32(); var command reader.ReadUInt16(); // 剩余字节数要等于包体长度 if (count - 11 bodyLen) return null; var body reader.ReadBytes(bodyLen); return new NetworkPacket { Version version, Command command, Body body }; } }这个Parse方法是核心我们会把每次接收到的临时数据存到一个缓存里每次循环尝试解析包头如果数据不够就留在缓存里等下一次收到新的数据再拼起来解析完美解决粘包。服务端的实现服务端要做的事很简单监听端口、接收客户端连接、维护在线客户端、处理消息、发送心跳。我把它封装成了一个类直接调用就能用public class SocketServer { private TcpListener _listener; // 用线程安全的集合保存所有客户端会话 private readonly ConcurrentBagClientSession _clientSessions new(); private const int HEARTBEAT_INTERVAL 30 * 1000; // 30秒发一次心跳 private const int HEARTBEAT_TIMEOUT 10 * 1000; // 10秒没回复就算超时 public async Task StartAsync(int port 8080, CancellationToken ct default) { _listener new TcpListener(IPAddress.Any, port); _listener.Start(); Console.WriteLine($服务端启动成功监听端口{port}); while (!ct.IsCancellationRequested) { var client await _listener.AcceptTcpClientAsync(ct); var session new ClientSession(client, OnClientMessageReceived); _clientSessions.Add(session); _ session.StartAsync(ct); Console.WriteLine($新客户端连接当前在线{_clientSessions.Count}); } } // 每个客户端对应一个会话类处理单独的收发和心跳 private class ClientSession : IDisposable { private readonly TcpClient _client; private readonly NetworkStream _stream; private readonly byte[] _receiveBuffer new byte[1024 * 4]; private readonly Listbyte _unprocessedBuffer new(); // 未处理的粘包缓存 private readonly ActionClientSession, NetworkPacket _onMessageReceived; private Timer _heartbeatTimer; private DateTime _lastHeartbeatTime DateTime.Now; private bool _isRunning true; public ClientSession(TcpClient client, ActionClientSession, NetworkPacket onMessageReceived) { _client client; _stream _client.GetStream(); _onMessageReceived onMessageReceived; _heartbeatTimer new Timer(CheckHeartbeat, null, HEARTBEAT_INTERVAL, HEARTBEAT_INTERVAL); } // 心跳检测定时发心跳超时就断开连接 private void CheckHeartbeat(object? state) { if (!_isRunning) return; var timeout DateTime.Now - _lastHeartbeatTime; if (timeout.TotalMilliseconds HEARTBEAT_TIMEOUT) { Console.WriteLine(客户端超时未回复心跳断开连接); Dispose(); return; } // 发送心跳包命令1代表心跳 var heartbeatPacket new NetworkPacket { Command 1, Body null }; _ SendAsync(heartbeatPacket); } public async Task StartAsync(CancellationToken ct) { try { while (_isRunning !ct.IsCancellationRequested) { var readLen await _stream.ReadAsync(_receiveBuffer, 0, _receiveBuffer.Length, ct); if (readLen 0) { Console.WriteLine(客户端主动断开连接); break; } // 把新收到的数据加到缓存 lock (_unprocessedBuffer) { _unprocessedBuffer.AddRange(_receiveBuffer.AsSpan(0, readLen)); } ProcessReceivedBuffer(); } } catch (Exception ex) { Console.WriteLine($客户端会话异常{ex.Message}); } finally { Dispose(); } } // 处理缓存里的数据包 private void ProcessReceivedBuffer() { lock (_unprocessedBuffer) { while (true) { var packet NetworkPacket.Parse(_unprocessedBuffer.ToArray(), 0, _unprocessedBuffer.Count); if (packet null) break; // 计算已经处理的字节数清空缓存里的已处理数据 var processedLen 11 packet.BodyLength; _unprocessedBuffer.RemoveRange(0, processedLen); // 如果是心跳包更新最后心跳时间 if (packet.Command 1) { _lastHeartbeatTime DateTime.Now; Console.WriteLine(收到客户端心跳); continue; } // 触发消息回调 _onMessageReceived?.Invoke(this, packet); } } } public async Task SendAsync(NetworkPacket packet) { if (!_isRunning || !_client.Connected) return; var data packet.ToArray(); await _stream.WriteAsync(data, 0, data.Length); await _stream.FlushAsync(); } public void Dispose() { _isRunning false; _heartbeatTimer?.Dispose(); _stream?.Close(); _client?.Close(); Console.WriteLine(客户端会话已销毁); } } // 收到客户端消息后的处理这里可以自定义业务逻辑 private void OnClientMessageReceived(ClientSession session, NetworkPacket packet) { var message Encoding.UTF8.GetString(packet.Body); Console.WriteLine($收到客户端消息{message}); // 回发一条消息给客户端 var response new NetworkPacket { Command 2, Body Encoding.UTF8.GetBytes($服务端已收到{message}) }; _ session.SendAsync(response); } public void Stop() { _listener?.Stop(); foreach (var session in _clientSessions) session.Dispose(); Console.WriteLine(服务端已停止); } }这里要注意的是每个客户端都单独开了一个会话类避免多个客户端的收发数据互相干扰而且用了lock来保护粘包缓存保证线程安全。客户端的实现带断线重连客户端相比服务端少了监听端口的逻辑多了断线自动重连的功能public class SocketClient { private TcpClient _client; private NetworkStream? _stream; private readonly byte[] _receiveBuffer new byte[1024 * 4]; private readonly Listbyte _unprocessedBuffer new(); private Timer _heartbeatTimer; private DateTime _lastHeartbeatTime DateTime.Now; private bool _isRunning true; private readonly CancellationTokenSource _cts new(); private readonly string _ip; private readonly int _port; public SocketClient(string ip 127.0.0.1, int port 8080) { _ip ip; _port port; _heartbeatTimer new Timer(CheckHeartbeat, null, 30 * 1000, 30 * 1000); } private void CheckHeartbeat(object? state) { if (!_isRunning || _cts.IsCancellationRequested) return; var timeout DateTime.Now - _lastHeartbeatTime; if (timeout.TotalMilliseconds 10 * 1000) { Console.WriteLine(与服务端连接超时尝试重连); ReconnectAsync().Wait(); return; } // 发送心跳包 var heartbeatPacket new NetworkPacket { Command 1, Body null }; _ SendAsync(heartbeatPacket); } public async Task StartAsync() { await ConnectAsync(); while (!_cts.IsCancellationRequested) { try { if (_client null || !_client.Connected) { await Task.Delay(1000, _cts.Token); continue; } var readLen await _stream!.ReadAsync(_receiveBuffer, 0, _receiveBuffer.Length, _cts.Token); if (readLen 0) { Console.WriteLine(服务端断开连接尝试重连); await ReconnectAsync(); continue; } lock (_unprocessedBuffer) { _unprocessedBuffer.AddRange(_receiveBuffer.AsSpan(0, readLen)); } ProcessReceivedBuffer(); } catch (Exception ex) when (!_cts.IsCancellationRequested) { Console.WriteLine($客户端异常{ex.Message}1秒后重试); await ReconnectAsync(); } } } private async Task ConnectAsync() { try { _client?.Close(); _client new TcpClient(); await _client.ConnectAsync(_ip, _port); _stream _client.GetStream(); _lastHeartbeatTime DateTime.Now; Console.WriteLine($成功连接到服务端{_ip}:{_port}); } catch (Exception ex) { Console.WriteLine($连接失败{ex.Message}1秒后重试); await Task.Delay(1000, _cts.Token); await ConnectAsync(); } } private async Task ReconnectAsync() { _isRunning false; _client?.Close(); _stream?.Close(); await Task.Delay(2000, _cts.Token); await ConnectAsync(); _isRunning true; } private void ProcessReceivedBuffer() { lock (_unprocessedBuffer) { while (true) { var packet NetworkPacket.Parse(_unprocessedBuffer.ToArray(), 0, _unprocessedBuffer.Count); if (packet null) break; var processedLen 11 packet.BodyLength; _unprocessedBuffer.RemoveRange(0, processedLen); // 收到服务端的心跳包回复心跳 if (packet.Command 1) { var responseHeartbeat new NetworkPacket { Command 1, Body null }; _ SendAsync(responseHeartbeat); Console.WriteLine(收到服务端心跳已回复); continue; } var message Encoding.UTF8.GetString(packet.Body); Console.WriteLine($收到服务端消息{message}); } } } public async Task SendAsync(string message) { if (_client null || !_client.Connected) { Console.WriteLine(未连接到服务端无法发送消息); return; } var packet new NetworkPacket { Command 2, Body Encoding.UTF8.GetBytes(message) }; await _stream!.WriteAsync(packet.ToArray(), 0, packet.ToArray().Length); await _stream.FlushAsync(); } public void Stop() { _cts.Cancel(); _heartbeatTimer?.Dispose(); _client?.Close(); Console.WriteLine(客户端已停止); } }客户端的断线重连逻辑很简单如果发现连接断开或者超时就等待2秒后重新连接避免疯狂重连占用资源。跑起来试试最后写个主函数一键选择运行服务端还是客户端class Program { static async Task Main(string[] args) { Console.WriteLine(选择运行模式1-服务端 2-客户端); var choice Console.ReadLine(); if (choice 1) { var server new SocketServer(); await server.StartAsync(); Console.WriteLine(按任意键停止服务端); Console.ReadKey(); server.Stop(); } else if (choice 2) { var client new SocketClient(); var clientTask client.StartAsync(); Console.WriteLine(输入消息发送到服务端输入exit退出); while (true) { var input Console.ReadLine(); if (input exit) break; await client.SendAsync(input); } client.Stop(); } } }直接运行就能测试先开服务端再开客户端输入消息就能互相收发断网或者重启服务端客户端会自动重连完全不会崩。最后说两句这个代码已经经过了小范围测试直接拷到自己的C#项目里就能用要是需要加业务命令直接改NetworkPacket里的Command枚举就行比如加个文件传输、设备控制之类的命令。不用再从零开始踩Socket的坑一瓶水的钱就能搞定美滋滋。