游戏网络编程(三)

(一)WebSocket简介

短连接:在传统的Http协议中,客户端和服务器端的通信方式是短连接的方式,也就是服务器端并不会保持一个和客户端的连接,在消息发送后,会断开这个连接,客户端下次通信时,必须再建立和服务器的新连接,这就是短连接。在短链接的情况下,客户端必须不停的主动发起请求,而服务器始终被动的响应请求,来推送回数据。这种方式用到游戏开发中,显然是不适合的。

长连接:那么与之相对的就是长连接了。在长连接的情况下,客户端和服务器端始终保持一条有效的连接,那么客户端并不需要不停的主动发送消息,而服务器端也能主动的推送消息到客户端。很类似前面介绍的Socket的收发方式。那么显然长连接是我们游戏网络开发所需要的。

WebSocket:正是有了这样的需求,所以产生了WebSocket这一协议。注意,WebSocket只是一种协议,并不是一种Socket。WebSocket可以在客户端和服务器端建立一种全双工的通信连接。其协议是基于Tcp的方式实现的。

(二)WebSocket基础知识

1.握手

WebSocket其实就是使用Tcp建立连接,那么当终端建立连接时,怎么才能知道是一般的Tcp方式还是WebSocket协议方式呢?这里就需要靠握手,简单的说,通过握手机制,终端就能判别建立的是什么样的连接,从而决定是以WebSocket方式来处理还是Tcp方式来处理消息。

如果我们是自己实现服务器端,其实我们在收包的时候,就是一般的Tcp Socket的收包,并没有什么不同,该怎么处理还是怎么处理。但对于客户端就不一样了。因为大部分情况,客户端是使用现有的浏览器来作为客户端代码的JS运行环境的(除非你连客户端浏览器环境也是自己实现)。现有浏览器必须明确的知道协议类型,才能正确的建立长连接,并处理WebSocket包,并使用相关的JS代码,所以握手就变的及其重要了。

当实现我们自己的服务器时,建立握手的意义在于正确的通知客户端,服务器可以接收并允许建立一条基于WebSocket协议的连接

握手请求类似于下面这样的一段信息,不同的浏览器可能不一样,因为不同的浏览器遵循的WebSocket协议版本可能并不一致。

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8
Accept-Language: zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: http://localhost:5754
Sec-WebSocket-Extensions: permessage-deflate
Sec-WebSocket-Key: DC8b7Irs1RsyDvP2iEdsUQ==
Connection: keep-alive, Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket

对于上面的内容,其实我们没必要知道太多,其中关键的是“Sec-WebSocket-Key”中的内容。稍后将做解释,我们先看服务器应该如何响应这样的握手。当服务器决定接收这个WebSocket连接时,服务器必须回发一段有效的Http response消息给客户端。这个很重要,因为只有发送正确的响应,客户端浏览器才能确认WebSocket请求被接收,才能正确的建立起WebSocket连接(其实说白了就是因为浏览器不是我们自己开发,假设你有那闲工夫,自己开发整个浏览器和WebSocket环境,握手协议想怎么定是你自己的事,否则就要遵循标准)。
正确的服务器返回响应如下:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

同样,不必过于关注具体内容,前面三行照抄就行了。我们只需要关注两个地方,一个是换行,一个是Sec-WebSocket-Accept

换行:上述消息中,前三行后必须跟一个换行符,最后一行后则要跟两个换行符

Sec-WebSocket-Accept*:这个值是一个经过加密处理的字符串,客户端将验证该值来判断是否成功建立WebSocket连接,因为这个值的正确与否相当重要。对该值的计算方法是,将发来请求时的Sec-WebSocket-Key与GUID值“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”连接,然后将新字符串进行SHA1加密,将加密结果进行Base64的编码转换得到。(要注意的时,连接用的GUID值就是黑体标粗的这个值,时固定的,我第一次看文档,还以为这个值只是个举例,后来才发现原来是个常量字符串)

Tips:
处理握手协议时,除了以上两点需要注意外,还有字符编码格式也会影响建立连接的成功性。所以最好换行符使用Environment.NewLine,而不要使用”\r\n”。另外生成的响应消息字符串,最好使用Encoding.UTF8编码,否则很容易因为编码问题,导致客户端无法识别,造成连接建立不成功。

