WebSocket 二、编写WebSocket服务器端

原文链接
WebSocket 协议:RFC 6455。或 rfc6455
SegmentFault:websocket 协议解析 [RFC6455]

WebSocket服务器就是一个承载TCP应用程序的主机,此TCP应用程序可用于监听遵循特定协议的服务器上的任何端口。
WebSocket服务可以用任何支持Berkeley套接字的服务器端编程语言编写,如C(++)或Python,甚至PHP服务器端JavaScript

本文需要了解的基础知识:
1,HTTP如何工作的。
2,TCP套接字。(socket)

阅读最新的官方WebSockets规范RFC 6455.第1节和第4-7节对于服务器实现者特别有意思。 第10节讨论安全性,你应该在暴露你的服务器之前仔细阅读它。

WebSocket服务器在本文只是做简单解释。 WebSocket服务器通常是独立的专用服务器(出于负载平衡或其他实际原因),所以您通常会使用反向代理(例如常规HTTP服务器)来检测WebSocket握手,预处理它们并将这些客户端发送给 一个真正的WebSocket服务器。 这意味着您不必使用cookie和身份验证处理程序来扩充服务器代码。。自动翻译看着别扭。

WebSocket 握手

首先,服务器必须要监听从客户端以标准TCP socket发送过来的socket连接。根据主机的平台,这可能已经为你处理。例如,假设你的服务器正在侦听example.com,端口8000,并且您的套接字服务器响应/ chat上的GET请求。

警告:服务器可能会监听它选择的任何端口,但是如果它选择了80或443以外的端口,它可能在防火墙和/或代理服务器上有问题。 端口443上的连接往往会更频繁,但是当然,这需要一个安全的连接(TLS / SSL)。 另外请注意,大多数浏览器(特别是Firefox 8+)不允许从安全页面连接到不安全的WebSocket服务器。

握手是WebSockets中的“Web”(WebSocket=Web+Socket,Socket连接需要握手,WebSocket握手的是Web)。 这是从HTTP到WS的桥梁(通过握手,将协议从http升级为TCP)。 在握手过程中,客户端与服务器端经过协商的,如果有是问题,任何一方都可以在完成前退出。 服务器必须小心了解客户端要求的所有内容,否则会引入安全问题。

1. 客户端握手请求
虽然是要构建WebSocket服务器, 客户端仍然必须启动WebSocket握手过程,所以在服务器端 必须要解析客户端的请求。
客户端会发送一个标准的Http请求 (HTTP 版本必须不低于1.1,http方法必须是GET):

Request URL:ws://www.example.com:8000/chat
Request Method:GET
Status Code:101 Switching Protocols

Host:example.com:8000
Connection:Upgrade
Upgrade:websocket
Sec-WebSocket-Key:jZnFsvHUJVQfW69xu3qQYA==
Sec-WebSocket-Version:13

客户端可以在这里请求扩展和/或子协议,查看详情。header里也会包含常见的User-Agent,Referer,Cookie或验证标头。 可以根据这些header做任何处理(比如做权限判断…); 它们并不直接与WebSocket相关。 忽视它们也是安全的。 在许多常见的设置中,反向代理已经处理了它们。

如果请求头有错误、或服务器端不能识别,服务器会发送一个“400错误的请求”,并立即关闭套接字,通常,会再HTTP Response的body中会携带握手失败的原因,但并不会显示(浏览器不会显示)。如果服务器不识别WebSockets的版本,服务器会返回一个header:Sec-WebSocket-Version 携带服务器能识别的版本,对应本文,如果请求的是v12,服务器不识别,会返回能识别的版本V13。现在,我们来看看最奇怪的标题Sec-WebSocket-Key。

Tip1: 所有的浏览器请求默认都会带上 Origin 头信息。 可以用此Header做安全性验证 (检查是否同源(域);黑/白名单, etc.) ,如果不允许此请求可返回403拒绝访问的代码。 不过要注意的是能发送request请求的不止有浏览器,还可能是其他代理,他们都能伪造一个Origin。 大多数应用程序会拒绝没有此header的请求。

Tip2: request-uri (本文对应/chat) 在规范中没有定义的含义. 很多人巧妙地使用它来让一个服务器处理多个WebSocket应用程序。比如: example.com/chat 用作多人聊天的应用,在同一台服务器主机上的话 example.com/game 用作多人游戏的应用。

Note: 常规的 HTTP 状态码只能在握手之前使用。握手成功后,必须使用不同的状态码 (在规范的第7.4节中定义)。

2.服务器端 握手响应
服务器接收到上面的请求后, 服务器应该发送一个看起来像这样的非常奇怪的(但仍然是HTTP)响应 (记住:每个 header 都是以 \r\n 结尾,最后一个Header结尾也会追加 \r\n ):

Request URL:ws://www.example.com:8000/chat
Request Method:GET
Status Code:101 Switching Protocols

Connection:Upgrade
Sec-WebSocket-Accept:hl41mB6NkGreZYMvgElDtZ9KpTQ=
Upgrade:WebSocket

另外,服务器可以在这里决定扩展/子协议请求; 详情参见 MiscellaneousSec-WebSocket-Accept 的值,必须是由服务器从 客户端 请求中的Header Sec-WebSocket-Key 和值计算产生,计算过程(rfc6455第1.3节):

  1. Sec-WebSocket-Accept 的值 拼接 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 (一个魔字符串),得出jZnFsvHUJVQfW69xu3qQYA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
  2. 计算jZnFsvHUJVQfW69xu3qQYA==258EAFA5-E914-47DA-95CA-C5AB0DC85B11SHA-1 hash 值。
  3. 将2中的hash值base64-encoded编码得出结果:hl41mB6NkGreZYMvgElDtZ9KpTQ=并返回。

