日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

牌类游戏使用微服务重构笔记(八): 游戏网关服务器

發(fā)布時間:2023/11/29 编程问答 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 牌类游戏使用微服务重构笔记(八): 游戏网关服务器 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

網(wǎng)關(guān)服務(wù)器

所謂網(wǎng)關(guān),其實就是維持玩家客戶端的連接,將玩家發(fā)的游戲請求轉(zhuǎn)發(fā)到具體后端服務(wù)的服務(wù)器,具有以下幾個功能點:

  • 長期運行,必須具有較高的穩(wěn)定性和性能
  • 對外開放,即客戶端需要知道網(wǎng)關(guān)的IP和端口,才能連接上來
  • 多協(xié)議支持
  • 統(tǒng)一入口,架構(gòu)中可能存在很多后端服務(wù),如果沒有一個統(tǒng)一入口,則客戶端需要知道每個后端服務(wù)的IP和端口
  • 請求轉(zhuǎn)發(fā),由于統(tǒng)一了入口,所以網(wǎng)關(guān)必須能將客戶端的請求轉(zhuǎn)發(fā)到準(zhǔn)確的服務(wù)上,需要提供路由
  • 無感更新,由于玩家連接的是網(wǎng)關(guān)服務(wù)器,只要連接不斷;更新后端服務(wù)器對玩家來說是無感知的,或者感知很少(根據(jù)實現(xiàn)方式不同)
  • 業(yè)務(wù)無關(guān)(對于游戲服務(wù)器網(wǎng)關(guān)不可避免的可能會有一點業(yè)務(wù))

對于http請求來說,micro框架本身已經(jīng)實現(xiàn)了api網(wǎng)關(guān),可以參閱之前的博客

牌類游戲使用微服務(wù)重構(gòu)筆記(二): micro框架簡介:micro toolkit

但是對于游戲服務(wù)器,一般都是需要長鏈接的,需要我們自己實現(xiàn)

連接協(xié)議

網(wǎng)關(guān)本身應(yīng)該是支持多協(xié)議的,這里就以websocket舉例說明我重構(gòu)過程中的思路,其他協(xié)議類似。首先選擇提供websocket連接的庫 推薦使用melody,基于websocket庫,語法非常簡單,數(shù)行代碼即可實現(xiàn)websocket服務(wù)器。我們的游戲需要websocket網(wǎng)關(guān)的原因在于客戶端不支持HTTP2,不能和grpc服務(wù)器直連