附上生成加密key值和生成响应返回消息的代码

private static byte[] PackHandShakeData(string secKeyAccept){    var responseBuilder = new StringBuilder();    responseBuilder.Append("HTTP/1.1 101 Switching Protocols" + Environment.NewLine);    responseBuilder.Append("Upgrade: websocket" + Environment.NewLine);    responseBuilder.Append("Connection: Upgrade" + Environment.NewLine);    responseBuilder.Append("Sec-WebSocket-Accept: " + secKeyAccept + Environment.NewLine + Environment.NewLine);    return Encoding.UTF8.GetBytes(responseBuilder.ToString());}private static string GetSecKeyAccetp(byte[] handShakeBytes, int bytesLength){    string handShakeText = Encoding.UTF8.GetString(handShakeBytes, 0, bytesLength);    string key = string.Empty;    var heads = handShakeText.Split("\n".ToCharArray());    foreach (var head in heads)    {        if (head.Contains("Sec-WebSocket-Key:"))        {            key = head;            key = head.Replace("Sec-WebSocket-Key:", "").Trim();        }    }}sc.Send(PackHandShakeData(GetSecKeyAccetp(buffer, length)));

2.帧数据

因为是基于Tcp Socket实现的,所以WebSocket实际的数据传输也是以流的方式传输。和Tcp一样,WebSocket有自己的传输帧格式。在这个格式中,WebSocket定义了消息字节流开始部分的字节的用途及含义。下面我们可以看示意图

0               1               2               30 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 ...                |+---------------------------------------------------------------+

byte1
(1)Fin代表数据是否结束,WebSocket会把较大的数据分成片发送,最后一片数据的Fin为1,代表数据片完结
(2)RSV1-RSV3是保留为,一般为0
(3)最后4bit代表Opcode,OpCode用来指示数据帧的类型。WebSocket的帧分为两大类型,数据帧和控制帧。
0x0 代表连续帧,也就因为这该帧数据是分片数据中的一片,并不是完整数据
0x1 代表数据是文本内容
0x2 代表数据时二进制流
0x3-0x7 保留给日后的非控制帧使用
0x8 代表该数据时一个关闭命令通知(下面会解释关闭)
0x9 代表Ping帧(同样下面会解释Ping)
0xA 代表Pong帧
0xB-0xF 保留给日后的控制帧使用

byte2
(1)Mask代表发来的帧中的数据,是否经过掩码处理,1为true,0为false,一般在客户端发给服务器端的数据中,该值都是1,也就是经过掩码处理,服务器发往客户端的不用掩码。(注意,所谓的客户端,服务端是相对的,接收WebSocket连接的那一端,也就是上面提到的回发加密处理的那一端是服务器端。这也解释了,为什么我们要遵循WebSocket标准来进行握手,否则客户端怎么知道自己发的数据得要掩码处理呢)
(2)后面7位代表数据帧的数据长度或者是一个长度指示。我自己理解为是一个长度预判。当数据长度不超过125字节时,该值就是实际的数据长度,当长度在126~65535时,该值为固定的126,超过65535,该值固定为127

byte3~byte4
当Payload len = 126时,保存的是该帧数据的16位真实长度

byte3~byte10
当Payload len = 127时,保存的是该帧数据64位的真实长度

注意,如果长度不超过125,那么byte3~byte10就不代表数据长度了,也就是说不会预留给数据长度用,而是给后续的帧头信息使用,后续帧头的字节信息左移

byte11~byte14
这4个字节代表掩码值,用客户端指定,每个包都不一样,只有经过掩码值的解码处理,才能获得正确的数据

由此可以看到,WebSocket的消息封包,服务器端至少需要2个字节,客户端至少6个字节

后续的字节就是实际发送的数据字节流了,下面是对数据帧解析的示例代码

