C#是Windows平台工业软件개발的主流언어,广泛적용于上位机、SCADA系统、데이터采集软件等领域。本文提供完整的C# Modbus TCP服务器和클라이언트实现代码,包括TCP监听、多클라이언트管理、Modbus프로토콜분석、레지스터 매핑、클라이언트读写操作以及完整的使用예제。代码基于.NET Framework 4.5+,可直接在Visual Studio中编译运行。
一、项目结构与依赖
1.1 개발환경
| 项目 | 要求 |
|---|---|
| 개발도구 | Visual Studio 2017+ / Visual Studio Code |
| .NET버전 | .NET Framework 4.5+ 或 .NET Core 2.0+ |
| 언어 | C# 7.0+ |
| NuGet包 | 无需第三方库,使用System.Net.Sockets |
| 项目유형 | 控制台적용 / Windows Forms / WPF |
1.2 레지스터 매핑
| 주소范围 | 유형 | 설명 |
|---|---|---|
| 0x0000-0x03FF | 코일(Coil) | 1024个可读写布尔量 |
| 0x0000-0x03FF | 이산 입력 | 1024个只读布尔量 |
| 0x0000-0x03FF | 유지 레지스터 | 1024个可读写16位整数 |
| 0x0000-0x03FF | 입력 레지스터 | 1024个只读16位整数 |
二、Modbus TCP服务器实现
2.1 데이터存储类(Modbus데이터Store.cs)
using System;
namespace ModbusTcpServer
{
/// <summary>
/// Modbus데이터存储,线程安全
/// </summary>
public class Modbus데이터Store
{
private readonly object _lock = new object();
// 코일(可读写布尔量)
private readonly bool[] _coils = new bool[1024];
// 이산 입력(只读布尔量)
private readonly bool[] _inputs = new bool[1024];
// 유지 레지스터(可读写16位)
private readonly ushort[] _holdingRegs = new ushort[1024];
// 입력 레지스터(只读16位)
private readonly ushort[] _inputRegs = new ushort[1024];
// 코일操作
public bool GetCoil(ushort 주소)
{
lock (_lock) { return _coils[주소]; }
}
public void SetCoil(ushort 주소, bool value)
{
lock (_lock) { _coils[주소] = value; }
}
public bool[] GetCoils(ushort 시작Addr, ushort count)
{
lock (_lock)
{
bool[] result = new bool[count];
Array.복사(_coils, 시작Addr, result, 0, count);
return result;
}
}
public void SetCoils(ushort 시작Addr, bool[] values)
{
lock (_lock)
{
Array.복사(values, 0, _coils, 시작Addr, values.Length);
}
}
// 이산 입력操作
public bool[] GetInputs(ushort 시작Addr, ushort count)
{
lock (_lock)
{
bool[] result = new bool[count];
Array.복사(_inputs, 시작Addr, result, 0, count);
return result;
}
}
public void SetInput(ushort 주소, bool value)
{
lock (_lock) { _inputs[주소] = value; }
}
// 유지 레지스터操作
public ushort GetHoldingReg(ushort 주소)
{
lock (_lock) { return _holdingRegs[주소]; }
}
public void SetHoldingReg(ushort 주소, ushort value)
{
lock (_lock) { _holdingRegs[주소] = value; }
}
public ushort[] GetHoldingRegs(ushort 시작Addr, ushort count)
{
lock (_lock)
{
ushort[] result = new ushort[count];
Array.복사(_holdingRegs, 시작Addr, result, 0, count);
return result;
}
}
public void SetHoldingRegs(ushort 시작Addr, ushort[] values)
{
lock (_lock)
{
Array.복사(values, 0, _holdingRegs, 시작Addr, values.Length);
}
}
// 입력 레지스터操作
public ushort[] GetInputRegs(ushort 시작Addr, ushort count)
{
lock (_lock)
{
ushort[] result = new ushort[count];
Array.복사(_inputRegs, 시작Addr, result, 0, count);
return result;
}
}
public void SetInputReg(ushort 주소, ushort value)
{
lock (_lock) { _inputRegs[주소] = value; }
}
}
}
2.2 Modbus TCP服务器类(ModbusTcpServer.cs)
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace ModbusTcpServer
{
public class ModbusTcpServer
{
private TcpListener _listener;
private Thread _listenThread;
private bool _isRunning;
private readonly Modbus데이터Store _데이터Store;
private readonly List<TcpClient> _clients = new List<TcpClient>();
private ushort _transactionId = 0;
public int Port { get; } = 502;
public Modbus데이터Store 데이터Store => _데이터Store;
public ModbusTcpServer(int port = 502)
{
Port = port;
_데이터Store = new Modbus데이터Store();
}
public void Start()
{
_listener = new TcpListener(IP주소.Any, Port);
_listener.Start();
_isRunning = true;
_listenThread = new Thread(ListenForClients)
{
IsBackground = true
};
_listenThread.Start();
Console.WriteLine($"Modbus TCP服务器已启动,监听포트:{Port}");
}
public void Stop()
{
_isRunning = false;
_listener?.Stop();
lock (_clients)
{
foreach (var client in _clients)
client.닫기();
_clients.지우기();
}
Console.WriteLine("Modbus TCP服务器已정지");
}
private void ListenForClients()
{
while (_isRunning)
{
try
{
TcpClient client = _listener.AcceptTcpClient();
lock (_clients) { _clients.Add(client); }
Thread clientThread = new Thread(HandleClient)
{
IsBackground = true
};
clientThread.Start(client);
Console.WriteLine($"클라이언트연결:{client.Client.RemoteEndPoint}");
}
catch (SocketException)
{
// 服务器정지时会触发
break;
}
}
}
private void HandleClient(object obj)
{
TcpClient client = (TcpClient)obj;
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[260]; // Modbus TCP最大ADU길이
try
{
while (_isRunning && client.Connected)
{
int bytesRead = stream.Read(buffer, 0, buffer.Length);
if (bytesRead == 0) break; // 클라이언트断开
// 분석MBAP头
ushort transId = (ushort)((buffer[0] << 8) | buffer[1]);
ushort protoId = (ushort)((buffer[2] << 8) | buffer[3]);
ushort length = (ushort)((buffer[4] << 8) | buffer[5]);
byte unitId = buffer[6];
byte funcCode = buffer[7];
// 处理请求并응답 생성
byte[] response = ProcessRequest(buffer, bytesRead, transId, unitId, funcCode);
// 보내기响应
stream.Write(response, 0, response.Length);
stream.Flush();
}
}
catch (Exception ex)
{
Console.WriteLine($"클라이언트异常:{ex.Message}");
}
finally
{
client.닫기();
lock (_clients) { _clients.Remove(client); }
Console.WriteLine($"클라이언트断开:{client.Client.RemoteEndPoint}");
}
}
private byte[] ProcessRequest(byte[] request, int length,
ushort transId, byte unitId, byte funcCode)
{
try
{
switch (funcCode)
{
case 0x01: return ReadCoils(request, transId, unitId);
case 0x02: return ReadDiscreteInputs(request, transId, unitId);
case 0x03: return ReadHolding회원가입s(request, transId, unitId);
case 0x04: return ReadInput회원가입s(request, transId, unitId);
case 0x05: return WriteSingleCoil(request, transId, unitId);
case 0x06: return WriteSingle회원가입(request, transId, unitId);
case 0x0F: return WriteMultipleCoils(request, transId, unitId);
case 0x10: return WriteMultiple회원가입s(request, transId, unitId);
default:
return BuildExceptionResponse(transId, unitId, funcCode, 0x01);
}
}
catch (IndexOutOfRangeException)
{
return BuildExceptionResponse(transId, unitId, funcCode, 0x02);
}
catch (Exception)
{
return BuildExceptionResponse(transId, unitId, funcCode, 0x04);
}
}
// 기능 코드01:读코일
private byte[] ReadCoils(byte[] req, ushort transId, byte unitId)
{
ushort 시작Addr = (ushort)((req[8] << 8) | req[9]);
ushort 수량 = (ushort)((req[10] << 8) | req[11]);
bool[] coils = _데이터Store.GetCoils(시작Addr, 수량);
byte byteCount = (byte)((수량 + 7) / 8);
byte[] response = new byte[9 + byteCount];
BuildMBAPHeader(response, transId, unitId, (ushort)(3 + byteCount));
response[7] = 0x01;
response[8] = byteCount;
for (int i = 0; i < 수량; i++)
{
if (coils[i])
response[9 + i / 8] |= (byte)(1 << (i % 8));
}
응답 반환;
}
// 기능 코드03:读유지 레지스터
private byte[] ReadHolding회원가입s(byte[] req, ushort transId, byte unitId)
{
ushort 시작Addr = (ushort)((req[8] << 8) | req[9]);
ushort 수량 = (ushort)((req[10] << 8) | req[11]);
ushort[] regs = _데이터Store.GetHoldingRegs(시작Addr, 수량);
byte byteCount = (byte)(수량 * 2);
byte[] response = new byte[9 + byteCount];
BuildMBAPHeader(response, transId, unitId, (ushort)(3 + byteCount));
response[7] = 0x03;
response[8] = byteCount;
for (int i = 0; i < 수량; i++)
{
response[9 + i * 2] = (byte)(regs[i] >> 8);
response[9 + i * 2 + 1] = (byte)(regs[i] & 0xFF);
}
응답 반환;
}
// 기능 코드05:写单个코일
private byte[] WriteSingleCoil(byte[] req, ushort transId, byte unitId)
{
ushort addr = (ushort)((req[8] << 8) | req[9]);
ushort value = (ushort)((req[10] << 8) | req[11]);
_데이터Store.SetCoil(addr, value == 0xFF00);
// 回显请求
byte[] response = new byte[12];
BuildMBAPHeader(response, transId, unitId, 6);
Array.복사(req, 7, response, 7, 5);
응답 반환;
}
// 기능 코드06:写单个레지스터
private byte[] WriteSingle회원가입(byte[] req, ushort transId, byte unitId)
{
ushort addr = (ushort)((req[8] << 8) | req[9]);
ushort value = (ushort)((req[10] << 8) | req[11]);
_데이터Store.SetHoldingReg(addr, value);
byte[] response = new byte[12];
BuildMBAPHeader(response, transId, unitId, 6);
Array.복사(req, 7, response, 7, 5);
응답 반환;
}
// 기능 코드10:写多个레지스터
private byte[] WriteMultiple회원가입s(byte[] req, ushort transId, byte unitId)
{
ushort 시작Addr = (ushort)((req[8] << 8) | req[9]);
ushort 수량 = (ushort)((req[10] << 8) | req[11]);
ushort[] values = new ushort[수량];
for (int i = 0; i < 수량; i++)
{
values[i] = (ushort)((req[13 + i * 2] << 8) | req[13 + i * 2 + 1]);
}
_데이터Store.SetHoldingRegs(시작Addr, values);
byte[] response = new byte[12];
BuildMBAPHeader(response, transId, unitId, 6);
response[7] = 0x10;
response[8] = req[8];
response[9] = req[9];
response[10] = req[10];
response[11] = req[11];
응답 반환;
}
// 构建MBAP头
private void BuildMBAPHeader(byte[] buffer, ushort transId, byte unitId, ushort length)
{
buffer[0] = (byte)(transId >> 8);
buffer[1] = (byte)(transId & 0xFF);
buffer[2] = 0x00; // 프로토콜ID高
buffer[3] = 0x00; // 프로토콜ID低
buffer[4] = (byte)(length >> 8);
buffer[5] = (byte)(length & 0xFF);
buffer[6] = unitId;
}
// 构建异常响应
private byte[] BuildExceptionResponse(ushort transId, byte unitId,
byte funcCode, byte exceptionCode)
{
byte[] response = new byte[9];
BuildMBAPHeader(response, transId, unitId, 3);
response[7] = (byte)(funcCode | 0x80);
response[8] = exceptionCode;
응답 반환;
}
}
}
三、Modbus TCP클라이언트实现
using System;
using System.Net.Sockets;
namespace ModbusTcpClient
{
public class ModbusTcpClient : IDisposable
{
private TcpClient _client;
private NetworkStream _stream;
private ushort _transactionId = 0;
public string Host { get; }
public int Port { get; }
public byte UnitId { get; set; } = 1;
public int Timeout { get; set; } = 3000;
public ModbusTcpClient(string host, int port = 502)
{
Host = host;
Port = port;
}
public void Connect()
{
_client = new TcpClient();
_client.ReceiveTimeout = Timeout;
_client.SendTimeout = Timeout;
_client.Connect(Host, Port);
_stream = _client.GetStream();
Console.WriteLine($"已연결到 {Host}:{Port}");
}
public void Disconnect()
{
_stream?.닫기();
_client?.닫기();
}
// 读유지 레지스터(기능 코드03)
public ushort[] ReadHolding회원가입s(ushort 시작Addr, ushort 수량)
{
byte[] request = BuildReadRequest(0x03, 시작Addr, 수량);
byte[] response = SendAndReceive(request);
if (response[7] != 0x03)
throw new ModbusException(response[7], response[8]);
byte byteCount = response[8];
ushort[] result = new ushort[byteCount / 2];
for (int i = 0; i < result.Length; i++)
{
result[i] = (ushort)((response[9 + i * 2] << 8) | response[9 + i * 2 + 1]);
}
return result;
}
// 读코일(기능 코드01)
public bool[] ReadCoils(ushort 시작Addr, ushort 수량)
{
byte[] request = BuildReadRequest(0x01, 시작Addr, 수량);
byte[] response = SendAndReceive(request);
if (response[7] != 0x01)
throw new ModbusException(response[7], response[8]);
byte byteCount = response[8];
bool[] result = new bool[수량];
for (int i = 0; i < 수량; i++)
{
result[i] = (response[9 + i / 8] & (1 << (i % 8))) != 0;
}
return result;
}
// 写单个레지스터(기능 코드06)
public void WriteSingle회원가입(ushort addr, ushort value)
{
byte[] request = new byte[12];
BuildMBAPHeader(request, 6);
request[7] = 0x06;
request[8] = (byte)(addr >> 8);
request[9] = (byte)(addr & 0xFF);
request[10] = (byte)(value >> 8);
request[11] = (byte)(value & 0xFF);
byte[] response = SendAndReceive(request);
if (response[7] != 0x06)
throw new ModbusException(response[7], response[8]);
}
// 写单个코일(기능 코드05)
public void WriteSingleCoil(ushort addr, bool value)
{
byte[] request = new byte[12];
BuildMBAPHeader(request, 6);
request[7] = 0x05;
request[8] = (byte)(addr >> 8);
request[9] = (byte)(addr & 0xFF);
request[10] = (byte)(value ? 0xFF : 0x00);
request[11] = 0x00;
byte[] response = SendAndReceive(request);
if (response[7] != 0x05)
throw new ModbusException(response[7], response[8]);
}
// 写多个레지스터(기능 코드10)
public void WriteMultiple회원가입s(ushort 시작Addr, ushort[] values)
{
byte byteCount = (byte)(values.Length * 2);
byte[] request = new byte[13 + byteCount];
BuildMBAPHeader(request, (ushort)(7 + byteCount));
request[7] = 0x10;
request[8] = (byte)(시작Addr >> 8);
request[9] = (byte)(시작Addr & 0xFF);
request[10] = (byte)(values.Length >> 8);
request[11] = (byte)(values.Length & 0xFF);
request[12] = byteCount;
for (int i = 0; i < values.Length; i++)
{
request[13 + i * 2] = (byte)(values[i] >> 8);
request[13 + i * 2 + 1] = (byte)(values[i] & 0xFF);
}
byte[] response = SendAndReceive(request);
if (response[7] != 0x10)
throw new ModbusException(response[7], response[8]);
}
private byte[] BuildReadRequest(byte funcCode, ushort 시작Addr, ushort 수량)
{
byte[] request = new byte[12];
BuildMBAPHeader(request, 6);
request[7] = funcCode;
request[8] = (byte)(시작Addr >> 8);
request[9] = (byte)(시작Addr & 0xFF);
request[10] = (byte)(수량 >> 8);
request[11] = (byte)(수량 & 0xFF);
return request;
}
private void BuildMBAPHeader(byte[] buffer, ushort length)
{
_transactionId++;
buffer[0] = (byte)(_transactionId >> 8);
buffer[1] = (byte)(_transactionId & 0xFF);
buffer[2] = 0x00;
buffer[3] = 0x00;
buffer[4] = (byte)(length >> 8);
buffer[5] = (byte)(length & 0xFF);
buffer[6] = UnitId;
}
private byte[] SendAndReceive(byte[] request)
{
_stream.Write(request, 0, request.Length);
_stream.Flush();
byte[] buffer = new byte[260];
int bytesRead = _stream.Read(buffer, 0, buffer.Length);
byte[] response = new byte[bytesRead];
Array.복사(buffer, response, bytesRead);
응답 반환;
}
public void Dispose()
{
Disconnect();
}
}
public class ModbusException : Exception
{
public byte 기능Code { get; }
public byte ExceptionCode { get; }
public ModbusException(byte funcCode, byte exceptionCode)
: base($"Modbus异常:기능 코드=0x{funcCode:X2},异常码=0x{exceptionCode:X2}")
{
기능Code = funcCode;
ExceptionCode = exceptionCode;
}
}
}
四、完整使用예제
using System;
using System.Threading;
namespace ModbusTcpDemo
{
class Program
{
static void Main(string[] args)
{
// 启动服务器
var server = new ModbusTcpServer.ModbusTcpServer(502);
server.Start();
// 设置一些初始데이터
server.데이터Store.SetHoldingReg(0, 100);
server.데이터Store.SetHoldingReg(1, 200);
server.데이터Store.SetHoldingReg(2, 300);
server.데이터Store.SetCoil(0, true);
server.데이터Store.SetInputReg(0, 512);
// 대기服务器启动
Thread.Sleep(500);
// 创建클라이언트
using (var client = new ModbusTcpClient.ModbusTcpClient("127.0.0.1", 502))
{
client.Connect();
client.UnitId = 1;
// 1. 读유지 레지스터
Console.WriteLine("=== 读유지 레지스터 ===");
ushort[] regs = client.ReadHolding회원가입s(0, 5);
for (int i = 0; i < regs.Length; i++)
Console.WriteLine($"레지스터{i}:{regs[i]}");
// 2. 写单个레지스터
Console.WriteLine("\n=== 写单个레지스터 ===");
client.WriteSingle회원가입(3, 999);
ushort[] check = client.ReadHolding회원가입s(3, 1);
Console.WriteLine($"쓰기后읽기:{check[0]}");
// 3. 读코일
Console.WriteLine("\n=== 读코일 ===");
bool[] coils = client.ReadCoils(0, 4);
for (int i = 0; i < coils.Length; i++)
Console.WriteLine($"코일{i}:{(coils[i] ? "ON" : "OFF")}");
// 4. 写코일
Console.WriteLine("\n=== 写코일 ===");
client.WriteSingleCoil(1, true);
coils = client.ReadCoils(1, 1);
Console.WriteLine($"코일1:{(coils[0] ? "ON" : "OFF")}");
// 5. 批量写레지스터
Console.WriteLine("\n=== 批量写레지스터 ===");
client.WriteMultiple회원가입s(10, new ushort[] { 11, 22, 33, 44 });
regs = client.ReadHolding회원가입s(10, 4);
for (int i = 0; i < regs.Length; i++)
Console.WriteLine($"레지스터{10 + i}:{regs[i]}");
}
Console.WriteLine("\n按任意键退出...");
Console.ReadKey();
server.Stop();
}
}
}
五、关键기술설명
5.1 MBAP头结构
Modbus TCP使用MBAP(Modbus Application Protocol)头替代RTU的슬레이브 주소和CRC。MBAP头共7바이트:트랜잭션 식별자(2바이트)、프로토콜 식별자(2바이트,固定0x0000)、길이(2바이트)、유닛 식별자(1바이트)。事务ID用于일치请求和响应,服务器必须原样返回。
5.2 多클라이언트处理
服务器使用TcpListener.AcceptTcpClient()接受연결,每个클라이언트分配独立线程处理。데이터存储类使用lock实现线程安全,确保多클라이언트同时访问时데이터一致性。
5.3 异常处理
Mod bus 예외 응답 의 가장 높은 기능 코 드는 1 (원 래 기능 코드 + 0 x 8 0) 이며 1 바 이트 예외 코 드가 뒤 따 릅니다 .일반적인 예외 코드 : 0 x 01 불법 기능 코드 , 0 x 02 불법 주소 , 0 x 03 불법 데이터 값 , 0 x 04 슬 레이 브 장치 장애 .
6. 테스트 및 검 증
- 使用Modbus Poll测试
- 使用Modbus Slave模拟
- Wireshark抓包
- 压力测试
VII . 확장 제안
- 异步모드
- 로그记录
- 설정文件
- 데이터持久化
- Modbus RTU게이트웨이
- 心跳检测
- 权限控制
이 문서 에서 제공하는 C # Mod bus TCP 서버 및 클 라이언 트 코 드는 산업 자동 화 프로젝트 개발 에 직접 사용할 수있는 완전한 표준 기능 코드를 구현 합니다 .서버 는 다 중 클 라이언 트 동 시 액세 스를 지원 하며 클 라이언 트는 간단한 API 인터페 이 스를 제공합니다 .로 컬 백 루 프 주소 (12 7. 0. 0. 1) 에서 테스트 를 통과 한 다음 실제 장치를 연결 하여 검 증을 하는 것이 좋습니다 .
답글 작성