<think>我们使用C#实现HJ212-2017协议的客户端,主要步骤包括:1.接收数据报文(通常通过TCP或UDP,这里以TCP为例)
2.解析数据报文(根据HJ212-2017协议格式)
3.校验数据(包括CRC16校验)4.将校验通过的数据存储到Oracle数据库以下是一个简化的实现步骤:###步骤1:创建TCP监听服务使用`TcpListener`监听指定端口,接收来自数据采集设备的数据。###步骤2:解析HJ212-2017协议报文HJ212-2017协议报文格式示例:```
##[数据段长度]QN=数据时间;ST=站台编号;CN=命令编号;PW=密码;MN=设备唯一标识;Flag=标志位;CP=数据区&&[数据区]CRC16\r\n```其中:-数据段长度:从`QN`开始到`&&`之前(不包括`&&`)的字符数,用固定4位数字表示,不足4位前面补零。
-数据区:由多个字段组成,例如`Field1=Value1;Field2=Value2;...`。- CRC16:对从`QN`开始到`&&`之前(不包括`&&`)的数据段进行CRC16校验(多项式0x1021,初始值0xFFFF)。###步骤3:CRC16校验实现HJ212协议规定的CRC16校验算法(CCITT标准)。###步骤4:存储到Oracle数据库使用Oracle的ADO.NET提供程序(如Oracle.ManagedDataAccess)将解析后的数据存入数据库。###代码示例####1.创建TCP监听服务```csharpusing System;
usingSystem.Net;
usingSystem.Net.Sockets;
usingSystem.Text;
usingSystem.Threading;
classHJ212Client{private TcpListener_listener;privateconst intPort =12345;//监听端口public voidStart(){_listener= newTcpListener(IPAddress.Any,Port);_listener.Start();Console.WriteLine("监听已启动,等待连接...");while(true){TcpClient client= _listener.AcceptTcpClient();Thread thread= newThread(HandleClient);thread.Start(client);}}privatevoid HandleClient(objectobj){TcpClientclient =(TcpClient)obj;NetworkStreamstream =client.GetStream();byte[] buffer= newbyte[1024];int bytesRead;try{while ((bytesRead= stream.Read(buffer,0,buffer.Length)) !=0){string dataReceived= Encoding.ASCII.GetString(buffer,0,bytesRead);Console.WriteLine($"收到数据: {dataReceived}");//解析并处理报文if(TryParseHJ212(dataReceived,out HJ212Datadata)){//校验通过,存储到数据库SaveToDatabase(data);}}}catch(Exceptionex){Console.WriteLine($"异常:{ex.Message}");}finally{client.Close();}}
}```
####2.解析报文和CRC校验```csharp//HJ212数据结构publicclass HJ212Data{publicstring QN {get;set;} //请求编码public stringST {get;set;} //系统编码publicstring CN{ get; set; }//命令编码public stringPW {get;set;} //密码public stringMN {get;set;} //设备标识publicstring Flag{ get; set; }//标志位public Dictionary<string,string>CP {get;set;} //数据区}
//解析HJ212报文privatebool TryParseHJ212(stringframe,out HJ212Datadata){data =null;//检查帧头和帧尾if (!frame.StartsWith("##")|| !frame.EndsWith("\r\n"))returnfalse;//提取数据段长度(4位)string lenStr =frame.Substring(2,4);if (!int.TryParse(lenStr,out intdataLen))return false;//提取数据段(从第6个字符开始,长度为dataLen)string dataSegment =frame.Substring(6,dataLen);//提取CRC16校验码(数据段后,结尾前,4个字符的十六进制)string crcStr= frame.Substring(6+ dataLen,4);if (!ushort.TryParse(crcStr,System.Globalization.NumberStyles.HexNumber, null, outushortreceivedCrc))returnfalse;//计算数据段的CRC16ushort calculatedCrc= CalculateCRC16(Encoding.ASCII.GetBytes(dataSegment));if (receivedCrc !=calculatedCrc){Console.WriteLine($"CRC校验失败:接收的CRC={receivedCrc:X4},计算的CRC={calculatedCrc:X4}");returnfalse;}//解析数据段(数据段格式:QN=...;ST=...;...&&)//注意:数据段最后是"&&",我们去掉它if(!dataSegment.EndsWith("&&"))return false;string dataContent =dataSegment.Substring(0, dataSegment.Length-2);//将数据段按分号分割string[]fields= dataContent.Split(';');data= newHJ212Data();data.CP= newDictionary<string, string>();foreach (string fieldin fields){int index= field.IndexOf('=');if (index <=0)continue;stringkey =field.Substring(0,index);stringvalue =field.Substring(index+1);switch(key){case "QN": data.QN= value; break;case "ST":data.ST =value;break;case"CN": data.CN= value; break;case "PW":data.PW =value;break;case"MN": data.MN= value; break;case "Flag":data.Flag =value;break;default://其他字段放入CPdata.CP[key] =value;break;}}return true;
}// CRC16计算(多项式0x1021,初始值0xFFFF)
privateushortCalculateCRC16(byte[]data){ushortcrc=0xFFFF;for(inti =0;i <data.Length; i++){crc^=(ushort)(data[i] <<8);for (int j=0;j <8; j++){if ((crc&0x8000) !=0)crc= (ushort)((crc<<1)^0x1021);elsecrc <<=1;}}returncrc;
}```
####3.存储到Oracle数据库首先,安装Oracle.ManagedDataAccess NuGet包。
```csharpusingOracle.ManagedDataAccess.Client;
privatevoid SaveToDatabase(HJ212Datadata){string connectionString ="UserId=your_username;Password=your_password;DataSource=your_data_source;";using (OracleConnectionconn =new OracleConnection(connectionString)){conn.Open();//根据实际表结构编写插入语句//假设我们有一个表,包含字段:QN,ST,CN,PW,MN,Flag,以及数据区的字段(这里以数据区中的B01为例)string sql= @"INSERT INTOHJ212_DATA (QN, ST, CN, PW, MN, Flag, B01,B02, ...)VALUES (:QN, :ST,:CN, :PW,:MN, :Flag,:B01,:B02,...)";using (OracleCommandcmd =new OracleCommand(sql, conn)){cmd.Parameters.Add("QN",data.QN);cmd.Parameters.Add("ST", data.ST);cmd.Parameters.Add("CN",data.CN);cmd.Parameters.Add("PW", data.PW);cmd.Parameters.Add("MN",data.MN);cmd.Parameters.Add("Flag", data.Flag);//添加数据区字段(假设数据区包含B01、B02等)foreach (var itemin data.CP){//注意:实际参数名需要与SQL中的占位符一致//这里假设数据区的键就是数据库字段名(注意防止SQL注入,但这里键是协议规定的,一般安全)cmd.Parameters.Add(item.Key,item.Value);}cmd.ExecuteNonQuery();}}
}```
###注意事项1. **协议细节**:上述代码仅作为示例,实际HJ212-2017协议可能更复杂,例如数据区可能有嵌套结构(如`DataTime=20230101120000;B01-Rtd=36.5,B01-Flag=N;...`),需要进一步解析。
2. **并发处理**:多设备连接时,需要考虑并发和性能问题,可以使用异步或线程池。
3. **数据库设计**:需要根据实际监测数据设计数据库表结构。4.**错误处理**:增加异常处理和日志记录。
5. **安全性**:在开放端口接收数据时,注意网络安全。
###扩展-对于复杂的数据区(CP),可能需要递归解析,例如遇到逗号分隔的复合值(如`B01-Rtd=36.5,B01-Flag=N`)可以进一步拆分成多个键值对。-考虑使用异步方法(如`async/await`)提高并发性能。§§相关问题§§1. HJ212-2017协议中数据区(CP)的复杂结构如何解析?
2.如何优化TCP服务以支持高并发连接?3.Oracle数据库连接池的最佳实践是什么?</think>下面我将详细介绍如何使用C#实现HJ212-2017协议客户端,包括接收数据报文、进行校验,并将数据存储到Oracle数据库的完整解决方案。
### 一、整体架构设计
```mermaid
graph TD
A[数据采集设备] -->|发送HJ212报文| B(网络接收模块)
B --> C[协议解析模块]
C --> D[CRC校验模块]
D -->|校验通过| E[数据存储模块]
D -->|校验失败| F[错误处理模块]
E --> G[Oracle数据库]
```
### 二、核心模块实现
#### 1. 网络接收模块(TCP监听)
```csharp
using System.Net;
using System.Net.Sockets;
using System.Text;
public class HJ212Receiver
{
private const int PORT = 12345; // 监听端口
private TcpListener _listener;
public void Start()
{
_listener = new TcpListener(IPAddress.Any, PORT);
_listener.Start();
Console.WriteLine($"HJ212监听已启动,端口: {PORT}");
while (true)
{
TcpClient client = _listener.AcceptTcpClient();
ThreadPool.QueueUserWorkItem(HandleClient, client);
}
}
private void HandleClient(object state)
{
using (TcpClient client = (TcpClient)state)
using (NetworkStream stream = client.GetStream())
{
byte[] buffer = new byte[4096];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
string rawData = Encoding.ASCII.GetString(buffer, 0, bytesRead);
Console.WriteLine($"收到原始数据: {rawData}");
HJ212Parser.ProcessData(rawData);
}
}
}
```
#### 2. 协议解析与校验模块
```csharp
public static class HJ212Parser
{
public static void ProcessData(string rawData)
{
// 1. 验证基本格式
if (!rawData.StartsWith("##") || !rawData.EndsWith("\r\n"))
throw new FormatException("无效的HJ212报文格式");
// 2. 提取数据段长度
string lenStr = rawData.Substring(2, 4);
if (!int.TryParse(lenStr, out int dataLength))
throw new FormatException("无效的数据长度字段");
// 3. 提取数据段和CRC
string dataSegment = rawData.Substring(6, dataLength);
string receivedCrc = rawData.Substring(6 + dataLength, 4);
// 4. CRC校验
string calculatedCrc = CalculateCrc(dataSegment);
if (receivedCrc != calculatedCrc)
throw new InvalidDataException($"CRC校验失败: 接收值={receivedCrc}, 计算值={calculatedCrc}");
// 5. 解析数据区
HJ212Data data = ParseDataSegment(dataSegment);
// 6. 存储到数据库
OracleDataSaver.SaveToDatabase(data);
}
private static string CalculateCrc(string data)
{
byte[] bytes = Encoding.ASCII.GetBytes(data);
ushort crc = 0xFFFF;
foreach (byte b in bytes)
{
crc ^= (ushort)(b << 8);
for (int i = 0; i < 8; i++)
{
crc = (crc & 0x8000) != 0
? (ushort)((crc << 1) ^ 0x1021)
: (ushort)(crc << 1);
}
}
return crc.ToString("X4"); // 返回4位十六进制
}
private static HJ212Data ParseDataSegment(string segment)
{
// 示例数据段: QN=20230815120000;ST=32;CN=2011;PW=123456;MN=010000A8900016F000169DC0;Flag=5;CP=&&DataTime=20230815120000;B01-Rtd=25.4,B01-Flag=N;B02-Rtd=50.1,B02-Flag=N&&
var data = new HJ212Data();
string[] parts = segment.Split(';');
foreach (string part in parts)
{
if (part.Contains("="))
{
string[] keyValue = part.Split('=', 2);
string key = keyValue[0];
string value = keyValue[1];
switch (key)
{
case "QN": data.RequestTime = value; break;
case "ST": data.SystemCode = value; break;
case "CN": data.CommandCode = value; break;
case "PW": data.Password = value; break;
case "MN": data.DeviceId = value; break;
case "Flag": data.Flags = value; break;
case "CP": ParseCpData(value, data); break;
}
}
}
return data;
}
private static void ParseCpData(string cpData, HJ212Data data)
{
// 去除CP字段的&&分隔符
if (cpData.StartsWith("&&") && cpData.EndsWith("&&"))
cpData = cpData.Substring(2, cpData.Length - 4);
string[] measurements = cpData.Split(';');
foreach (string measurement in measurements)
{
if (measurement.Contains("="))
{
string[] parts = measurement.Split('=', 2);
data.Measurements[parts[0]] = parts[1];
}
}
}
}
public class HJ212Data
{
public string RequestTime { get; set; }
public string SystemCode { get; set; }
public string CommandCode { get; set; }
public string Password { get; set; }
public string DeviceId { get; set; }
public string Flags { get; set; }
public Dictionary<string, string> Measurements { get; } = new Dictionary<string, string>();
}
```
#### 3. Oracle数据库存储模块
```csharp
using Oracle.ManagedDataAccess.Client;
public static class OracleDataSaver
{
private const string ConnectionString =
"User Id=your_user;Password=your_pwd;Data Source=(DESCRIPTION=(ADDRESS=(PROTOCOL=TCP)" +
"(HOST=your_host)(PORT=1521))(CONNECT_DATA=(SERVICE_NAME=your_service)));";
public static void SaveToDatabase(HJ212Data data)
{
using (var conn = new OracleConnection(ConnectionString))
{
conn.Open();
// 1. 保存主表信息
using (var cmd = new OracleCommand(
"INSERT INTO HJ212_MAIN (REQUEST_TIME, SYSTEM_CODE, COMMAND_CODE, DEVICE_ID, FLAGS) " +
"VALUES (:reqTime, :sysCode, :cmdCode, :devId, :flags)", conn))
{
cmd.Parameters.Add("reqTime", data.RequestTime);
cmd.Parameters.Add("sysCode", data.SystemCode);
cmd.Parameters.Add("cmdCode", data.CommandCode);
cmd.Parameters.Add("devId", data.DeviceId);
cmd.Parameters.Add("flags", data.Flags);
cmd.ExecuteNonQuery();
}
// 2. 保存测量数据
foreach (var measurement in data.Measurements)
{
using (var cmd = new OracleCommand(
"INSERT INTO HJ212_MEASUREMENTS (DEVICE_ID, PARAM_CODE, VALUE, REQUEST_TIME) " +
"VALUES (:devId, :param, :value, :reqTime)", conn))
{
cmd.Parameters.Add("devId", data.DeviceId);
cmd.Parameters.Add("param", measurement.Key);
cmd.Parameters.Add("value", measurement.Value);
cmd.Parameters.Add("reqTime", data.RequestTime);
cmd.ExecuteNonQuery();
}
}
}
}
}
```
### 三、数据库表结构设计
```sql
-- 主表存储设备信息
CREATE TABLE HJ212_MAIN (
ID NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
REQUEST_TIME VARCHAR2(14) NOT NULL, -- 格式: yyyyMMddHHmmss
SYSTEM_CODE VARCHAR2(10) NOT NULL,
COMMAND_CODE VARCHAR2(10) NOT NULL,
DEVICE_ID VARCHAR2(30) NOT NULL,
FLAGS VARCHAR2(10),
RECEIVE_TIME TIMESTAMP DEFAULT SYSTIMESTAMP
);
-- 测量数据表
CREATE TABLE HJ212_MEASUREMENTS (
ID NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
DEVICE_ID VARCHAR2(30) NOT NULL,
PARAM_CODE VARCHAR2(20) NOT NULL, -- 如 B01-Rtd
VALUE VARCHAR2(50) NOT NULL,
REQUEST_TIME VARCHAR2(14) NOT NULL,
RECEIVE_TIME TIMESTAMP DEFAULT SYSTIMESTAMP,
CONSTRAINT fk_device FOREIGN KEY (DEVICE_ID) REFERENCES HJ212_MAIN(DEVICE_ID)
);
```
### 四、部署与使用步骤
1. **安装依赖**:
```bash
Install-Package Oracle.ManagedDataAccess
```
2. **启动服务**:
```csharp
class Program
{
static void Main()
{
var receiver = new HJ212Receiver();
receiver.Start();
}
}
```
3. **配置调整**:
- 修改`OracleDataSaver`中的连接字符串
- 根据实际设备调整端口号
- 根据监测需求扩展`HJ212Data`类
4. **错误处理增强**:
```csharp
try
{
HJ212Parser.ProcessData(rawData);
}
catch (Exception ex)
{
File.AppendAllText("error.log", $"{DateTime.Now}: {ex.Message}\n");
// 可选:发送错误响应给设备
}
```
### 五、性能优化建议
1. **连接池管理**:
```csharp
// 在app.config中配置
<oracle.manageddataaccess.client>
<version number="*">
<settings>
<setting name="Connection Pooling" value="True"/>
<setting name="Min Pool Size" value="5"/>
<setting name="Max Pool Size" value="50"/>
</settings>
</version>
</oracle.manageddataaccess.client>
```
2. **批量插入优化**:
```csharp
// 使用OracleBulkCopy进行批量插入
var bulkCopy = new OracleBulkCopy(conn);
bulkCopy.DestinationTableName = "HJ212_MEASUREMENTS";
bulkCopy.WriteToServer(measurementsDataTable);
```
3. **异步处理**:
```csharp
ThreadPool.QueueUserWorkItem(async state =>
{
await OracleDataSaver.SaveToDatabaseAsync(data);
});
```
### 六、注意事项
1. **协议版本**:HJ212-2017与旧版的主要区别在于:
- 增加了数据段长度字段
- 修改了CRC校验范围
- 扩展了数据区格式[^1]
2. **安全考虑**:
- 使用防火墙限制访问IP
- 数据库连接字符串加密存储
- 实施SQL参数化防止注入
3. **数据完整性**:
- 添加数据库事务处理
- 实现重试机制
- 添加ACK响应确认