这是 Qwen AI生成的
C# Socket 编程完全指南:从入门到工业级应用
本文将带你从 Socket 的基础概念入手,深入到工业通信的实际应用,特别适合关注工业自动化(如 S7、Modbus TCP)的开发者。
Socket 是什么?
Socket 是网络编程的底层抽象,你可以把它想象成电话机:
- 电话机 ↔
Socket对象 - 电话号码 ↔ IP 地址 + 端口号
- 通话内容 ↔ 你发送/接收的字节(
byte[]) - 运营商网络 ↔ TCP 或 UDP 协议
在工业自动化中,Socket 是连接 PLC、仪表、HMI 的唯一桥梁。
为什么选择 Socket?
- 灵活性:可实现任何网络协议(S7、Modbus、自定义)
- 性能:直接操作字节,无中间层开销
- 跨平台:C#、Java、C++ 都有 Socket API
核心参数详解
创建 Socket 时需要三个参数:
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);1. AddressFamily:地址族
| 值 | 说明 | 使用场景 |
|---|---|---|
InterNetwork | IPv4 | 99% 工业通信 |
InterNetworkV6 | IPv6 | 新设备支持 |
Unix | Unix 域套接字 | Linux 进程通信 |
2. SocketType:套接字类型
| 值 | 说明 | 特点 | 适用协议 |
|---|---|---|---|
Stream | 流式 | 可靠、有序、连接导向 | TCP(Modbus TCP、S7) |
Dgram | 数据报 | 快速、无连接 | UDP(广播、心跳) |
3. ProtocolType:协议类型
| 值 | 说明 | 配合使用 |
|---|---|---|
Tcp | 传输控制协议 | Stream |
Udp | 用户数据报协议 | Dgram |
💡 工业开发推荐:InterNetwork + Stream + Tcp常用方法实战
客户端模式(连 PLC)
// 1. 创建连接
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.ReceiveTimeout = 5000; // 5秒超时
await socket.ConnectAsync("192.168.0.10", 102); // S7 端口
// 2. 发送数据
byte[] request = BuildS7Request(); // 你的协议报文
await socket.SendAsync(request, SocketFlags.None);
// 3. 接收响应
byte[] buffer = new byte[1024];
int received = await socket.ReceiveAsync(buffer, SocketFlags.None);
byte[] response = buffer[..received];
// 4. 关闭连接
socket.Shutdown(SocketShutdown.Both);服务端模式(模拟设备)
// 1. 监听
var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
listener.Bind(new IPEndPoint(IPAddress.Any, 502)); // Modbus 端口
listener.Listen(10);
// 2. 接受连接
while (true)
{
var client = await listener.AcceptAsync();
_ = Task.Run(() => HandleClient(client)); // 异步处理
}
// 3. 处理客户端
async Task HandleClient(Socket client)
{
try
{
byte[] buffer = new byte[1024];
int received = await client.ReceiveAsync(buffer, SocketFlags.None);
// 处理请求...
await client.SendAsync(response, SocketFlags.None);
}
finally
{
client.Close();
}
}性能优化技巧
问题:频繁 new byte[] 导致 GC 压力
错误做法:
// 每次都分配新数组 → GC 灾难
byte[] buffer = new byte[1024];正确做法 1:ArrayPool(推荐)
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);
try
{
// 使用 buffer...
await socket.SendAsync(buffer.AsMemory(0, length));
}
finally
{
ArrayPool<byte>.Shared.Return(buffer); // 必须归还!
}正确做法 2:预分配缓冲区
public class ModbusClient
{
private readonly byte[] _sendBuffer = new byte[1024]; // 预分配
private readonly byte[] _recvBuffer = new byte[2048];
public async Task<byte[]> SendAndReceiveAsync(byte[] request)
{
await _socket.SendAsync(new ArraySegment<byte>(request, 0, request.Length));
int received = await _socket.ReceiveAsync(_recvBuffer, SocketFlags.None);
return _recvBuffer[..received].ToArray();
}
}粘包与拆包处理
什么是粘包/拆包?
TCP 是字节流,没有消息边界。你发 3 条消息,对方可能收到 "123"(粘包)或 "12" + "3"(拆包)。
解决方案:按协议长度字段读取
// 确保读取指定字节数(解决拆包)
public static async Task ReceiveAllAsync(Socket socket, byte[] buffer, int offset, int count)
{
int totalReceived = 0;
while (totalReceived < count)
{
int received = await socket.ReceiveAsync(
new ArraySegment<byte>(buffer, offset + totalReceived, count - totalReceived),
SocketFlags.None);
if (received == 0) throw new IOException("连接断开");
totalReceived += received;
}
}
// 读取完整 Modbus TCP 报文
public async Task<byte[]> ReadModbusMessageAsync(Socket socket)
{
// 1. 读 6 字节头部(含长度字段)
byte[] header = new byte[6];
await ReceiveAllAsync(socket, header, 0, 6);
// 2. 解析长度(大端)
int length = IPAddress.NetworkToHostOrder(BitConverter.ToInt16(header, 4));
// 3. 读取剩余数据
byte[] data = new byte[length];
await ReceiveAllAsync(socket, data, 0, length);
// 4. 拼接完整报文
byte[] fullMessage = new byte[6 + length];
Array.Copy(header, fullMessage, 6);
Array.Copy(data, 0, fullMessage, 6, length);
return fullMessage;
}多设备并发管理
每个设备一个独立连接
public class PlcClient
{
private readonly string _ip;
private readonly int _port;
private Socket? _socket;
private readonly byte[] _sendBuffer = new byte[1024];
private readonly byte[] _recvBuffer = new byte[2048];
public async Task ConnectAsync()
{
_socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
_socket.ReceiveTimeout = 5000;
await _socket.ConnectAsync(_ip, _port);
}
public async Task<byte[]> SendAndReceiveAsync(byte[] request)
{
await _socket!.SendAsync(new ArraySegment<byte>(request, 0, request.Length));
return await ReadFullResponseAsync();
}
}
// 并行处理多个设备
var devices = new[]
{
new PlcClient("192.168.0.10", 102),
new PlcClient("192.168.0.11", 502),
new PlcClient("192.168.0.12", 102)
};
await Task.WhenAll(devices.Select(d => d.ConnectAsync()));
// 持续轮询
while (true)
{
var tasks = devices.Select(async d =>
{
try
{
byte[] request = BuildRequest(d);
byte[] response = await d.SendAndReceiveAsync(request);
ProcessResponse(d, response);
}
catch (Exception ex)
{
Console.WriteLine($"设备 {d.Ip} 错误: {ex.Message}");
}
});
await Task.WhenAll(tasks);
await Task.Delay(100); // 100ms 轮询间隔
}网卡绑定技巧
指定使用 WiFi 或以太网
// 获取 WiFi 网卡 IP
string wifiIp = "192.168.1.100";
IPAddress localIp = IPAddress.Parse(wifiIp);
// 绑定到特定网卡
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.Bind(new IPEndPoint(localIp, 0)); // 0 = 系统分配端口
await socket.ConnectAsync("192.168.1.10", 102); // 通过 WiFi 连接自动选择正确网卡
public static IPAddress SelectLocalIpForTarget(string targetIp)
{
var target = IPAddress.Parse(targetIp);
var localIps = NetworkInterface.GetAllNetworkInterfaces()
.Where(nic => nic.OperationalStatus == OperationalStatus.Up)
.SelectMany(nic => nic.GetIPProperties().UnicastAddresses)
.Where(ip => ip.Address.AddressFamily == AddressFamily.InterNetwork)
.Select(ip => ip.Address);
// 匹配同网段的本地 IP
foreach (IPAddress localIp in localIps)
{
if (IsSameSubnet(localIp, target, "255.255.255.0"))
return localIp;
}
return IPAddress.Any; // 无匹配则用默认
}工业通信最佳实践
1. 可靠性保证
- 超时设置:
socket.ReceiveTimeout = 5000 - 粘包处理:按协议长度字段读取
- 自动重连:捕获异常后重建连接
2. 性能优化
- 预分配缓冲区:避免频繁 GC
- 异步 I/O:使用
Async方法 - 连接复用:长连接而非短连接
3. 安全性
- 验证 IP 地址:防止非法连接
- 协议校验:检查报文格式
- 资源管理:使用
using语句
4. 调试技巧
- 抓包分析:用 Wireshark 查看实际字节
- 日志记录:记录收发的原始字节
- 错误处理:捕获
SocketException
总结
Socket 编程是工业通信的核心技能。掌握以下要点:
- 参数选择:
InterNetwork + Stream + Tcp是工业标准 - 性能优化:用
ArrayPool避免 GC,预分配缓冲区 - 粘包处理:按协议长度字段读取完整报文
- 并发管理:每个设备独立连接 + 任务
- 网卡绑定:通过
Bind()指定特定网卡
通过本文的学习,你已经具备了使用 C# Socket 进行工业通信开发的能力。记住:实践是最好的老师,多写代码,多抓包分析,你会越来越熟练!
本文基于 .NET 8 + C# 12 编写,适用于工业自动化、物联网、网络编程等场景。
本文由 jxxxy 创作,采用 知识共享署名4.0 国际许可协议进行许可。
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。