久久国产成人av_抖音国产毛片_a片网站免费观看_A片无码播放手机在线观看,色五月在线观看,亚洲精品m在线观看,女人自慰的免费网址,悠悠在线观看精品视频,一级日本片免费的,亚洲精品久,国产精品成人久久久久久久

分享

云風(fēng)的BLOG: Protocol Buffers for C

 思考的軌跡 2011-12-01

Protocol Buffers for C

我一直不太滿意 google protocol buffers 的默認(rèn)設(shè)計,。為每個 message type 生成一大坨 C++ 代碼讓我很難受,。而且官方?jīng)]有提供 C 版本,,第三方的 C 版本 也不讓我滿意,。

這種設(shè)計很難讓人做動態(tài)語言的 binding ,而大多數(shù)動態(tài)語言往往又沒有強(qiáng)類型檢查,,采用生成代碼的方式并沒有特別的好處,,反而有很大的性能損失(和通常做一個 bingding 庫的方式比較)。比如官方的 Python 庫,,完全可以在運(yùn)行時,,根據(jù)協(xié)議,把那些函數(shù)生成出來,,而不必用離線的工具生成代碼,。

去年的時候我曾經(jīng)寫過一個 lua 版本的庫 。為了獨(dú)立于官方版本,,我甚至還用 lpeg 寫了一個 .proto 文件的解析器,。用了大約不到 100 行 lua 代碼就可以解析出 .proto 文件內(nèi)的協(xié)議內(nèi)容??梢宰?lua 庫直接加載文本的協(xié)議描述文件,。(這個東西這次幫了我大忙)

這次,我重新做項目,,又碰到 protobuf 協(xié)議解析問題,,想從頭好好解決一下,。上個月一開始,我想用 luajit 好好編寫一個純 lua 版,。猜想,,利用 luajit 和 ffi 可以達(dá)到不錯的性能。但是做完以后,,發(fā)現(xiàn)和 C++ 版本依然有差距 (大約只能達(dá)到 C++ 版本的 25% ~ 33% 左右的速度) ,,比我去年寫的 C + Lua binding 的方式要差。但是,,去年寫的那一份 C 代碼和 Lua 代碼結(jié)合太多,。所以我萌生了重新寫一份 C 實現(xiàn)的想法。

做到一半的時候,,有網(wǎng)友指出,,有個 googler 最近也在做類似的工作。μpb 這個項目在這里 ,。這里他寫了一大篇東西闡述為什么做這樣一份東西,,大體上和我的初衷一致。不過他的 api 設(shè)計的不太好,,我覺得太難用,。所以這個項目并不妨礙我完成我自己的這一份。


C 版本之所以很難把 api 設(shè)計好,,是因為 C 缺乏必要的數(shù)據(jù)結(jié)構(gòu),。而且沒有垃圾回收,缺乏數(shù)據(jù)類型的元信息,。

考慮再三,,我決定提供兩套 api ,滿足不同的需求,。

當(dāng)性能要求不太高的時候,,僅僅滿足 C 語言開發(fā)的便捷需要,提供一套簡單易用的 api 操作 protobuf 格式的 message ,。我稱之為 message api ,。

大體上有兩組 api :

對于編碼 protobuf 的消息,使用 rmessage 相關(guān) api

struct pbc_rmessage * pbc_rmessage_new(struct pbc_env * env, const char * typename , struct pbc_slice * slice);
void pbc_rmessage_delete(struct pbc_rmessage *);

uint32_t pbc_rmessage_integer(struct pbc_rmessage * , const char *key , int index, uint32_t *hi);
double pbc_rmessage_real(struct pbc_rmessage * , const char *key , int index);
const char * pbc_rmessage_string(struct pbc_rmessage * , const char *key , int index, int *sz);
struct pbc_rmessage * pbc_rmessage_message(struct pbc_rmessage *, const char *key, int index);
int pbc_rmessage_size(struct pbc_rmessage *, const char *key);

對于解碼消息,,使用 wmessage 相關(guān) api

