近几年随着各类国产化的崛起,无论是硬件还是软件,都开始倡导国产化。电力领域也开始了国产化的进程,包括现在很多项目都明确要求了网关,融合终端等必须是国产芯片,很多省的配电房iot项目也是国产化的应用(关于iot相关的介绍,放到后面了)。
一、背景
我们先看报批稿中关于开展cms61850研究的背景
总的来说呢,61850是一个好的协议,一个好的面向对象的思想,是面向未来的电力协议。但是因为国际版在61850中引入了MMS协议。这个就显得有点多余了,使得协议变得十分复杂,不利于维护更改,性能也比较低。所以在61850引入国内之处,熟悉相应协议的大家都知道,思科的mmslite,迅速引入了项目中,被收割了一波。为了更好的纯粹61850,加入一些安全策略,cms61850就应运而生了。我们再看一张两者对比的架构图
左边为cms61850架构,右边为国际版61850。可以看的出,国内版核心业务,都是直接基于tcp之上,这与国际版在tcp之上各种封装简化了很多,也易于理解。
二、编码
前两年的时候,倒是听过国产61850,但是相应的项目很少,大部分项目用的还是国际版61850。用mmslite足以。但去年的时候,有一些用户开始询问国产61850的事,我就开始准备去研究这个协议。但是公司目前也没有什么开发计划,就业余的时间搞搞了。差不多去年十二月份左右开始研究,后来阳了过后,停了一段时间,今年开始,又继续开整了。
那么问题来了,我们该怎么去开发呢。我觉得开发这个协议至少得又两个必要因素:
一、对61850协议有一定的认识,比如相关概念,SCL语法等都要掌握
二、一定的工作经验加良好的编码能力
当这两点都具备的时候,我们可以尝试着开始了。首先从架构上来看,我门从战略上藐视它,不过也就一个基于TCP的协议。作为开发老鸟,谁还没写过socket相关的程序呢,这个问题不大。剩下的无非就是报文分析呗。就在我们这样想的时候,我们遇到了第一个拦路虎,编码。
国际版61850采用的是ber编码,而cms61850采用的是per编码,具体来说是紧缩型per,也就是APER。关于这两个孰优孰劣,其实我觉得没必要去长篇大论分析,按我的猜想,纯粹因为跟国际版区分开选择了per,不然怎么体现国产呢😄。什么能减轻带宽啦,性能提高啦,我觉得不是必要因素
APER编码相关文档,见下面链接
链接:https://pan.baidu.com/s/1oGlkbUw8rHX2WOiKcZ6b6Q
提取码:xm9k
其中的ASN.1编码文档整理写的是相当好,只可惜不知道作者。另一个是规范,比较晦涩难懂,选看。
那我们怎么开发呢?之前的文章,或多或少,我也提到过,站在巨人的肩膀上,会更加顺手。本来我也想自己根据协议文档开发一个,但是后来发现,如果还想实现根据asn文件生成相应代码,那工作量还是不小。这时候,我们需要借鉴三方库了,在asn1编码支持中,比较出名的是asn1c这个代码。立马github克隆下来。可以发现它支持的编码规则还是比较多的,如下:
但是遗憾的是,该库并未实现APER,倒是UPER实现了。但是在评论中又发现了基于该库的另一个分支实现了APER。
果断再次克隆下来,按照流程进行相应APER编解码,然后与上述文档中的一些示例还是不一样。这个APER与国标的还是有一点差别。到这里也不要放弃,毕竟asn1的框架,从编解码,生成相应代码文件这块支持还是比较友好的。所以决定对其进行改造。忘记补充一点,开发CMS61850还得需要一个很重要的本领,调试。调试,可以很方便的帮我们理解陌生代码以及修改相应代码
最终经过修改,得到了能正确编解码的APER。后面会给出相应测试程序。
使用步骤,先编写asn文档,这个直接从审批稿中拷贝就可以,以协商为例内如如下:
然后执行指令 ./asn1c ./61850CMS.asn -D ./out -gen-APER -no-gen-BER -no-gen-OER -no-gen-XER -no-gen-JER -no-gen-example -fcompound-names
就可以生成一堆协商相关的头文件和c文件,是直接可以加入到我们的项目中使用的.
#ifndef _Associate_RequestPDU_H_
#define _Associate_RequestPDU_H_
#include <asn_application.h>
/* Including external dependencies */
#include "VisibleString129.h"
#include <OCTET_STRING.h>
#include "UtcTime.h"
#include <constr_SEQUENCE.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Associate-RequestPDU */
typedef struct Associate_RequestPDU {
VisibleString129_t *serverAccessPointReference; /* OPTIONAL */
struct Associate_RequestPDU__authenticationParameter {
OCTET_STRING_t signatureCertificate;
UtcTime_t signedTime;
OCTET_STRING_t signedValue;
/* Context for parsing across buffer boundaries */
asn_struct_ctx_t _asn_ctx;
} *authenticationParameter;
/* Context for parsing across buffer boundaries */
asn_struct_ctx_t _asn_ctx;
} Associate_RequestPDU_t;
/* Implementation */
extern asn_TYPE_descriptor_t asn_DEF_Associate_RequestPDU;
#ifdef __cplusplus
}
#endif
#endif /* _Associate_RequestPDU_H_ */
#include <asn_internal.h>
三、实现
其实编码能搞定的话,实现业务来说,无非就是花时间编写调试了。毕竟cms61850实现的接口还是非常多的。这里先简单介绍,相应代码编写,后面代码可能会部分开源,到时会写的更为详细些。仍然以协商为例
/// 注册协商部分处理函数,code为1
CServiceManager::instance()->attachFunc(1, "Associate", base::function(&CAssociate::associate, this));
ServiceError CAssociate::associate(const std::string &funcName, const NetMessage &message, std::string &response)
{
m_mutex.lock();
auto info = m_mapTcpInfo[message.clientId];
m_mutex.unlock();
info->status = AssociateFail;
/// 协商一开始状态假定为Fail
CServiceManager::instance()->setStatus(message.clientId, CServiceManager::AssociateFail);
CSafeStruct<Associate_RequestPDU> reqPtr;
/// 解码协商请求,填充到Associate_RequestPDU中
if (!Decode(reqPtr, message.buf, message.len))
{
errorf("deocde %s failed\n", funcName.c_str());
return ServiceError_decode_error;
}
/// 打印请求内容
PrintAPER(reqPtr);
auto &sclInfo = CSCLParse::instance()->getSCLInfo();
if (reqPtr->serverAccessPointReference != NULL && reqPtr->serverAccessPointReference->buf != NULL)
{
std::string serverPoint = (char *)reqPtr->serverAccessPointReference->buf;
if (serverPoint != sclInfo.accessPoint)
{
return ServiceError_instance_not_available;
}
}
/// 封装响应结构体
CSafeStruct<Associate_ResponsePDU> respPtr;
if (reqPtr->authenticationParameter != NULL)
{
/// 应用层安全校验
if (!verifySoftSafe(reqPtr->authenticationParameter))
{
warnf("verify app safe failed\n");
return ServiceError_failed_due_to_communications_constraint;
}
respPtr->authenticationParameter = CallocPtr(Associate_ResponsePDU::Associate_ResponsePDU__authenticationParameter);
/// 回传检验信息
if (!setSoftSafeAuth(respPtr->authenticationParameter))
{
warnf("set app safe param failed\n");
return ServiceError_failed_due_to_communications_constraint;
}
}
copyStrToOctStr(m_stationId, &respPtr->associationId);
respPtr->serviceError = ServiceError_no_error;
/// 对响应结构体进行编码
if (!Encode(respPtr, response))
{
errorf("encode %s response failed\n", funcName.c_str());
return ServiceError_failed_due_to_communications_constraint;
}
PrintAPER(respPtr);
info->status = Associate;
CServiceManager::instance()->setStatus(message.clientId, CServiceManager::SUCCESS);
return ServiceError_no_error;
}
函数设计返回值为ServiceError,因为有些接口是要又否定响应相应的值。后续的接口开发与此处类似,只需要编写相应处理函数,注册进管理即可。对asn1c熟悉的同学可能会觉得,怎么没有看到asn1c相关的接口呢,上述代码都是经过封装的。比如解码相关接口封装如下:
bool decode(const asn_TYPE_descriptor_s *td, void **sptr, const void *buffer, size_t size)
{
asn_dec_rval_t ret = aper_decode_complete(td, sptr, buffer, size);
return ret.code == RC_OK ? true : false;
}
程序中为了减少用户对asn1c代码的理解,秉着学习的越少,用的越好的理念,进行了封装,也就是客户只需要了解简单少许接口,即可编程。比如为了减少代码的书写,为了更好的控制内存释放。在c语言中,比如在一个函数中申请了内存,每一个return之前都需要进行相应释放。显得繁琐啰嗦,所以在很多c代码中,我们可以看到goto语句的应用,进行统一处理。但是这里我们选择c++,就有它自己的处理方式,可以模仿智能指针,写一个简单的智能管理类,同时为了不让客户再去理解asn1c中的接口,进行了封装,如下
template<typename T>
class CSafeStruct;
#define SafeDef(type) \
template<>\
class CSafeStruct<type> {\
using realType = DEFType(type);\
public:\
explicit CSafeStruct() : m_bRelese(true) {m_ptr = CallocPtr(realType); m_stuPtr = StructPtr(type);}\
CSafeStruct(realType *ptr) : m_bRelese(false) { m_ptr = ptr; m_stuPtr = StructPtr(type);}\
~CSafeStruct() {if (m_bRelese) FreeStruct(type, m_ptr);}\
public:\
realType *get() {return m_ptr;}\
realType **getRef(){return &m_ptr;}\
realType *operator->() {return m_ptr;}\
realType& operator*() {return *m_ptr;}\
asn_TYPE_descriptor_t *getTypePtr() {return m_stuPtr;}\
private:\
realType *m_ptr;\
asn_TYPE_descriptor_t *m_stuPtr;\
bool m_bRelese;\
};
在使用中,只要用CSafeStruct进行封装,在函数结束时,会在析构中自动释放内存。
四、结尾
后续代码接口部分,会在后面文章中继续跟新。
已上传部分开源代码,见下: