一個自定義的自報報文格式(用于傳感器自報上傳數(shù)據(jù))
本報文格式不能處理粘包問題,因為處理粘包問題的成本太高,會極大的降低服務(wù)端的處理效率以及增加內(nèi)存消耗,如果傳輸速度很高,建議使用UDP,UDP傳輸速度快,并且應(yīng)用層做響應(yīng),增加重傳機(jī)制可以有效的保證數(shù)據(jù)的可靠性,目前我做的RTU升級等功能都是使用UDP完成,服務(wù)器端開銷小,并且不會粘包。
通訊建議:不要做什么握手機(jī)制,直接發(fā)送數(shù)據(jù),服務(wù)器收到了就響應(yīng),收到響應(yīng)后就結(jié)束通訊,握手增加耗電又增加流量消耗,并且在GPRS通訊上是非常浪費資源的,本來網(wǎng)絡(luò)就不可靠,直接發(fā)送數(shù)據(jù),服務(wù)器收到了就響應(yīng)通訊就結(jié)束了,如果弄成握手方式,就是發(fā)送請求,握手成功,發(fā)送數(shù)據(jù),等待響應(yīng),結(jié)束通訊,就增加了1個來回,特別是之前用過MQTT多了幾個來回,太浪費通訊資源了。
?
使用HEX格式的通訊協(xié)議非常高效,不管是發(fā)送打包還是接收拆包,1個結(jié)構(gòu)體就能搞定,無需一個字節(jié)一個字節(jié)解析,如果用字符串json方式,使用單片機(jī)作為客戶端就很為難了,一般內(nèi)存不會很多,并且解析json需要的內(nèi)存會非常多。
?
報文全部使用小端模式,便于C語言處理。
注:由于我之前做了一個支持10W個設(shè)備的數(shù)據(jù)解析軟件,其實占用的內(nèi)存非常少,并且對CPU消耗很低,我們使用了一個很低端的服務(wù)器就能做到每秒1K條以上的數(shù)據(jù)處理,并且每條數(shù)據(jù)都在1秒內(nèi)得到響應(yīng),剛開始的時候還好,后面因為設(shè)備越來越多,導(dǎo)致服務(wù)器搜索設(shè)備變得很漫長(我的設(shè)備數(shù)據(jù)都是放到內(nèi)存中,使用了一個指針數(shù)組進(jìn)行管理),由于設(shè)備會動態(tài)的增加,不能對序列號進(jìn)行排序,因此查找設(shè)備的索引時間將會不可控,解析一條數(shù)據(jù)非常快,存儲是異步的,有1W條的緩存,但是唯獨查找設(shè)備這個看似簡單的過程變得非常復(fù)雜,由于之前的協(xié)議我沒有增加索引,我只能對設(shè)備進(jìn)行分組,我建了1W個指針數(shù)組,根據(jù)設(shè)備尾號后4位進(jìn)行分組,這樣會增加幾十兆的內(nèi)存消耗,但是查找卻變得非常迅速。通過這個事情之后,我決定在協(xié)議中直接增加索引,服務(wù)端收到索引后直接取出當(dāng)前設(shè)備的配置與SN進(jìn)行對比,如果相同就直接解析存儲數(shù)據(jù),如果不相同就進(jìn)行查找即可,沒有就進(jìn)行新建,最后建議將設(shè)備分組與協(xié)議中增加索引進(jìn)行整合,集各自的優(yōu)點,可以最大限度的提高數(shù)據(jù)的解析能力,當(dāng)每個設(shè)備都通訊一次之后,解析任何一個設(shè)備上傳的數(shù)據(jù)的時間都是一樣的,并且是最小的。
禁忌:解析數(shù)據(jù)是千萬不要把配置什么的都放到數(shù)據(jù)庫,數(shù)據(jù)庫相比內(nèi)存實在是慢太多了,而且時間不可控,數(shù)據(jù)庫本身就不是非??煽浚ㄗh將數(shù)據(jù)庫操作做異步處理,中間用FIFO通訊,這樣數(shù)據(jù)解析線程可以以最高效的方式運(yùn)行(一個線程輕松解析成千上萬條數(shù)據(jù),通過增加一個線程可以提高1倍的處理能力)。
?
存儲設(shè)備實時信息方式
我存儲設(shè)備相關(guān)信息的方式是,每個設(shè)備有一個自己的統(tǒng)一的結(jié)構(gòu)體,比如我最大支持10000個設(shè)備,我就會先申請1個指針數(shù)組,大小為10000,占用40000B,也就不到40KB內(nèi)存。然后每添加一個設(shè)備,就給這個指針申請一個內(nèi)存,存放設(shè)備的配置信息,同時會使用托管的線程池異步向數(shù)據(jù)庫中增加一個設(shè)備。
但是如果有幾千上萬個設(shè)備的時候,尋找這個設(shè)備的配置就變得很吃力了(相對來說,內(nèi)存中遍歷都會比數(shù)據(jù)庫快),這個時候我就會對設(shè)備進(jìn)行分組,比如分100組,最好的情況下每個組有100個設(shè)備,但是不排除有一種可能,一個組中有超過100個設(shè)備的呀,我就要做一個二維指針數(shù)組,分100個數(shù)組,每個數(shù)組中1000個設(shè)備指針,這樣內(nèi)存占用就會是100*1000*4,也還好400K,但是如果一個組超過了1000個就不好辦了,同樣如果是10W個設(shè)備的時候,這個分組就很吃內(nèi)存了,我的做法是每個分組用個變量進(jìn)行記錄,每個組給50個設(shè)備指針,當(dāng)這個組超過50個之后,我重新對這個指針進(jìn)行申請內(nèi)存,大小為上一次的+50個,然后把之前的數(shù)據(jù)拷貝過來,釋放掉之前的數(shù)據(jù),這樣就可以動態(tài)的進(jìn)行分組控制,實現(xiàn)效率的提升與內(nèi)存的最小消耗。
使用分組提高查找的一個例子
//查找指定SN的設(shè)備索引 //返回索引;-1:沒有找到 //2017-12-27?:?會使用分組進(jìn)行快速查找索引,只能在單線程中使用,不要跨線程搜索 int?FindSN(char?pSN[16]) { DWORD?Hash; BYTE?temp[3]; int?GroupIndex; ONE_DEVICE_DATA_TYPE?*pDeviceData; if?(pSN?==?nullptr)?return?-1; pSN[15]?=?0; if(strlen(pSN)?!=?15)?return-1; //檢查最后3位是否為000-999 temp[0]?=?pSN[12]?-?'0'; //字符轉(zhuǎn)換為數(shù)字 temp[1]?=?pSN[13]?-?'0'; //字符轉(zhuǎn)換為數(shù)字 temp[2]?=?pSN[14]?-?'0'; //字符轉(zhuǎn)換為數(shù)字 if?(temp[0]?>?9?||?temp[1]?>?9?||?temp[2]?>?9) { return?-1; } GroupIndex?=?temp[0]?*?100?+?temp[1]?*?10?+?temp[2];//計算當(dāng)前設(shè)備的分組索引 if?(GroupIndex?>=?DEVICE_GROUP_CNT)?return?-1; //分組索引無效 Hash?=?USER_LIB.BKDRHash(pSN); //計算哈希結(jié)果 //在指定的分組內(nèi)去搜索當(dāng)前設(shè)備 for?(DWORD?i?=?0;?i?<?this->GroupDeviceCnt[GroupIndex];?i++) { pDeviceData?=?(ONE_DEVICE_DATA_TYPE?*)this->pDeviceDataGroupPointerBuff[GroupIndex][i]; //遍歷當(dāng)前分組 if?(pDeviceData->Config.Hash?==?Hash) //先判斷哈希結(jié)果是否正確 { if?(strcmp(pSN,?pDeviceData->Config.SN)?==?0)?return?pDeviceData->Index; //找到了,返回索引 } } return?-1; }
添加設(shè)備的例子,先添加到全局的配置數(shù)組中,然后再把指針存儲一份到分組索引中
//添加一個設(shè)備到配置緩沖區(qū),返回索引,DeviceCnt?>=?DEVICE_MAX_CNT) //設(shè)備數(shù)量超出范圍 { *pError?=?"設(shè)備配置超出范圍了"; return?-1; } pConfig->SN[15]?=?0; if?(strlen(pConfig->SN)?!=?15) { *pError?=?"設(shè)備序列號必須為15位"; return?-1; } //檢查最后3位是否為000-999 temp[0]?=?pConfig->SN[12]?-?'0'; //字符轉(zhuǎn)換為數(shù)字 temp[1]?=?pConfig->SN[13]?-?'0'; //字符轉(zhuǎn)換為數(shù)字 temp[2]?=?pConfig->SN[14]?-?'0'; //字符轉(zhuǎn)換為數(shù)字 if?(temp[0]?>?9?||?temp[1]?>?9?||?temp[2]?>?9) { return?-1; } GroupIndex?=?temp[0]?*?100?+?temp[1]?*?10?+?temp[2]; //計算當(dāng)前設(shè)備的分組索引 if?(GroupIndex?>=?DEVICE_GROUP_CNT) { *pError?=?"無效的分組索引"; return?-1; } if?(FindSN(pConfig->SN)?>=?0) //尋找指定地址的設(shè)備是否存在 { *pError?=?"當(dāng)前設(shè)備地址已經(jīng)存在"; return?-1; } p?=?new?ONE_DEVICE_DATA_TYPE; //申請內(nèi)存 memset(p,?0,?sizeof(ONE_DEVICE_DATA_TYPE)); //清控配置區(qū)域 memcpy(&p->Config,?pConfig,?sizeof(DEVICE_CONFIG_TYPE)); //拷貝數(shù)據(jù) p->Config.Hash?=?USER_LIB.BKDRHash(p->Config.SN); //生成SN的哈希值 p->DbStatus.ReadHistCongifCnt?=?p->DbStatus.ReadHistStatusCnt?=?p->DbStatus.WriteHistCongifCnt?=?-1; //將歷史記錄條數(shù)置為-1無效值 p->Index?=?this->DeviceCnt; //記錄當(dāng)前設(shè)備的索引 //將當(dāng)前設(shè)備編號進(jìn)行分組 if?(this->AddDeviceToGroup(GroupIndex,?(DWORD)p)?==?false) { *pError?=?"添加設(shè)備到分組失敗!"; return?-1; } U32_DeviceDataPointerBuff[this->DeviceCnt]?=?(DWORD)p; //存放指針 pDeviceDataPointerBuff[this->DeviceCnt++]?=?p; //存儲指針 return?(this->DeviceCnt?-?1); //返回當(dāng)前的索引 }
添加設(shè)備到分組中,如果分組滿了將會進(jìn)行擴(kuò)充
//添加一個設(shè)備到指定分組中,如果分組慢,將會申請擴(kuò)容 //不會進(jìn)行編號等檢查,只能在AddDevice中被調(diào)用 bool?AddDeviceToGroup(int?GroupIndex,?DWORD?DeviceDataPointer) { if?(GroupIndex?<?0?||?GroupIndex?>=?DEVICE_GROUP_CNT)?return?false; if?(this->GroupSize[GroupIndex]?>=?DEVICE_MAX_CNT)?return?false; //分組大小不能超過總設(shè)備限制 if?(this->GroupDeviceCnt[GroupIndex]?>?this->GroupSize[GroupIndex])?return?false; if?(this->GroupDeviceCnt[GroupIndex]?==?this->GroupSize[GroupIndex]) //需要擴(kuò)容 { if?(DeviceGroupExpansion(GroupIndex)?==?false) //擴(kuò)容失敗,返回 { SYS_LOG.Write(__FILE__?+?__LINE__?+?"分組擴(kuò)容失敗,分組索引"?+?GroupIndex+"rn"); return?false; } SYS_LOG.Write("分組"?+?GroupIndex?+?"擴(kuò)容成功rn"); } this->pDeviceDataGroupPointerBuff[GroupIndex][this->GroupDeviceCnt[GroupIndex]]?=?DeviceDataPointer; //保存當(dāng)前設(shè)備指針 this->GroupDeviceCnt[GroupIndex]?++; //分組內(nèi)的設(shè)備數(shù)量增加 return?true; }
//為指定的分組進(jìn)行擴(kuò)容,會先復(fù)制緩沖區(qū),然后重新申請,再釋放之前的緩沖區(qū)(注意:使用分組索引只能在一個線程中使用) //必須分組已經(jīng)滿了再調(diào)用 bool?DeviceGroupExpansion(int?GroupIndex) { if?(GroupIndex?<?0?||?GroupIndex?>=?DEVICE_GROUP_CNT)?return?false; if?(this->GroupSize[GroupIndex]?>=?DEVICE_MAX_CNT)?return?false; //分組大小不能超過總設(shè)備限制 DWORD?*p?=?this->pDeviceDataGroupPointerBuff[GroupIndex]; //先復(fù)制之前的指針 DWORD?Size?=?this->GroupSize[GroupIndex]; //記錄之前分組容量 this->GroupSize[GroupIndex]?+=?GROUP_DEVICE_INC; //當(dāng)前分組容量增加 this->pDeviceDataGroupPointerBuff[GroupIndex]?=?new?DWORD[this->GroupSize[GroupIndex]]; //重新為當(dāng)前分組申請內(nèi)存 memcpy(this->pDeviceDataGroupPointerBuff[GroupIndex],?p,?Size*sizeof(DWORD)); //將之前的分組設(shè)備信息拷貝到新緩沖區(qū)中 USER_DELTE(p); //釋放掉之前的舊緩沖區(qū) return?true; }
后面我會擴(kuò)展使用這個協(xié)議的下位機(jī)與服務(wù)端框架,并提供相應(yīng)的測試?yán)印?br />