Go語言自亮相以來并沒有展示一個(gè)明確的方向,Google員工將Go語言稱為一個(gè)“試驗(yàn)性語言”,稱其試圖融合Python等動(dòng)態(tài)語言的開發(fā)速度和C或C++等編譯語言的性能和安全。一位Go語言的支持者概括而言Go語言如下:簡單、快速、安全、并發(fā)、快樂編程、開源;但Go語言缺乏方向以及其“集大成者”的嘗試很容易會(huì)導(dǎo)致其學(xué)貓不成學(xué)狗也不成,淪為四不像。盡管如此,編者仍然覺得Go語言有相當(dāng)大的潛力:很多開發(fā)者對(duì)它感興趣——不僅它的最初設(shè)計(jì)者陣容強(qiáng)大,而且在參與修改源代碼的人群中也不乏大牛級(jí)人物。這很有可能幫助Go語言找到適合自己的方向,開拓系統(tǒng)編程的新方向。
創(chuàng)新互聯(lián)公司堅(jiān)持“要么做到,要么別承諾”的工作理念,服務(wù)領(lǐng)域包括:做網(wǎng)站、成都網(wǎng)站制作、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣等服務(wù),滿足客戶于互聯(lián)網(wǎng)時(shí)代的黃石網(wǎng)站設(shè)計(jì)、移動(dòng)媒體設(shè)計(jì)的需求,幫助企業(yè)找到有效的互聯(lián)網(wǎng)解決方案。努力成為您成熟可靠的網(wǎng)絡(luò)建設(shè)合作伙伴!
寫在最前面:由于現(xiàn)在游戲基本上采用全球大區(qū)的模式,全球玩家在同一個(gè)大區(qū)進(jìn)行游戲,傳統(tǒng)的單服模式已經(jīng)不能夠滿足當(dāng)前的服務(wù)需求,所以現(xiàn)在游戲服務(wù)器都在往微服務(wù)架構(gòu)發(fā)展。當(dāng)前我們游戲也是利用微服務(wù)架構(gòu)來實(shí)現(xiàn)全球玩家同服游戲。
玩家每次斷線(包括切換網(wǎng)絡(luò)/超時(shí)斷線)后應(yīng)該會(huì)重新連接服務(wù)器,重連成功的話可以繼續(xù)當(dāng)前情景繼續(xù)游戲,但是之前寫的底層重連機(jī)制一直不能生效,導(dǎo)致每次玩家斷線后重連都失敗,要從賬號(hào)登陸開始重新登陸,該文章寫在已經(jīng)定位了重連問題是由SLB引起后,提出的解決方案。
每次重連后,客戶端向SLB發(fā)送建立連接,SLB都會(huì)重新分配一個(gè)網(wǎng)關(guān)節(jié)點(diǎn),導(dǎo)致客戶端連接到其他網(wǎng)關(guān),重連失敗。
會(huì)話保持的作用是什么?
開啟SLB會(huì)話保持功能后,SLB會(huì)記錄客戶端的IP地址,在一定時(shí)間內(nèi),自動(dòng)將同一個(gè)IP的連接轉(zhuǎn)發(fā)到上次連接的網(wǎng)關(guān)。
在網(wǎng)絡(luò)不穩(wěn)定的情況下,游戲容易心跳或者發(fā)包超時(shí),開啟會(huì)話保持,能解決大部分情況下的重連問題。
但是在切換網(wǎng)絡(luò)的時(shí)候,手機(jī)網(wǎng)絡(luò)從Wifi切換成4G,自身IP會(huì)變,這時(shí)候連接必定和服務(wù)器斷開,需要重新建立連接。由于IP已經(jīng)變化,SLB不能識(shí)別到是同一個(gè)客戶端發(fā)出的請(qǐng)求,會(huì)將連接轉(zhuǎn)發(fā)到其他網(wǎng)關(guān)節(jié)點(diǎn)。所以使用TCP連接的情況下,SLB開啟會(huì)話保持并不能解決所有的重連問題。
另外某些時(shí)刻,手機(jī)頻繁開啟和斷開WI-FI,有時(shí)候可能不會(huì)斷開網(wǎng)絡(luò),這并不是因?yàn)?G切換WI-FI時(shí)網(wǎng)絡(luò)沒斷開,從4G切換到Wi-Fi網(wǎng)絡(luò),因?yàn)镮P變了,服務(wù)器不能識(shí)別到新的IP,連接肯定是斷開的。這時(shí)候網(wǎng)絡(luò)沒斷開,主要是因?yàn)楝F(xiàn)在智能手機(jī)會(huì)對(duì)4G和Wi-Fi網(wǎng)絡(luò)做個(gè)權(quán)重判斷,當(dāng)Wi-Fi網(wǎng)絡(luò)頻繁打開關(guān)閉時(shí),手機(jī)會(huì)判斷Wi-Fi網(wǎng)絡(luò)不穩(wěn)定,所有流量都走4G。所以網(wǎng)絡(luò)沒斷開是因?yàn)橐恢笔褂?G連接,才沒有斷開。想要驗(yàn)證,只需要切換Wi-Fi時(shí),把4G網(wǎng)絡(luò)關(guān)閉,這樣流量就必定走Wi-Fi。
上面說過,四層的TCP協(xié)議主要是基于IP來實(shí)現(xiàn)會(huì)話保持。但是切換網(wǎng)絡(luò)的時(shí)候客戶端的IP會(huì)變。所以要解決切換網(wǎng)絡(luò)時(shí)的重連問題,只有兩個(gè)方法:1. 當(dāng)客戶端成功連接網(wǎng)關(guān)節(jié)點(diǎn)后,記錄下網(wǎng)關(guān)節(jié)點(diǎn)的IP,下次重連后不經(jīng)過SLB,直接向網(wǎng)關(guān)節(jié)點(diǎn)發(fā)送連接請(qǐng)求。2.使用 SLB的七層(HTTP)轉(zhuǎn)發(fā)服務(wù)。
當(dāng)客戶端經(jīng)過SLB將連接轉(zhuǎn)發(fā)到網(wǎng)關(guān)時(shí),二次握手驗(yàn)證成功后向客戶端發(fā)送自己節(jié)點(diǎn)的IP,這樣客戶端下次連接的時(shí)候就能直接連接網(wǎng)關(guān)節(jié)點(diǎn)。但是這樣會(huì)暴露網(wǎng)關(guān)的IP地址,為安全留下隱患。
如果不希望暴露網(wǎng)關(guān)的IP地址,就需要增加一層代理層,SLB將客戶端請(qǐng)求轉(zhuǎn)發(fā)到代理層,代理層再根據(jù)客戶端帶有的key,轉(zhuǎn)發(fā)到正確的網(wǎng)關(guān)節(jié)點(diǎn)上。增加一層代理層,不僅會(huì)增加請(qǐng)求的響應(yīng)時(shí)間,還會(huì)增加整體框架的復(fù)雜度。
阿里云的七層SLB會(huì)話保持服務(wù),主要是基于cookie的會(huì)話保持??蛻舳嗽谕?wù)器發(fā)送HTTP請(qǐng)求后,服務(wù)器會(huì)返回客戶端一個(gè)Response,SLB會(huì)在這時(shí)候,將經(jīng)過的Response插入或者重寫cookie??蛻舳双@取到這個(gè)cookie,下次請(qǐng)求時(shí)會(huì)帶上cookie,SLB判斷Request的Headers里面有cookie,就將連接轉(zhuǎn)發(fā)到之前的網(wǎng)關(guān)節(jié)點(diǎn)。
HTTP是短鏈接,我們游戲是長連接,所以用HTTP肯定不合適。但是可以考慮基于HTTP的WebSocket。
什么是WebSocket?
WSS(Web Socket Secure)是WebSocket的加密版本。
SLB對(duì)WebSocket的支持
查看阿里云SLB文檔對(duì)WS的支持,說明SLB是支持WS協(xié)議的,并且SLB對(duì)于WS無需配置,只需要選用HTTP監(jiān)聽時(shí),就能夠轉(zhuǎn)發(fā)WS協(xié)議。說明WS協(xié)議在SLB這邊看來就是一個(gè)HTTP,這樣WS走的也是七層的轉(zhuǎn)發(fā)服務(wù)。只要SLB能夠正常識(shí)別WS握手協(xié)議里Request的cookie和正常識(shí)別服務(wù)器返回的Response并且往里面插入cookie,就可以利用會(huì)話保持解決重連問題。
Go語言實(shí)現(xiàn)WS服務(wù)器有兩種方法,一種是利用golang.org/x/net下的websocket包,另外一種方法就是自己解讀Websocket協(xié)議來實(shí)現(xiàn),由于WS協(xié)議一樣是基于TCP協(xié)議之上,完全可以通過監(jiān)聽TCP端口來實(shí)現(xiàn)。
客戶端發(fā)送Request消息
服務(wù)器返回Response消息
其中服務(wù)器返回的Sec-WebSocket-Accept字段,主要是用于客戶端需要驗(yàn)證服務(wù)器是否支持WS。RFC6455文檔中規(guī)定,在WebSocket通信協(xié)議中服務(wù)端為了證實(shí)已經(jīng)接收了握手,它需要把兩部分的數(shù)據(jù)合并成一個(gè)響應(yīng)。一部分信息來自客戶端握手的Sec-WebSocket-Keyt頭字段:Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==。對(duì)于這個(gè)字段,服務(wù)端必須得到這個(gè)值(頭字段中經(jīng)過base64編碼的值減去前后的空格)并與GUID"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"組合成一個(gè)字符串,這個(gè)字符串對(duì)于不懂WebSocket協(xié)議的網(wǎng)絡(luò)終端來說是不能使用的。這個(gè)組合經(jīng)過SHA-1掩碼,base64編碼后在服務(wù)端的握手中返回。如果這個(gè)Sec-WebSocket-Accept計(jì)算錯(cuò)誤瀏覽器會(huì)提示:Sec-WebSocket-Accept dismatch
如果返回成功,Websocket就會(huì)回調(diào)onopen事件
游戲服務(wù)器的使用的TCP協(xié)議,是在協(xié)議的包頭使用4Byte來聲明本協(xié)議長度,然后將協(xié)議一次性發(fā)送。但是在WS協(xié)議是通過Frame形式發(fā)送的,會(huì)將一條消息分為幾個(gè)frame,按照先后順序傳輸出去。這樣做會(huì)有幾個(gè)好處:
websocket的協(xié)議格式:
參數(shù)說明如下:
阿里云的SLB開啟HTTP監(jiān)聽后,會(huì)檢查過往的Request和Response請(qǐng)求,收到服務(wù)器返回的Response后,會(huì)往Response插入一個(gè)Cookie
客戶端收到服務(wù)器的Response后,可以在Header中查到有個(gè)“Set-Cookie”字段,里面是SLB插入的Cookie值
客戶端斷開連接后,下次發(fā)送請(qǐng)求需要往Headers插入Cookie字段
分別在阿里云的兩臺(tái)ECS實(shí)例上部署WS服務(wù)器,打開8000端口,開啟一個(gè)SLB服務(wù),SLB服務(wù)選擇HTTP方式監(jiān)聽,并且打開會(huì)話保持功能,Cookie處理方式選擇植入Cookie。Demo服務(wù)器沒有做HTTP健康監(jiān)聽的處理,健康檢查這塊可以先關(guān)掉。
在兩臺(tái)ECS上啟動(dòng)WS服務(wù)器,然后本地運(yùn)行客戶端,分別測試兩臺(tái)服務(wù)器是否能正常連接,測試完畢后,測試SLB能否正常工作。服務(wù)器和SLB都正常的情況下,運(yùn)行客戶端,客戶端會(huì)得到以下結(jié)果
收到的三次Cookie都相同,說明Cookie是有正常植入工作的,并且三次都被SLB正確抓取了。
收到的三次serverId也都是同樣的值,說明三次都是同一個(gè)ECS上的服務(wù)器響應(yīng)。
至此,驗(yàn)證成功。
Websocket+SLB會(huì)話保持能夠解決超時(shí)重連和切換網(wǎng)絡(luò)時(shí)重連的問題。
參考:
阿里云會(huì)話保持
解答Wi-Fi與4G網(wǎng)絡(luò)切換的困惑
WebSocket的實(shí)現(xiàn)原理
阿里云SLB對(duì)WebSocket的支持
HTTP Headers和Cookie
我們可以看到 gorilla/websocket中的examples中有一個(gè)聊天室的demo。
我們進(jìn)入該項(xiàng)目可以看到里面有這樣的一些內(nèi)容
按照官方的運(yùn)行方式來運(yùn)行這個(gè)項(xiàng)目
在瀏覽器中打開8080端口,可以看到該項(xiàng)目可以被成功運(yùn)行了。
就是這樣一個(gè)簡單的demo。
然后我們?nèi)タ匆幌滤木唧w實(shí)現(xiàn)。
在這個(gè)項(xiàng)目中首先定義了一個(gè)hub的結(jié)構(gòu)體:
這個(gè)結(jié)構(gòu)體中,clients代表所有已經(jīng)注冊(cè)的用戶,broadcast管道會(huì)存儲(chǔ)客戶端發(fā)送來的信息。 register是一個(gè)*Client類型的管道,用于存儲(chǔ)新注冊(cè)的用戶,unregister管道反之。
我們打開main.go,main函數(shù)的源碼為:
在這里首先會(huì)新開一個(gè)goroutine,去跑hub的run方法,run方法中一個(gè)死循環(huán),不停地去輪詢hub中的內(nèi)容
如果取到了新用戶,就加入到clients中,如果取到了信息,就循環(huán)所有的client,將信息寫到client.send中。
我們看到在請(qǐng)求路徑為根的時(shí)候,它會(huì)請(qǐng)求一個(gè)函數(shù),而這個(gè)函數(shù)就是將home.html發(fā)送到客戶端。
而在請(qǐng)求路徑為“/ws”的時(shí)候,他會(huì)執(zhí)行一個(gè)serveWS的函數(shù)。
每當(dāng)一個(gè)新的用戶進(jìn)來之后,首先將連接升級(jí)為長連接,然后將當(dāng)前的client寫到register中,由hub.run函數(shù)去做處理。然后開啟兩個(gè)goroutine,一個(gè)去讀client中發(fā)送來的數(shù)據(jù),一個(gè)將數(shù)據(jù)寫入到所有的client中,去發(fā)送給用戶。
這就是整個(gè)聊天室的實(shí)現(xiàn)原理。
我們?cè)趍ian函數(shù)中,首先初始化配置文件,然后新建http連接。
這個(gè)連接創(chuàng)建之后,監(jiān)聽服務(wù)器的9999端口。如果url的路徑后綴為 "/ws",就轉(zhuǎn)發(fā)到ws/ws.go中的IndexHandler方法中。
這個(gè)方法中首先我們創(chuàng)建一個(gè)websocket的Upgrader實(shí)例,然后我們使用Upgrader的upgrade方法來升級(jí)一下我們的連接為長連接。
升級(jí)完成之后會(huì)返回一個(gè)*websocket.Conn的連接,我們之后所有的關(guān)于連接的操作,都是基于該conn的。
在該連接完成之后,我們將連接存放到一個(gè)名為Client的map中,以便之后管理更為方便。
之后,我們啟動(dòng)一個(gè)goroutine來讀取連接中發(fā)送的信息內(nèi)容,再根據(jù)內(nèi)容進(jìn)行相應(yīng)的操作。
WebSockets通過TCP連接提供客戶端與服務(wù)器之間的雙向即時(shí)通信。這意味著,我們可以維護(hù)一個(gè)TCP連接,然后發(fā)送和監(jiān)聽該連接上的消息,而不是不斷地通過新建TCP連接去輪詢web服務(wù)器的更新。
在Go的生態(tài)中,WebSocket協(xié)議有幾個(gè)不同的實(shí)現(xiàn)。有些庫是協(xié)議的純實(shí)現(xiàn)。另外一些人則選擇在WebSocket協(xié)議的基礎(chǔ)上構(gòu)建,為他們特定的用例創(chuàng)建更好的抽象。
下面是一個(gè)不全面的Go WebSocket協(xié)議實(shí)現(xiàn)列表:
在線拍賣是以實(shí)時(shí)通信為核心的行業(yè)之一。在一場拍賣中,幾秒鐘的時(shí)間就決定了你是贏了還是失去了一件你一直想要的收藏品。
讓我們以gorilla/websocket庫實(shí)現(xiàn)的簡單拍賣應(yīng)用程序作為本文的示例。
首先,我們將定義兩個(gè)非常簡單的結(jié)構(gòu)體Bid和Auction,我們將在WebSocket處理程序中使用它們。 Auction 有一個(gè)Bid方法,我們將使用該方法接收客戶端發(fā)送來的競價(jià)請(qǐng)求。
這兩種類型都相當(dāng)簡單,包含的字段非常少。NewAuction構(gòu)造函數(shù)構(gòu)建一個(gè)帶有持續(xù)時(shí)間、itemID和*Bids的Aution實(shí)例。
我們將通過 Bid 方法來實(shí)現(xiàn)拍賣的競標(biāo)動(dòng)作:
Auction的Bid方法就是物品競拍發(fā)生的地方。它接收一個(gè) amount 和 userID 作為參數(shù),并向 Auction 對(duì)象中添加Bid實(shí)例。而且它會(huì)檢查競拍是否結(jié)束以及的競拍價(jià)格是否大于已有的最大競價(jià)。如果這些條件中的任何一個(gè)不滿足,它將向調(diào)用者返回適當(dāng)?shù)腻e(cuò)誤。
有了結(jié)構(gòu)體定義和Bid方法,讓我們深入到WebSockets機(jī)制。
想象一下,一個(gè)可以在拍賣中實(shí)時(shí)出價(jià)的網(wǎng)站。它通過WebSockets發(fā)送的每一條JSON消息都會(huì)包含用戶的標(biāo)識(shí)符( UserID )和出價(jià)的金額( amount )。一旦服務(wù)器接受了消息,它將參與競價(jià)并向客戶端返回一個(gè)競拍結(jié)果。
在服務(wù)器端,此通信將由 net/http 處理程序完成。它將處理所有WebSocket的業(yè)務(wù)邏輯,有幾個(gè)值得注意的步驟:
1、將接收到的HTTP連接升級(jí)為WebSocket連接。
2、接收來自客戶端的消息。
3、從消息中解碼出bid對(duì)象。
4、參與競價(jià)。
5、 向客戶端發(fā)送競拍結(jié)果。
下面我們來實(shí)現(xiàn)這個(gè)處理程序。首先定義 inbound 和 outbound 消息類型,用于接收和發(fā)送客戶端消息。
它們都分別表示入站/出站消息,這就是在客戶端和服務(wù)器之間的交互數(shù)據(jù)。 inbound 入站消息將表示一個(gè)出價(jià),而 outbound 類型表示一個(gè)簡單的返回消息,其Body中包含一些文本。
接下來定義 bidsHandler ,包含ServeHTTP方法實(shí)現(xiàn)HTTP連接的升級(jí):
首先定義 websocket.Upgrader ,接收處理程序的 http.ResponseWriter 和 *http.Resquest 并升級(jí)連接。 因?yàn)檫@只是一個(gè)應(yīng)用程序示例 upgrader.CheckOrigin 方法將只返回true,而不檢查傳入請(qǐng)求的來源。一旦 upgrader 完成連接的升級(jí),將返回 *websocket.Conn 對(duì)象保存在 ws 變量中。 *websocket.Conn 將接收所有客戶端發(fā)送來的消息,也是處理程序讀取請(qǐng)求內(nèi)容的地方。同樣,處理程序?qū)?huì)向 *websocket.Conn 寫入消息,它將向客戶端發(fā)送響應(yīng)消息。
for 循環(huán)做了幾件事:首先,使用 ws.ReadMessage() 讀取websocket消息,改函數(shù)返回消息類型(二進(jìn)制或文本)和消息內(nèi)容( m )以及可能發(fā)生的錯(cuò)誤( err )。然后,檢查客戶端是否意外地關(guān)閉了連接。
錯(cuò)誤處理完成并讀取到消息,我們將使用 json.Unmarshal 對(duì)其進(jìn)行解碼。接著調(diào)Bid方法參與競拍。然后使用 json.Marshal 對(duì)返回內(nèi)容進(jìn)行序列化,使用 ws.WriteMessage 方法發(fā)送給客戶端。
盡管編寫WebSocket處理程序比普通HTTP處理程序要復(fù)雜得多,但測試它們很簡單。事實(shí)上,測試WebSockets處理程序就像測試HTTP處理程序一樣簡單。這是因?yàn)閃ebSockets是在HTTP上構(gòu)建的,所以測試WebSockets使用的工具與測試HTTP服務(wù)器相同。
首先添加測試用例:
首先,我們從定義測試用例開始。每個(gè)用例有一個(gè) name ,這是測試用例的可讀名稱。此外,每個(gè)測試用例都有一個(gè) bids 切片和一個(gè)duration持續(xù)時(shí)間,用于創(chuàng)建一個(gè)測試拍賣對(duì)象 Auction 。測試用例還有一個(gè)入站消息 inbound 和一個(gè)出站回復(fù) outbound —這是測試用例將發(fā)送給處理程序并期望從處理程序返回的消息。
在TestBidsHandler中我們添加三種不同的測試用例——一個(gè)是客戶端發(fā)起了錯(cuò)誤的報(bào)價(jià),低于目前最大報(bào)價(jià),另一個(gè)測試用例,客戶端添加了一個(gè)正常的報(bào)價(jià),第三個(gè)客戶端參與的拍賣已結(jié)束。
下面完成測試函數(shù):
我們?cè)趕ubtest函數(shù)體中添加了一些新函數(shù)。 newWSServe r將創(chuàng)建一個(gè)測試服務(wù)器并將其升級(jí)為WebSocket連接,同時(shí)返回服務(wù)器和WebSocket連接。然后, sendMessage 函數(shù)將通過WebSocket連接將消息從測試用例發(fā)送到測試服務(wù)器。之后,通過 receiveWSMessage ,我們將從服務(wù)器讀取響應(yīng),并通過將其與測試用例的進(jìn)行比較來斷言其正確性。
那么,這些新的函數(shù)的作用是什么呢?讓我們逐一分析。
newWSServer 函數(shù)使用 httptest.NewServer 函數(shù)將處理程序掛載到測試HTTP服務(wù)器上。通過 httpToWS ,實(shí)現(xiàn)了將服務(wù)器的 URL 轉(zhuǎn)為websocket URL (它只是將URL中的 http 協(xié)議替換為 ws ,或?qū)?https 替換為 wss 協(xié)議)。
為了建立WebSocket連接,我們使用 WebSocket.DefaultDialer ,它是一個(gè)所有字段都設(shè)置為默認(rèn)值的dialer。調(diào)用 Dial 方法通過WebSocket服務(wù)器URL (wsURL)返回WebSocket連接。
sendMessage 函數(shù)接收一個(gè)WebSocket連接和 inbound 消息作為參數(shù)。將消息序列化成json以二進(jìn)制格式在websocket連接中發(fā)送。
receiveWSMessage 函數(shù)以 ws WebSocket連接為參數(shù),通過 ws.ReadMessage() 讀取請(qǐng)求消息,然后反序列化成 outbound 類型返回。
如果我們運(yùn)行測試,我們將看到它們通過: