牌類游戲使用微服務重構筆記(八): 游戲網關服務器

網關服務器

所謂網關,其實就是維持玩家客戶端的連接,將玩家發的游戲請求轉發到具體后端服務的服務器,具有以下幾個功能點:

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

對于http請求來說,micro框架本身已經實現了api網關,可以參閱之前的博客

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

但是對于游戲服務器,一般都是需要長鏈接的,需要我們自己實現

連接協議

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

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)}
}
復制代碼

請求轉發

網關可以收取或發送數據,并且數據結構比較統一都是[]byte,這一點是不是很像grpc stream,因此就可以使用protobufoneof特性來定義請求和響應,可參照上期博客

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

定義gateway.basic.proto,對網關收/發的消息進行歸類

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

對于reqnotify都是客戶端的無狀態請求,對應后端的無狀態服務器,這里僅需要實現自己的路由規則即可,比如

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

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

我們的項目對于此類請求都走http了,并沒有通過這個網關, 僅有一些基本的req,比如authReq處理session認證。主要考慮是http簡單、無狀態、好維護,再加上此類業務對實時性要求也不高。

grpc stream轉發

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

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

并且需要定義一個對應的路由請求,來處理轉發到哪一臺后端服務器上(實現不同也可以不需要),這里會涉及到一點業務,例如

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

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

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

address := "xxxxxxx" // 選擇后的服務器地址
ctx := context.Background() // 頂層context
m := make(map[string]string) // some metadata
streamCtx, cancelFunc := context.WithCancel(ctx) // 復制一個子context// 建立stream 雙向流
stream, err := xxxClient.Stream(metadata.NewContext(streamCtx, m), client.WithAddress(address))// 存儲在session上
session.Set("stream", stream)
session.Set("cancelFunc", cancelFunc)// 啟動一個goroutine 收取stream消息并轉發
go func(c context.Context, s pb.xxxxxStream) {// 退出時關閉 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發來的消息...}}
}(streamCtx, stream)
復制代碼

轉發就比較簡單了,直接上代碼

對于某個stream的請求

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

添加轉發代碼

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}
}
復制代碼

當需要關閉某個stream時, 只需要調用對應的cancelFunc即可

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

使用oneOf的好處

由于接收請求的入口統一,使用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)}
}
復制代碼

熱更新

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

坑!

  • 這樣的網關,看似沒什么問題,然而跑上一段時間使用pprof觀測會發現goroutine和內存都在緩慢增長,也就是存在goroutine leak!,原因在于 micro源碼在包裝grpc時,沒有對關閉stream完善,只有收到io.EOF的錯誤時才會關閉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
}
復制代碼

并且有一個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()
}
復制代碼

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

  • melody的session在getset數據時會發生map的讀寫競爭而panic,可以查看issue,解決方法也比較簡單

一起學習

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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/387794.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/387794.shtml
英文地址,請注明出處:http://en.pswp.cn/news/387794.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

配置獨立于系統的PYTHON環境

配置獨立于系統的PYTHON環境 python 當前用戶包 一種解決方案是在利用本機的python環境的基礎上&#xff0c;將python的包安裝在當前user的.local文件夾下 一共有兩種方式來實現pip的時候安裝到當前user 設置pip配置文件 pip.conf 一種是在~/.pip文件夾下的pip配置文件pip.conf…

好程序員技術教程分享JavaScript運動框架

好程序員技術教程分享JavaScript運動框架&#xff0c;有需要的朋友可以參考下。 JavaScript的運動&#xff0c;即讓某元素的某些屬性由一個值變到另一個值的過程。如讓div的width屬性由200px變到400px&#xff0c;opacity屬性由0.3變到1.0&#xff0c;就是一個運動過程。 實現運…

linux 下mysql等php的安裝 lnmp

訪問https://lnmp.org/install.html按照步驟安裝 當下載執行完 wget -c http://soft.vpser.net/lnmp/lnmp1.3.tar.gz && tar zxf lnmp1.3.tar.gz && cd lnmp1.3 && ./install.shlnmp 要到.install.sh下改一下下載地址&#xff0c;把http直接更換成…

單純形法

單純形法 如果目標函數中所有系數都非正&#xff0c;那么顯然這些變量直接取0是最優的&#xff0c;所以此時答案為即為常數項。 我們要做的就是通過轉化把目標函數的系數全部搞成非負。 思路就是用非基變量替換基變量。 先找到一個目標函數中系數為正的變量&#xff0c;在所有限…

洛谷P1828 香甜的黃油 Sweet Butter

香甜的黃油 Sweet Butter 黃油真的是這么做的嗎&#xff1f;&#xff01;&#xff01;&#xff01;[惶恐] 這道題是Dijkstra算法的簡單變形 通過題意我們要找到一個點使奶牛所在點的路程和最短。通過Dijkstra的模板我們可以求的一點到其他任一點的最短路徑&#xff0c;那么我們…

JAVA List集合轉Page(分頁對象)

