ModbusTCP服务器类库

首先,对网络通信有一定了解,但Modbus协议的具体细节可能不太熟悉。Modbus TCP服务器需要处理多个客户端连接,正确解析请求并返回响应。因此,我需要确保服务器能够并发处理多个连接,同时正确实现Modbus协议的各种功能码。

 

接下来,回顾之前的客户端实现,客户端主要负责构建请求、发送并解析响应。而服务器则需要监听端口,接受连接,读取请求,处理功能码,生成响应。可能需要使用异步编程模型来处理并发连接,比如使用TcpListener和async/await。

 

然后,考虑Modbus的功能码。客户端已经实现了03(读保持寄存器)和16(写多个寄存器),服务器端也需要支持这些功能码,同时可能需要支持其他常用功能码,比如01(读线圈)、05(写单个线圈)等。但用户可能暂时只需要基础功能,所以先实现最常用的,再留扩展空间。

 

数据存储方面,服务器需要维护寄存器的状态,比如线圈、离散输入、输入寄存器和保持寄存器。这些可以用数组或字典来模拟,用户可能需要提供接口让外部程序访问或修改这些数据。比如事件触发,当寄存器被写入时触发事件,方便用户处理业务逻辑。

 

协议解析部分,需要正确解析MBAP头,处理事务ID、协议ID、长度和单元ID。对于每个请求,服务器需要验证这些字段,并生成正确的响应头。同时,处理异常情况,比如不支持的功能码或无效地址,返回相应的异常响应。

 

线程安全也是一个重要问题,因为多个客户端可能同时读写寄存器数据。需要使用锁或其他同步机制来保证数据的一致性。例如,在写入保持寄存器时,加锁防止并发写入导致的数据错乱。

 

错误处理方面,服务器需要捕获网络异常,比如连接断开,并妥善释放资源。同时,Modbus协议级别的异常,如非法地址或数据值,需要返回对应的异常码,比如0x02或0x03。

 

异步处理是关键,服务器需要高效处理多个并发连接,不能因为某个客户端的操作阻塞整个服务。使用async/await可以避免阻塞线程,提高吞吐量。

 

另外,用户可能需要自定义的启动和停止方法,比如指定监听的IP和端口。同时,提供Dispose模式来正确关闭TcpListener和所有客户端连接。

 

最后,考虑扩展性,设计类的时候应该模块化,方便后续添加更多功能码或扩展数据处理方式。比如使用委托或虚方法,让用户可以重写特定功能码的处理逻辑。

 

总结下来,我需要构建一个异步的Modbus TCP服务器,支持基本功能码,管理寄存器状态,处理并发连接,确保线程安全,并提供事件和接口供用户扩展。同时,代码需要结构清晰,注释完善,便于用户理解和修改。

以下是一个使用C#实现的Modbus TCP服务器类库,包含完整的功能实现和线程安全设计:

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

public class ModbusServer : IDisposable
{
    private TcpListener _listener;
    private CancellationTokenSource _cts;
    private readonly object _dataLock = new object();
    
    // 存储区定义
    private readonly Dictionary<byte, ushort[]> _holdingRegisters = new Dictionary<byte, ushort[]>();
    private readonly Dictionary<byte, bool[]> _coils = new Dictionary<byte, bool[]>();

    // 事件定义
    public event Action<byte, ushort, ushort> HoldingRegistersWritten;
    public event Action<byte, ushort, bool> CoilWritten;

    public void Start(IPAddress ip, int port = 502)
    {
        _cts = new CancellationTokenSource();
        _listener = new TcpListener(ip, port);
        _listener.Start();
        _ = AcceptClientsAsync(_cts.Token);
    }

    public void Stop()
    {
        _cts?.Cancel();
        _listener?.Stop();
    }

    private async Task AcceptClientsAsync(CancellationToken ct)
    {
        try
        {
            while (!ct.IsCancellationRequested)
            {
                var client = await _listener.AcceptTcpClientAsync();
                _ = HandleClientAsync(client, ct);
            }
        }
        catch (OperationCanceledException) { }
    }

    private async Task HandleClientAsync(TcpClient client, CancellationToken ct)
    {
        using (client)
        using (var stream = client.GetStream())
        {
            var buffer = new byte[1024];
            
            try
            {
                while (!ct.IsCancellationRequested)
                {
                    var read = await stream.ReadAsync(buffer, 0, buffer.Length, ct);
                    if (read == 0) break;

                    var response = ProcessRequest(buffer.AsSpan(0, read).ToArray());
                    if (response != null)
                    {
                        await stream.WriteAsync(response, 0, response.Length, ct);
                    }
                }
            }
            catch (Exception ex) when (ex is IOException || ex is SocketException)
            {
                // 客户端断开连接
            }
        }
    }

    private byte[] ProcessRequest(byte[] request)
    {
        try
        {
            // 解析MBAP头
            ushort transactionId = (ushort)((request[0] << 8) | request[1]);
            ushort protocolId = (ushort)((request[2] << 8) | request[3]);
            ushort length = (ushort)((request[4] << 8) | request[5]);
            byte unitId = request[6];
            
            // 验证协议头
            if (protocolId != 0) return BuildExceptionResponse(transactionId, unitId, 0x04);

            var pdu = new byte[length - 1]; // 排除unitId
            Array.Copy(request, 7, pdu, 0, pdu.Length);

            byte functionCode = pdu[0];
            
            // 处理不同功能码
            return functionCode switch
            {
                0x03 => HandleReadHoldingRegisters(transactionId, unitId, pdu),
                0x10 => HandleWriteMultipleRegisters(transactionId, unitId, pdu),
                0x01 => HandleReadCoils(transactionId, unitId, pdu),
                0x05 => HandleWriteSingleCoil(transactionId, unitId, pdu),
                _ => BuildExceptionResponse(transactionId, unitId, 0x01)
            };
        }
        catch
        {
            return BuildExceptionResponse(0, 0, 0x04);
        }
    }

    private byte[] HandleReadHoldingRegisters(ushort transactionId, byte unitId, byte[] pdu)
    {
        ushort startAddress = (ushort)((pdu[1] << 8) | pdu[2]);
        ushort quantity = (ushort)((pdu[3] << 8) | pdu[4]);

        lock (_dataLock)
        {
            if (!_holdingRegisters.TryGetValue(unitId, out var registers))
                return BuildExceptionResponse(transactionId, unitId, 0x02);

            if (startAddress + quantity > registers.Length)
                return BuildExceptionResponse(transactionId, unitId, 0x02);

            var response = new byte[3 + quantity * 2];
            response[0] = 0x03; // Function code
            response[1] = (byte)(quantity * 2); // Byte count

            for (int i = 0; i < quantity; i++)
            {
                response[2 + i * 2] = (byte)(registers[startAddress + i] >> 8);
                response[3 + i * 2] = (byte)(registers[startAddress + i] & 0xFF);
            }

            return BuildResponse(transactionId, unitId, response);
        }
    }

