重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
Go语言自亮相以来并没有展示一个明确的方向,Google员工将Go语言称为一个“试验性语言”,称其试图融合Python等动态语言的开发速度和C或C++等编译语言的性能和安全。一位Go语言的支持者概括而言Go语言如下:简单、快速、安全、并发、快乐编程、开源;但Go语言缺乏方向以及其“集大成者”的尝试很容易会导致其学猫不成学狗也不成,沦为四不像。尽管如此,编者仍然觉得Go语言有相当大的潜力:很多开发者对它感兴趣——不仅它的最初设计者阵容强大,而且在参与修改源代码的人群中也不乏大牛级人物。这很有可能帮助Go语言找到适合自己的方向,开拓系统编程的新方向。
创新互联公司坚持“要么做到,要么别承诺”的工作理念,服务领域包括:做网站、成都网站制作、企业官网、英文网站、手机端网站、网站推广等服务,满足客户于互联网时代的黄石网站设计、移动媒体设计的需求,帮助企业找到有效的互联网解决方案。努力成为您成熟可靠的网络建设合作伙伴!
写在最前面:由于现在游戏基本上采用全球大区的模式,全球玩家在同一个大区进行游戏,传统的单服模式已经不能够满足当前的服务需求,所以现在游戏服务器都在往微服务架构发展。当前我们游戏也是利用微服务架构来实现全球玩家同服游戏。
玩家每次断线(包括切换网络/超时断线)后应该会重新连接服务器,重连成功的话可以继续当前情景继续游戏,但是之前写的底层重连机制一直不能生效,导致每次玩家断线后重连都失败,要从账号登陆开始重新登陆,该文章写在已经定位了重连问题是由SLB引起后,提出的解决方案。
每次重连后,客户端向SLB发送建立连接,SLB都会重新分配一个网关节点,导致客户端连接到其他网关,重连失败。
会话保持的作用是什么?
开启SLB会话保持功能后,SLB会记录客户端的IP地址,在一定时间内,自动将同一个IP的连接转发到上次连接的网关。
在网络不稳定的情况下,游戏容易心跳或者发包超时,开启会话保持,能解决大部分情况下的重连问题。
但是在切换网络的时候,手机网络从Wifi切换成4G,自身IP会变,这时候连接必定和服务器断开,需要重新建立连接。由于IP已经变化,SLB不能识别到是同一个客户端发出的请求,会将连接转发到其他网关节点。所以使用TCP连接的情况下,SLB开启会话保持并不能解决所有的重连问题。
另外某些时刻,手机频繁开启和断开WI-FI,有时候可能不会断开网络,这并不是因为4G切换WI-FI时网络没断开,从4G切换到Wi-Fi网络,因为IP变了,服务器不能识别到新的IP,连接肯定是断开的。这时候网络没断开,主要是因为现在智能手机会对4G和Wi-Fi网络做个权重判断,当Wi-Fi网络频繁打开关闭时,手机会判断Wi-Fi网络不稳定,所有流量都走4G。所以网络没断开是因为一直使用4G连接,才没有断开。想要验证,只需要切换Wi-Fi时,把4G网络关闭,这样流量就必定走Wi-Fi。
上面说过,四层的TCP协议主要是基于IP来实现会话保持。但是切换网络的时候客户端的IP会变。所以要解决切换网络时的重连问题,只有两个方法:1. 当客户端成功连接网关节点后,记录下网关节点的IP,下次重连后不经过SLB,直接向网关节点发送连接请求。2.使用 SLB的七层(HTTP)转发服务。
当客户端经过SLB将连接转发到网关时,二次握手验证成功后向客户端发送自己节点的IP,这样客户端下次连接的时候就能直接连接网关节点。但是这样会暴露网关的IP地址,为安全留下隐患。
如果不希望暴露网关的IP地址,就需要增加一层代理层,SLB将客户端请求转发到代理层,代理层再根据客户端带有的key,转发到正确的网关节点上。增加一层代理层,不仅会增加请求的响应时间,还会增加整体框架的复杂度。
阿里云的七层SLB会话保持服务,主要是基于cookie的会话保持。客户端在往服务器发送HTTP请求后,服务器会返回客户端一个Response,SLB会在这时候,将经过的Response插入或者重写cookie。客户端获取到这个cookie,下次请求时会带上cookie,SLB判断Request的Headers里面有cookie,就将连接转发到之前的网关节点。
HTTP是短链接,我们游戏是长连接,所以用HTTP肯定不合适。但是可以考虑基于HTTP的WebSocket。
什么是WebSocket?
WSS(Web Socket Secure)是WebSocket的加密版本。
SLB对WebSocket的支持
查看阿里云SLB文档对WS的支持,说明SLB是支持WS协议的,并且SLB对于WS无需配置,只需要选用HTTP监听时,就能够转发WS协议。说明WS协议在SLB这边看来就是一个HTTP,这样WS走的也是七层的转发服务。只要SLB能够正常识别WS握手协议里Request的cookie和正常识别服务器返回的Response并且往里面插入cookie,就可以利用会话保持解决重连问题。
Go语言实现WS服务器有两种方法,一种是利用golang.org/x/net下的websocket包,另外一种方法就是自己解读Websocket协议来实现,由于WS协议一样是基于TCP协议之上,完全可以通过监听TCP端口来实现。
客户端发送Request消息
服务器返回Response消息
其中服务器返回的Sec-WebSocket-Accept字段,主要是用于客户端需要验证服务器是否支持WS。RFC6455文档中规定,在WebSocket通信协议中服务端为了证实已经接收了握手,它需要把两部分的数据合并成一个响应。一部分信息来自客户端握手的Sec-WebSocket-Keyt头字段:Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==。对于这个字段,服务端必须得到这个值(头字段中经过base64编码的值减去前后的空格)并与GUID"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"组合成一个字符串,这个字符串对于不懂WebSocket协议的网络终端来说是不能使用的。这个组合经过SHA-1掩码,base64编码后在服务端的握手中返回。如果这个Sec-WebSocket-Accept计算错误浏览器会提示:Sec-WebSocket-Accept dismatch
如果返回成功,Websocket就会回调onopen事件
游戏服务器的使用的TCP协议,是在协议的包头使用4Byte来声明本协议长度,然后将协议一次性发送。但是在WS协议是通过Frame形式发送的,会将一条消息分为几个frame,按照先后顺序传输出去。这样做会有几个好处:
websocket的协议格式:
参数说明如下:
阿里云的SLB开启HTTP监听后,会检查过往的Request和Response请求,收到服务器返回的Response后,会往Response插入一个Cookie
客户端收到服务器的Response后,可以在Header中查到有个“Set-Cookie”字段,里面是SLB插入的Cookie值
客户端断开连接后,下次发送请求需要往Headers插入Cookie字段
分别在阿里云的两台ECS实例上部署WS服务器,打开8000端口,开启一个SLB服务,SLB服务选择HTTP方式监听,并且打开会话保持功能,Cookie处理方式选择植入Cookie。Demo服务器没有做HTTP健康监听的处理,健康检查这块可以先关掉。
在两台ECS上启动WS服务器,然后本地运行客户端,分别测试两台服务器是否能正常连接,测试完毕后,测试SLB能否正常工作。服务器和SLB都正常的情况下,运行客户端,客户端会得到以下结果
收到的三次Cookie都相同,说明Cookie是有正常植入工作的,并且三次都被SLB正确抓取了。
收到的三次serverId也都是同样的值,说明三次都是同一个ECS上的服务器响应。
至此,验证成功。
Websocket+SLB会话保持能够解决超时重连和切换网络时重连的问题。
参考:
阿里云会话保持
解答Wi-Fi与4G网络切换的困惑
WebSocket的实现原理
阿里云SLB对WebSocket的支持
HTTP Headers和Cookie
我们可以看到 gorilla/websocket中的examples中有一个聊天室的demo。
我们进入该项目可以看到里面有这样的一些内容
按照官方的运行方式来运行这个项目
在浏览器中打开8080端口,可以看到该项目可以被成功运行了。
就是这样一个简单的demo。
然后我们去看一下它的具体实现。
在这个项目中首先定义了一个hub的结构体:
这个结构体中,clients代表所有已经注册的用户,broadcast管道会存储客户端发送来的信息。 register是一个*Client类型的管道,用于存储新注册的用户,unregister管道反之。
我们打开main.go,main函数的源码为:
在这里首先会新开一个goroutine,去跑hub的run方法,run方法中一个死循环,不停地去轮询hub中的内容
如果取到了新用户,就加入到clients中,如果取到了信息,就循环所有的client,将信息写到client.send中。
我们看到在请求路径为根的时候,它会请求一个函数,而这个函数就是将home.html发送到客户端。
而在请求路径为“/ws”的时候,他会执行一个serveWS的函数。
每当一个新的用户进来之后,首先将连接升级为长连接,然后将当前的client写到register中,由hub.run函数去做处理。然后开启两个goroutine,一个去读client中发送来的数据,一个将数据写入到所有的client中,去发送给用户。
这就是整个聊天室的实现原理。
我们在mian函数中,首先初始化配置文件,然后新建http连接。
这个连接创建之后,监听服务器的9999端口。如果url的路径后缀为 "/ws",就转发到ws/ws.go中的IndexHandler方法中。
这个方法中首先我们创建一个websocket的Upgrader实例,然后我们使用Upgrader的upgrade方法来升级一下我们的连接为长连接。
升级完成之后会返回一个*websocket.Conn的连接,我们之后所有的关于连接的操作,都是基于该conn的。
在该连接完成之后,我们将连接存放到一个名为Client的map中,以便之后管理更为方便。
之后,我们启动一个goroutine来读取连接中发送的信息内容,再根据内容进行相应的操作。
WebSockets通过TCP连接提供客户端与服务器之间的双向即时通信。这意味着,我们可以维护一个TCP连接,然后发送和监听该连接上的消息,而不是不断地通过新建TCP连接去轮询web服务器的更新。
在Go的生态中,WebSocket协议有几个不同的实现。有些库是协议的纯实现。另外一些人则选择在WebSocket协议的基础上构建,为他们特定的用例创建更好的抽象。
下面是一个不全面的Go WebSocket协议实现列表:
在线拍卖是以实时通信为核心的行业之一。在一场拍卖中,几秒钟的时间就决定了你是赢了还是失去了一件你一直想要的收藏品。
让我们以gorilla/websocket库实现的简单拍卖应用程序作为本文的示例。
首先,我们将定义两个非常简单的结构体Bid和Auction,我们将在WebSocket处理程序中使用它们。 Auction 有一个Bid方法,我们将使用该方法接收客户端发送来的竞价请求。
这两种类型都相当简单,包含的字段非常少。NewAuction构造函数构建一个带有持续时间、itemID和*Bids的Aution实例。
我们将通过 Bid 方法来实现拍卖的竞标动作:
Auction的Bid方法就是物品竞拍发生的地方。它接收一个 amount 和 userID 作为参数,并向 Auction 对象中添加Bid实例。而且它会检查竞拍是否结束以及的竞拍价格是否大于已有的最大竞价。如果这些条件中的任何一个不满足,它将向调用者返回适当的错误。
有了结构体定义和Bid方法,让我们深入到WebSockets机制。
想象一下,一个可以在拍卖中实时出价的网站。它通过WebSockets发送的每一条JSON消息都会包含用户的标识符( UserID )和出价的金额( amount )。一旦服务器接受了消息,它将参与竞价并向客户端返回一个竞拍结果。
在服务器端,此通信将由 net/http 处理程序完成。它将处理所有WebSocket的业务逻辑,有几个值得注意的步骤:
1、将接收到的HTTP连接升级为WebSocket连接。
2、接收来自客户端的消息。
3、从消息中解码出bid对象。
4、参与竞价。
5、 向客户端发送竞拍结果。
下面我们来实现这个处理程序。首先定义 inbound 和 outbound 消息类型,用于接收和发送客户端消息。
它们都分别表示入站/出站消息,这就是在客户端和服务器之间的交互数据。 inbound 入站消息将表示一个出价,而 outbound 类型表示一个简单的返回消息,其Body中包含一些文本。
接下来定义 bidsHandler ,包含ServeHTTP方法实现HTTP连接的升级:
首先定义 websocket.Upgrader ,接收处理程序的 http.ResponseWriter 和 *http.Resquest 并升级连接。 因为这只是一个应用程序示例 upgrader.CheckOrigin 方法将只返回true,而不检查传入请求的来源。一旦 upgrader 完成连接的升级,将返回 *websocket.Conn 对象保存在 ws 变量中。 *websocket.Conn 将接收所有客户端发送来的消息,也是处理程序读取请求内容的地方。同样,处理程序将会向 *websocket.Conn 写入消息,它将向客户端发送响应消息。
for 循环做了几件事:首先,使用 ws.ReadMessage() 读取websocket消息,改函数返回消息类型(二进制或文本)和消息内容( m )以及可能发生的错误( err )。然后,检查客户端是否意外地关闭了连接。
错误处理完成并读取到消息,我们将使用 json.Unmarshal 对其进行解码。接着调Bid方法参与竞拍。然后使用 json.Marshal 对返回内容进行序列化,使用 ws.WriteMessage 方法发送给客户端。
尽管编写WebSocket处理程序比普通HTTP处理程序要复杂得多,但测试它们很简单。事实上,测试WebSockets处理程序就像测试HTTP处理程序一样简单。这是因为WebSockets是在HTTP上构建的,所以测试WebSockets使用的工具与测试HTTP服务器相同。
首先添加测试用例:
首先,我们从定义测试用例开始。每个用例有一个 name ,这是测试用例的可读名称。此外,每个测试用例都有一个 bids 切片和一个duration持续时间,用于创建一个测试拍卖对象 Auction 。测试用例还有一个入站消息 inbound 和一个出站回复 outbound —这是测试用例将发送给处理程序并期望从处理程序返回的消息。
在TestBidsHandler中我们添加三种不同的测试用例——一个是客户端发起了错误的报价,低于目前最大报价,另一个测试用例,客户端添加了一个正常的报价,第三个客户端参与的拍卖已结束。
下面完成测试函数:
我们在subtest函数体中添加了一些新函数。 newWSServe r将创建一个测试服务器并将其升级为WebSocket连接,同时返回服务器和WebSocket连接。然后, sendMessage 函数将通过WebSocket连接将消息从测试用例发送到测试服务器。之后,通过 receiveWSMessage ,我们将从服务器读取响应,并通过将其与测试用例的进行比较来断言其正确性。
那么,这些新的函数的作用是什么呢?让我们逐一分析。
newWSServer 函数使用 httptest.NewServer 函数将处理程序挂载到测试HTTP服务器上。通过 httpToWS ,实现了将服务器的 URL 转为websocket URL (它只是将URL中的 http 协议替换为 ws ,或将 https 替换为 wss 协议)。
为了建立WebSocket连接,我们使用 WebSocket.DefaultDialer ,它是一个所有字段都设置为默认值的dialer。调用 Dial 方法通过WebSocket服务器URL (wsURL)返回WebSocket连接。
sendMessage 函数接收一个WebSocket连接和 inbound 消息作为参数。将消息序列化成json以二进制格式在websocket连接中发送。
receiveWSMessage 函数以 ws WebSocket连接为参数,通过 ws.ReadMessage() 读取请求消息,然后反序列化成 outbound 类型返回。
如果我们运行测试,我们将看到它们通过: