声明:原创作品,涉及的开源程序代码学习和研究,严禁用于商业目的。 如有任何问题,欢迎和我交流:408797506@qq.com(微信:408797506)
相关视频学习链接:
一. 背景
程序设计的思想来自于easycwmp官网,看过或者用过easycwmp的工程师应该都知道,该开源代码还有商业版,而且价格不菲。easycwmp官网如是说:DataModel is developped with shell as free solution and with C as commercial solution.。开源代码用来学习还是值得的,若是用于商业产品可能就会显得"力不从心"。幸运的是,我有机会阅读了Works Systems公司和broadcom公司的tr069代码,架构设计与easycwmp的设计"如出一辙",不知是谁抄袭了谁?下图是easycwmp官网的程序架构图。但是,个人对于Works Systems公司的代码"情有独钟"并且进行了重新开发和利用,使得程序更加高效易用和移植性。基于此,本文就着重介绍在商业代码中如何高效,便捷的实现DataModel 和CWMP core分离, 后续若有机会可以介绍一下broadcom公司的程序设计仅供参考。本质上大同小异。

二. 程序设计概要
下图是tr069程序的单进程处理流程。大致分为:配置文件解析模块,日志模块,设备xml解析模块,任务模块以及事件处理模块(有关联),多线程模块(可插入模块)等。
原则上,CWMP core的程序代码不需要修改,主要是根据客户的需求修正或者进行"插入式的"新增事件类型和模块化处理。而设备相关的程序,我们封装成了一个动态库(libcwmp.so),便于独立编译和维护开发。

(附: 高清PDF版下载路径http://download.youkuaiyun.com/detail/eryunyong/9731487)
对于单一的产品线,程序采用"单进程多线程"的思想。这样比较容易简洁,而且方便维护。若是多功能的产品,即一个设备上需要运行多个"CWMP进程", 那么我们使用了创建"多个子进程"的方法,每个子进程根据配置文件的不同从而实现设备的不同需求(该功能待完善)。比如现在家庭或者企业网关产品越来越需要"智慧""智能"的需求,如何让设备与手机互连互通,保证安全方便高效的前提下,既可以被运营商(卖方)管理,同时又可以被自己(买方)控制管理,这是一个值得思考的问题,也是工程师需要考虑的技术。
3.1 配置文件解析(libconf)
根据配置文件的全路径和内容初始化数据结构,使用例子如下:


1 char conf_file[PATH_MAX] = {0};
2 conf_t *tmp = NULL;
3
4 tmp = conf_load(conf_file);
5 count = conf_get_int(tmp, "global:count", 0);
3.2 日志模块
为了便于和Linux的syslog统一和管理,这里定义的日志等级与syslog一致。
CWMP_LOG_EMERG ----------------------> EMERG = 0
CWMP_LOG_ALERT ----------------------> ALERT = 1
CWMP_LOG_CRIT ----------------------> CRIT = 2
CWMP_LOG_ERROR ----------------------> ERROR = 3
CWMP_LOG_WARN ---------------------> WARN = 4
CWMP_LOG_NOTICE ---------------------> NOTICE= 5
CWMP_LOG_INFO ---------------------> INFO = 6
CWMP_LOG_DEBUG ---------------------> DEBUG = 7
3.3 XML解析模块
使用libexpat库函数解析设备XML格式文件,以及CWMP和ACS之间交换的SOAP消息。device.xml文件内容如下:


1 <TR069 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
2 <trf>
3 <obj name="InternetGatewayDevice">
4 <param name="DeviceSummary" getval_func="CpeGetDeviceSummary"></param>
5 <param name="LANDeviceNumberOfEntries" type="2" getval_func="CpeGetLANDeviceNumberOfEntries"></param>
6 <param name="WANDeviceNumberOfEntries" type="2" getval_func="CpeGetWANDeviceNumberOfEntries"></param>
7 <obj name="DeviceInfo" noti_rw="1">
8 <param name="SpecVersion" getval_func="CpeGetDeviceInfoSpecVersion"/>
9 <param name="HardwareVersion" getval_func="CpeGetDeviceInfoHardwareVersion"></param>
10 <param name="SoftwareVersion" getval_func="CpeGetDeviceInfoSoftwareVersion"></param>
11 <param name="Manufacturer" getval_func="CpeGetDeviceInfoManufacturer"></param>
12 <param name="SerialNumber" getval_func="CpeGetDeviceInfoSerialNumber"></param>
13 <param name="ManufacturerOUI" getval_func="CpeGetDeviceInfoManufacturerOUI"></param>
14 <param name="ProvisioningCode" rw="1" getval_func="CpeGetDeviceInfoProvisioningCode" setval_func="CpeSetDeviceInfoProvisioningCode"></param>
15 <param name="ProductClass" getval_func="CpeGetDeviceInfoProductClass"></param>
16 <param name="DeviceType" getval_func="CpeGetDeviceInfoDeviceType"></param>
17 <param name="ModelName" getval_func="CpeGetDeviceInfoModelName"></param>
18 <param name="CpeWANAddress" noti_rw="1" rw="1" getval_func="CpeGetCpeWANAddress" setval_func="CpeSetCpeWANAddress"/>
19 </obj>
20 <obj name="ManagementServer">
21 <param name="ConnectionRequestURL" getval_func="CpeGetManagementServerConnectionRequestURL"></param>
22 <param name="ConnectionRequestUsername" rw="1" noti_rw="1" getval_func="CpeGetManagementServerConnectionRequestUsername" setval_func="CpeSetManagementServerConnectionRequestUsername"></param>
23 <param name="ConnectionRequestPassword" rw="1" noti_rw="1" getval_func="CpeGetManagementServerConnectionRequestPassword" setval_func="CpeSetManagementServerConnectionRequestPassword"></param>
24 <param name="Username" rw="1" noti_rw="1" getval_func="CpeGetManagementServerUsername" setval_func="CpeSetManagementServerUsername"></param>
25 <param name="Password" rw="1" noti_rw="1" getval_func="CpeGetManagementServerPassword" setval_func="CpeSetManagementServerPassword"></param>
26 <param name="ParameterKey" getval_func="CpeGetManagementServerParameterKey" setval_func="CpeSetManagementServerParameterKey"/>
27 <param name="URL" rw="1" noti_rw="1" getval_func="CpeGetManagementServerUrl" setval_func="CpeSetManagementServerUrl"></param>
28 <param name="PeriodicInformEnable" rw="1" noti_rw="1" type="3" getval_func="CpeGetManagementServerPeriodicInformEnable" setval_func="CpeSetManagementServerPeriodicInformEnable"></param>
29 <param name="PeriodicInformInterval" rw="1" noti_rw="1" type="2" getval_func="CpeGetManagementServerPeriodicInformInterval" setval_func="CpeSetManagementServerPeriodicInformInterval"></param>
30 <param name="PeriodicInformTime" rw="1" type="4" getval_func="CpeGetManagementServerPeriodicInformTime" setval_func="CpeSetManagementServerPeriodicInformTime"></param>
31 </obj>
32
33 <obj name="Time">
34 <param name="Enable" rw="1" type="3" getval_func="CpeGetTimeEnable" setval_func="CpeSetTimeEnable"></param>
35 <param name="NTPServer1" rw="1" getval_func="CpeGetTimeNTPServer1" setval_func="CpeSetTimeNTPServer1"></param>
36 <param name="CurrentLocalTime" type="4" getval_func="CpeGetTimeCurrentLocalTime"></param>
37 </obj>
38 <obj name="X_CT-COM_MonitorCollector">
39 <param name="Enable" noti_rw="1" rw="1" type="3" getval_func="CpeGet_MonitorEnable" setval_func="CpeSet_MonitorEnable"></param>
40 <obj name="MonitorConfig" rw="1" addobj_func="TRF_Add_MonitorConfig" delobj_func="TRF_Del_MonitorConfig" refresh_func="TRF_Refresh_MonitorConfig">
41 <obj name="0">
42 <param name="ParaList" noti_rw="1" rw="1" getval_func="CpeGet_MonitorConfig_ParaList" setval_func="CpeSet_MonitorConfig_ParaList"></param>
43 <param name="TimeList" rw="1" type="2" getval_func="CpeGet_MonitorConfig_TimeList" setval_func="CpeSet_MonitorConfig_TimeList"></param>
44 </obj>
45 </obj>
46 </obj>
47 <obj name="LANDevice">
48 <obj name="1">
49 <param name="LANEthernetInterfaceNumberOfEntries" type="2" getval_func="CpeGetLANEthernetInterfaceNumberOfEntries"/>
50 </obj>
51 </obj>
52 <obj name="ObjTest" rw="1" addobj_func="TRF_Add_ObjTest" delobj_func="TRF_Del_ObjTest" refresh_func="TRF_Refresh_ObjTest">
53 <obj name="0">
54 <param name="TestEnabled" rw="1" type="3" getval_func="CpeGetObjTest_TestEnabled" setval_func="CpeSetObjTest_TestEnabled"/>
55 </obj>
56 </obj>
57 </obj>
58 </trf>
59
60 <devlib name="/usr/lib/libcwmp.so"></devlib>
61
62 <auth name="dev_get_auth"></auth>
63
64 <listenport name="dev_get_listenport"></listenport>
65 <wanparamname name="dev_get_wanparam_name"></wanparamname>
66
67 <bootstrap name="dev_bootstrap"></bootstrap>
68 <init name="dev_init"></init>
69 <reboot name="dev_reboot"></reboot>
70
71 <factoryreset name="dev_factoryreset"></factoryreset>
72 <download name="dev_download"></download>
73 <acsstatus name="dev_set_acs_status"></acsstatus>
74 <urldnsresolve name="dev_url_dns_resolve"></urldnsresolve>
75
76 <upload name="dev_upload"></upload>
77 <cwmpenable name="dev_cwmp_enable"/>
78
79 <informlist>
80 <inform name="InternetGatewayDevice.DeviceInfo.ModelName"/>
81 <inform name="InternetGatewayDevice.DeviceInfo.DeviceType"/>
82 </informlist>
83
84 <eventlist>
85 <event name="X CT-COM BIND"></event>
86 </eventlist>
87
88
89 </TR069>
InternetGatewayDevice是整个参数树的根。obj表示这是一个对象,obj可以读,可以写,当obj的name为0时,表示该obj可以是个模板,为创建后面的实例提供一个模板,当ACS查询时,不会把obj name为0的Obj发送给ACS。Obj的rw=1,表示该obj可以添加子obj,通过addobj_func来添加,通过delobj_func来删除,refresh_func表示刷新该obj下的信息。 Obj的noti_rw =1认为可以设置该obj的属性,譬如notify属性,如果设置了obj的属性,则认为该obj下的所以子树都有该属性。Param表示一个参数项,参数可以读,可以写,通过getval_func来读,通过setval_func来写。noti_rw=1认为可以设置该Param的属性,譬如notify属性。Type的含义如下:
string -------------------------> 0
int --------------------------> 1
unsigned int -----------------------> 2
bool -------------------------> 3
datetime -----------------------> 4
base64 -----------------------> 5
long -------------------------> 6
unsigned long ----------------------> 7
hex binary -----------------------> 8
object -------------------------> 9
3.4 TASK任务模块
根据任务队列中的消息类型进行处理,把需要发送给ACS的事件event消息加入事件队列。


1 //诊断
2 #define TASK_DIAG 1
3 //重启
4 #define TASK_REBOOT 2
5 //恢复出厂设置
6 #define TASK_FACTORY 3
7 //download
8 #define TASK_DOWNLOAD 4
9 //upload
10 #define TASK_UPLOAD 5
11 //change ACS URL
12 #define TASK_CHANGE_ACS_URL 6
13 #define TASK_SUBDEVICE 7
14 #define TASK_ADD_EVENT 8
15 #define TASK_ADD_INFORM 9
16 #define TASK_CLEAR_EVENT 10
17
18 #define TASK_VPN_RESTART 20
19 #define TASK_SYSLOG_RESTART 21
20 #define TASK_FIREWARE_RESTART 22
21
22 #define TASK_OTHER 99
3.5 事件处理模块
使用event_handle函数来处理事件,通过信号量来等待是否需要处理的事件,以及从事件队列中获取处理的事件。同理,在其他线程函数中,通过置信号量,将事件加入队列中来通知该模块处理。
STATUS_IDLE = 0, /* 空闲状态 */
STATUS_INIT, /* 初始化,获取ACS的URL,以及发送Inform消息*/
STATUS_CONN, /* CPE和ACS处于连接状态,并处理ACS下发的任务 */
STATUS_ERROR, /* 发生错误 */
STATUS_FINS, /* 结束事件处理 */
3.6 其他多线程模块
为了实现"低耦合高内聚"的模块化思想,程序设计采用了多线程来实现。比如:周期上报Inform事件, 根据tr069规范监测参数变化的上报事件,检测WAN口地址变化的事件,STUN线程,DHCP发现ACS地址事件。
四. 数据结构
1) cwmp_context结构体是CWMP进程处理的上下文,主要包括初始化设备参数树,Value change,监视参数变化,记录事件等。