struct pbc_wmessage * pbc_wmessage_new(struct pbc_env * env, const char *typename);
void pbc_wmessage_delete(struct pbc_wmessage *);

void pbc_wmessage_integer(struct pbc_wmessage *, const char *key, uint32_t low, uint32_t hi);
void pbc_wmessage_real(struct pbc_wmessage *, const char *key, double v);
void pbc_wmessage_string(struct pbc_wmessage *, const char *key, const char * v, int len);
struct pbc_wmessage * pbc_wmessage_message(struct pbc_wmessage *, const char *key);
void * pbc_wmessage_buffer(struct pbc_wmessage *, struct pbc_slice * slice);

pbc_rmessage_newpbc_rmessage_delete 用來構(gòu)造和釋放 pbc_rmessage 結(jié)構(gòu),。從結(jié)構(gòu)中取出的子消息,字符串,,都是由它來保證生命期的,。這樣不需要用戶做過于繁雜的對象構(gòu)建和銷毀工作,。

對于 repeated 的數(shù)據(jù),,沒有額外再引入新的數(shù)據(jù)類型,。而是把 message 內(nèi)部的所有域都視為 repeated 。這種設(shè)計,,可以極大的精簡需要的 api ,。

我們用 pbc_rmessage_size 可以查詢 message 中某個 field 被重復(fù)了多少次。如果消息中并沒有編碼入這個 field ,,它能返回 0 感知到,。

我把所有的基本數(shù)據(jù)類型全部統(tǒng)一成了三種:integer , string , real 。bool 類型被當(dāng)成 integer 處理,。enum 類型即可以是 string ,,也可以是 integer 。用 pbc_rmessage_string 時,,可以取到 enum 的名字,;用 pbc_rmessage_integer 則取得 id 。

pbc_rmessage_message 可以獲得一個子消息,,這個返回的對象不必顯式的銷毀,,它的生命期掛接在父節(jié)點(diǎn)上。即使消息中沒有編碼入某個子消息,,這個 api 依然可以正確的返回,。從中取出的子域都將是默認(rèn)值。

integer 不區(qū)分 32bit 數(shù)和 64bit 數(shù),。當(dāng)你能肯定你需要的整數(shù)可以用 32bit 描述時,,pbc_rmessage_integer 的最后一個參數(shù)可以傳 NULL ,忽略高 32bit 的數(shù)據(jù),。

wmessage 的用法更像是不斷的向一個未關(guān)閉的消息包類壓數(shù)據(jù),。當(dāng)你把整個消息的內(nèi)容都填完后,可以用 pbc_wmessage_buffer 返回一個 slice ,。這個 slice 里包含了 buffer 的指針和長度,。

需要注意的是,如果使用 pbc_wmessage_integer 壓入一個負(fù)數(shù),,一定要將高位傳 -1 ,。因為接口一律把傳入?yún)?shù)當(dāng)成是無符號的整數(shù)。

考慮到某些內(nèi)部實現(xiàn)的性能,,以及后面講提到的 pattern api 的方便性(如果你完全拿這個庫做 C/S 通訊),。建議所有的 string 都在末尾加上 \0 。因為,,這樣在解碼的時候,,可以將字符串指針直接指向數(shù)據(jù)包內(nèi),而不需要額外復(fù)制一份出來,。

pbc_wmessage_string 可以壓入非 \0 結(jié)尾的字符串,,因為壓入的數(shù)據(jù)長度是由參數(shù)制定的,。當(dāng)然你也可以不自己計算長度。如果長度參數(shù)傳 <=0 的話,,庫會幫你調(diào)用 strlen 檢測,。并且將最終的長度減去這個負(fù)數(shù)。即,,如果你傳 -1 ,,就會幫你多壓入最后的一個 \0 字節(jié)。


Pattern API 可以得到更高的性能,。更快的速度和更少的內(nèi)存占用量,。更重要的是,對于比較小的消息包,,如果你使用得當(dāng),,使用 pattern api 甚至不會觸發(fā)哪怕一次堆上的內(nèi)存分配操作。api 工作時的所有的臨時內(nèi)存都在棧上,。

