關於網路協議封裝的一些新想法

2021-08-01 18:46:22 字數 3324 閱讀 3913

最近業餘時間在寫乙個小遊戲。在為客戶端封裝socket層時頭腦一熱,有了一些新的想法, 在這裡記錄一下。

客戶端使用的是unity3d引擎。而在unity3d中,基礎的socket庫只提供兩種模式,一種是阻塞模式,一種是非同步callback模式。

一般都需要基於這兩種模式下進一步封裝,才可以更方便的使用。

諮詢了幾個做客戶端的並搜了一下,發現大家的慣用手法都是開乙個執行緒去使用socket阻塞去讀,然後把讀到的資料通過佇列傳回主線程進行處理。

但是也許是單執行緒的思維模式已經深入我心了,所以我個人並不是很喜歡這個實現。

在我的設想中,我希望能夠在直接在主線程完成對socket的讀寫及拆包工作。這僅僅是客戶端自己的資料,理論上量不會太大,所以即使把這部分工作放入主線程也不會影響渲染。

但是在這種設計下,阻塞模式和非同步callback模式都不太合適。因為阻塞模式在read時會使主線程卡住影響渲染,而callback模式則很容易掉入callback hell。

只有非阻塞模式才能滿足需要。即,呼叫socket.read函式時,可以傳入任意大小的長度,但是不管有沒有讀到資料socket.read一定會立即返回。

我基於callback模式重新抽象出了netsocket模組。

netsocket模組提供了connect, read, send, close等4個介面。

netsocket.connect提供非阻塞連線而netsocket.close提供非阻塞關閉。

netsocket.read可以提定任意讀取長度,但是不管是否能讀取到資料,它都會立即返回。

netsocket.send可以傳送任意長度資料,並且一定會立即返回。

有了這一組介面後,就可以在主線程毫無估計的去操作socket而不用提心阻塞及併發問題了。

有了可用的socket元件,下面就需要封裝協議包的組成布局了。

為了不粘包,一般都會首先在包頭加2~4個位元組的包長,指出後面還有多少個資料屬於當前這乙個包的內容。這個包頭長度一般用於資料報拆分。

不管是客戶端還是伺服器,都需要有乙個東西,可以識別這個資料報的內容是什麼,那就是command id,即協議id。

一般來講client向server請求的並不是都可以成功,如果出錯,伺服器需要指出這個協議id的出錯資訊,即錯誤碼。

而幾乎99%的協議請求都不能100%保證必成功,因此將錯誤碼加入包頭部分是合理的,那麼一整個協議包的內容可能就是這個樣子的。

————————–

|包長度|協議id|錯誤碼|協議內容|

————————–

如包長度,協議id和協議內容出現在協議包內都是毫無疑問的事,但是錯誤碼很讓人糾結。

雖然99%的請求都不一定100%成功,但是也並不會100%失敗。而在請爾成功時,協議包依然攜帶了乙個0錯誤碼(一般0為success),我認為這是一種無意義的浪費。

在糾結了一段時間之後,我修改了協議包的組成布局。將錯誤碼從包頭中去掉,如下:

———————

|包長度|命令碼|協議內容|

———————

至於返回錯誤碼,我把這件事交給了乙個通用協議,協議內容定義如下:

struct error

所有請求出錯後,都不再返回相應的協議id,而是用乙個error的協議取代。error協議id對應的協議結構體是error。

error::cmd用於指出是哪個命令出錯了,而error::err用於指出這個命令的出錯碼。

在接收到error協議之後,上層自動將error協議轉換為error::cmd所對應的協議,呼叫並將error::err作為錯誤碼傳給error::cmd對應的處理函式。

由此,我們就可以做到,如果不需要錯誤碼,就不必承受它所帶來的開銷。

封裝完資料報結構,下面就是封裝協議序列化了。

傳送功能一般沒什麼好說的,序列化成byte array,然後直接發出去即可。

接收協議就比較麻煩,因為不管怎麼樣總覺得這樣不夠完美。最常用的封裝方式一般如下:

void process_cmd1(int cmd, byte dat)

void process()

...}

這種方式最大的問題就是,隨著命令條數的增加,case會越來越長,不利於閱讀。並且每乙個函式的開頭都有兩行固定用於解析協議的話。

當然case的問題,其實很容易就可以優化掉,只要實現乙個map/dictionary就可以了,比如下面**:

修改**

dictionary protocol = new dictionary();

void register(int cmd, callback_t cb)

void process()

增加**

void start()

但是他依然解決不掉每個協議處理函式最開頭的那兩行協議解析語句。

接收協議部分的封裝我並不陌生,在寫伺服器程式時,我不止一次實現過上述類似的**,但都只能做到類似map/dictionary的樣子(在強型別語言中)。

這一次在實現時,突發奇想。如果在呼叫netprotocol.register函式時,提前把協議包new好,並與cmd進行關聯。

那麼在處理協議時就可以把ack.pares(dat)之類的協議解析語句,直接放入netprotocol.process函式中處理。

但是這裡需要有乙個前提就是所有的協議包都需要有乙個基類,並且這個基類提供parse介面。假設所有的協議包都繼承自class wire。那麼**看上去可能就是下面這個樣子。

修改**

dictionary protocol_cb = new dictionary();

dictionary protocol_obj = new dictionary();

void register(int cmd, wire obj, callback_t cb)

void process()

...} 增加**

void process_cmd1(int cmd, wire dat)

...void start()

其實這麼做只是省了一行**而已,似乎並不值得如此大費周張。但是,它的意義在於,我們可以借用這種方式,打破在process函式中不可以處理協議反序列化的困境。

在此基礎上,我們還可以更近一步,將cmd1和cmd1_packet進行關聯,這樣在上層我們就可以完全弱化掉cmd的存在,來降低上層應用的使用負擔。

在這次的實現中,我正是這樣做的。

當然,這需要使用的類protobuf工具做一些支援,比如可以從cmd1_packet物件反查出與其對應的協議id。剛好我自己實現的zproto是支援這種功能的。

關於OCR,一些想法

ocr一般分為兩種 1,根據給定的字元特徵集合,提取未知字元的特徵進行匹配識別 典型例子 gocr 2,不知道字元特徵,但給出提取特徵的規則,通過機器學習training來獲取某個字符集的特徵集,對未知字元進行匹配識別。典型例子 tesseract 第一種方法簡單,在某些場合很高效,但比較侷限,字符...

關於tv app的一些想法

以前是做iptv機頂盒的,現在是做網際網路電視機頂盒的,在技術上的區別是不大的。通過這些年與電信,廣電打交道,現在對產品有了一些小想法。那麼在顯示上都是以web為主,用web來顯示epg內容,用osd來顯示狀態。但是隨著android的出現,現在大部分機頂盒或電視劇集廠家,都開始了智慧型之旅。乙個是...

關於敏捷的一些想法

敏捷軟體開發宣言 個體和互動 勝過 過程和工具 可以工作的軟體 勝過 面面俱到的文件 客戶合作 勝過 合同談判 響應變化 勝過遵循計畫 今天看了robert martin的ppp一書的第一部分,敏捷開發 回顧了自己曾經加盟過的幾個公司,經歷過的大大小小的專案,感慨良多。這些公司中不乏奉過程開發為寶典...