五邑隱俠,本名關健昌,12年遊戲生涯。 本教程以Go語言為例。
一、網路層
網路遊戲客戶端除了全域性登入使用http請求外,一般通過socket長連線與服務端保持連線。go語言的net包提供網路socket長連線相關操作。
對於服務端,一般經歷 Listen、Accept兩個步驟實現與客戶端連線。
func main() { l, err := net.Listen("tcp4", ":8080") if err != nil { return } for { c, err := l.Accept() if err != nil { break } fmt.Println("accept connect: ", c.RemoteAddr()) } }
客戶端通過 Dial 連線服務端
func main() { c, err := net.DialTimeout("tcp4", "127.0.0.1:8080", time.Second*time.Duration(8)) if err != nil { return } fmt.Println("connect with: ", c.LocalAddr()) }
連線 c(net.Conn型別)的主要方法是 Read、Write,
func Read(b []byte) (n int, err error) func Write(b []byte) (n int, err error)
一般連線建立後,每個連線分別建立讀和寫兩個 go rountine 進行讀迴圈和寫迴圈。
func (c *Conn) open() { go c.readLoop() go c.writeLoop() }
雖然go底層是基於epoll邊緣觸發,但是並沒有暴露介面通知什麼時候有可讀資料、可寫等。為了避免輪詢,一般在 go rountine 裡阻塞的讀、寫。由於net.Conn只提供 Close() error,沒辦法只停止讀,等待寫結束再關閉連線。一般讀做超時處理,超時後如果有關閉標記,則不再嘗試讀。
func SetReadDeadline(t time.Time) error
連線層需要通知使用者連線的狀態,所以引入連線監聽 interface
type ConnListener interface { OnConnOpen(c net.Conn) error OnConnClose(c net.Conn) error OnConnError(c net.Conn, err error) OnConnRead(c net.Conn) error OnConnWrite(c net.Conn) error }
使用者只需要實現自己的監聽者監聽連線的各個生命週期,由讀寫的 go rountine 驅動業務邏輯執行。
二、P2P層
網路層提供簡單的連線開啟、關閉和讀寫操作。為了建立服務端程式之間,以及服務端程式與客戶端之間通訊的基礎,引入P2P層(端對端層)。
P2P層包含端資訊 P2pEnd、協議包P2pPack、P2P網路P2pNet
1、P2pEnd定義這個端的型別、編號(例如:遊戲分服編號1,閘道器服編號3)、寫佇列。還包含一些防禦資訊用於對連線進行監控
type P2pEnd struct { EndType uint8 EndNo uint16 QueWritePacks chan *P2pPack }
2、P2pPack定義包資訊,包括源型別、編號,目標型別、編號,資料,這有利於包的路由。除此以外還有一些控制資訊做更精細的處理。
type P2pPack struct { SrcEnd uint8 SrcNo uint16 DstEnd uint8 DstNo uint16 Payload []byte }
這裡的包格式是通用包格式,Payload裡包含業務包包頭,根據業務需求定義自己的包格式。
3、P2pNet是一個端對端網路,維護該通訊端所有的連線。作為一個通訊端,它首先有自己的端型別、編號
type P2pNet struct { endType uint8 endNo uint16 }
然後要記錄其他端與連線的互相對映
type P2pNet struct { endType uint8 endNo uint16 mapConn2End map[net.Conn]*P2pEnd mapId2Conn map[uint32]net.Conn }
所有連線接收到的包放到一個chan裡,方便做分發處理
type P2pNet struct { endType uint8 endNo uint16 mapConn2End map[net.Conn]*P2pEnd mapId2Conn map[uint32]net.Conn queReadPacks chan *ReadPackWrap }
還有一些其他的控制資訊。
在P2P層維護的是端與端之間的連線,所以需要提供註冊協議,用於向服務方告知自己的端型別和編號。
func (r *P2pNet) Register(dstEnd uint8, dstNo uint16) error
服務方在 OnConnOpen(c net.Conn) error(自動分配編號) 或者 OnConnRead(c net.Conn) error 得到的包是註冊包時(由協議指定編號)對連線進行繫結。
除了註冊協議,底層的心跳 ping、pong 也在P2P層處理,還有一些防禦相關的處理,對業務層透明。
這樣建立起一張端對端通訊網。這張網的底層基於網路層做通訊,通過實現 ConnListener 驅動連線讀寫,讀包放到 P2pNet.queReadPacks,寫包放到對應端的 P2pEnd.QueWritePacks。
為了驅動業務邏輯,類似網路層,在P2P層也引入監聽的 interface,使用者通過實現該interface來驅動業務邏輯
type P2pListener interface { OnP2pConn(p2p *P2pNet, endType uint8, endNo uint16) OnP2pCall(p2p *P2pNet, pack *P2pPack) OnP2pClose(p2p *P2pNet, endType uint8, endNo uint16) OnP2pError(p2p *P2pNet, err error) }
三、關於防禦
連線層的防禦一般就是檢測異常連線,把異常連線踢掉,避免佔用socket資源。
1、最簡單的,通過心跳來判斷連線是否活躍,清除非活躍連線複用這部分socket。連線可以分為主動活躍和被動活躍兩種模式。主動活躍的連線,會主動發心跳包過來,通過頻率去檢測心跳包,如果超時都沒收到心跳包,可以踢掉。被動活躍連線,需要定時給它發心跳報活,避免被對方踢掉。
2、沒有業務包的連線。如果一個連線從連線開始,只發心跳包,限定時間內從來不發業務包,這個連線要踢掉。
基於1、2點,連線從連線開始必須在限定時間內發業務包,後續必須通過發業務包或者心跳包來維護連線。
3、限制 IP 關聯的連線數,一般同個區域網的玩家 IP 會一樣,但是也可能是伺服器在被攻擊。現在有些遊戲上線,會被模擬玩家連線撐滿服務,導致真實玩家無法進入遊戲。通過加 IP 關聯的連線數限制來增加攻擊成本。
4、發包頻率檢測,例如我們設定最大15幀/s,每隔2分鐘檢測一次,如果請求包間隔平均時間小於66ms,可以踢掉。
5、限制最大的包大小,收到超過最大限制的包,則踢掉連線。
網路通訊介紹到這裡,接下來聊聊業務的服務機制和rpc機制。