『7x24小时有问必答』

前言

在工业自动化领域,上位机与 PLC 的通信是核心环节之一。随着 C# 在工业软件开发中的广泛应用,如何实现 C# 与汇川 PLC 的高效稳定通信成为众多开发关注的问题。
本文将详细介绍两种主流实现方案:基于标准 Modbus TCP 协议的通信方式,以及使用汇川官方 API 的专用通信方式
两种方案各有优劣,开发者可根据项目实际需求选择最适合的方案。
方案
实现方式
适用场景
使用标准 Modbus TCP 协议
Modbus TCP 协议
快速开发,对稳定性要求高的项目
使用汇川官方 API
ModbusTcpAPI.dll;StandardModbusApi.dll
深度控制通信过程的项目

一、方案一:使用标准 Modbus TCP 协议

Modbus TCP 是一种广泛应用的工业通信协议,具有跨品牌兼容性强的特点,适用于多品牌 PLC 混合使用的场景。

1.1 Modbus TCP 通讯帧格式说明

1.png

1.2 常用功能码详解

命令码 0x01/0x02:读线圈

请求帧格式:事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x01/0x02 + 线圈起始地址 + 线圈数量
序号
数据 (字节)
意义
字节数量
说明
1
事务元标识符
MODBUS 请求/响应事务处理的识别码
2 个字节
-
2
协议标识符
0=MODBUS 协议
2 个字节
-
3
长度
以下字节的数量
2 个字节
-
4
单元标识符
主站请求标识符
1 个字节
-
5
0x01/0x02(命令码)
读线圈
1 个字节
-
6
线圈起始地址
高位在前,低位在后
2 个字节
见线圈编址
7
线圈数量
高位在前,低位在后(N)
2 个字节
N 最大为 2000
响应帧格式:事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x01/0x02 + 字节数 + 线圈状态
序号
数据 (字节)
意义
字节数量
说明
1
事务元标识符
MODBUS 请求/响应事务处理的识别码
2 个字节
-
2
协议标识符
0=MODBUS 协议
2 个字节
-
3
长度
以下字节的数量
2 个字节
-
4
单元标识符
复制主站请求标识符
1 个字节
-
5
0x01/0x02(命令码)
读线圈
1 个字节
-
6
字节数
值:[(N+7)/8]
1 个字节
-
7
线圈状态
[(N+7)/8]个字节
可变
每 8 个线圈合为一个字节

命令码 0x03/0x04:读寄存器

请求帧格式:事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x03/0x04 + 寄存器起始地址 + 寄存器数量
响应帧格式:事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x03/0x04 + 字节数 + 寄存器值

其他常用命令码

命令码
功能
请求帧格式
响应帧格式
0x05
写单线圈
事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x05 + 线圈地址 + 线圈状态
同请求帧
0x06
写单个寄存器
事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x06 + 寄存器地址 + 寄存器值
同请求帧
0x0f
写多个线圈
事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x0f + 线圈起始地址 + 线圈数量 + 字节数 + 线圈状态
事务元标识符 + 协议标识符 + 长度 + 单元标识符 + 0x0f + 线圈起始地址 + 线圈数
0x10
写多个寄存器
从机地址 + 0x10 + 寄存器起始地址 + 寄存器数量 + 字节数 + 寄存器值 + CRC 检验
从机地址 + 0x10 + 线圈起始地址 + 线圈数量 + CRC 检验

报文示例:写多个寄存器

请求报文2F 52 00 00 00 09 01 10 00 01 00 01 02 00 64
响应报文2F 52 00 00 00 06 01 10 00 01 00 01
2.png

1.3 协议实现代码

internal  classModbusProtocol  :  IDisposable