bool close = (buffer[0] & 0x08) == 0x08;//暂时不处理,服务器端暂时只接收ping,不作服务器端主动发ping的考虑bool ping = (buffer[0] & 0x09) == 0x09;bool pong = (buffer[0] & 0x0A) == 0x0A;bool fin = (buffer[0] & 0x80) == 0x80; // 1bit,1表示最后一帧    bool mask_flag = (buffer[1] & 0x80) == 0x80; // 是否包含掩码    ...//足够读取分隔符string data = null;try{    int payload_len = buffer[1] & 0x7F; // 数据长度        byte[] masks = new byte[4];    byte[] payload_data;    if (payload_len == 126)    {        Array.Copy(buffer, 4, masks, 0, 4);        payload_len = (UInt16)(buffer[2] << 8 | buffer[3]);        payload_data = new byte[payload_len];        Array.Copy(buffer, 8, payload_data, 0, payload_len);    }    else if (payload_len == 127)    {        Array.Copy(buffer, 10, masks, 0, 4);        byte[] uInt64Bytes = new byte[8];        for (int i = 0; i < 8; i++)        {            uInt64Bytes[i] = buffer[9 - i];        }        UInt64 len = BitConverter.ToUInt64(uInt64Bytes, 0);        payload_data = new byte[len];        for (UInt64 i = 0; i < len; i++)        {            payload_data[i] = buffer[i + 14];        }    }    else    {        Array.Copy(buffer, 2, masks, 0, 4);        payload_data = new byte[payload_len];        Array.Copy(buffer, 6, payload_data, 0, payload_len);    }    for (var i = 0; i < payload_len; i++)    {        payload_data[i] = (byte)(payload_data[i] ^ masks[i % 4]);    }

3.关闭连接

有握手,那么当然就讲关闭了,网上很多教程往往只说明了建立握手,但是对于关闭WebSocket连接去只字未提。WebSocket的关闭,在实际操作中经常遇到的有三种情况,一种是浏览器的关闭,一种是我们js代码主动关闭,还有一种是浏览器刷新(没错,刷新,我一开始没注意这个问题)。而无论哪种方式,对于WebSocket来说,它必须发一个关闭的控制帧数据到对端。也就是上面提到的Opcode必须为0x8。
在发送了一个关闭的控制帧后,应用就不应该继续发送数据,而对端在收到一个关闭控制帧后,也必须尽快发送一个关闭帧回应。(这里所谓尽快,其实是可控的,并不是立刻,你可以等到你的收发结束后,才立刻发送一个关闭回应)。发送关闭帧后的端,将不再处理收到的数据。

关闭帧可能会包含数据,如果其包含数据,那么前两个字节一定是一个无符号整型所代表的状态码,代表了发生关闭的原因

4.Ping/Pong

WebSocket基于Tcp,同时它也改进了Tcp的一些实现特性。比如WebSocket自带Ping/Pong,以此来实现其保持长连接的特性。使用Tcp时,我们往往要自己实现心跳,但WebSocket的Ping/Pong则完全替我们实现了心跳。不过很讽刺的是,虽然其WebSocket标准明确的实现了Ping/Pong但是现在各浏览器,或是WebSocket库,并没有提供发送Ping/Pong的API,也就是你如果不是自己实现WebSocket的协议的话,这Ping/Pong根本是没法发的。
但目前的浏览器或者JS库,虽然不同供发Ping的API,但它们可以接收Ping处理,并回发Pong数据。所以在我的项目里,由于我们自己实现WebSocket的服务器端协议,所以自己实现发Ping数据,然后处理浏览器返回的Pong数据来检测了心跳。
另外,当一端收到多次ping时,并不需要返回每一个响应,只要返回最近一次Ping的Pong响应即可

(三)WebSocket理解误区

1.分包,粘包,连包,半包

网上很多资料都说WebSocket不会粘包,半包。OK,这是正确的,因为上述将数据帧的时候我们已经看到WebSocket会将大的数据,自动分片发送。所以WebSocket会自动分包发送,因为这种分包发送,WebSocket的数据不会溢出接收缓冲区,所以也不会有半包的情况发送。