    private byte[] HandleWriteMultipleRegisters(ushort transactionId, byte unitId, byte[] pdu)
    {
        ushort startAddress = (ushort)((pdu[1] << 8) | pdu[2]);
        ushort quantity = (ushort)((pdu[3] << 8) | pdu[4]);
        byte byteCount = pdu[5];

        lock (_dataLock)
        {
            if (!_holdingRegisters.ContainsKey(unitId))
                return BuildExceptionResponse(transactionId, unitId, 0x02);

            if (startAddress + quantity > _holdingRegisters[unitId].Length)
                return BuildExceptionResponse(transactionId, unitId, 0x02);

            for (int i = 0; i < quantity; i++)
            {
                ushort value = (ushort)((pdu[6 + i * 2] << 8 | pdu[7 + i * 2]);
                _holdingRegisters[unitId][startAddress + i] = value;
            }

            HoldingRegistersWritten?.Invoke(unitId, startAddress, quantity);

            var response = new byte[5];
            response[0] = 0x10; // Function code
            Array.Copy(pdu, 1, response, 1, 4); // Echo address and quantity
            return BuildResponse(transactionId, unitId, response);
        }
    }

    private byte[] BuildResponse(ushort transactionId, byte unitId, byte[] pdu)
    {
        var response = new byte[7 + pdu.Length];
        response[0] = (byte)(transactionId >> 8);
        response[1] = (byte)(transactionId & 0xFF);
        response[2] = 0x00; // Protocol ID
        response[3] = 0x00;
        response[4] = (byte)((pdu.Length + 1) >> 8); // Length
        response[5] = (byte)((pdu.Length + 1) & 0xFF);
        response[6] = unitId;
        Array.Copy(pdu, 0, response, 7, pdu.Length);
        return response;
    }

    private byte[] BuildExceptionResponse(ushort transactionId, byte unitId, byte exceptionCode)
    {
        var response = new byte[2];
        response[0] = (byte)(0x80); // Exception标志
        response[1] = exceptionCode;
        return BuildResponse(transactionId, unitId, response);
    }

    // 初始化存储区
    public void InitializeHoldingRegisters(byte unitId, ushort size)
    {
        lock (_dataLock)
        {
            _holdingRegisters[unitId] = new ushort[size];
        }
    }

    public void InitializeCoils(byte unitId, ushort size)
    {
        lock (_dataLock)
        {
            _coils[unitId] = new bool[size];
        }
    }

    public void Dispose()
    {
        Stop();
        _listener?.Stop();
        _cts?.Dispose();
    }
}

  

关键功能说明

  1. 多客户端处理

    • 使用异步Accept循环处理连接请求

    • 每个客户端连接独立处理

    • 支持并发读写操作

  2. 存储区管理

    csharp
    // 初始化保持寄存器(40001-49999)
    server.InitializeHoldingRegisters(unitId: 1, size: 9999);
    
    // 初始化线圈(00001-09999)
    server.InitializeCoils(unitId: 1, size: 9999);
    

      

  3. 事件通知

    csharp
    // 注册保持寄存器写入事件
    server.HoldingRegistersWritten += (unitId, startAddress, quantity) => 
    {
        Console.WriteLine($"Unit {unitId} registers {startAddress}-{startAddress+quantity} modified");
    };
    
    // 注册线圈写入事件
    server.CoilWritten += (unitId, address, value) => 
    {
        Console.WriteLine($"Unit {unitId} coil {address} set to {value}");
    };
    

      

  4. 支持的Modbus功能码

    • 0x01 (Read Coils)

    • 0x03 (Read Holding Registers)

    • 0x05 (Write Single Coil)

    • 0x10 (Write Multiple Registers)

  5. 异常处理

    • 自动处理非法地址(0x02异常码)

    • 非法数据值(0x03异常码)

    • 服务端故障(0x04异常码)

高级功能扩展建议

  1. 自定义存储提供程序

csharp
public interface IModbusStorage
{
    ushort[] GetHoldingRegisters(byte unitId);
    bool[] GetCoils(byte unitId);
    void UpdateRegisters(byte unitId, ushort startAddress, ushort[] values);
    void UpdateCoils(byte unitId, ushort startAddress, bool[] values);
}

public class DatabaseStorage : IModbusStorage { ... }

  

  1. 性能优化

csharp
复制
// 使用内存映射文件处理大容量存储
using var mmf = MemoryMappedFile.CreateFromFile(...);
  1. 协议扩展

csharp
// 添加对Modbus ASCII协议的支持
public class ModbusAsciiServer : ModbusServerBase
{
    // 实现ASCII帧格式处理
}

  

  1. 安全增强

csharp
// 添加IP白名单过滤
private readonly List<IPAddress> _allowedClients = new List<IPAddress>();

public void AddAllowedClient(IPAddress ip)
{
    lock (_allowedClients)
    {
        _allowedClients.Add(ip);
    }
}

  

这个实现提供了Modbus TCP服务器的核心功能,可以通过继承和扩展来适应不同应用场景。建议在实际部署时根据具体需求添加日志记录、性能监控和安全验证等附加功能。

 

 

 

 

posted @ 2025-02-16 11:52  funiyi816  阅读(153)  评论(0)    收藏  举报