package mainimport ("github.com/micro/go-web""gopkg.in/olahol/melody.v1""log""net/http" )func main() {// New web serviceservice := web.NewService(web.Name("go.micro.api.gateway"))// parse command lineservice.Init()// new melodym := melody.New()// Handle websocket connectionservice.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {_ = m.HandleRequest(w, r)})// handle connection with new sessionm.HandleConnect(func(session *melody.Session) {})// handle disconnectionm.HandleDisconnect(func(session *melody.Session) {})// handle messagem.HandleMessage(func(session *melody.Session, bytes []byte) {})// run serviceif err := service.Run(); err != nil {log.Fatal("Run: ", err)} } 復(fù)制代碼

請求轉(zhuǎn)發(fā)

網(wǎng)關(guān)可以收取或發(fā)送數(shù)據(jù),并且數(shù)據(jù)結(jié)構(gòu)比較統(tǒng)一都是[]byte,這一點是不是很像grpc stream,因此就可以使用protobuf的oneof特性來定義請求和響應(yīng),可參照上期博客

牌類游戲使用微服務(wù)重構(gòu)筆記(六): protobuf爬坑

定義gateway.basic.proto,對網(wǎng)關(guān)收/發(fā)的消息進行歸類

message Message {oneof message {Req req = 1; // 客戶端請求 要求響應(yīng)Rsp rsp = 2; // 服務(wù)端響應(yīng)Notify notify = 3; // 客戶端推送 不要求響應(yīng)Event event = 4; // 服務(wù)端推送Stream stream = 5; // 雙向流請求Ping ping = 6; // pingPong pong = 7;// pong} } 復(fù)制代碼

對于req、notify都是客戶端的無狀態(tài)請求,對應(yīng)后端的無狀態(tài)服務(wù)器,這里僅需要實現(xiàn)自己的路由規(guī)則即可,比如

message Req {string serviceName = 1; // 服務(wù)名string method = 2; // 方法名bytes args = 3; // 參數(shù)google.protobuf.Timestamp timestamp = 4; // 時間戳... } 復(fù)制代碼
  • serviceName 調(diào)用rpc服務(wù)器的服務(wù)名
  • method 調(diào)用rpc服務(wù)器的方法名
  • args 調(diào)用參數(shù)
  • timestamp 請求時間戳,用于客戶端對服務(wù)端響應(yīng)做匹配識別,模擬http請求req-rsp

思路與micro toolkit的api網(wǎng)關(guān)類似(rpc 處理器),比較簡單,可參照之前的博客。

我們的項目對于此類請求都走h(yuǎn)ttp了,并沒有通過這個網(wǎng)關(guān), 僅有一些基本的req,比如authReq處理session認(rèn)證。主要考慮是http簡單、無狀態(tài)、好維護,再加上此類業(yè)務(wù)對實時性要求也不高。

grpc stream轉(zhuǎn)發(fā)

游戲服務(wù)器一般都是有狀態(tài)的、雙向的、實時性要求較高,req-rsp模式并不適合,就需要網(wǎng)關(guān)進行轉(zhuǎn)發(fā)。每添加一種grpc后端服務(wù)器,僅需要在oneof中添加一個stream來拓展

message Stream {oneof stream {room.basic.Message roomMessage = 1; // 房間服務(wù)器game.basic.Message gameMessage = 2; // 游戲服務(wù)器mate.basic.Message mateMessage = 3; // 匹配服務(wù)器} } 復(fù)制代碼

并且需要定義一個對應(yīng)的路由請求,來處理轉(zhuǎn)發(fā)到哪一臺后端服務(wù)器上(實現(xiàn)不同也可以不需要),這里會涉及到一點業(yè)務(wù),例如

message JoinRoomStreamReq {room.basic.RoomType roomType = 1;string roomNo = 2; } 復(fù)制代碼

這里根據(jù)客戶端的路由請求的房間號和房間類型,網(wǎng)關(guān)來選擇正確的房間服務(wù)器(甚至可能鏈接到舊版本的房間服務(wù)器上)

選擇正確的服務(wù)器后,建立stream 雙向流

address := "xxxxxxx" // 選擇后的服務(wù)器地址 ctx := context.Background() // 頂層context m := make(map[string]string) // some metadata streamCtx, cancelFunc := context.WithCancel(ctx) // 復(fù)制一個子context// 建立stream 雙向流 stream, err := xxxClient.Stream(metadata.NewContext(streamCtx, m), client.WithAddress(address))// 存儲在session上 session.Set("stream", stream) session.Set("cancelFunc", cancelFunc)// 啟動一個goroutine 收取stream消息并轉(zhuǎn)發(fā) go func(c context.Context, s pb.xxxxxStream) {// 退出時關(guān)閉 streamdefer func() {session.Set("stream", nil)session.Set("cancelFunc", nil)if err := s.Close(); err != nil {// do something with close err}}()for {select {case <-c.Done():// do something with ctx cancelreturndefault:res, err := s.Recv()if err != nil {// do something with recv errreturn}// send to session 這里可以通過oneof包裝告知客戶端是哪個stream發(fā)來的消息...}} }(streamCtx, stream) 復(fù)制代碼

轉(zhuǎn)發(fā)就比較簡單了,直接上代碼

對于某個stream的請求

message Stream {oneof stream {room.basic.Message roomMessage = 1; // 房間服務(wù)器game.basic.Message gameMessage = 2; // 游戲服務(wù)器mate.basic.Message mateMessage = 3; // 匹配服務(wù)器} } 復(fù)制代碼

添加轉(zhuǎn)發(fā)代碼

s, exits := session.Get("stream") if !exits {return }if stream, ok := s.(pb.xxxxStream); ok {err := stream.Send(message)if err != nil {log.Println("send err:", err)return} } 復(fù)制代碼

當(dāng)需要關(guān)閉某個stream時, 只需要調(diào)用對應(yīng)的cancelFunc即可

if v, e := session.Get("cancelFunc"); e {if c, ok := v.(context.CancelFunc); ok {c()} } 復(fù)制代碼

使用oneOf的好處

由于接收請求的入口統(tǒng)一,使用oneof就可以一路switch case,每添加一個req或者一種stream只需要添加一個case, 代碼看起來還是比較簡單、清爽的

func HandleMessageBinary(session *melody.Session, bytes []byte) {var msg pb.Messageif err := proto.Unmarshal(bytes, &msg); err != nil {// do somethingreturn}defer func() {err := recover()if err != nil {// do something with panic}}()switch x := msg.Message.(type) {case *pb.Message_Req:handleReq(session, x.Req)case *pb.Message_Stream:handleStream(session, x.Stream)case *pb.Message_Ping:handlePing(session, x.Ping)default:log.Println("unknown req type")} }func handleStream(session *melody.Session, message *pb.Stream) {switch x := message.Stream.(type) {case *pb.Stream_RoomMessage:handleRoomStream(session, x.RoomMessage)case *pb.Stream_GameMessage:handleGameStream(session, x.GameMessage)case *pb.Stream_MateMessage:handleMateStream(session, x.MateMessage)} } 復(fù)制代碼

熱更新

對于游戲熱更新不停服還是挺重要的,我的思路將會在之后的博客里介紹,可以關(guān)注一波 嘿嘿

坑!

  • 這樣的網(wǎng)關(guān),看似沒什么問題,然而跑上一段時間使用pprof觀測會發(fā)現(xiàn)goroutine和內(nèi)存都在緩慢增長,也就是存在goroutine leak!,原因在于 micro源碼在包裝grpc時,沒有對關(guān)閉stream完善,只有收到io.EOF的錯誤時才會關(guān)閉grpc的conn連接
func (g *grpcStream) Recv(msg interface{}) (err error) {defer g.setError(err)if err = g.stream.RecvMsg(msg); err != nil {if err == io.EOF {// #202 - inconsistent gRPC stream behavior// the only way to tell if the stream is done is when we get a EOF on the Recv// here we should close the underlying gRPC ClientConncloseErr := g.conn.Close()if closeErr != nil {err = closeErr}}}return } 復(fù)制代碼

并且有一個TODO

// Close the gRPC send stream // #202 - inconsistent gRPC stream behavior // The underlying gRPC stream should not be closed here since the // stream should still be able to receive after this function call // TODO: should the conn be closed in another way? func (g *grpcStream) Close() error {return g.stream.CloseSend() } 復(fù)制代碼

解決方法也比較簡單,自己fork一份源碼改一下關(guān)閉stream的時候同時關(guān)閉conn(我們的業(yè)務(wù)是可以的因為在grpc stream客戶端和服務(wù)端均實現(xiàn)收到err后關(guān)閉stream),或者等作者更新用更科學(xué)的方式關(guān)閉

  • melody的session在get和set數(shù)據(jù)時會發(fā)生map的讀寫競爭而panic,可以查看issue,解決方法也比較簡單

一起學(xué)習(xí)

本人學(xué)習(xí)golang、micro、k8s、grpc、protobuf等知識的時間較短,如果有理解錯誤的地方,歡迎批評指正,可以加我微信一起探討學(xué)習(xí)

總結(jié)

以上是生活随笔為你收集整理的牌类游戏使用微服务重构笔记(八): 游戏网关服务器的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。