三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

C# Socket编程:工业级TCP通讯与文件传输实战

C# Socket编程:工业级TCP通讯与文件传输实战

1. 项目概述:Socket通讯的核心价值与应用场景

在工业控制、物联网设备管理和分布式系统中,稳定可靠的网络通讯是核心基础设施。C#的Socket编程提供了一套完整的TCP/IP协议栈操作接口,能够实现设备间的长连接通讯、实时数据传输和远程文件交换。不同于HTTP等短连接协议,Socket通讯特别适合需要保持持久连接、高频交互的业务场景。

以工业生产线监控为例,上位机(客户端)需要持续接收来自PLC控制器(服务端)的设备状态数据,同时可能随时下发控制指令或更新程序文件。这种场景下,断线自动重连机制和文件传输功能就成为刚需。当网络波动或设备重启时,系统必须能在无人干预的情况下恢复连接,并确保数据完整性。

2. 核心架构设计

2.1 通讯协议设计

采用自定义的二进制协议帧结构,包含以下字段:

[Header(2B)][Length(4B)][Command(2B)][Data(nB)][CRC(2B)]
  • Header:固定为0xAA55,用于帧同步
  • Length:Data部分的字节长度(含Command)
  • Command:指令类型(如0x0001表示心跳包,0x0002表示文件传输请求)
  • Data:有效载荷数据
  • CRC:CCITT标准的CRC16校验值

这种设计相比纯文本协议(如JSON)具有以下优势:

  1. 数据包体积减少40%-60%
  2. 解析效率提升3-5倍
  3. 通过CRC校验可检测传输错误

2.2 断线检测与重连机制

实现三级断线检测策略:

  1. 心跳检测:每30秒发送心跳包,超时3次未响应判定为断线
  2. Socket错误监听:通过Poll方法检测连接状态
  3. 发送失败重试:连续3次发送失败触发重连

重连算法采用指数退避策略:

int retryCount = 0; while(!IsConnected && retryCount < 5) { Thread.Sleep(1000 * (int)Math.Pow(2, retryCount)); Connect(); retryCount++; }

2.3 文件传输设计

文件传输采用分块机制,每个数据包包含:

  • 文件MD5校验值
  • 当前分块序号
  • 分块数据(默认8KB)
  • 总块数

接收端会:

  1. 创建临时文件
  2. 按序号写入数据块
  3. 接收完成后校验MD5
  4. 重命名临时文件为正式文件

3. 关键代码实现

3.1 Socket服务端实现

public class FileServer { private Socket _listener; private ConcurrentDictionary<string, FileTransfer> _transfers = new(); public void Start(int port) { _listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _listener.Bind(new IPEndPoint(IPAddress.Any, port)); _listener.Listen(10); Task.Run(() => { while(true) { var client = _listener.Accept(); Task.Run(() => HandleClient(client)); } }); } private void HandleClient(Socket client) { var buffer = new byte[1024]; try { while(client.Connected) { int received = client.Receive(buffer); if(received == 0) continue; var packet = ParsePacket(buffer, received); switch(packet.Command) { case 0x0001: // 心跳 SendHeartbeat(client); break; case 0x0002: // 文件传输 ProcessFileTransfer(client, packet.Data); break; } } } catch(SocketException) { client.Dispose(); } } }

3.2 断线重连实现

public class ReconnectableClient { private Timer _heartbeatTimer; private int _failedCount; private Socket _socket; public event Action OnDisconnected; public event Action OnReconnected; public void StartConnect(string ip, int port) { Task.Run(() => { while(true) { try { _socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); _socket.Connect(ip, port); _heartbeatTimer = new Timer(30000); _heartbeatTimer.Elapsed += SendHeartbeat; _heartbeatTimer.Start(); OnReconnected?.Invoke(); StartReceive(); break; } catch { Thread.Sleep(5000); } } }); } private void StartReceive() { var buffer = new byte[8192]; while(true) { try { int received = _socket.Receive(buffer); if(received == 0) throw new SocketException(); _failedCount = 0; ProcessData(buffer, received); } catch { OnDisconnected?.Invoke(); _heartbeatTimer.Stop(); StartConnect(_socket.RemoteEndPoint); break; } } } }

3.3 文件分块传输实现

public void SendFile(string filePath, Socket client) { using var fileStream = File.OpenRead(filePath); byte[] buffer = new byte[8192]; int bytesRead; long totalSent = 0; string md5 = ComputeMD5(filePath); // 发送文件头 var header = new FileHeader { FileName = Path.GetFileName(filePath), FileSize = new FileInfo(filePath).Length, MD5 = md5 }; SendPacket(client, 0x0002, Serialize(header)); // 分块发送 while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) > 0) { var chunk = new FileChunk { Sequence = totalSent / 8192, Data = buffer.Take(bytesRead).ToArray() }; SendPacket(client, 0x0003, Serialize(chunk)); totalSent += bytesRead; } // 发送结束标记 SendPacket(client, 0x0004, Encoding.UTF8.GetBytes(md5)); }

4. 性能优化技巧

4.1 Socket参数调优

_socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); _socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); _socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true);
  • ReuseAddress:允许快速重用端口
  • KeepAlive:启用TCP保活机制
  • NoDelay:禁用Nagle算法,减少小包延迟

4.2 异步IO优化

使用SocketAsyncEventArgs实现高性能异步IO:

public class AsyncSocket { private Stack<SocketAsyncEventArgs> _argsPool = new(); public void StartReceive(Socket socket) { if(!_argsPool.TryPop(out var args)) { args = new SocketAsyncEventArgs(); args.SetBuffer(new byte[8192], 0, 8192); args.Completed += OnIoCompleted; } if(!socket.ReceiveAsync(args)) ProcessReceive(args); } private void OnIoCompleted(object sender, SocketAsyncEventArgs e) { switch(e.LastOperation) { case SocketAsyncOperation.Receive: ProcessReceive(e); break; case SocketAsyncOperation.Send: _argsPool.Push(e); break; } } }

4.3 内存管理

  1. 使用ArrayPool共享缓冲区:
var buffer = ArrayPool<byte>.Shared.Rent(8192); try { socket.Receive(buffer); // 处理数据... } finally { ArrayPool<byte>.Shared.Return(buffer); }
  1. 避免大对象分配:
  • 文件传输时使用FileStream直接读写
  • 使用Memory 代替byte[]操作

5. 常见问题与解决方案

5.1 连接超时问题

典型错误:

System.Net.Sockets.SocketException: A connection attempt failed because the connected party did not properly respond after a period of time

解决方案:

  1. 检查防火墙设置
  2. 增加连接超时时间:
socket.ConnectAsync(ip, port).Wait(5000); // 5秒超时

5.2 地址已在使用

典型错误:

System.Net.Sockets.SocketException: Only one usage of each socket address is normally permitted

解决方案:

socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);

5.3 大数据量传输卡顿

优化方案:

  1. 增加Socket发送缓冲区:
socket.SendBufferSize = 65536;
  1. 使用异步发送避免阻塞
  2. 实现流量控制(滑动窗口协议)

5.4 跨平台兼容性问题

在Linux上运行.NET Core时注意:

  1. 文件路径使用Path.Combine()
  2. 换行符统一处理:
text = text.Replace("\r\n", "\n").Replace("\r", "\n");
  1. 权限问题:确保对socket端口有访问权限

6. 测试方案设计

6.1 单元测试要点