1 struct cwmp_context{
2 file_context_t file_ctx; //配置文件
3 trf_param_t param_root; //参数树根节点
4 dev_info_t dev_info; //设备信息
5
6 void *handle_lib; //设备library的handle
7
8 int acs_port; //监听ACS的端口
9 int acs_retrycount; //连接ACS重试次数
10 int notify_interval; //监视参数变化的间隔时间
11
12 pthread_mutex_t mutex_attr;
13 hash_t *ht_attr; //记录参数属性,需要上报的。
14
15 pthread_mutex_t mutex_val_change;
16 hash_t *ht_val_change; //记录Value Change
17
18 char **inform_array; //需要上报的参数项数组
19 int inform_count; //需要上报的参数项总数
20
21 pthread_mutex_t mutex_inform_tmp;
22 char **inform_array_tmp; //临时需要上报的参数项数组
23 int inform_count_tmp; //临时需要上报的参数项总数
24
25 pthread_mutex_t mutex_evt;
26 int evt_count;
27 event_info_t *evt_array; //记录事件
28 event_global_t evt_global_info; //记录由于重启需要保存的信息
29 sem_t sem_send_acs; //发送给acs信息的信号量
30
31
32 pthread_mutex_t mutex_task; //for task_list
33 list_t *task_list; //task list
34 sem_t sem_task;
35
36 pthread_mutex_t mutex_param; //对参数进行加锁,防止多线程操作时导致程序不稳定
37 //仅在对参数有操作的线程中加锁
38 trans_t transfer_info; //用于Download和Upload
39 };
2)EventType主要定义了规范中的事件类型


