C# Socket类 学习
in C# with 0 comment

这是 Qwen AI生成的

C# Socket 编程完全指南:从入门到工业级应用

本文将带你从 Socket 的基础概念入手,深入到工业通信的实际应用,特别适合关注工业自动化(如 S7、Modbus TCP)的开发者。

Socket 是什么?

Socket 是网络编程的底层抽象,你可以把它想象成电话机

在工业自动化中,Socket 是连接 PLC、仪表、HMI 的唯一桥梁

为什么选择 Socket?


核心参数详解

创建 Socket 时需要三个参数:

var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

1. AddressFamily:地址族

说明使用场景
InterNetworkIPv499% 工业通信
InterNetworkV6IPv6新设备支持
UnixUnix 域套接字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. 可靠性保证

2. 性能优化

3. 安全性

4. 调试技巧


总结

Socket 编程是工业通信的核心技能。掌握以下要点:

  1. 参数选择InterNetwork + Stream + Tcp 是工业标准
  2. 性能优化:用 ArrayPool 避免 GC,预分配缓冲区
  3. 粘包处理:按协议长度字段读取完整报文
  4. 并发管理:每个设备独立连接 + 任务
  5. 网卡绑定:通过 Bind() 指定特定网卡

通过本文的学习,你已经具备了使用 C# Socket 进行工业通信开发的能力。记住:实践是最好的老师,多写代码,多抓包分析,你会越来越熟练!

本文基于 .NET 8 + C# 12 编写,适用于工业自动化、物联网、网络编程等场景。