/*** version 1.0* author: fwjia*/ import java.util.List;public class PageModel<T> {/**** 當前頁*/private int page 1;/**** 總頁數*/public int totalPages 0;/**** 每頁數據條數*/private int pageRecorders;/**** 總頁數*/private int totalRows 0;/**** 每頁…

分區分表實驗用的語句

--查看索引 select * from DBA_IND_PARTITIONS &#xff54;; select status,t.* from dba_indexes t where t.OWNERGANSUSC; select count(*) from ACT_HI_VARINST SELECT ALTER INDEX || TABLE_OWNER || . || INDEX_NAME || UNUSABLE; UNUSABLE_INDEX FROM ALL_INDEX…

分布式數據庫數據一致性的原理、與技術實現方案

http://youzhixueyuan.com/the-principle-and-technology-realization-of-distributed-data-consistency.html 背景 可用性&#xff08;Availability&#xff09;和一致性&#xff08;Consistency&#xff09;是分布式系統的基本問題&#xff0c;先有著名的CAP理論定義過分布式…

模塊之re模塊 —— 正則

#‘match’只匹配從左向右第一個值是否在中括號的范圍內&#xff0c;如果沒有就返回None 如果有就直接打印一個對象&#xff0c;加上.group()就可以返回你要找的區間里面的值&#xff0c;如果沒有找到對應的值&#xff0c;加上‘.group()’會報錯 #‘search’ 默認是從整個st…

centos7 docker

Docker 是一個開源工具&#xff0c;它可以讓創建和管理 Linux 容器變得簡單。容器就像是輕量級的虛擬機&#xff0c;并且可以以毫秒級的速度來啟動或停止。Docker 幫助系統管理員和程序員在容器中開發應用程序&#xff0c;并且可以擴展到成千上萬的節點。 容器和 VM&#xff08…

批處理ping指定ip列表

for /f in (filename) do (command) for /f %i in (C:\ip.txt) do (ping %i -n 1 && echo %i 通 >>IP.txt || echo %i 不通 >>IP1.txt) 有返回寫入ip.txt 沒有寫入ip1.txt轉載于:https://blog.51cto.com/2216859/2384188

Intellij Idea 2017創建web項目及tomcat部署實戰

相關軟件&#xff1a;Intellij Idea2017、jdk16、tomcat7 Intellij Idea直接安裝&#xff08;可根據需要選擇自己設置的安裝目錄&#xff09;&#xff0c;jdk使用1.6/1.7/1.8都可以&#xff0c;主要是配置好系統環境變量&#xff0c;tomcat7上tomcat的官網下載壓縮包解壓即可。…

docker ssh

1&#xff0c;首先&#xff0c;需要從Docker官網獲得centos或Ubuntu鏡像 2&#xff0c;當本地已有Ubuntu鏡像后&#xff08;大概200M左右大小&#xff09;&#xff0c;使用如下命令 [cpp]view plaincopy docker run -t -i ubuntu /bin/bash 即可啟動一個容器&#xff0c;并放…

[BFS]JZOJ 4672 Graph Coloring

Description 現在你有一張無向圖包含n個節點m條邊。最初&#xff0c;每一條邊都是藍色或者紅色。每一次你可以將一個節點連接的所有邊變色&#xff08;從紅變藍&#xff0c;藍變紅&#xff09;。找到一種步數最小的方案&#xff0c;使得所有邊的顏色相同。Input 第一行包含兩個…

實現繼承的方式

/*** 借助構造函數實現繼承*/function Parent1(){this.name "parent1";}Parent1.prototype.say function(){};function Child1(){//將父構造函數的this指向子構造函數的實例上Parent1.call(this);//applythis.type "child1";}console.log(new Child1);/…

Vue源碼: 關于vm.$watch()內部原理

vm.$watch()用法 關于vm.$watch()詳細用法可以見官網。 大致用法如下: <script>const app new Vue({el: "#app",data: {a: {b: {c: c}}},mounted () {this.$watch(function () {return this.a.b.c}, this.handle, {deep: true,immediate: true // 默認會初始化…

docker啟動順序

VMDocker: 用戶名:root 密碼:XXXXXXXXXXXXX docker run -i -t -d -p 8081:8080 -p 23:22 67591570dd29 /bin/bash 常用命令 啟動停止: service docker start service docker stop 所有鏡像:docker images 當前執行:docker ps 提交保存docker容器: docker commit 進入到對應服…

js時鐘倒計時

JS倒計時Date 代碼如下&#xff1a; 1 <style type"text/css">2 * {3 margin: 0;4 padding: 0;5 }6 7 #box {8 border: 1px solid cyan;9 background-color: #000; 10 height: 50px; 11 width: 500px; 12 margin: 100px auto 0; 13 border-radius: 20px; 14 te…

JAVA的值傳遞問題

為什么 Java 中只有值傳遞&#xff1f; 首先回顧一下在程序設計語言中有關將參數傳遞給方法&#xff08;或函數&#xff09;的一些專業術語。按值調用(call by value)表示方法接收的是調用者提供的值&#xff0c;而按引用調用&#xff08;call by reference)表示方法接收的是調…