ModbusTcp協議是基於tcp的,但不是說一定要透過tcp協議才能傳輸,只要能傳輸二進位制的地方都可以。比如WebSocket協議。
但由於目前我只有tcp上面的modbus伺服器實現,所以我必須先用tcp連線藉助已有工具來驗證我的伺服器是否寫正確。
效果
ModBusTCP協議報文
ModBusTCP協議報文比較複雜,主要是區分了3組型別
- 請求和響應
- 讀和寫
- 多個資料和單個資料
理論上將報文格式的種類從1種變成了8種。
但是在響應讀資料時,沒有區分資料量,而是到請求中去查詢,聽說是為了減少傳輸量。因此實際格式種類是3組6種。
-
請求讀資料包文
事務識別符號 協議識別符號 報文長度 單元識別符號 功能碼 起始地址 讀取數量 欄位長度 2 byte 2 byte 2 byte 1 byte 1 byte 2 byte 2 byte 資料型別 ushort ushort ushort byte byte ushort ushort 自增 一般為0 6,從單元識別符號開始算 一般為0 連結 -
響應讀資料包文
事務識別符號 協議識別符號 報文長度 單元識別符號 功能碼 資料位元組數 資料 2 byte 2 byte 2 byte 1 byte 1 byte 1 byte n byte 值得注意的是,響應報文中沒有包含請求的資料數量,只能從請求中獲得。比如是7個線圈還是8個線圈是看不出來的。
這其實就限制了程式結構,應該在同一個上下文中宣告請求和響應變數。 -
請求寫單個資料包文
事務識別符號 協議識別符號 報文長度 單元識別符號 功能碼 起始地址 資料 2 byte 2 byte 2 byte 1 byte 1 byte 2 byte 2 byte -
響應寫單個資料包文
事務識別符號 協議識別符號 報文長度 單元識別符號 功能碼 起始地址 資料 2 byte 2 byte 2 byte 1 byte 1 byte 2 byte 2 byte 請求寫單個資料的響應實際上是原樣返回請求。
寫1位線圈和寫16位暫存器都傳輸了2個位元組,這很奇怪。
原來是ModBusTCP定義寫單個線圈用0xff00表示1,0x0000表示0。 -
請求寫多個資料包文
事務識別符號 協議識別符號 報文長度 單元識別符號 功能碼 起始地址 數量 位元組數 資料 2 byte 2 byte 2 byte 1 byte 1 byte 2 byte 2 byte 1 byte n byte 請求寫多個資料時,如果是線圈,現在又變成了一位代表一個資料,而不是原來的2位元組代表單個資料
還需要注意的是modbus採用大端傳輸,意味著一個位元組中如果寫了四個線圈,要從右向左數。 -
響應寫多個資料包文
事務識別符號 協議識別符號 報文長度 單元識別符號 功能碼 起始地址 數量 2 byte 2 byte 2 byte 1 byte 1 byte 2 byte 2 byte
報文類定義
從功能碼欄位往後,欄位不確定,所以統一用Data表示。
在各個響應方法里根據不同功能碼對資料進行處理。
//WebModbus.cs
// Modbus報文
public class ADUMessage
{
// 事務識別符號
public ushort Transaction { get; set; }
// 協議識別符號
public ushort Protocol { get; set; }
// 報文長度
public ushort Length { get; set; }
// 單元識別符號
public byte Unit { get; set; }
// 功能碼
public byte FunctionCode { get; set; }
// 資料
public byte[] Data { get; set; }
public static ADUMessage Deserialize(byte[] buffer){}
public static byte[] Serialze(ADUMessage message){}
}
資料模型
modbusTCP協議規定了伺服器實現線圈、暫存器2種資料模型,每種又分為只讀、可讀可寫。於是伺服器一共要提供4個儲存棧。
因為報文起始地址欄位有2個位元組,所以定址範圍是0-65535
棧地址 | 線圈棧 | 離散量輸入棧 | 只讀暫存器棧 | 讀寫暫存器棧 |
---|---|---|---|---|
65535 | 0 | 0 | 0xFF C1 | 0x00 00 |
65534 | 0 | 1 | 0x00 00 | 0x00 00 |
... | 1 | 0 | 0x00 00 | 0x00 00 |
1 | 0 | 0 | 0x00 00 | 0x08 00 |
0 | 0 | 0 | 0x00 00 | 0x00 1A |
其中線圈棧和離散量輸入棧是bool型別的,實際實現的時候可以是數字0、1,也可以是布林true,false,還可以是字串"on","off"
如何儲存,儲存的資料型別是什麼?這不重要。重要的是傳輸的時候採用規定格式就行。
- 傳輸格式
-
採用第3種報文寫單線圈時,如果要傳輸true,要使用0xFF 00。傳輸false,要使用0x00 00
-
其它時候都用一個bit位表示true和false
比如線圈棧,從地址0開始讀9個資料。就是小端位元組序0x0f 00。0 1 2 3 4 5 6 7 8 ... 1 1 1 1 0 0 0 0 0 0 當然,要從請求報文中知道擷取取響應資料的哪幾位。
-
- 模型類定義
//WebModbus.cs
// 資料倉儲,144KB
public class DataStore
{
// 讀寫16位暫存器,64KB
public ushort[] HoldingRegisters;
// 只讀16位暫存器,64KB
public ushort[] InputRegisters;
// 讀寫1位線圈,8KB
public bool[] CoilDiscretes;
// 只讀1位線圈,8KB
public bool[] CoilInputs;
public DataStore()
{
HoldingRegisters = new ushort[65536];
InputRegisters = new ushort[65536];
CoilDiscretes = new bool[65536];
CoilInputs = new bool[65536];
}
// 讀 讀寫16位暫存器
public ushort[] ReadHoldingRegisters(ushort startIndex, ushort length){}
// 讀 只讀16位暫存器
public ushort[] ReadInputRegisters(ushort startIndex, ushort length){}
// 讀 讀寫1位線圈
public bool[] ReadCoilDiscretes(ushort startIndex, ushort length){}
// 讀 只讀1位線圈
public bool[] ReadCoilInputs(ushort startIndex, ushort length){}
// 寫 讀寫16位暫存器
public void WriteHoldingRegisters(ushort startIndex, ushort[] data){}
//寫 讀寫1位線圈
public void WriteCoilDiscretes(ushort startIndex, bool[] data){}
}
功能碼
功能碼主要用來操作這4個棧,因為棧分讀寫和只讀,寫入還分為多個資料和單個資料。所以主要有8種功能碼
- 0x01 讀線圈棧
- 0x02 讀離散量輸入棧
- 0x03 讀只讀暫存器棧
- 0x04 讀讀寫暫存器棧
- 0x05 寫一個資料到線圈棧
- 0x06 寫一個資料到讀寫暫存器棧
- 0x0F 寫多個資料到線圈棧
- 0x10 寫多個資料到讀寫暫存器棧
- 當然還有其它的,但不影響主要功能,這裡就不說明了
相應的
- 第1、2種報文使用01、02、03、04功能碼
- 第3、4種報文使用05、06功能碼
- 第5、6種報文使用0F、10功能碼
位元組序大小端
位元組序這個東西還是當初上作業系統課時老師講過,以後再沒接觸。不過要自己處理位元組流時又面臨這個問題了。
modbusTcp主要採用大端位元組序方式傳輸資料。
為什麼說主要呢,因為唯獨線圈資料是採用小端的方式傳輸的,其它資料都是大端傳輸。這要特別注意。
- C#實現時可以使用
BinaryReader
和BinaryWriter
來處理接收到的和要傳輸的位元組流。
但這兩個類預設都是按照小端位元組序來處理資料。比如reader.ReadUInt16()
,如果我們傳的位元組流是0x00 01,它會以為是ushort 256,而不是ushort 1。
所以需要我們過載override
這個方法,翻轉位元組陣列,按照大端方式讀取。
//BigEndianBinaryReader.cs
public override short ReadInt16()
{
var data = base.ReadBytes(2);
Array.Reverse(data);
return BitConverter.ToInt16(data, 0);
}
伺服器處理流程
modbus伺服器要做的就是
-
接收訊息,反序列化為訊息物件
-
然後判斷怎麼響應
-
讀取資料棧,構造訊息
-
序列化後響應
-
核心方法
//WebModbus.cs
public ADUMessage HandleRequest(byte[] buffer)
{
ADUMessage request = ADUMessage.Deserialize(buffer);
switch (request.FunctionCode)
{
//讀 讀寫線圈
case 0x01:
return Response_01(request);
//讀 只讀線圈
case 0x02:
return Response_02(request);
//讀 讀寫暫存器
case 0x03:
return Response_03(request);
//讀 只讀暫存器
case 0x04:
return Response_04(request);
//寫 讀寫一個線圈
case 0x05:
return Response_05(request);
//寫 讀寫一個暫存器
case 0x06:
return Response_06(request);
//寫 讀寫多個線圈
case 0x0f:
return Response_0f(request);
//寫 讀寫多個暫存器
case 0x10:
return Response_10(request);
default:
return Response_01(request);
}
}
完整程式碼
程式所需命令列引數的一種方式是在專案檔案種指定,這在除錯時比較方便
<PropertyGroup>
<StartArguments>5234</StartArguments>
</PropertyGroup>
WebModbus.cs
/// <summary>
/// 資料倉儲,144KB
/// </summary>
public class DataStore
{
/// <summary>
/// 讀寫16位暫存器,64KB
/// </summary>
public ushort[] HoldingRegisters;
/// <summary>
/// 只讀16位暫存器,64KB
/// </summary>
public ushort[] InputRegisters;
/// <summary>
/// 讀寫1位線圈,8KB
/// </summary>
public bool[] CoilDiscretes;
/// <summary>
/// 只讀1位線圈,8KB
/// </summary>
public bool[] CoilInputs;
public DataStore()
{
HoldingRegisters = new ushort[65536];
InputRegisters = new ushort[65536];
CoilDiscretes = new bool[65536];
CoilInputs = new bool[65536];
}
/// <summary>
/// 讀 讀寫16位暫存器
/// </summary>
/// <param name="startIndex"></param>
/// <param name="length"></param>
/// <returns></returns>
public ushort[] ReadHoldingRegisters(ushort startIndex, ushort length)
{
return HoldingRegisters.Take(new Range(new Index(startIndex), new Index(startIndex + length))).ToArray();
}
/// <summary>
/// 讀 只讀16位暫存器
/// </summary>
/// <param name="startIndex"></param>
/// <param name="length"></param>
/// <returns></returns>
public ushort[] ReadInputRegisters(ushort startIndex, ushort length)
{
return InputRegisters.Take(new Range(new Index(startIndex), new Index(startIndex + length))).ToArray();
}
/// <summary>
/// 讀 讀寫1位線圈
/// </summary>
/// <param name="startIndex"></param>
/// <param name="length"></param>
/// <returns></returns>
public bool[] ReadCoilDiscretes(ushort startIndex, ushort length)
{
return CoilDiscretes.Take(new Range(new Index(startIndex), new Index(startIndex + length))).ToArray();
}
/// <summary>
/// 讀 只讀1位線圈
/// </summary>
/// <param name="startIndex"></param>
/// <param name="length"></param>
/// <returns></returns>
public bool[] ReadCoilInputs(ushort startIndex, ushort length)
{
return CoilInputs.Take(new Range(new Index(startIndex), new Index(startIndex + length))).ToArray();
}
/// <summary>
/// 寫 讀寫16位暫存器
/// </summary>
/// <param name="startIndex"></param>
/// <param name="data"></param>
public void WriteHoldingRegisters(ushort startIndex, ushort[] data)
{
for (int i = 0; i < data.Length; i++)
{
if (startIndex+i < 65536)
{
HoldingRegisters[startIndex + i] = data[i];
}
}
}
/// <summary>
/// 寫 讀寫1位線圈
/// </summary>
/// <param name="startIndex"></param>
/// <param name="data"></param>
public void WriteCoilDiscretes(ushort startIndex, bool[] data)
{
for (int i = 0; i < data.Length; i++)
{
if (startIndex + i < 65536)
{
CoilDiscretes[startIndex + i] = data[i];
}
}
}
}
/// <summary>
/// Modbus報文
/// </summary>
public class ADUMessage
{
/// <summary>
/// 事務識別符號
/// </summary>
public ushort Transaction { get; set; }
/// <summary>
/// 協議識別符號
/// </summary>
public ushort Protocol { get; set; }
/// <summary>
/// 報文長度
/// </summary>
public ushort Length { get; set; }
/// <summary>
/// 單元識別符號
/// </summary>
public byte Unit { get; set; }
/// <summary>
/// 功能碼
/// </summary>
public byte FunctionCode { get; set; }
/// <summary>
/// 資料
/// </summary>
public byte[] Data { get; set; }
public static ADUMessage Deserialize(byte[] buffer)
{
//BinaryReader讀取方式是小端(右邊是高位元組),而modbus是大端傳輸(左邊是高位元組)
BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(buffer));
ADUMessage adu = new ADUMessage()
{
Transaction = reader.ReadUInt16(),
Protocol = reader.ReadUInt16(),
Length = reader.ReadUInt16(),
Unit = reader.ReadByte(),
FunctionCode = reader.ReadByte(),
Data = reader.ReadBytes(buffer.Length - 8)
};
return adu;
}
public static byte[] Serialze(ADUMessage message)
{
using (MemoryStream ms=new MemoryStream())
{
BinaryWriter writer = new BigEndianBinaryWriter(ms);
writer.Write(message.Transaction);
writer.Write(message.Protocol);
writer.Write(message.Length);
writer.Write(message.Unit);
writer.Write(message.FunctionCode);
writer.Write(message.Data);
return ms.ToArray();
}
}
}
public class WebModbusServer
{
public DataStore store = new DataStore();
public ADUMessage HandleRequest(byte[] buffer)
{
ADUMessage request = ADUMessage.Deserialize(buffer);
switch (request.FunctionCode)
{
//讀 讀寫線圈
case 0x01:
return Response_01(request);
//讀 只讀線圈
case 0x02:
return Response_02(request);
//讀 讀寫暫存器
case 0x03:
return Response_03(request);
//讀 只讀暫存器
case 0x04:
return Response_04(request);
//寫 讀寫一個線圈
case 0x05:
return Response_05(request);
//寫 讀寫一個暫存器
case 0x06:
return Response_06(request);
//寫 讀寫多個線圈
case 0x0f:
return Response_0f(request);
//寫 讀寫多個暫存器
case 0x10:
return Response_10(request);
default:
return Response_01(request);
}
}
public byte[] CoilToBytes(bool[] bools)
{
int byteCount = (bools.Length + 7) / 8; // 計算所需的位元組數
byte[] bytes = new byte[byteCount];
for (int i = 0; i < bools.Length; i++)
{
int byteIndex = i / 8; // 計算當前布林值應該儲存在哪個位元組中
int bitIndex = i % 8; // 計算當前布林值應該儲存在位元組的哪個位上
if (bools[i])
{
// 設定對應位為 1
bytes[byteIndex] |= (byte)(1 << bitIndex);
}
else
{
// 對應位保持為 0,無需額外操作
}
}
return bytes;
}
/// <summary>
/// 讀 讀寫線圈
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private ADUMessage Response_01(ADUMessage request)
{
BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
BinaryWriter writer;
ushort StartAddress, DataNumber;
StartAddress = reader.ReadUInt16();
DataNumber = reader.ReadUInt16();
bool[] data = store.ReadCoilDiscretes(StartAddress, DataNumber);
byte[] coilBytes = CoilToBytes(data);
byte[] dataBytes = new byte[coilBytes.Length + 1];
writer = new BinaryWriter(new MemoryStream(dataBytes));
writer.Write((byte)coilBytes.Length);
writer.Write(coilBytes);
ADUMessage response = new ADUMessage()
{
Transaction = request.Transaction,
Protocol = request.Protocol,
Length = (ushort)(dataBytes.Length + 2),
Unit = request.Unit,
FunctionCode = request.FunctionCode,
Data = dataBytes,
};
return response;
}
/// <summary>
/// 讀 只讀線圈
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private ADUMessage Response_02(ADUMessage request)
{
BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
BinaryWriter writer;
ushort StartAddress, DataNumber;
StartAddress = reader.ReadUInt16();
DataNumber = reader.ReadUInt16();
bool[] data = store.ReadCoilInputs(StartAddress, DataNumber);
byte[] coilBytes = CoilToBytes(data);
byte[] dataBytes = new byte[coilBytes.Length + 1];
writer = new BinaryWriter(new MemoryStream(dataBytes));
writer.Write((byte)coilBytes.Length);
writer.Write(coilBytes);
ADUMessage response = new ADUMessage()
{
Transaction = request.Transaction,
Protocol = request.Protocol,
Length = (ushort)(dataBytes.Length + 2),
Unit = request.Unit,
FunctionCode = request.FunctionCode,
Data = dataBytes,
};
return response;
}
/// <summary>
/// 讀 讀寫暫存器
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private ADUMessage Response_03(ADUMessage request)
{
BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
BinaryWriter writer;
ushort StartAddress, DataNumber;
StartAddress = reader.ReadUInt16();
DataNumber = reader.ReadUInt16();
ushort[] data = store.ReadHoldingRegisters(StartAddress, DataNumber);
byte[] dataBytes = new byte[data.Length * 2 + 1];
writer = new BigEndianBinaryWriter(new MemoryStream(dataBytes));
writer.Write((byte)(data.Length * 2));
foreach (ushort value in data)
{
writer.Write(value);
}
Array.Resize(ref dataBytes, dataBytes.Length + 1);
ADUMessage response = new ADUMessage()
{
Transaction = request.Transaction,
Protocol = request.Protocol,
Length = (ushort)(dataBytes.Length + 2),
Unit = request.Unit,
FunctionCode = request.FunctionCode,
Data = dataBytes,
};
return response;
}
/// <summary>
/// 讀 只讀暫存器
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private ADUMessage Response_04(ADUMessage request)
{
BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
BinaryWriter writer;
ushort StartAddress, DataNumber;
StartAddress = reader.ReadUInt16();
DataNumber = reader.ReadUInt16();
ushort[] data = store.ReadInputRegisters(StartAddress, DataNumber);
byte[] dataBytes = new byte[data.Length * 2 + 1];
writer = new BigEndianBinaryWriter(new MemoryStream(dataBytes));
writer.Write((byte)(data.Length * 2));
foreach (ushort value in data)
{
writer.Write(value);
}
Array.Resize(ref dataBytes, dataBytes.Length + 1);
ADUMessage response = new ADUMessage()
{
Transaction = request.Transaction,
Protocol = request.Protocol,
Length = (ushort)(dataBytes.Length + 2),
Unit = request.Unit,
FunctionCode = request.FunctionCode,
Data = dataBytes,
};
return response;
}
/// <summary>
/// 寫 讀寫一個線圈
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private ADUMessage Response_05(ADUMessage request)
{
BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
ushort StartAddress, coli;
StartAddress = reader.ReadUInt16();
coli = reader.ReadUInt16();
store.WriteCoilDiscretes(StartAddress, new bool[] { coli ==0xff00?true:false});
return request;
}
/// <summary>
/// 寫 讀寫一個暫存器
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private ADUMessage Response_06(ADUMessage request)
{
BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
ushort StartAddress, register;
StartAddress = reader.ReadUInt16();
register = reader.ReadUInt16();
store.WriteHoldingRegisters(StartAddress, new ushort[] { register });
return request;
}
/// <summary>
/// 寫 讀寫多個線圈
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private ADUMessage Response_0f(ADUMessage request)
{
BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
ushort StartAddress, DataNumber;
StartAddress = reader.ReadUInt16();
DataNumber = reader.ReadUInt16();
byte byteNumber = reader.ReadByte();
//線圈是小端傳輸
byte[] bytes = reader.ReadBytes(byteNumber);
bool[] data=new bool[DataNumber];
byte index = 0;
foreach (var item in bytes)
{
//1000 0000
byte rr = (byte)0x01;
for (int i = 0; i < 8; i++)
{
if (index< DataNumber)
{
var result = rr & item;
if (result > 0)
{
data[index] = true;
}
else
{
data[index] = false;
}
//0100 0000
rr <<= 1;
index++;
}
else
{
break;
}
}
}
store.WriteCoilDiscretes(StartAddress, data);
return request;
}
/// <summary>
/// 寫 讀寫多個暫存器
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private ADUMessage Response_10(ADUMessage request)
{
//暫存器是大端傳輸
BinaryReader reader = new BigEndianBinaryReader(new MemoryStream(request.Data));
ushort StartAddress, DataNumber;
StartAddress = reader.ReadUInt16();
DataNumber = reader.ReadUInt16();
byte byteNumber = reader.ReadByte();
ushort[] data = new ushort[byteNumber / 2];
for (int i = 0; i < data.Length; i++)
{
data[i] = reader.ReadUInt16();
}
store.WriteHoldingRegisters(StartAddress, data);
return request;
}
}
BigEndianBinaryReader.cs
public class BigEndianBinaryReader : BinaryReader
{
public BigEndianBinaryReader(Stream input) : base(input)
{
}
public override short ReadInt16()
{
var data = base.ReadBytes(2);
Array.Reverse(data);
return BitConverter.ToInt16(data, 0);
}
public override ushort ReadUInt16()
{
var data = base.ReadBytes(2);
Array.Reverse(data);
return BitConverter.ToUInt16(data, 0);
}
public override int ReadInt32()
{
var data = base.ReadBytes(4);
Array.Reverse(data);
return BitConverter.ToInt32(data, 0);
}
public override uint ReadUInt32()
{
var data = base.ReadBytes(4);
Array.Reverse(data);
return BitConverter.ToUInt32(data, 0);
}
public override long ReadInt64()
{
var data = base.ReadBytes(8);
Array.Reverse(data);
return BitConverter.ToInt64(data, 0);
}
public override float ReadSingle()
{
var data = base.ReadBytes(4);
Array.Reverse(data);
return BitConverter.ToSingle(data, 0);
}
public override double ReadDouble()
{
var data = base.ReadBytes(8);
Array.Reverse(data);
return BitConverter.ToDouble(data, 0);
}
// 可以繼續新增其他方法來支援更多資料型別的大端讀取
}
public class BigEndianBinaryWriter : BinaryWriter
{
public BigEndianBinaryWriter(Stream input) : base(input)
{
}
public override void Write(ushort value)
{
var bytes = BitConverter.GetBytes(value);
Array.Reverse(bytes);
base.Write(bytes);
}
public override void Write(short value)
{
var bytes = BitConverter.GetBytes(value);
Array.Reverse(bytes);
base.Write(bytes);
}
public override void Write(uint value)
{
var bytes = BitConverter.GetBytes(value);
Array.Reverse(bytes);
base.Write(bytes);
}
public override void Write(int value)
{
var bytes = BitConverter.GetBytes(value);
Array.Reverse(bytes);
base.Write(bytes);
}
public override void Write(ulong value)
{
var bytes = BitConverter.GetBytes(value);
Array.Reverse(bytes);
base.Write(bytes);
}
public override void Write(long value)
{
var bytes = BitConverter.GetBytes(value);
Array.Reverse(bytes);
base.Write(bytes);
}
// 可以繼續新增其他方法來支援更多資料型別的大端寫入
}
Program.cs
internal class Program
{
static WebModbusServer webModbusServer;
static void Main(string[] args)
{
webModbusServer = new WebModbusServer();
//伺服器
if (args.Length == 1)
{
//webModbusServer.store.WriteCoilDiscretes(0, new bool[] { true, true });
//webModbusServer.store.CoilInputs[0] = true;
//webModbusServer.store.CoilInputs[1] = true;
StartServer(args[0]);
}
}
private static void StartServer(string args)
{
int serverPort = Convert.ToInt32(args);
var server = new TcpListener(IPAddress.Parse("127.0.0.1"), serverPort);
Console.WriteLine($"TCP伺服器 127.0.0.1:{serverPort}");
server.Start();
int cnt = 0;
Task.Run(async () =>
{
List<TcpClient> clients = new List<TcpClient>();
while (true)
{
TcpClient client = await server.AcceptTcpClientAsync();
clients.Add(client);
cnt++;
var ep = client.Client.RemoteEndPoint as IPEndPoint;
Console.WriteLine($"TCP客戶端_{cnt} {ep.Address}:{ep.Port}");
//給這個客戶端開一個聊天執行緒
//作業系統將會根據遊客埠對應表將控制權交給對應遊客執行緒
StartModbus(client);
}
}).Wait();
}
public static async Task StartModbus(TcpClient client)
{
var buffer = new byte[1024 * 4];
while (client.Connected)
{
int msgLength = await client.Client.ReceiveAsync(new ArraySegment<byte>(buffer));
//關閉連線時會接收到一次空訊息,不知道為什麼
if (msgLength>0)
{
PrintBytes(buffer.Take(msgLength).ToArray(), "請求 ");
ADUMessage response = webModbusServer.HandleRequest(buffer.Take(msgLength).ToArray());
await client.Client.SendAsync(ADUMessage.Serialze(response));
PrintBytes(ADUMessage.Serialze(response), "響應 ");
}
}
}
public static void PrintBytes(byte[] bytes,string prefix="")
{
Console.Write(prefix);
for (int i = 0; i < bytes.Length; i++)
{
if (i < 2)
{
Console.ForegroundColor = ConsoleColor.Red;
}
else if(i<4)
{
Console.ForegroundColor = ConsoleColor.Green;
}
else if(i<6)
{
Console.ForegroundColor= ConsoleColor.Blue;
}
else if (i < 7)
{
Console.ForegroundColor = ConsoleColor.Yellow;
}
else if (i<8)
{
Console.ForegroundColor = ConsoleColor.DarkCyan;
}
else
{
Console.ForegroundColor = ConsoleColor.White;
}
Console.Write(bytes[i].ToString("X2") + " ");
}
Console.WriteLine();
}
}