skynet 的 1.0 版已經(jīng)發(fā)布了 3 個(gè) alpha 版,等穩(wěn)定以后將發(fā)布 beta 版本。
最近的問題主要集中在一些我們在老項(xiàng)目中沒有使用到的特性上面。尤其是 sproto 這個(gè)模塊,我希望它將來作為 skynet 推薦的通訊協(xié)議,但我們老的項(xiàng)目開始的比 sproto 的項(xiàng)目早,所以早期項(xiàng)目全部使用的是 google protocol buffers (以及我自己做的實(shí)現(xiàn))。 隨著新項(xiàng)目的開展,我們公司內(nèi)部開始大面積使用 sproto ,也就發(fā)現(xiàn)了一些 bug ,在最近集中修復(fù)。
由于 skynet 使用多 lua VM 結(jié)構(gòu),為了不在每個(gè) VM 里重復(fù)加載 sproto 協(xié)議,最近增加了 sproto 協(xié)議對象的共享。這個(gè)作為未寫入 sproto 文檔的特性提供,當(dāng)然也不影響 sproto 在其它領(lǐng)域的使用。不過加這個(gè)特性比較匆忙,第一次提交時(shí)在 gc 方面遺留了一個(gè) bug ,有可能導(dǎo)致多個(gè) VM 重復(fù)釋放 C 對象,問題已經(jīng)在倉庫最新的提交中修復(fù)。
另一個(gè)為了 skynet 的應(yīng)用而特別加上的特性是讓 sproto 的 decode 可以接收指針(lightuserdata)。固然只接受 string 會讓實(shí)現(xiàn)更穩(wěn)固一些,不過在 skynet 里很多地方 string 和 lightuserdata + size 是通用的,所以就順帶支持了。這樣可以減少一次內(nèi)存拷貝。
根據(jù)使用的同學(xué)的需求,在 sproto 的 lua bingding 里增加了更為詳細(xì)的出錯(cuò)提示,這可以幫助實(shí)際使用時(shí)的錯(cuò)誤定位。另外,還增加更為嚴(yán)格的類型檢查。缺少這些檢查應(yīng)該算是 bug ,因?yàn)槭褂?sproto 而不是 json 這種的無格式的協(xié)議,就是為了可以多做一些類型檢查的。復(fù)雜類型(在 lua 里用 table 實(shí)現(xiàn))不檢查還會導(dǎo)致進(jìn)程掛掉,這是絕對不可以接受的。
最后一個(gè)嚴(yán)重的 bug 是設(shè)計(jì)上的。
sproto 的 encode C API 采用的是 callback 的方式。由使用者(通常是其它語言的 binding )提供一個(gè) callback 函數(shù),C 核心根據(jù) sproto 協(xié)議,每個(gè)字段調(diào)用一次這個(gè)函數(shù)。
如果它返回 -1 表示編碼錯(cuò)誤(一般是 buffer 不夠大),會讓 C 核心的編碼過程錯(cuò)誤返回。
如果它返回 0 表示這個(gè)字段不存在。這是因?yàn)?sproto 是允許字段不存在的,不存在的字段不會被編碼進(jìn)最終的串。另外,對數(shù)組的編碼也依賴它。如果在編碼一個(gè)數(shù)組時(shí)返回 0 ,表示數(shù)組結(jié)束。
其它情況應(yīng)返回一個(gè)正數(shù),表示當(dāng)前需要編碼的對象的長度。
對于簡單類型,如 boolean ,integer ,一般返回的是固定值。boolean 返回 4 ,integer 返回 4 或 8 (提示 C 核心這個(gè)整數(shù)是 32bit 還是 64bit 的)。最終編碼不一定按這個(gè)數(shù)字來,且 callback 函數(shù)得到的寫入地址也并非最終 buffer 的地址。C 核心會提供一個(gè)地址對齊的地址,然后根據(jù) sproto 的編碼協(xié)議來轉(zhuǎn)換到最終 buffer 中,同時(shí)還要處理大小端問題。
對于不定長類型,如 string 或自定義類型。這個(gè)長度會幫助 C 核心了解應(yīng)該將 buffer 指針后移多少字節(jié)。callback 函數(shù)將直接把數(shù)據(jù)寫入最終的 buffer 。而問題就出在這里。
當(dāng) string 是一個(gè)空串時(shí),由于空串的長度為 0 ,會讓 C 核心誤會這個(gè)字段并不存在,這導(dǎo)致所有的空串無法編碼。更嚴(yán)重的是,如果是字符串?dāng)?shù)組,碰到空串就會停止編碼這個(gè)數(shù)組。最終的修補(bǔ)方案是,約定在編碼 string 的時(shí)候,應(yīng)該返回字符串長度 + 1 。這屬于一個(gè)設(shè)計(jì)問題,所以除了 lua binding 之外,別的語言的 binding 也需要修改。好在目前已知的 python binding 也是我們公司的同學(xué)實(shí)現(xiàn)的,應(yīng)該馬上能改過來。
對于用戶類型,沒有 string 這個(gè)問題。即使是空的對象,也有一個(gè)數(shù)據(jù)頭。所以不可能為 0 。對于空對象,不在數(shù)組中時(shí),目前的 lua binding 會返回 0 ,讓 C 核心跳過這個(gè)字段,而在數(shù)組中時(shí),則會返回一個(gè)空的數(shù)據(jù)頭。
利用 sproto 實(shí)現(xiàn)的 sharemap 也被查出一個(gè) bug ,不過這個(gè) bug 不屬于 sproto 。它的 metatable 被不小心循環(huán)引用了,如果一個(gè)字段不存在會導(dǎo)致 lua 檢測出 metatable 循環(huán)引用而出錯(cuò)。
還有一個(gè)問題是在使用 httpc 時(shí)發(fā)現(xiàn)的,雖然已經(jīng)知道,但因?yàn)橛玫牟欢嘁矝]有特別在意。這次在正式版發(fā)布前,還是給出解決方案:
skynet 的 socket 層在處理域名的時(shí)候直接調(diào)用了系統(tǒng) api getaddrinfo ,這會阻塞住線程。由于 skynet 的 socket 是單線程的,所以一旦做域名查詢,會導(dǎo)致 skynet 所有的 socket 消息處理阻塞。一般我們不會使用域名,即使用,也是在數(shù)據(jù)庫第一次連接的時(shí)候,通常發(fā)生在 skynet 進(jìn)程啟動(dòng)的時(shí)候,所以影響不大。但一旦使用 httpc 模塊,就很容易向外連接一個(gè)域名了。
由于系統(tǒng)并不提供異步的域名解析方法,很多其它網(wǎng)絡(luò)庫的做法是使用額外的線程去查詢域名。我并不想針對這個(gè)需求而大幅度修改已經(jīng)穩(wěn)定了的 skynet socket 層,所以提供了獨(dú)立的解決方案:那就是在上層自己使用 dns 協(xié)議發(fā)送 udp 包查詢。為了讓 httpc 模塊可以使用它,對其也做了一點(diǎn)改變,允許用戶連接一個(gè) IP 地址,而自己在 http header 里填寫 host 字段。
另外,在充當(dāng) http 客戶端時(shí),http 服務(wù)器往往會在返回的 header 中填寫多個(gè) Set-Cookie 字段,之前對同名的 header 中字段沒有正確的處理,現(xiàn)在做了修正。(多個(gè)同名字段會生成一個(gè) table )
更多信息請查看IT技術(shù)專欄