寫(xiě)在最前面:由于現(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)來(lái)實(shí)現(xiàn)全球玩家同服游戲。
玩家每次斷線(包括切換網(wǎng)絡(luò)/超時(shí)斷線)后應(yīng)該會(huì)重新連接服務(wù)器,重連成功的話可以繼續(xù)當(dāng)前情景繼續(xù)游戲,但是之前寫(xiě)的底層重連機(jī)制一直不能生效,導(dǎo)致每次玩家斷線后重連都失敗,要從賬號(hào)登陸開(kāi)始重新登陸,該文章寫(xiě)在已經(jīng)定位了重連問(wèn)題是由SLB引起后,提出的解決方案。
當(dāng)前游戲架構(gòu):
客戶端從一個(gè)Account服務(wù)器登陸并且拉取游戲服務(wù)器的所有SLB地址
客戶端ping所有SLB地址,選擇延遲最低的一個(gè)SLB進(jìn)行連接
客戶端連接SLB,SLB將連接轉(zhuǎn)發(fā)到一個(gè)網(wǎng)關(guān)節(jié)點(diǎn)建立連接
問(wèn)題:
每次重連后,客戶端向SLB發(fā)送建立連接,SLB都會(huì)重新分配一個(gè)網(wǎng)關(guān)節(jié)點(diǎn),導(dǎo)致客戶端連接到其他網(wǎng)關(guān),重連失敗。
解決方法:
1. 開(kāi)啟SLB會(huì)話保持功能
會(huì)話保持的作用是什么?
將同一客戶端的會(huì)話請(qǐng)求轉(zhuǎn)發(fā)給指定的一個(gè)后端服務(wù)器處理。
負(fù)載均衡支持什么類(lèi)型的會(huì)話保持?
-四層(TCP協(xié)議)服務(wù),負(fù)載均衡系統(tǒng)是基于源IP的會(huì)話保持。四層會(huì)話保持的最長(zhǎng)時(shí)間是3600秒。
-七層(HTTP/HTTPS協(xié)議)服務(wù),負(fù)載均衡系統(tǒng)是基于cookie的會(huì)話保持。植入cookie的會(huì)話保持的最長(zhǎng)時(shí)間是86400秒(24小時(shí))。
開(kāi)啟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í),開(kāi)啟會(huì)話保持,能解決大部分情況下的重連問(wèn)題。
但是在切換網(wǎng)絡(luò)的時(shí)候,手機(jī)網(wǎng)絡(luò)從Wifi切換成4G,自身IP會(huì)變,這時(shí)候連接必定和服務(wù)器斷開(kāi),需要重新建立連接。由于IP已經(jīng)變化,SLB不能識(shí)別到是同一個(gè)客戶端發(fā)出的請(qǐng)求,會(huì)將連接轉(zhuǎn)發(fā)到其他網(wǎng)關(guān)節(jié)點(diǎn)。所以使用TCP連接的情況下,SLB開(kāi)啟會(huì)話保持并不能解決所有的重連問(wèn)題。
另外某些時(shí)刻,手機(jī)頻繁開(kāi)啟和斷開(kāi)WI-FI,有時(shí)候可能不會(huì)斷開(kāi)網(wǎng)絡(luò),這并不是因?yàn)?G切換WI-FI時(shí)網(wǎng)絡(luò)沒(méi)斷開(kāi),從4G切換到Wi-Fi網(wǎng)絡(luò),因?yàn)镮P變了,服務(wù)器不能識(shí)別到新的IP,連接肯定是斷開(kāi)的。這時(shí)候網(wǎng)絡(luò)沒(méi)斷開(kāi),主要是因?yàn)楝F(xiàn)在智能手機(jī)會(huì)對(duì)4G和Wi-Fi網(wǎng)絡(luò)做個(gè)權(quán)重判斷,當(dāng)Wi-Fi網(wǎng)絡(luò)頻繁打開(kāi)關(guān)閉時(shí),手機(jī)會(huì)判斷Wi-Fi網(wǎng)絡(luò)不穩(wěn)定,所有流量都走4G。所以網(wǎng)絡(luò)沒(méi)斷開(kāi)是因?yàn)橐恢笔褂?G連接,才沒(méi)有斷開(kāi)。想要驗(yàn)證,只需要切換Wi-Fi時(shí),把4G網(wǎng)絡(luò)關(guān)閉,這樣流量就必定走Wi-Fi。
2. 切換網(wǎng)絡(luò)時(shí)的重連問(wèn)題
上面說(shuō)過(guò),四層的TCP協(xié)議主要是基于IP來(lái)實(shí)現(xiàn)會(huì)話保持。但是切換網(wǎng)絡(luò)的時(shí)候客戶端的IP會(huì)變。所以要解決切換網(wǎng)絡(luò)時(shí)的重連問(wèn)題,只有兩個(gè)方法:1. 當(dāng)客戶端成功連接網(wǎng)關(guān)節(jié)點(diǎn)后,記錄下網(wǎng)關(guān)節(jié)點(diǎn)的IP,下次重連后不經(jīng)過(guò)SLB,直接向網(wǎng)關(guān)節(jié)點(diǎn)發(fā)送連接請(qǐng)求。2.使用 SLB的七層(HTTP)轉(zhuǎn)發(fā)服務(wù)。
2.1 客戶端直接向網(wǎng)關(guān)發(fā)送請(qǐng)求連接
當(dāng)客戶端經(jīng)過(guò)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ù)雜度。
2.2 利用SLB的七層(HTTP)轉(zhuǎn)發(fā)服務(wù)
阿里云的七層SLB會(huì)話保持服務(wù),主要是基于cookie的會(huì)話保持??蛻舳嗽谕?wù)器發(fā)送HTTP請(qǐng)求后,服務(wù)器會(huì)返回客戶端一個(gè)Response,SLB會(huì)在這時(shí)候,將經(jīng)過(guò)的Response插入或者重寫(xiě)cookie??蛻舳双@取到這個(gè)cookie,下次請(qǐng)求時(shí)會(huì)帶上cookie,SLB判斷Request的Headers里面有cookie,就將連接轉(zhuǎn)發(fā)到之前的網(wǎng)關(guān)節(jié)點(diǎn)。
HTTP是短鏈接,我們游戲是長(zhǎng)連接,所以用HTTP肯定不合適。但是可以考慮基于HTTP的WebSocket。
什么是WebSocket?
WebSocket (WS)是HTML5一種新的協(xié)議,它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信,能更好地節(jié)省服務(wù)器資源和帶寬并達(dá)到實(shí)時(shí)通訊。WebSocket建立在TCP之上,同HTTP一樣通過(guò)TCP來(lái)傳輸數(shù)據(jù),但是它和HTTP最大不同是:
WebSocket是一種雙向通信協(xié)議,在建立連接后,WebSocket服務(wù)器和Browser/Client Agent都能主動(dòng)的向?qū)Ψ桨l(fā)送或接收數(shù)據(jù),就像Socket一樣;WebSocket需要類(lèi)似TCP的客戶端和服務(wù)器端通過(guò)握手連接,連接成功后才能相互通信。
WSS(Web Socket Secure)是WebSocket的加密版本。
WebSocket在建立連接的時(shí)候,會(huì)依賴HTTP協(xié)議進(jìn)行一次握手,這時(shí)候客戶端會(huì)給服務(wù)器發(fā)送Request,服務(wù)器也會(huì)給客戶端返回一個(gè)Response,并且HTTP其實(shí)也是建立在TCP之上的通信協(xié)議。
SLB對(duì)WebSocket的支持
如何在阿里云負(fù)載均衡上啟用WS/WSS支持?
無(wú)需配置,當(dāng)選用HTTP監(jiān)聽(tīng)時(shí),默認(rèn)支持無(wú)加密版本W(wǎng)ebSocket協(xié)議(WS協(xié)議);當(dāng)選擇HTTPS監(jiān)聽(tīng)時(shí),默認(rèn)支持加密版本的WebSocket協(xié)議(WSS協(xié)議)。
WSS/WS協(xié)議支持的約束如下:
負(fù)載均衡與ECS后端服務(wù)的連接采用HTTP/1.1,建議后端服務(wù)器采用支持HTTP/1.1的Web Server。
若負(fù)載均衡與后端服務(wù)超過(guò)60秒無(wú)消息交互,會(huì)主動(dòng)斷開(kāi)連接,如需要維持連接一直不中斷,需要主動(dòng)實(shí)現(xiàn)?;顧C(jī)制,每60秒內(nèi)進(jìn)行一次報(bào)文交互。
查看阿里云SLB文檔對(duì)WS的支持,說(shuō)明SLB是支持WS協(xié)議的,并且SLB對(duì)于WS無(wú)需配置,只需要選用HTTP監(jiān)聽(tīng)時(shí),就能夠轉(zhuǎn)發(fā)WS協(xié)議。說(shuō)明WS協(xié)議在SLB這邊看來(lái)就是一個(gè)HTTP,這樣WS走的也是七層的轉(zhuǎn)發(fā)服務(wù)。只要SLB能夠正常識(shí)別WS握手協(xié)議里Request的cookie和正常識(shí)別服務(wù)器返回的Response并且往里面插入cookie,就可以利用會(huì)話保持解決重連問(wèn)題。
go語(yǔ)言實(shí)現(xiàn)Websocket服務(wù)器
Go語(yǔ)言實(shí)現(xiàn)WS服務(wù)器有兩種方法,一種是利用golang.org/x/net下的websocket包,另外一種方法就是自己解讀Websocket協(xié)議來(lái)實(shí)現(xiàn),由于WS協(xié)議一樣是基于TCP協(xié)議之上,完全可以通過(guò)監(jiān)聽(tīng)TCP端口來(lái)實(shí)現(xiàn)。
1.握手
客戶端發(fā)送Request消息
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Version: 13
服務(wù)器返回Response消息
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
其中服務(wù)器返回的Sec-WebSocket-Accept字段,主要是用于客戶端需要驗(yàn)證服務(wù)器是否支持WS。RFC6455文檔中規(guī)定,在WebSocket通信協(xié)議中服務(wù)端為了證實(shí)已經(jīng)接收了握手,它需要把兩部分的數(shù)據(jù)合并成一個(gè)響應(yīng)。一部分信息來(lái)自客戶端握手的Sec-WebSocket-Keyt頭字段:Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==。對(duì)于這個(gè)字段,服務(wù)端必須得到這個(gè)值(頭字段中經(jīng)過(guò)base64編碼的值減去前后的空格)并與GUID"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"組合成一個(gè)字符串,這個(gè)字符串對(duì)于不懂WebSocket協(xié)議的網(wǎng)絡(luò)終端來(lái)說(shuō)是不能使用的。這個(gè)組合經(jīng)過(guò)SHA-1掩碼,base64編碼后在服務(wù)端的握手中返回。如果這個(gè)Sec-WebSocket-Accept計(jì)算錯(cuò)誤瀏覽器會(huì)提示:Sec-WebSocket-Accept dismatch
如果返回成功,Websocket就會(huì)回調(diào)onopen事件
2.傳輸協(xié)議
游戲服務(wù)器的使用的TCP協(xié)議,是在協(xié)議的包頭使用4Byte來(lái)聲明本協(xié)議長(zhǎng)度,然后將協(xié)議一次性發(fā)送。但是在WS協(xié)議是通過(guò)Frame形式發(fā)送的,會(huì)將一條消息分為幾個(gè)frame,按照先后順序傳輸出去。這樣做會(huì)有幾個(gè)好處:
- a、大數(shù)據(jù)的傳輸可以分片傳輸,不用考慮到數(shù)據(jù)大小導(dǎo)致的長(zhǎng)度標(biāo)志位不足夠的情況。
- b、和http的chunk一樣,可以邊生成數(shù)據(jù)邊傳遞消息,即提高傳輸效率。
websocket的協(xié)議格式:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
參數(shù)說(shuō)明如下:
* FIN:1位,用來(lái)表明這是一個(gè)消息的最后的消息片斷,當(dāng)然第一個(gè)消息片斷也可能是最后的一個(gè)消息片斷;
* RSV1, RSV2, RSV3: 分別都是1位,如果雙方之間沒(méi)有約定自定義協(xié)議,那么這幾位的值都必須為0,否則必須斷掉WebSocket連接;
* Opcode: 4位操作碼,定義有效負(fù)載數(shù)據(jù),如果收到了一個(gè)未知的操作碼,連接也必須斷掉,以下是定義的操作碼:
* %x0 表示連續(xù)消息片斷
* %x1 表示文本消息片斷
* %x2 表未二進(jìn)制消息片斷
* %x3-7 為將來(lái)的非控制消息片斷保留的操作碼
* %x8 表示連接關(guān)閉
* %x9 表示心跳檢查的ping
* %xA 表示心跳檢查的pong
* %xB-F 為將來(lái)的控制消息片斷的保留操作碼
* Mask: 1位,定義傳輸?shù)臄?shù)據(jù)是否有加掩碼,如果設(shè)置為1,掩碼鍵必須放在masking-key區(qū)域,客戶端發(fā)送給服務(wù)端的所有消息,此位的值都是1;
* Payload length: 傳輸數(shù)據(jù)的長(zhǎng)度,以字節(jié)的形式表示:7位、7+16位、或者7+64位。如果這個(gè)值以字節(jié)表示是0-125這個(gè)范圍,那這個(gè)值就表示傳輸數(shù)據(jù)的長(zhǎng)度;如果這個(gè)值是126,則隨后的兩個(gè)字節(jié)表示的是一個(gè)16進(jìn)制無(wú)符號(hào)數(shù),用來(lái)表示傳輸數(shù)據(jù)的長(zhǎng)度;如果這個(gè)值是127,則隨后的是8個(gè)字節(jié)表示的一個(gè)64位無(wú)符合數(shù),這個(gè)數(shù)用來(lái)表示傳輸數(shù)據(jù)的長(zhǎng)度。多字節(jié)長(zhǎng)度的數(shù)量是以網(wǎng)絡(luò)字節(jié)的順序表示。負(fù)載數(shù)據(jù)的長(zhǎng)度為擴(kuò)展數(shù)據(jù)及應(yīng)用數(shù)據(jù)之和,擴(kuò)展數(shù)據(jù)的長(zhǎng)度可能為0,因而此時(shí)負(fù)載數(shù)據(jù)的長(zhǎng)度就為應(yīng)用數(shù)據(jù)的長(zhǎng)度。
* Masking-key: 0或4個(gè)字節(jié),客戶端發(fā)送給服務(wù)端的數(shù)據(jù),都是通過(guò)內(nèi)嵌的一個(gè)32位值作為掩碼的;掩碼鍵只有在掩碼位設(shè)置為1的時(shí)候存在。
* Payload data: (x+y)位,負(fù)載數(shù)據(jù)為擴(kuò)展數(shù)據(jù)及應(yīng)用數(shù)據(jù)長(zhǎng)度之和。
* Extension data: x位,如果客戶端與服務(wù)端之間沒(méi)有特殊約定,那么擴(kuò)展數(shù)據(jù)的長(zhǎng)度始終為0,任何的擴(kuò)展都必須指定擴(kuò)展數(shù)據(jù)的長(zhǎng)度,或者長(zhǎng)度的計(jì)算方式,以及在握手時(shí)如何確定正確的握手方式。如果存在擴(kuò)展數(shù)據(jù),則擴(kuò)展數(shù)據(jù)就會(huì)包括在負(fù)載數(shù)據(jù)的長(zhǎng)度之內(nèi)。
* Application data: y位,任意的應(yīng)用數(shù)據(jù),放在擴(kuò)展數(shù)據(jù)之后,應(yīng)用數(shù)據(jù)的長(zhǎng)度=負(fù)載數(shù)據(jù)的長(zhǎng)度-擴(kuò)展數(shù)據(jù)的長(zhǎng)度。
3.SLB植入的Cookie處理
阿里云的SLB開(kāi)啟HTTP監(jiān)聽(tīng)后,會(huì)檢查過(guò)往的Request和Response請(qǐng)求,收到服務(wù)器返回的Response后,會(huì)往Response插入一個(gè)Cookie
植入cookie: 此種方法下,您只需要指定cookie的過(guò)期時(shí)間。客戶端第一次訪問(wèn)時(shí),負(fù)載均衡服務(wù)在返回請(qǐng)求中植入cookie(即在HTTP/HTTPS響應(yīng)報(bào)文中插入SERVERID字串),下次客戶端攜帶此cookie訪問(wèn),負(fù)載均衡服務(wù)會(huì)將請(qǐng)求定向轉(zhuǎn)發(fā)給之前記錄到的ECS實(shí)例上。
客戶端收到服務(wù)器的Response后,可以在Header中查到有個(gè)“Set-Cookie”字段,里面是SLB插入的Cookie值
Set-Cookie:SERVERID=1d94100b7e13c96fa2979b58edda2aa0|1587613640|1587613640;Path=/
客戶端斷開(kāi)連接后,下次發(fā)送請(qǐng)求需要往Headers插入Cookie字段
Cookie:SERVERID=1d94100b7e13c96fa2979b58edda2aa0|1587613640|1587613640;Path=/
WS服務(wù)器與客戶端Demo
1.WS客戶端DEMO(JAVA實(shí)現(xiàn))
package com.zhenyouqu.wsclient;
import org.java_websocket.drafts.Draft;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;
import org.java_websocket.enums.ReadyState;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.concurrent.CountDownLatch;
public class wsclient {
static String cookie = null;
private static Logger logger = LoggerFactory.getLogger(WebSocketClient.class);
public static WebSocketClient client;
static CountDownLatch countDownLatch;
public static void main(String[] args) throws InterruptedException {
tryConnect();
//因?yàn)閃ebSocketClient請(qǐng)求是異步返回調(diào)用,所以需要等待上一次返回設(shè)置Cookie后,再設(shè)置Cookie進(jìn)行請(qǐng)求
countDownLatch.await();
tryConnect();
countDownLatch.await();
tryConnect();
}
public static void tryConnect() throws InterruptedException {
try {
countDownLatch = new CountDownLatch(1);
Draft draft = new Draft_6455();
HashMap<String, String> headers = new HashMap<>();
if(cookie != null) {
headers.put("Cookie", cookie);
}
client = new WebSocketClient(new URI("ws://101.133.195.232:8000"),draft, headers) {
@Override
public void onOpen(ServerHandshake serverHandshake) {
cookie = serverHandshake.getFieldValue("Set-Cookie");
System.out.println("收到Cookie:"+cookie);
countDownLatch.countDown();
}
@Override
public void onMessage(String msg) {
System.out.println("收到消息==========\n"+msg);
if(msg.equals("over")){
client.close();
}
}
@Override
public void onClose(int i, String s, boolean b) {
logger.info("鏈接已關(guān)閉");
}
@Override
public void onError(Exception e){
e.printStackTrace();
logger.info("發(fā)生錯(cuò)誤已關(guān)閉");
}
};
} catch (URISyntaxException e) {
e.printStackTrace();
}
client.connect();
//logger.info(client.getDraft());
while(!client.getReadyState().equals(ReadyState.OPEN)){
logger.info("正在連接...");
}
//連接成功,發(fā)送信息
client.send("哈嘍,連接一下啊");
//等待三秒 接受數(shù)據(jù)
Thread.sleep(1000);
client.close();
}
}
2.WS服務(wù)器DEMO
package main
import (
"crypto/sha1"
"encoding/base64"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"strings"
)
var serverId = flag.Int("serverId", 1, "input serverId")
func main() {
flag.Parse()
ln, err := net.Listen("tcp",":8000")
if err != nil {
log.Panic(err)
}
for {
conn, err := ln.Accept()
if err != nil {
log.Println("Accept err:", err)
}
handleConnection(conn)
}
}
func handleConnection(conn net.Conn) {
defer conn.Close()
content := make([]byte, 1024)
_, err := conn.Read(content)
log.Println(string(content))
if err != nil {
log.Println(err)
}
isHttp := false
// 先暫時(shí)這么判斷
if string(content[0:3]) == "GET" {
isHttp = true;
}
log.Println("isHttp:", isHttp)
if isHttp {
headers := parseHandshake(string(content))
log.Println("headers", headers)
secWebsocketKey := headers["Sec-WebSocket-Key"]
// NOTE:這里省略其他的驗(yàn)證
guid := "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
// 計(jì)算Sec-WebSocket-Accept
h := sha1.New()
log.Println("accept raw:", secWebsocketKey + guid)
io.WriteString(h, secWebsocketKey + guid)
accept := make([]byte, 28)
base64.StdEncoding.Encode(accept, h.Sum(nil))
log.Println(string(accept))
response := "HTTP/1.1 101 Switching Protocols\r\n"
response = response + "Sec-WebSocket-Accept: " + string(accept) + "\r\n"
response = response + "Connection: Upgrade\r\n"
response = response + "Upgrade: websocket\r\n\r\n"
log.Println("response:", response)
if lenth, err := conn.Write([]byte(response)); err != nil {
log.Println(err)
}else {
log.Println("send len:", lenth)
}
wssocket := NewWsSocket(conn)
for {
data, err := wssocket.ReadIframe()
if err != nil {
log.Println("readIframe err:" , err)
break
}
log.Println("read data:", string(data))
err = wssocket.SendIframe([]byte(fmt.Sprintf("serverId:%d",*serverId)))
if err != nil {
log.Println("sendIframe err:" , err)
break
}
log.Println("send data")
}
}else {
log.Println("receive tcp content")
log.Println(string(content))
// 直接讀取
}
}
type WsSocket struct {
MaskingKey []byte
Conn net.Conn
}
func NewWsSocket(conn net.Conn) *WsSocket {
return &WsSocket{Conn: conn}
}
func (this *WsSocket)SendIframe(data []byte) error {
// 這里只處理data長(zhǎng)度<125的
if len(data) >= 125 {
return errors.New("send iframe data error")
}
lenth := len(data)
maskedData := make([]byte, lenth)
for i := 0; i < lenth; i++ {
if this.MaskingKey != nil {
maskedData[i] = data[i] ^ this.MaskingKey[i % 4]
}else {
maskedData[i] = data[I]
}
}
this.Conn.Write([]byte{0x81})
var payLenByte byte
if this.MaskingKey != nil && len(this.MaskingKey) != 4 {
payLenByte = byte(0x80) | byte(lenth)
this.Conn.Write([]byte{payLenByte})
this.Conn.Write(this.MaskingKey)
}else {
payLenByte = byte(0x00) | byte(lenth)
this.Conn.Write([]byte{payLenByte})
}
this.Conn.Write(data)
return nil
}
func (this *WsSocket)ReadIframe() (data []byte, err error){
err = nil
//第一個(gè)字節(jié):FIN + RSV1-3 + OPCODE
opcodeByte := make([]byte, 1)
this.Conn.Read(opcodeByte)
//斷開(kāi)連接
if len(opcodeByte)==1 && opcodeByte[0] ==0 {
return opcodeByte, errors.New("close connect error")
}
FIN := opcodeByte[0] >> 7
RSV1 := opcodeByte[0] >> 6 & 1
RSV2 := opcodeByte[0] >> 5 & 1
RSV3 := opcodeByte[0] >> 4 & 1
OPCODE := opcodeByte[0] & 15
log.Println(RSV1,RSV2,RSV3,OPCODE)
//OPCODE==8 連接關(guān)閉
if OPCODE == 8 {
return opcodeByte, errors.New("close connect normal")
}
//心跳ping
if OPCODE == 9 {
//TODO: 返回心跳pong
return opcodeByte, nil
}
payloadLenByte := make([]byte, 1)
this.Conn.Read(payloadLenByte)
payloadLen := int(payloadLenByte[0] & 0x7F)
mask := payloadLenByte[0] >> 7
if payloadLen == 127 {
extendedByte := make([]byte, 8)
this.Conn.Read(extendedByte)
}
maskingByte := make([]byte, 4)
if mask == 1 {
this.Conn.Read(maskingByte)
this.MaskingKey = maskingByte
}
payloadDataByte := make([]byte, payloadLen)
this.Conn.Read(payloadDataByte)
log.Println("data:", payloadDataByte)
dataByte := make([]byte, payloadLen)
//TODO: 需要優(yōu)化
for i := 0; i < payloadLen; i++ {
if mask == 1 {
dataByte[i] = payloadDataByte[i] ^ maskingByte[i % 4]
}else {
dataByte[i] = payloadDataByte[I]
}
}
if FIN == 1 {
data = dataByte
return
}
nextData, err := this.ReadIframe()
if err != nil {
return
}
data = append(data, nextData...)
return
}
func parseHandshake(content string) map[string]string {
headers := make(map[string]string, 10)
lines := strings.Split(content, "\r\n")
for _,line := range lines {
if len(line) >= 0 {
words := strings.Split(line, ":")
if len(words) == 2 {
headers[strings.Trim(words[0]," ")] = strings.Trim(words[1], " ")
}
}
}
return headers
}
3.部署實(shí)驗(yàn)
分別在阿里云的兩臺(tái)ECS實(shí)例上部署WS服務(wù)器,打開(kāi)8000端口,開(kāi)啟一個(gè)SLB服務(wù),SLB服務(wù)選擇HTTP方式監(jiān)聽(tīng),并且打開(kāi)會(huì)話保持功能,Cookie處理方式選擇植入Cookie。Demo服務(wù)器沒(méi)有做HTTP健康監(jiān)聽(tīng)的處理,健康檢查這塊可以先關(guān)掉。
在兩臺(tái)ECS上啟動(dòng)WS服務(wù)器,然后本地運(yùn)行客戶端,分別測(cè)試兩臺(tái)服務(wù)器是否能正常連接,測(cè)試完畢后,測(cè)試SLB能否正常工作。服務(wù)器和SLB都正常的情況下,運(yùn)行客戶端,客戶端會(huì)得到以下結(jié)果
收到Cookie:SERVERID=1d94100b7e13c96fa2979b58edda2aa0|1587619495|1587619495;Path=/
收到消息==========
serverId:1
收到Cookie:SERVERID=1d94100b7e13c96fa2979b58edda2aa0|1587619496|1587619495;Path=/
收到消息==========
serverId:1
收到Cookie:SERVERID=1d94100b7e13c96fa2979b58edda2aa0|1587619497|1587619495;Path=/
收到消息==========
serverId:1
收到的三次Cookie都相同,說(shuō)明Cookie是有正常植入工作的,并且三次都被SLB正確抓取了。
收到的三次serverId也都是同樣的值,說(shuō)明三次都是同一個(gè)ECS上的服務(wù)器響應(yīng)。
至此,驗(yàn)證成功。
Websocket+SLB會(huì)話保持能夠解決超時(shí)重連和切換網(wǎng)絡(luò)時(shí)重連的問(wèn)題。
參考:
阿里云會(huì)話保持
解答Wi-Fi與4G網(wǎng)絡(luò)切換的困惑
WebSocket的實(shí)現(xiàn)原理
阿里云SLB對(duì)WebSocket的支持
HTTP Headers和Cookie