{

       private  Socket _socket;

       privatereadonlyobject  _lock =  newobject();

       privateushort  _transactionId =  0;

       privatebool  _disposed =  false;

       // Pre-allocated buffers

       privatereadonlybyte[] _sendBuffer =  newbyte[512];

       privatereadonlybyte[] _recvBuffer =  newbyte[4096];

       public  IPAddress IpAddress {  get;  privateset; }

       publicint  Port {  get;  privateset; }

       publicint  Timeout {  get;  set; } =  1000;

       publicbool  IsConnected => _socket?.Connected ??  false;

       // Expected lengths for current operation

       privateushort  _writeLen;

       privateushort  _readLen;

       public  ModbusProtocol(string  ip,  int  port,  int  timeout =  1000)

      {

             if  (!IPAddress.TryParse(ip,  outvar  address))

                   thrownew  ArgumentException("Invalid IP address",  nameof(ip));

            IpAddress = address;

            Port = port;

            Timeout = timeout;

      }

       #region  Connection

       public  bool  Connect()

      {

             lock  (_lock)

            {

                   try

                  {

                         if  (_socket?.Connected ==  true)

                               returntrue;

                        _socket?.Dispose();

                        _socket =  new  Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)

                        {

                              ReceiveTimeout = Timeout,

                              SendTimeout = Timeout,

                              NoDelay =  true

                        };

                         var  result = _socket.BeginConnect(new  IPEndPoint(IpAddress, Port),  null,  null);

                         bool  success = result.AsyncWaitHandle.WaitOne(Timeout,  true);

                         if  (success && _socket.Connected)

                        {

                              _socket.EndConnect(result);

                               returntrue;

                        }

                        _socket.Close();

                         returnfalse;

                  }

                   catch

                  {

                         returnfalse;

                  }

            }

      }

       public  bool  Disconnect()

      {

             lock  (_lock)

            {

                   try

                  {

                         if  (_socket !=  null)

                        {

                               if  (_socket.Connected)

                                    _socket.Shutdown(SocketShutdown.Both);

                              _socket.Close();

                              _socket.Dispose();

                              _socket =  null;

                        }

                         returntrue;

                  }

                   catch

                  {

                         returnfalse;

                  }

            }

      }

       #endregion

       #region  Modbus Functions

       ///  <summary>

       ///  Read Coils - Function 01 (mbtcpfcn01)

       ///  </summary>

       public  int  ReadCoils(int  address,  int  count,  byte[] rxdBuffer,  ref  int  rxdLength)

      {

             lock  (_lock)

            {

                   if  (!IsConnected)  return0;

                  BuildReadRequest(ModbusCmd.ModbusCmd_Read_Coil_01, address, count);

                   return  DataExchange(rxdBuffer,  ref  rxdLength);

            }

      }

       ///  <summary>

       ///  Read Discrete Inputs - Function 02 (mbtcpfcn02)

       ///  </summary>

       public  int  ReadDiscreteInputs(int  address,  int  count,  byte[] rxdBuffer,  ref  int  rxdLength)

      {

             lock  (_lock)

            {

                   if  (!IsConnected)  return0;

                  BuildReadRequest(ModbusCmd.ModbusCmd_Read_Coil_02, address, count);

                   return  DataExchange(rxdBuffer,  ref  rxdLength);

            }

      }

       ///  <summary>

       ///  Read Holding Registers - Function 03 (mbtcpfcn03)

       ///  </summary>

       public  int  ReadHoldingRegisters(int  address,  int  count,  byte[] rxdBuffer,  ref  int  rxdLength)

      {

             lock  (_lock)

            {

                   if  (!IsConnected)  return0;

                  BuildReadRequest(ModbusCmd.ModbusCmd_Read_Regs_03, address, count);

                   return  DataExchange(rxdBuffer,  ref  rxdLength);

            }

      }

       ///  <summary>

       ///  Read Input Registers - Function 04 (mbtcpfcn04)

       ///  </summary>

       public  int  ReadInputRegisters(int  address,  int  count,  byte[] rxdBuffer,  ref  int  rxdLength)

      {

             lock  (_lock)

            {

                   if  (!IsConnected)  return0;

                  BuildReadRequest(ModbusCmd.ModbusCmd_Read_Regs_04, address, count);

                   return  DataExchange(rxdBuffer,  ref  rxdLength);

            }

      }

       ///  <summary>

       ///  Write Single Coil - Function 05 (mbtcpfcn05)

       ///  </summary>

       public  int  WriteSingleCoil(int  address,  int  value,  byte[] rxdBuffer,  ref  int  rxdLength)

      {

             lock  (_lock)

            {

                   if  (!IsConnected)  return0;

                  BuildWriteSingleRequest(ModbusCmd.ModbusCmd_Write_Coil, address,  value);

                   return  DataExchange(rxdBuffer,  ref  rxdLength);

            }

      }

       ///  <summary>

       ///  Write Single Register - Function 06 (mbtcpfcn06)

       ///  </summary>

       public  int  WriteSingleRegister(int  address,  int  value,  byte[] rxdBuffer,  ref  int  rxdLength)

      {

             lock  (_lock)

            {

                   if  (!IsConnected)  return0;

                  BuildWriteSingleRequest(ModbusCmd.ModbusCmd_Write_Regs, address,  value);

                   return  DataExchange(rxdBuffer,  ref  rxdLength);

            }

      }

       ///  <summary>

       ///  Write Multiple Coils - Function 15 (mbtcpfcn15)

       ///  </summary>

       public  int  WriteMultipleCoils(int  address,  int  count,  byte[] txdBuffer,  byte[] rxdBuffer,  ref  int  rxdLength)

      {

             lock  (_lock)

            {

                   if  (!IsConnected)  return0;

                  BuildWriteMultipleRequest(ModbusCmd.ModbusCmd_Write_MutlCoils, address, count, txdBuffer);

                   return  DataExchange(rxdBuffer,  ref  rxdLength);

            }

      }

       ///  <summary>

       ///  Write Multiple Registers - Function 16 (mbtcpfcn16)

       ///  </summary>

       public  int  WriteMultipleRegisters(int  address,  int  count,  byte[] txdBuffer,  byte[] rxdBuffer,  ref  int  rxdLength)

      {

             lock  (_lock)

            {

                   if  (!IsConnected)  return0;

                  BuildWriteMultipleRequest(ModbusCmd.ModbusCmd_Write_MutlRegs, address, count, txdBuffer);

                   return  DataExchange(rxdBuffer,  ref  rxdLength);

            }

      }

       #endregion

       #region  Request Building

       private  void  BuildReadRequest(ModbusCmd functionCode,  int  address,  int  count)

      {

             ushort  transId = _transactionId++;

             // MBAP Header

            _sendBuffer[0] = (byte)(transId >>  8);

            _sendBuffer[1] = (byte)(transId &  0xFF);

            _sendBuffer[2] =  0;  // Protocol ID

            _sendBuffer[3] =  0;

            _sendBuffer[4] =  0;  // Length (high)

            _sendBuffer[5] =  6;  // Length (low) - Unit ID + FC + Addr + Count

             // PDU

            _sendBuffer[6] =  0xFF;

            _sendBuffer[7] = (byte)functionCode;

            _sendBuffer[8] = (byte)(address >>  8);

            _sendBuffer[9] = (byte)(address &  0xFF);

            _sendBuffer[10] = (byte)(count >>  8);

            _sendBuffer[11] = (byte)(count &  0xFF);

            _writeLen =  12;

             // Calculate expected response length

             if  (functionCode == ModbusCmd.ModbusCmd_Read_Coil_01 ||

                  functionCode == ModbusCmd.ModbusCmd_Read_Coil_02)

            {

                   // Coils:   MBAP(6) + UnitID(1) + FC(1) + ByteCount(1) + Data((count+7)/8)

                  _readLen = (ushort)(9  + ((count +  7) >>  3));

            }

             else

            {

                   // Registers: MBAP(6) + UnitID(1) + FC(1) + ByteCount(1) + Data(count*2)

                  _readLen = (ushort)(9  + count *  2);

            }

      }

       private  void  BuildWriteSingleRequest(ModbusCmd functionCode,  int  address,  int  value)

      {

             ushort  transId = _transactionId++;

            _sendBuffer[0] = (byte)(transId >>  8);

            _sendBuffer[1] = (byte)(transId &  0xFF);

            _sendBuffer[2] =  0;

            _sendBuffer[3] =  0;

            _sendBuffer[4] =  0;

            _sendBuffer[5] =  6;

            _sendBuffer[6] =  0xFF;

            _sendBuffer[7] = (byte)functionCode;

            _sendBuffer[8] = (byte)(address >>  8);

            _sendBuffer[9] = (byte)(address &  0xFF);

             if  (functionCode == ModbusCmd.ModbusCmd_Write_Coil)

            {

                   // Coil: 0xFF00 = ON, 0x0000 = OFF

                   ushort  coilValue = (value  &  1) ==  1  ? (ushort)0xFF00  : (ushort)0x0000;

                  _sendBuffer[10] = (byte)(coilValue >>  8);

                  _sendBuffer[11] = (byte)(coilValue &  0xFF);

            }

             else

            {

                   // Register:   big-endian

                  _sendBuffer[10] = (byte)(value  >>  8);

                  _sendBuffer[11] = (byte)(value  &  0xFF);

            }

            _writeLen =  12;

            _readLen =  12;  // Echo response

      }

       private  void  BuildWriteMultipleRequest(ModbusCmd functionCode,  int  address,  int  count,  byte[] txdBuffer)

      {

             ushort  transId = _transactionId++;

            _sendBuffer[0] = (byte)(transId >>  8);

            _sendBuffer[1] = (byte)(transId &  0xFF);

            _sendBuffer[2] =  0;

            _sendBuffer[3] =  0;

             int  offset =  6;

            _sendBuffer[offset++] =  0xFF;

            _sendBuffer[offset++] = (byte)functionCode;

            _sendBuffer[offset++] = (byte)(address >>  8);

            _sendBuffer[offset++] = (byte)(address &  0xFF);

            _sendBuffer[offset++] = (byte)(count >>  8);

            _sendBuffer[offset++] = (byte)(count &  0xFF);

             int  byteCount;

             if  (functionCode == ModbusCmd.ModbusCmd_Write_MutlCoils)

            {

                   // Coils: byte count = (count + 7) / 8

                  byteCount = (count +  7) >>  3;

                  _sendBuffer[offset++] = (byte)byteCount;

                   // Copy packed bits directly

                   for  (int  i =  0; i < byteCount; i++)

                  {

                        _sendBuffer[offset++] = txdBuffer;

                  }

            }

             else// Write Multiple Registers

            {

                   // Registers: byte count = count * 2

                  byteCount = count *  2;

                  _sendBuffer[offset++] = (byte)byteCount;

                   // Copy data with byte swap (little-endian to big-endian)

                   //WriteFilterOfMutlWrite swap bytes

                   for  (int  i =  0; i < count; i++)

                  {

                         int  srcOffset = i *  2;

                         // Swap bytes:   host order (little-endian) to network order (big-endian)

                        _sendBuffer[offset++] = txdBuffer[srcOffset +  1];  // High byte

                        _sendBuffer[offset++] = txdBuffer[srcOffset];        // Low byte

                  }

            }

             ushort  pduLen = (ushort)(7  + byteCount);

            _sendBuffer[4] = (byte)(pduLen >>  8);

            _sendBuffer[5] = (byte)(pduLen &  0xFF);

            _writeLen = (ushort)(6  + pduLen);

            _readLen =  12;  // Standard response for write multiple

      }

       #endregion

       #region  Data Exchange

       ///  <summary>

       ///  Send request and receive response

       ///  </summary>

       private  int  DataExchange(byte[] rxdBuffer,  ref  int  rxdLength)

      {

             // Send with retry

             int  iRet =  -1;

             for  (int  i =  0; i <  2; i++)

            {

                   try

                  {

                        iRet = _socket.Send(_sendBuffer, _writeLen, SocketFlags.None);

                         if  (iRet >  0)

                               break;

                  }

                   catch

                  {

                        Disconnect();

                        Thread.Sleep(200);

                  }

            }

             if  (iRet <=  0)

                   return0;  // FALSE

             // Receive with retry

             int  totalReceived =  0;

             for  (int  i =  0; i <  10  && totalReceived < _readLen; i++)

            {

                   try

                  {

                         int  received = _socket.Receive(

                              _recvBuffer,

                              totalReceived,

                              _readLen - totalReceived,

                              SocketFlags.None);

                         if  (received >  0)

                        {

                              totalReceived += received;

                        }

                         else

                        {

                              Thread.Sleep(1);

                        }

                  }

                   catch

                  {

                        Thread.Sleep(1);

                  }

            }

             // Additional receive loop if partial data

             if  (totalReceived >  0  && totalReceived < _readLen)

            {

                   int  retryCount =  0;

                   while  (totalReceived < _readLen && retryCount <  3)

                  {

                         try

                        {

                               int  received = _socket.Receive(

                                    _recvBuffer,

                                    totalReceived,

                                    _readLen - totalReceived,

                                    SocketFlags.None);

                               if  (received >  0)

                                    totalReceived += received;

                        }

                         catch  { }

                        Thread.Sleep(5);

                        retryCount++;

                  }

            }

            rxdLength = totalReceived;

             if  (totalReceived <=  0)

                   return0;  // FALSE

             // Copy to output buffer

            Array.Copy(_recvBuffer, rxdBuffer, Math.Min(totalReceived, rxdBuffer.Length));

             // Verify response

             if  (totalReceived == _readLen &&

                  _sendBuffer[0] == _recvBuffer[0] &&

                  _sendBuffer[1] == _recvBuffer[1])

            {

                   return1;  // TRUE - Success

            }

             return0;  // FALSE

      }

       #endregion

       #region  Response Parsing

       ///  <summary>

       ///  Parse Modbus response

       ///  </summary>

       public  static  int  ParseReadResponse(byte[] pData,  out  byte[] pReturnData,  int  nCount)

      {

             int  nOffset =  7;  // Skip MBAP header (6 bytes) + Unit ID (1 byte)

             byte  byteMode = pData[nOffset];  // Function code

            nOffset++;

             byte  byteLen = pData[nOffset];  // Byte count from response

            nOffset++;

             switch  (byteMode)

            {

                   case0x01:  // Read Coils

                   case0x02:  // Read Discrete Inputs

                        {

                               // Output: 1 byte per coil (0 or 1)

                              pReturnData =  newbyte[nCount];

                               int  nIndex =  0;

                               for  (int  i =  0; i < nCount; i++)

                              {

                                     byte  byData = pData[nOffset];

                                    pReturnData = (byte)((byData >> nIndex) &  0x01);

                                    nIndex++;

                                     if  (nIndex >=  8)

                                    {

                                          nIndex =  0;

                                          nOffset++;

                                    }

                              }

                        }

                         break;

                   case0x03:  // Read Holding Registers

                   case0x04:  // Read Input Registers

                        {

                               // Output: Raw bytes with byte order swapped

                              pReturnData =  newbyte[byteLen];

                               for  (int  i =  0; i < byteLen; i +=  2)

                              {

                                     // Swap bytes:   network order (big-endian) to host order (little-endian)

                                    pReturnData = pData[nOffset +  1];        // Low byte

                                    pReturnData[i +  1] = pData[nOffset];        // High byte

                                    nOffset +=  2;

                              }

                        }

                         break;

                   default:

                        pReturnData =  newbyte[byteLen];

                        Array.Copy(pData, nOffset, pReturnData,  0, byteLen);

                         break;

            }

             return  byteLen;

      }

       #endregion

       #region  Utility Functions

       ///  <summary>

       ///  Convert byte array (each byte 0 or 1) to packed bits

       ///  </summary>

       public  static  bool  Byte2Bit(byte[] pSrcData,  byte[] pDesData,  int  nCount)

      {

             int  nBitIndex =  0;

             int  desIndex =  0;

            pDesData[0] =  0;

             for  (int  i =  0; i < nCount; i++)

            {

                   if  (nBitIndex >=  8)

                  {

                        nBitIndex =  0;

                        desIndex++;

                        pDesData[desIndex] =  0;

                  }

                   if  (pSrcData ==  1)

                  {

                        pDesData[desIndex] |= (byte)(1  << nBitIndex);

                  }

                  nBitIndex++;

            }

             returntrue;

      }

       ///  <summary>

       ///  Convert octal to decimal

       ///  </summary>

       public  static  int  Oct2Int(int  nOctData)

      {

             int  nRtnData =  0;

             int  nData = nOctData;

             int  power =  0;

             while  (nData >  0)

            {

                   int  digit = nData %  10;

                  nRtnData += digit * (int)Math.Pow(8, power);

                  nData /=  10;

                  power++;

            }

             return  nRtnData;

      }

       ///  <summary>

       ///  Check if number is valid octal - matches

       ///  </summary>

       public  static  bool  IsOct(int  num)

      {

             if  (num ==  0)  returntrue;

             while  (num >  0)

            {

                   int  digit = num %  10;

                   if  (digit >  7)

                         returnfalse;

                  num /=  10;

            }

             returntrue;

      }

       #endregion

       #region  IDisposable

       public  void  Dispose()

      {

             if  (!_disposed)

            {

                  Disconnect();

                  _disposed =  true;

            }

      }

       #endregion

}

  提示:目前网上已有许多封装完善的 Modbus TCP 协议库,如 NModbus、HslCommunication 等,可直接引用使用。但了解底层通信原理对提升技术能力大有裨益。

二、方案二:汇川官方 API

2.1 准备工作

将以下两个动态链接库文件复制到创建的项目目录下:
ModbusTcpAPI.dll
StandardModbusApi.dll
3.png
资源下载:Modbus Api (2021-03-19).zip

链接:https://pan.baidu.com/s/13zAyIXmJ5nOd_pLH87-I1Q

提取码:ijkl

2.2 实现代码

汇川官方 API 提供了更直接的控制方式,支持访问 PLC 的特殊寄存器。
public  classInovancePLC

{

      [DllImport("StandardModbusApi.dll")]

       public  static  extern  bool  Init_ETH_String(string  ip,  int  netId =  0,  int  port =  502);

      [DllImport("StandardModbusApi.dll")]

       public  static  extern  int  H5u_Read_Soft_Elem(SoftElemType type,  int  startAddr,  int  count,  byte[] buffer,  int  netId =  0);

      [DllImport("StandardModbusApi.dll")]

       public  static  extern  int  H5u_Write_Soft_Elem(SoftElemType type,  int  startAddr,  int  count,  byte[] buffer,  int  netId =  0);

       publicenum  SoftElemType

      {

            REGI_H5U_Y =  0x30,   // 输出继电器

            REGI_H5U_X =  0x31,   // 输入继电器

            REGI_H5U_M =  0x33,   // 内部继电器

            REGI_H5U_D =  0x35     // 数据寄存器

      }

       public  static  void  Main()

      {

             // 初始化连接

             if  (!Init_ETH_String("192.168.1.100")) {

                  Console.WriteLine("PLC 连接失败");

                   return;

            }

             // 读取 D 寄存器示例

             byte[] buffer =  newbyte[4];

             int  result = H5u_Read_Soft_Elem(SoftElemType.REGI_H5U_D,  0,  2, buffer);

             if  (result ==  0) {

                   ushortvalue  = BitConverter.ToUInt16(buffer,  0);

                  Console.WriteLine($"D0 值:{value}");

            }

             // 写入 M 寄存器示例

             ushort  writeValue =  1;

             byte[] writeBuffer = BitConverter.GetBytes(writeValue);

            H5u_Write_Soft_Elem(SoftElemType.REGI_H5U_M,  0,  1, writeBuffer);

      }

}