相關(guān) api 如下:

struct pbc_pattern * pbc_pattern_new(struct pbc_env * , const char * message, const char *format, ...);
void pbc_pattern_delete(struct pbc_pattern *);
int pbc_pattern_pack(struct pbc_pattern *, void *input, struct pbc_slice * s);
int pbc_pattern_unpack(struct pbc_pattern *, struct pbc_slice * s , void * output);

我們首先需要創(chuàng)建一個 pattern 做編碼和解碼用,。一個簡單的例子是這樣的:

message Person {
  required string name = 1;
  required int32 id = 2; 
  optional string email = 3;
}

這樣一個消息,對于在 C 的結(jié)構(gòu)體中,,你可能希望是這樣: struct Person { pbcslice name; int32t id; pbc_slice email; } 這里使用 pbc_slice 來表示一個 string ,。因為對于 message 來說,里面的字符串是有長度的,。并且不一定以 \0 結(jié)尾,。slice 同樣可以表示一個尚未解開的子消息。

我們使用 pbc_pattern_new 可以讓 pbc 認(rèn)識這個結(jié)構(gòu)的內(nèi)存布局,。

struct pbc_pattern * Person_p = pbc_pattern_new(env , "Person" ,
  "name %s id %d email %s",
  offsetof(struct Person , name),
  offsetof(struct Person , id),
  offsetof(struct Person , email));

然后就可以用 pbc_pattern_packpbc_pattern_unpack 編碼和解碼了,。pattern 的定義過程冗長而且容易出錯(你也可以考慮用機(jī)器生成它們)。但我相信在性能及其敏感的場合,,這些是值得的,,如果你覺得寫這些不值得,可以考慮用回上面的 message api ,。

對于 repeated 的數(shù)據(jù),,pattern api 把他們看成一個數(shù)組 pbc_array

有這樣一組 api 可以用來操作它:

int pbc_array_size(pbc_array);
uint32_t pbc_array_integer(pbc_array array, int index, uint32_t *hi);
double pbc_array_real(pbc_array array, int index);
struct pbc_slice * pbc_array_slice(pbc_array array, int index);

void pbc_array_push_integer(pbc_array array, uint32_t low, uint32_t hi);
void pbc_array_push_slice(pbc_array array, struct pbc_slice *);
void pbc_array_push_real(pbc_array array, double v);

數(shù)組是個略微復(fù)雜一些的數(shù)據(jù)結(jié)構(gòu),,但如果你的數(shù)據(jù)不多的話,,它也不會牽扯到堆上的額外內(nèi)存分配。不過既然有可能調(diào)用這些 api 可能額外分配內(nèi)存,,那么你必須手工清除這些內(nèi)存,。而且在第一次使用前,必須初始化這個數(shù)據(jù)結(jié)構(gòu) (memset 為 0 是可以的),。

void pbc_pattern_set_default(struct pbc_pattern * , void *data);
void pbc_pattern_close_arrays(struct pbc_pattern *, void *data);

pbc_pattern_set_default 可以把一塊內(nèi)存,,以一個 pattern 的形式,,初始化所有的域。包括其中的數(shù)組的初始化,。

pbc_pattern_close_arrays 使用完一塊數(shù)據(jù),需要手工調(diào)用這個 api ,,關(guān)閉這個數(shù)據(jù)塊中的數(shù)組,。


關(guān)于 Extension ,我最后放棄了直接支持,。沒有提供類似 get extension 的 api ,。這是因為,我們可以更簡單的去處理 extension ,。我把所有的 extension field 都加了前綴,,如果需要,可以用拼接字符串的方式獲得消息包內(nèi)的擴(kuò)展域,。


最后介紹的是 pbc 的環(huán)境,。

struct pbc_env * pbc_new(void);
void pbc_delete(struct pbc_env *);
int pbc_register(struct pbc_env *, struct pbc_slice * slice);

pbc 庫被設(shè)計成沒有任何全局變量,這樣你想在多線程環(huán)境用會比較安全,。雖然庫并沒有考慮線程安全問題,,但在不同的線程中使用不同的環(huán)境是完全沒有問題的。

