最近公司游戲准備內測,但是原來的網絡層寫得很是混亂,搞的已經無法維護,結果不得不重寫一遍。這坨爛攤子交到了我的手上,花了兩個星期重寫了公司的網絡層,登錄驗證以及網關服務器。內測目前表現十分穩定。
我是及其反對重復造輪子的,所以一開始主張使用boost asio來做我們的網絡層(asio寫得非常優美,我自認為自己再怎么寫也是無法達到這種水准的)。結果卻遭到了一些人的反對,其觀點無非是boost太龐大之類。既然如此,還是服從上級吧,不過心中仍然在想,大?難道服務器硬盤不夠嗎?
編寫一個網絡庫,很多地方需要考慮和選擇,我列舉幾個點:
1. proactor還是reactor設計?
asio使用的是proactor設計,windows完成端口和epoll統一成異步api,我感覺設計的非常優美。但是組內的人並不喜歡這種function bind回調的模式。結果我只能使用reactor的設計。
2.使用什么手段來管理應用層buffer,buffer list? ringbuffer?
在我們原來的設計中,采用預先分配一塊ringbuff的方式來做應用層緩沖區,其好處就是可以在Read的過程中減少一次內存拷貝,其缺點也很明顯,ringbuffer采用mmap分配,需要占用一個描述符,並且大小是固定的,這樣邏輯封包就不能超過ringbuffer的最大長度。具體怎么實現大家可以google之。總之我認為,網絡層的設計不應該限制邏輯怎么實現。所以我重寫網絡層放棄了這個設計。
我采用了buffer list來管理應用層buffer,應用層buff由很多個固定大小的塊連接,每塊大小為8K。應用層數據往buffer head塊寫,寫滿之后new一塊新的buffer塊,header指向新的塊繼續寫。讀也一樣,讀在tail端讀,讀完之后,delete掉尾端那塊,把tail指向tail-1塊,繼續讀。數據處理讀寫都經過兩次copy,每次read和write能盡量操作更多數據,減少read write次數
a. 讀: tcp緩沖->應用層緩沖區->實際的包
b. 寫: 實際包->應用層緩沖區->tcp緩沖區
例如,某一幀,服務端向邏輯端要發送1000個封包30K的數據,那我應用層將有4個buff塊,只需要4次write即可將數據寫入tcp緩沖區。
3.使用epoll et 還是 lt?
這個東西被很多人都說爛了,但大多數都不夠細致,並沒說到重點。實際編寫過程中很多小細節需要注意。需要注意的是寫事件。
a. 對於et來說,應用層向tcp緩沖區寫,有可能應用層數據寫完了,但是tcp緩沖沒有寫到EAGAIN事件,那么此時需要在應用層做個標記,表明tcp緩沖區是可寫的,否則,由於et是只觸發一次,應用層就再也不會被通知緩沖區可寫了。
b. 對於lt來說,應用層確實會每次通知可寫事件,問題在於,如果應用層沒數據需要往Tcp緩沖區寫的話,epoll還是會不停的通知你可寫,這時候需要把描述符移出epoll,避免多次無效的通知
設計過程中,由於需要兼容一些以前的東西,某些地方不得不作出一些犧牲了,例如必須重用我們原來的epoll dispatch類做數據驅動.當然也有些便利的東西,例如原來server是個單線程無阻塞的網絡庫,那我重寫自然也不需要考慮多線程了。最后各種妥協,完成了如下設計:
稍微解釋一下,Descriptor類是對描述符的封裝,兩個子類TSocket是用於收發數據,TAcceptor用於處理連接事件。TSession里面封裝了一些通用的session處理邏輯。提供兩個虛函數Read和Write用於往應用層緩沖區讀寫數據。TServer封裝了通用的Server邏輯,例如超時無數據則斷開session。有了TSession和TServer之后,要寫一個server就很簡單了,直接繼承這兩個類即可。