FYI:这个看起来很复杂的过程存在,所以客户端很明显服务器是否支持WebSocket。这很重要,因为如果服务器接受WebSockets连接但将数据解释为HTTP请求,则可能会出现安全问题。 (不太懂。这样做会用在什么地方?rfc6455最后有必要从头到尾看一遍)

经过以上步骤后,连接就建立起来了,就可以数据交互了。

tips:在服务器响应握手之前,可用发送其他header,像 Set-Cookie 验证权限、重定向以及其他状态码。

3.维护客户端连接
这与WebSocket协议无关,单有必要一提:服务器有必要跟踪客户的套接字,以避免多次与已完成握手的客户端再次握手。 同一个客户端IP地址可以尝试连接多次,如果服务器不维护客户端的连接列表,可能会遭到 拒绝服务攻击12

数据帧交换

web客户端和服务器端可以随时相互发送消息–这就是WebSocket的方便之处,但是提取消息的过程就比较麻烦了。尽管所有帧都遵循相同的特定格式,但从客户端到服务器的数据都是使用异或加密(XOR)的(使用32位密钥),详情在rfc6455 第5节

1 .数据帧格式
每个数据帧格式 (从Client<==>Server) 如下:

帧格式:  
​​
      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 ...                |
     +---------------------------------------------------------------+

每个数据帧供32位(4字节)

名称长度注释
FIN1bit标明这一帧是否是整个消息体的最后一帧,0则接续listen更多的部分
RSV1 RSV2 RSV31bit保留位,必须为0,如果不为0,则标记为连接失败
opcode4bit操作位,定义这一帧的类型
Mask1bit标明承载的内容是否需要用掩码进行异或
Masking-key0 or 4bytes掩码异或运算用的key
Payload length7bit or 7 +16bit or 7 + 64bit承载体的长度(后续会解释为什么会有3种长度)

1. 第4-7位(opcode)操作码
在websocket中,我们定义了几种操作类型,也就是表明了数据包的行为,数据包大体可分为两种,一种是字符数据包 (string),一种是字节数据包 (byte) 不同的数据包使用不同的opcode来传输,opcode定义如下:

操作码说明
%x0标明这一个数据包是上一个数据包的延续,它是一个延长帧 (continuation frame)
%x1标明这个数据包是一个字符帧 (text frame)
%x2标明这个数据包是一个字节帧 (binary frame)
%x3-7保留值,供未来的非控制帧使用
%x8标明这个数据包是用来告诉对方,我方需要关闭连接
%x9标明这个数据包是一个心跳请求 (ping)
%xA标明这个数据包是一个心跳响应 (pong)
%xB-F保留至,供未来的控制帧使用

2. 第8位(MASK )表明是否被编码,也即:承载的内容是否需要用掩码进行异或 提取数据。客户端到服务器的消息必须要加密,所以服务器期望值应该是1(实际上,规范的第5.1节指出,如果客户端发送了未使用掩码(MASK)的消息,则服务器必须与客户端断开连接)。
将帧发送回客户端时,请勿mask it,也不要设置mask位。 我们稍后会解释masking。 注意:即使使用安全套接字,您也必须mask消息。RSV1-3可以忽略,它们用于扩展。(不明所以)
3. 解码Payload长度
Payload Length位占用了可选的7bit或者7 + 16bit 或者 7 + 64bit。
读取Payload数据,必须要指定读到那里为止。因此获知负载数据长度很重要。这个过程稍微有点复杂,要以下这些步骤:

 1.  读取9-15位 (包括9和15位本身),并转换为无符号整数。如果值小于或等于125,这个值就是长度;如果是 126,请转到步骤 2。如果它是 127,请转到步骤 3。
 2. 读取接下来的 16 位并转换为无符号整数,并作为长度。
 3. 读取接下来的 64 位并转换为无符号整数,并作为长度(**最高有效位必须为0**)。

4. 读取并 Unmasking Data。
我们需要使用掩码对payload的每一个字节进行异运算(解码),客户端到服务器掩码 rfc6455 5.3节
八位位组:即字节
变换数据的八位位组i (”transformed-octet-i”)是原始数据的八位位组i(”original-octet-i”)异或(XOR)i取模4位置的 掩码键的八位位组(”masking-key-octet-j”):
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j

/// <summary>
/// 根据payloadData、mask 解码
/// </summary>
/// <param name="payloadData">payloadData:字节数组。承载的数据</param>
/// <param name="mask">掩码 4字节:字节数组。掩码</param>
/// <returns></returns>
private byte[] GetDecodedPayloadData(byte[] payloadData, byte[] mask)
{
    byte[] DecodedPayloadData = new byte[payloadData.Length];
    for (var i = 0; i < payloadData.Length; i++)
    {
        DecodedPayloadData[i] = (byte)(payloadData[i] ^ mask[i % 4]);
    }
    return DecodedPayloadData;
}

可通过Encoding.UTF8.GetString(DecodedPayloadData); 取得请求中的数据。
5. 报文段
FIN和opcode字段一起工作,将消息分成不同的帧。 这被称为报文段。 报文段仅在操作码0x0至0x2上可用。

翻译困难:先看RFC6455规范中文版,之后再上传一个栗子,对照着规范说明解析。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值