每個環(huán)境需要獨(dú)立注冊需要的消息類型,,傳入一個 protobuf 庫官方工具生成的 .pb 數(shù)據(jù)塊即可,。以 slice 的形式傳入,register 完后,,這塊數(shù)據(jù)內(nèi)存可以釋放,。

這個數(shù)據(jù)塊其實是以 google.protobuf.FileDescriptorSet 類型來編碼的。這個數(shù)據(jù)類型非常繁雜,,使得 bootstrap 過程及其難寫,,這個在后面會談到。


全部代碼我已經(jīng)開源方在 github 上了,,可以在 https://github.com/cloudwu/pbc 取到代碼,。詳細(xì)的用法也可以從那些 test 文件中找到例子。

這個東西很難寫,,所以代碼很亂,,在寫這篇 blog 的時候我還沒有開始整理代碼的結(jié)構(gòu)。大家想用的將就用,,請善待 bug 和它的朋友們,。

用一個復(fù)雜的 protobuf 協(xié)議來描述協(xié)議本身,真的很淡疼,。當(dāng)我們沒有任何一個可用的協(xié)議解析庫前,,我們無法理解任何 protobuf 協(xié)議,。這是一個先有雞還是先有蛋的問題。就是說,,我很難憑空寫出一個 pbc_register 的 api ,,因為它需要先 register 一個 google.protobuf.FileDescriptorSet 類型才能開始分析輸入的包。

不依賴庫本身去解析 google.protobuf.FileDescriptorSet 本身的定義是非常麻煩的,。當(dāng)然我可以利用 google 官方的工具生成 google.protobuf.FileDescriptorSet 的 C++ 解析類開始工作,。但我偏偏又不希望給這個東西帶來過多的依賴。

一開始我希望自定義一種更簡單的格式來描述協(xié)議本身,,沒有過多的層次結(jié)構(gòu),,只是一個平坦的數(shù)組。這樣手工解析就有可能,。本來我想給 protoc 寫一個 plugin ,,生成自定義的協(xié)議格式。后來放棄了這個方案,,因為希望庫用起來更簡單一些,。

但是這個方案還是部分使用了。這就是源代碼中 bootstrap.c 部分的緣由,。它讀入一個更簡單版本的 google.protobuf.FileDescriptorSet 的描述,。這塊數(shù)據(jù)是事先生成好的,放在 descriptor.pbc.h 里,。生成這塊數(shù)據(jù)使用了我去年完成的 lua 庫,。相關(guān)的 lua 代碼就沒有放出來了。當(dāng)然到了今天,,pbc 本身足夠完善,,我們可以用 pbc 寫一個 C 版本。有興趣的同學(xué),,可以在 test_pbc.c 的基礎(chǔ)上修改,。


這個玩意很難寫, 主要是那個雞生蛋,蛋生雞的問題,導(dǎo)致我在實現(xiàn)過程的很長時間里,腦子里都糨糊一般. 所以很多代碼實現(xiàn)的很糟糕, 但又不舍得刪(因為難為我把它們寫出來了, 重寫很難調(diào)錯). 希望一邊寫一邊優(yōu)化的壞習(xí)慣, 在對于這種比較難實現(xiàn)的東西上, 讓我編寫的好生痛苦. 為了效率, 我甚至寫了三個針對不同情況處理的 map .

中間因為想法改變, api 設(shè)計改變,廢棄了好幾千行代碼. 最終也就是這個樣子了. 有空再重新理一下.

最終, 還有許多細(xì)節(jié)可以進(jìn)一步優(yōu)化, 比如如果只針對小頭的機(jī)器做, 許多不必要的代碼都可以省略掉. 對于 packed 數(shù)組也值得進(jìn)一步優(yōu)化. 甚至可以考慮加一點(diǎn) JIT .

事情總算告一段落了。連續(xù)寫了 5000 行代碼,,我需要休息一下,。

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,,不代表本站觀點(diǎn),。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,,謹(jǐn)防詐騙,。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多