WebSocket+SLB(負(fù)載均衡)會(huì)話保持解決重連問(wèn)題

寫(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):

  1. 客戶端從一個(gè)Account服務(wù)器登陸并且拉取游戲服務(wù)器的所有SLB地址

  2. 客戶端ping所有SLB地址,選擇延遲最低的一個(gè)SLB進(jìn)行連接

  3. 客戶端連接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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容