三、方案对比分析

对比项
Modbus TCP 方案
汇川 API 方案
兼容性
高(不限品牌)
仅限汇川 PLC
性能
一般
功能支持
标准功能
特殊寄存器访问
实现复杂度

总结

完成程序开发后,建议先使用 Modbus 调试工具进行通信测试,确保基础通信正常。推荐使用  Modbus Poll  或  Modbus Slave  等工具。
使用 Modbus Slave 软件时,建议开启 Log 记录功能(路径:Display → Communication Traffic → Log),这样发送和接收的报文会保存在 Log 文件中,便于后续排查问题和协议分析。
方案选择建议
若项目需要兼容多品牌 PLC,优先选择  Modbus TCP 标准协议
若项目仅使用汇川 PLC 且需要深度控制,建议选择  汇川官方 API
对于快速原型开发,可直接使用成熟的第三方库(如 HslCommunication)
理解底层通信原理有助于在遇到问题时快速定位和解决,建议开发者在引用第三方库的同时,也要掌握基础协议的实现方式。

关键词

最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。也可以加入微信公众号[DotNet技术匠]  社区,与其他热爱技术的同行一起交流心得,共同成长!
作者:DotNET探索求知
出处:mp.weixin.qq.com/s/TNXT-vTDqU7FJtrF8k85hQ
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!

END

方便大家交流、资源共享和共同成长
纯技术交流群,需要加入的小伙伴请扫码,并备注加群

推荐阅读

觉得有收获?不妨分享让更多人受益
关注「DotNet技术匠」,共同提升技术实力

收藏
点赞
分享
在看

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码