但是关于粘包,和连包,我看到一部分资料都说不会。因为WebSocket具有帧头信息,所以不会粘包?这是不完全正确的,要知道Tcp的报文也是具有包头信息的,只不过Socket已经处理了。而且经过我对我们项目服务器实际压力测试,发现WebSocket会粘包,连包。不同的是,WebSocket的数据中拥有包头信息,但Tcp没有(实际开发中,我们自己一定会加个包头来分割封包的,WebSocket只是替我们设计了一个包头而已),但对这个包头分割的处理,还是要我们自己完成,WebSocket不会代劳,如果我们自己不处理,抱歉,妥妥的粘包,连包

以上就是对WebSocket的一些简单的理解心得和解释,详细的内容,大家可以去官网下载标准的文档看,不过要注意一定要下最新的,我一开始下的是06版本,结果怎么弄都发现控制帧的数据代码不对。

个人理解观点,如有错误,欢迎讨论指正。

(0)

相关推荐

  • 什么是流式输出?

    阿里妹导读:流式输出在阿里内部已经遍地开花,大家耳熟能详却又好奇不已.清楚的是知道它是性能利器从而提升业务转化,不清楚的是到底什么样的技术才算是流式输出?支撑流式输出的技术理论又有哪些?流式输出适合什 ...

  • C# Socket通信 Networkstream数据接收不全 | Code Bye

    c/s客户端用C#的Socket进行通信.开单独的接收线程使用networkstream进行数据接收 方法大体上跟网上流传的一样 do {    int readSize = mNetworkstre ...

  • 迅为4412开发板Qt网络编程-UDP实现服务器和客户端

    UDP 协议是开放式,无连接,不可靠的传输层通信协议,但它收发数据的速度相对于 TCP 快很多,常用在传输音视频等数据量非常大的场合.udp 网络编程只需要使用一个类 QUdpSocket. 本实验中 ...

  • 老曹眼中的网络编程基础

    我们是幸运的,因为我们拥有网络.网络是一个神奇的东西,它改变了你和我的生活方式,改变了整个世界. 然而,网络的无标度和小世界特性使得它又是复杂的,无所不在,无所不能,以致于我们无法区分甚至无法描述. ...

  • 4412开发板Qt网络编程-TCP实现服务器和客户端

    网络编程有 TCP 和 UDP,TCP 编程需要用到俩个类:QTcpServer 和 QTcpSocket. 1 TCP 实现服务器和客户端 TCP 协议(Transmission Control P ...

  • 网络编程及前端面试题!Python入门

    当我们学习Python时,需要掌握的的知识有很多,除了有关Python的专业知识外,我们还需要学习网络编程.前端等知识,对此这篇文章为大家总结一下Python常见面试题之网络编程及前端的问题. 第一: ...

  • 3月手游买量观察榜:休闲游戏霸占前三,万国觉醒、天地劫紧随其后

    截止3月19日,近30日榜单前20名里有5成左右为网赚游戏,其中<阿伟爱消消>.<阿伟弹力珠>.游戏平台233乐园买量最高,位居榜单前三名.中.重度游戏<万国觉醒> ...

  • 陆文三 | 小时的游戏活动(三)

    小时的游戏活动 (三) 敲竹梗也是伙伴们的活动之一,工具是两根一尺五寸长的竹段,粗细一般如手指那样.竹梗讲究一点的,往往会用实心的竹子,这种竹子不多,需要花时间去找,谁要是有一副实心的竹梗,谁就会得到 ...

  • 【汇总】Python网络编程框架有哪些?

    伴随着人工智能时代的到来,Python这门语言变得非常受欢迎,成为了很多开发人员的热捧,甚至还有不少小白.其他岗位就业人员想要转行学习Python,尤其是想要从事网络编程的人员,那么你知道Python ...

  • day21 网络编程(下)

    版权声明:本博客转载自路飞学城Python全栈开发培训课件,仅用于学习之用,严禁用于商业用途. 欢迎访问路飞学城官网:https://www.luffycity.com/ 课程目标:学会网络编程开发的 ...

  • 中国“黑客教父”:瘫痪日本70%的网络,三拒马云一番话获赞无数

    中国“黑客教父”:瘫痪日本70%的网络,三拒马云一番话获赞无数