我们知道WebSocket为了兼容HTTP协议,是在HTTP协议的基础之上进行升级得到的。在客户端和服务器端建立HTTP连接之后,客户端会向服务器端发送一个升级到webSocket的协议。首先,WebSocket连接必须由浏览器发起,我们来看看WebSocket连接是如何创建的。
在 JavaScript 中,WebSocket 构造函数有两种重载形式:
var socket = new WebSocket('ws://example.com');
var socket = new WebSocket('ws://example.com', 'chat');
在以上两种形式中,url 参数表示要连接的 WebSocket 服务器的 URL 地址。protocols 参数是可选的,用于指定子协议。如果指定了子协议,服务器会根据客户端请求的子协议选择是否接受连接。 需要注意的是,浏览器支持的子协议数量和类型可能有限制,具体取决于浏览器的实现。
请求协议是一个标准的HTTP请求,格式如下:
GET ws://localhost:3000/ws/chat HTTP/1.1
Host: localhost
Upgrade: websocket
Connection: Upgrade
Origin: http://localhost:3000
Sec-WebSocket-Key: client-random-string
Sec-WebSocket-Version: 13
ps:Websocket链接: 首先浏览器与服务器建立了TCP链接,在浏览器与服务器建立链接的时候是不发送数据的,只是发送三次握手所需要的协议头数据,不带有负载数据。浏览器要在第一次发送数据时候要发送上文格式的数据。
注意,这里的HTTP版本必须是1.1以上。HTTP的请求方法必须是GET
通过设置Upgrade和Connection这两个header,表示我们准备升级到webSocket了。除了这里列的属性之外,其他的HTTP自带的header属性都是可以接受的。
这里还有两个比较特别的header,他们是Sec-WebSocket-Version和Sec-WebSocket-Key。
先看一下Sec-WebSocket-Version, 它表示的是客户端请求的WebSocket的版本号。如果服务器端并不明白客户端发送的请求,则会返回一个400 ("Bad Request"),在这个返回中,服务器端会返回失败的信息。
如果是不懂客户端发送的Sec-WebSocket-Version,服务器端同样会将Sec-WebSocket-Version返回,以告知客户端。
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
首先, Sec-WebSocket-Key 是一个 Base64 encode 的值,这个是浏览器随机生成的
然后, Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~
最后, Sec-WebSocket-Version 是告诉服务器所使用的 Websocket Draft (协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的
该请求和普通的HTTP请求有几点不同:
随后,服务器如果接受该请求,就会返回如下响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: server-random-string
Sec-WebSocket-Protocol: chat
该响应代码101表示本次连接的HTTP协议即将被更改,更改后的协议就是Upgrade: websocket指定的WebSocket协议。
这里的Sec-WebSocket-Accept是根据客户端请求中的Sec-WebSocket-Key来生成的。具体而言是将客户端发送的Sec-WebSocket-Key 和 字符串"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 进行连接。然后使用SHA1算法求得其hash值。
最后将hash值进行base64编码即可。
当服务器端返回Sec-WebSocket-Accept之后,客户端可以对其进行校验,已完成整个握手过程。
之所以要使用webSocket是因为client和server可以随时随地发送消息。这是websocket的神奇所在。那么发送的消息是什么格式的呢?我们来详细看一下。
client和server端进行沟通的消息是以一个个的frame的形式来传输的。frame的格式如下:
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 ... |
+---------------------------------------------------------------+
针对前面的格式概览图,这里逐个字段进行讲解,如有不清楚之处,可参考协议规范,或留言交流。
FIN:1个比特。
如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是是消息(message)的最后一个分片(fragment)。
RSV1, RSV2, RSV3:各占1个比特。
一般情况下全为0。当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用WebSocket扩展,连接出错。
Opcode: 4个比特。
操作代码,Opcode的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection)。可选的操作代码如下:
%x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
%x1:表示这是一个文本帧(frame)
%x2:表示这是一个二进制帧(frame)
%x3-7:保留的操作代码,用于后续定义的非控制帧。
%x8:表示连接断开。
%x9:表示这是一个ping操作。
%xA:表示这是一个pong操作。
%xB-F:保留的操作代码,用于后续定义的控制帧。
Mask: 1个比特。
表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。
如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。
如果Mask是1,那么在Masking-key中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask都是1。
掩码的算法、用途在下一小节讲解。
Payload length:
数据载荷的长度,单位是字节。为7位,或7+16位,或1+64位。
假设数Payload length === x,如果
x为0~126:数据的长度为x字节。
x为126:后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。
x为127:后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。
此外,如果payload length占用了多个字节的话,payload length的二进制表达采用网络序(big endian,重要的位在前)。
Masking-key:0或4字节(32位)
所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。
如何掩码:
服务器解码:基于异或操作的特性,结果与原始操作数进行XOR操作将得到另一个原始操作数。所以解码与掩码的执行过程完全相同。
下面是一个C#的解码过程,同样适用于掩码
public static byte[] DecodePayload(byte[] payload, byte[] mask)
{
byte[] decodedPayload = new byte[payload.Length];
for (int i = 0; i < payload.Length; i++)
{
decodedPayload[i] = (byte)(payload[i] ^ mask[i % 4]);
}
return decodedPayload;
}
备注:载荷数据的长度,不包括mask key的长度。
Payload data:(x+y) 字节
载荷数据:包括了扩展数据、应用数据。其中,扩展数据x字节,应用数据y字节。
扩展数据:如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。
应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应用数据的长度。
服务器在解码WebSocket数据时,需要按照WebSocket协议的规定进行解码操作。
WebSocket数据帧的解码过程如下:
在C#中对 payload解析案例如下
/// <summary>
/// 读取数据,websocket也需要处理分包和粘包
/// </summary>
/// <param name="buffer">socket缓冲区</param>
/// <param name="offset">偏移量</param>
/// <param name="count">数据包可用长度</param>
/// <returns>处理是否成功</returns>
public override bool ProcessReceive(byte[] buffer, int offset, int count)
{
this.ActiveDateTime = DateTime.Now;
try
{
//第一个字节最低四位 用来表示 消息类型的,不同的语言,这四位的值可能不一样
//&00001111,取出第一个字节后面四位
//websocket 第一个字节后面四位决定了应该如何解析后续的数据载荷(data payload)
/*
%x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
%x1:表示这是一个文本帧(frame)
%x2:表示这是一个二进制帧(frame)
%x3-7:保留的操作代码,用于后续定义的非控制帧。
%x8:表示连接断开。
%x9:表示这是一个ping操作。
%xA:表示这是一个pong操作。
%xB-F:保留的操作代码,用于后续定义的控制帧。
*/
var pac_type = (buffer[0] & 0xF); //得到消息类型
if (pac_type == 1) //websocket文本协议
{
string resqust = string.Empty;
bool result = AnalyticData(buffer, count, out resqust);
if (!result) return false;
//收到消息调用相关委托(通知客户端管理程序)
_asyncClientManager.On_ReceiveHandle(this.AsyncSocketUserToken, new ReceiveEventArgs()
{
Message = resqust,
EndPrint = AsyncSocketUserToken.ConnectSocket.RemoteEndPoint.AddressFamily.ToString()
});
//暂时把消息返回就好
{
_outgoingDataAssembler.Clear();
_outgoingDataAssembler.AddMessage(resqust);
DoSendResult();
}
//交给命令解析组件解析,解析完毕交给业务组件,最后业务组件应答客户端
//string command = AnalyzeCommon(resqust);
//if (!string.IsNullOrWhiteSpace(command))
//{
// TelnetClientManager.SendData(AsyncSocketUserToken.ConnectSocket, this.Encode.GetBytes(command));
//}
}
else if (pac_type == 2)
{
//消息的数据类型为二进制的
}
else if (pac_type == 7)//http协议通过GET访问
{
string packetStr = Encode.GetString(buffer);
if (Regex.Match(packetStr.ToLower(), "upgrade: websocket").Value != "")//websocket握手协议
{
string endHandShake = GetSecKeyAccetp(packetStr, count);
byte[] data = PackHandShakeData(endHandShake); //握手,打包握手信息
_asyncClientManager.SendAsyncEvent(this.AsyncSocketUserToken, data, 0, data.Length); //应答握手包
_asyncClientManager.On_ReceiveHandle(this.AsyncSocketUserToken, new ReceiveEventArgs()
{
Message = "握手消息",
EndPrint = AsyncSocketUserToken.ConnectSocket.RemoteEndPoint.AddressFamily.ToString()
});
return true;
}
else//其他http的GET请求
{
return false;
}
}
else if (pac_type == 8)//websocket退出协议
{
_asyncClientManager.On_ReceiveHandle(this.AsyncSocketUserToken, new ReceiveEventArgs()
{
Message = "断开连接",
EndPrint = AsyncSocketUserToken.ConnectSocket.RemoteEndPoint.AddressFamily.ToString()
});
_asyncClientManager.CloseClientSocket(AsyncSocketUserToken);
return false;
}
else if (pac_type == 9)//firefox固有的websocket PING协议 需应答任意字符,否则浏览器会自动下发断开连接
{
_asyncClientManager.SendAsyncEvent(this.AsyncSocketUserToken, this.Encode.GetBytes("1"), 0, 1);
}
else if (pac_type == 10)//IE10以上固有的websocket PONG协议
{
return true;
}
else if (pac_type == 12)
{
if (buffer.Length == 23)//flash请求策略文件(特有:flash建立socket前需策略文件)
{
byte[] data = this.Encode.GetBytes("<cross-domain-policy><allow-access-from domain=\"*\" to-ports=\"*\" /></cross-domain-policy>\0");
_asyncClientManager.SendAsyncEvent(AsyncSocketUserToken, data, 0, data.Length);
}
}
return true;
}
catch (Exception ex)
{
throw;
}
}
/// <summary>
/// 解析客户端数据包
/// </summary>
/// <param name="recBytes">服务器接收的数据包</param>
/// <param name="recByteLength">有效数据长度</param>
/// <returns></returns>
public bool AnalyticData(byte[] recBytes, int recByteLength, out string dataMessage)
{
dataMessage = string.Empty;
DynamicBufferManager receiveBuffer = AsyncSocketUserToken.ReceiveBuffer;
if (recByteLength < 2) { return true; } //websocket 协议 头部最少两个字节来确定消息
//第一个字节: 最高位如果是1表示该消息,是消息的尾部,如果是0表示后续还有数据包
//最低四位 用来表示 消息类型的,不同的语言,这四位的值可能不一样
bool fin = (recBytes[0] & 0x80) == 0x80; // 1bit,1表示最后一帧 &10000000 ,取出第一位
if (!fin)
{
return true;// 超过一帧暂不处理
}
//第二个字节: 最高位 0/1表示 后面是否跟着4个字节的掩码
//剩下的7位如果值<=125 表示 后面的数据大小 就是该七位的大小
//剩下的7位如果值=126 表示后面跟着2个字节的,这两个字节 用来存数据的大小字节
//剩下的7位如果值=127 表示后面跟着8个字节的,这八个字节 用来存数据的大小字节
bool mask_flag = (recBytes[1] & 0x80) == 0x80; // 是否包含掩码
if (!mask_flag)
{
return false;// 不包含掩码的暂不处理
}
int payload_len = recBytes[1] & 0x7F; // 数据长度 &0111 1111,取出剩余7位的值进行计算
byte[] masks = new byte[4];
byte[] payload_data;
if (payload_len == 126) //剩下的7位如果值 = 126 表示后面跟着2个字节的,这两个字节 用来存数据的大小字节
{
Array.Copy(recBytes, 4, masks, 0, 4);
payload_len = (UInt16)(recBytes[2] << 8 | recBytes[3]);
payload_data = new byte[payload_len];
Array.Copy(recBytes, 8, payload_data, 0, payload_len);
}
else if (payload_len == 127) //剩下的7位如果值=127 表示后面跟着8个字节的,这八个字节 用来存数据的大小字节
{
Array.Copy(recBytes, 10, masks, 0, 4);
byte[] uInt64Bytes = new byte[8];
for (int i = 0; i < 8; i++)
{
uInt64Bytes[i] = recBytes[9 - i];
}
UInt64 len = BitConverter.ToUInt64(uInt64Bytes, 0);
payload_data = new byte[len];
for (UInt64 i = 0; i < len; i++)
{
payload_data[i] = recBytes[i + 14];
}
}
else //剩下的7位如果值 <= 125 表示 后面的数据大小 就是该七位的大小
{
Array.Copy(recBytes, 2, masks, 0, 4);
payload_data = new byte[payload_len];
Array.Copy(recBytes, 6, payload_data, 0, payload_len);
}
for (var i = 0; i < payload_len; i++)
{
payload_data[i] = (byte)(payload_data[i] ^ masks[i % 4]); //进行反掩码
}
receiveBuffer.WriteBuffer(payload_data, 0, payload_len); //写入如数据
if (fin)
{
if ((receiveBuffer.DataCount > 10 * 1024 * 1024)) //最大Buffer异常保护
{
return false;
}
byte[] data = new byte[receiveBuffer.DataCount];
Array.Copy(receiveBuffer.Buffer, 0, data, 0, receiveBuffer.DataCount);
receiveBuffer.Clear(receiveBuffer.DataCount); //从缓存中清理
//最后一帧,
dataMessage = Encoding.UTF8.GetString(data);
}
return true;
}
FIN和Opcode 组合起来正好一个字节的长度。
FIN可以和opcode一起配合使用,用来发送长消息。
FIN=1表示,是最后一个消息。 0x1表示是text消息,0x2是0,表示是二净值消息,0x0表示消息还没有结束,所以0x0通常和FIN=0 一起使用。
上文说到,服务器发送数据给socket client时候不需要掩码,因此发送数据封包则简单的多。下面是服务器发送数据的封包处理
/// <summary>
/// 打包服务器数据(应答数据打包)
/// </summary>
/// <param name="message">数据</param>
/// <returns>数据包</returns>
private static byte[] PackData(string message)
{
/*
* 第一个字节可以是 10000001 FIN,RSV1, RSV2, RSV3,Opcode(4bit)
* 0x81=10000001 websocket协议的数据头, FIN=1 表示这是消息(message)的最后一个分片, 最后1是opcode位
%x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
%x1:表示这是一个文本帧(frame)
%x2:表示这是一个二进制帧(frame)
%x3-7:保留的操作代码,用于后续定义的非控制帧。
%x8:表示连接断开。
%x9:表示这是一个ping操作。
%xA:表示这是一个pong操作。
%xB-F:保留的操作代码,用于后续定义的控制帧。
*第二个字节 Mask+Payload len Mask: 0/1 表示是否包含掩码,Payload len: 7位,表示数据长度,如果值<=125 表示 后面的数据大小 就是该七位的大小 if (payload_len <= 125) emp.Length<126
*第三个字节第四个字节 如果第二个字节的值=126 表示后面跟着2个字节的,这两个字节 用来存数据的大小字节 if (payload_len == 126) temp.Length<=Max(int32)
*第如果第二个字节的值=127 表示后面跟着8个字节的,这八个字节 用来存数据的大小字节 if (payload_len == 127) emp.Length>Max(int32)
服务器发送的数据帧不需要掩码,所以第二个字节的值应该是 0 + 数据长度 01111111
*/
byte[] contentBytes = null;
byte[] temp = Encoding.UTF8.GetBytes(message);
if (temp.Length < 126)
{
contentBytes = new byte[temp.Length + 2];
contentBytes[0] = 0x81;
contentBytes[1] = (byte)temp.Length;
Array.Copy(temp, 0, contentBytes, 2, temp.Length);
}
else if (temp.Length < 0xFFFF)
{
contentBytes = new byte[temp.Length + 4];
contentBytes[0] = 0x81;
contentBytes[1] = 126;
contentBytes[2] = (byte)(temp.Length >> 8);
contentBytes[3] = (byte)(temp.Length & 0xFF);
Array.Copy(temp, 0, contentBytes, 4, temp.Length);
}
else
{
// 超长内容
contentBytes = new byte[temp.Length + 10];
contentBytes[0] = 0x81;
contentBytes[1] = 127;
contentBytes[2] = (byte)((temp.Length >> 56) & 0xFF);
contentBytes[3] = (byte)((temp.Length >> 48) & 0xFF);
contentBytes[4] = (byte)((temp.Length >> 40) & 0xFF);
contentBytes[5] = (byte)((temp.Length >> 32) & 0xFF);
contentBytes[6] = (byte)((temp.Length >> 24) & 0xFF);
contentBytes[7] = (byte)((temp.Length >> 16) & 0xFF);
contentBytes[8] = (byte)((temp.Length >> 8) & 0xFF);
contentBytes[9] = (byte)(temp.Length & 0xFF);
Array.Copy(temp, 0, contentBytes, 10, temp.Length);
}
return contentBytes;
}
在客户端和服务器端进行握手的过程中,在标准的websocket协议基础之上,客户端还可以发送Extensions或者Subprotocols。这两个有什么区别呢?
首先这两个都是通过HTTP头来设置的。但是两者还是有很大的不同。Extensions可以对WebSocket进行控制,并且修改payload,而subprotocols只是定义了payload的结构,并不会对其进行修改。
Extensions是可选的,而Subprotocols是必须的。
你可以将Extensions看做是数据压缩,它是在webSocket的基础之上,对数据进行压缩或者优化操作,可以让发送的消息更短。
而Subprotocols 表示的是消息的格式,比如使用soap或者wamp。
子协议是在WebSocket协议基础上发展出来的协议,主要用于具体的场景的处理,它是是在WebSocket协议之上,建立的更加严格的规范。
比如,客户端请求服务器时候,会将对应的协议放在Sec-WebSocket-Protocol头中:
GET /socket HTTP/1.1
...
Sec-WebSocket-Protocol: soap, wamp
服务器端会根据支持的类型,做对应的返回,如:
Sec-WebSocket-Protocol: soap
本文链接:https://blog.nnwk.net/article/129
有问题请留言。版权所有,转载请在显眼位置处保留文章出处,并留下原文连接
Leave your question and I'll get back to you as soon as I see it. All rights reserved. Please keep the source and links
友情链接:
子卿全栈
全部评论