  1. 连接稳定性测试:
  • 模拟网络抖动(使用工具如Clumsy)
  • 测试连续100次重连成功率
  1. 文件传输完整性测试:
  • 传输1GB大文件验证MD5
  • 随机中断传输测试续传功能
  1. 性能基准测试:
  • 1000并发连接时的吞吐量
  • 不同包大小下的传输效率

6.2 自动化测试脚本

[Test] public void TestReconnect() { var client = new ReconnectableClient(); int disconnectCount = 0; client.OnDisconnected += () => disconnectCount++; // 模拟网络中断 TestNetwork.DropConnection(); Thread.Sleep(10000); Assert.IsTrue(client.IsConnected); Assert.AreEqual(1, disconnectCount); }

6.3 压力测试方案

使用Locust模拟大规模客户端:

from locust import HttpUser, task, between class SocketUser(HttpUser): @task def send_file(self): self.client.post("/upload", files={ 'file': open('test.dat', 'rb') })

测试指标:

  • 连接建立成功率 > 99.9%
  • 平均重连时间 < 5秒
  • 文件传输错误率 < 0.01%

7. 生产环境部署建议

7.1 Windows服务封装

将服务端封装为Windows服务:

public class SocketService : ServiceBase { private FileServer _server; protected override void OnStart(string[] args) { _server = new FileServer(); _server.Start(8080); } protected override void OnStop() { _server.Stop(); } }

安装命令:

sc create SocketService binPath=C:\app\Server.exe start=auto

7.2 Linux系统配置

  1. 增加文件描述符限制:
ulimit -n 65535
  1. 配置systemd服务:
[Unit] Description=Socket Server [Service] ExecStart=/usr/bin/dotnet /app/Server.dll Restart=always User=root [Install] WantedBy=multi-user.target

7.3 监控方案

  1. 关键指标监控:
  • 当前连接数
  • 重连次数
  • 传输吞吐量
  1. 使用Prometheus+Grafana展示:
public class SocketMetrics { private readonly Counter _reconnectCounter; public SocketMetrics() { _reconnectCounter = Metrics.CreateCounter( "socket_reconnects_total", "Total reconnection attempts"); } public void OnReconnect() => _reconnectCounter.Inc(); }

8. 安全增强措施

8.1 传输加密方案

使用TLS1.3加密通信:

var sslStream = new SslStream(new NetworkStream(socket)); sslStream.AuthenticateAsClient(hostname);

证书生成:

openssl req -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365

8.2 身份验证设计

双向认证流程:

  1. 客户端发送设备ID和签名
  2. 服务端验证签名有效性
  3. 交换会话密钥

实现示例:

public bool Authenticate(DeviceInfo device, byte[] signature) { using var rsa = RSA.Create(); rsa.ImportParameters(_publicKey); return rsa.VerifyData( device.GetSignData(), signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); }

8.3 防注入攻击

  1. 文件名消毒处理:
string safeName = Path.GetFileName(fileName) .Replace("..", "") .Replace("/", "") .Replace("\\", "");
  1. 数据包长度校验:
if(packet.Length > MAX_PACKET_SIZE) throw new ProtocolViolationException();
  1. 设置接收超时:
socket.ReceiveTimeout = 5000;

9. 扩展功能实现

9.1 断点续传实现

服务端记录传输进度:

public class FileTransfer { public string FileId { get; set; } public long Position { get; set; } public void WriteChunk(long offset, byte[] data) { using var file = File.OpenWrite(FilePath); file.Position = offset; file.Write(data, 0, data.Length); Position = offset + data.Length; } }

客户端请求续传:

var request = new ResumeRequest { FileId = fileId, StartPosition = GetLocalFileSize(tempFile) }; SendPacket(socket, 0x0005, Serialize(request));

9.2 多客户端管理

使用ConcurrentDictionary管理会话:

public class SessionManager { private ConcurrentDictionary<string, ClientSession> _sessions = new(); public void AddSession(Socket client) { var session = new ClientSession(client); _sessions.TryAdd(session.Id, session); client.Disconnected += () => _sessions.TryRemove(session.Id, out _); } }

9.3 带宽限制

实现令牌桶算法限速:

public class RateLimiter { private int _tokens; private int _capacity; private object _lock = new object(); public bool TryAcquire(int n) { lock(_lock) { if(_tokens >= n) { _tokens -= n; return true; } return false; } } public void Refill(int n) { lock(_lock) { _tokens = Math.Min(_tokens + n, _capacity); } } }

10. 调试与诊断技巧

10.1 网络抓包分析

使用Wireshark过滤Socket通信:

tcp.port == 8080 && (tcp.flags.syn || tcp.flags.fin || tcp.flags.reset)

关键分析点:

  1. 三次握手是否完成
  2. FIN/RST包出现时机
  3. 重传包比例

10.2 日志记录规范

结构化日志示例:

logger.LogInformation("File transfer {FileId} progress {Progress}%", fileId, progress);

日志包含要素:

  • 时间戳
  • 会话ID
  • 关键操作
  • 性能指标

10.3 性能诊断工具

  1. 使用dotnet-counters监控:
dotnet-counters monitor --process-id PID System.Runtime Microsoft.AspNetCore.Hosting
  1. 使用PerfView分析线程池:
  • 检查ThreadPool starvation情况
  • 分析IO完成端口使用率
  1. 内存转储分析:
dotnet-dump collect --process-id PID
← 返回列表