1 typedef enum
2 {
3 EVENT_BOOTSTRAP = 0,
4 EVENT_BOOT,
5 EVENT_PERIODIC,
6 EVENT_SCHEDULED,
7 EVENT_VALUECHANGE,
8 EVENT_KICKED,
9 EVENT_CONNECTIONREQUEST,
10 EVENT_TRANSFERCOMPLETE,
11 EVENT_DIAGNOSTICSCOMPLETE,
12 EVENT_REQUESTDOWNLOAD,
13 EVENT_AUTONOMOUSTRANSFERCOMPLETE,
14 EVENT_MREBOOT,
15 EVENT_MSCHEDULEINFORM,
16 EVENT_MDOWNLOAD,
17 EVENT_MUPLOAD,
18 EVENT_MAXCOUNT
19 }EventType;
对于自定义的事件类型可以通过xml中如下定义:
<eventlist>
<event name="X CT-COM BIND"></event>
</eventlist>
3)


1 struct trf_param
2 {
3 char name[PARAM_NAME_LEN+1]; //参数名
4 int type; //参数类型 trf_datatype_e
5 int writable; //是否可写。0:不可写,1:可写,如果object
6 //可以Add,则可写
7 int max_instance; //属于Object, 最大instance值,-1表示无限制
8 int notification; //属于Parameter, 0:off,1:passive,2:active
9 unsigned char noti_rw; //属于Parameter, 是否可以设置上报属性,0 不可以 1 可以
10 unsigned long acl; /*属于Parameter, access list */
11 TRFGetParamValueFunc getparamval_func; //属于Parameter, 取得参数值函数
12 TRFSetParamValueFunc setparamval_func; //属于Parameter, 设置参数值函数
13 TRFAddObjectFunc addobject_func; //属于Object, AddObject
14 TRFDelObjectFunc delobject_func; //属于Object, DeleteObject
15 TRFRefreshFunc refresh_func; //属于Object, 刷新
16 struct trf_param *parent; //父节点
17 struct trf_param *child; //子节点
18 struct trf_param *nextSibling; //兄弟节点
19 };
定义树形结构的节点,每个节点拥有自己的属性和方法。
4) 设备相关函数


1 <devlib name="/usr/lib/libcwmp.so"/> Libary的位置
2 <auth name="dev_get_auth"/> 是否需要开启ACS的认证
3 <listenport name="dev_get_listenport"/> CWMP的监听端口
4 <wanparamname name="dev_get_wanparam_name"/> 取得WAN口的参数项名称的全路径
5 <bootstrap name="dev_bootstrap"/> 判断是否取得首次连接到ACS的标志
6 <init name="dev_init"/> 初始化设备操作
7 <reboot name="dev_reboot"/> 设备的reboot方法
8 <download name="dev_download"/> 设备的download的方法,包括下载,升级之类的方法
9 <upload name="dev_upload"/> 设备upload的方法,包括生成配置文件,上传日志等方法
10 <cwmpenable name="dev_cwmp_enable"/> 判断是否启动CWMP进程
11 <urldnsresolve name="dev_url_dns_resolve"></urldnsresolve> ACS的URL解析
12 ...
13 其他参数项相关的操作函数
五. 总结
CWMP core与Datamodel分离,通过不断的调试和实践,并应用于不同的运营商(电信,联通,移动),使得CWMP core的程序不断成熟和稳定。
对于相同的功能,我们仅仅需要修改device.xm就可以实现需求,而不用去修改代码;
对于新增的参数项或者节点开发,我们仅需要开发设备相关的库;
对于新增的事件或者ACS下发的任务,修改library的同时我们只需要稍微修改CWMP core的程序就可以达到目的;
对于新增模块,我们采用线程"插入"的思想来实现,而不用修改程序的主体架构。