go語言游戲服務端開發(二)——網絡通信


五邑隱俠,本名關健昌,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、限制最大的包大小,收到超過最大限制的包,則踢掉連接。
6、設置寫超時(例如5秒),超時則踢掉連接,避免客戶端一直阻塞不接收導致緩存的包越來越多
 
業務層可以做這些限制:
1、連接驗證,沒做登錄或者重連,直接發其他業務請求包,踢掉連接
2、登錄失敗,踢掉連接
 
網絡通信介紹到這里,接下來聊聊業務的服務機制和rpc機制。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM