编写WebSocket服务器

应用:https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#The_WebSocket_handshake

WebSocket服务器是一个应用程序,它侦听遵循特定协议的TCP服务器的任何端口,就这么简单。创建自定义服务器的任务往往会吓到人们; 但是,在您选择的平台上实现简单的WebSocket服务器很容易。

WebSocket服务器可以用任何能够使用Berkeley套接字的服务器端编程语言编写,例如C(++),Python,PHP服务器端JavaScript。这不是任何特定语言的教程,而是作为便于编写自己的服务器的指南。

本文假设您已经熟悉HTTP的工作原理,并且您具有中等水平的编程经验。根据语言支持,可能需要TCP套接字的知识。本指南的范围是介绍编写WebSocket服务器所需的最低知识。

注意:阅读最新的官方WebSockets规范RFC 6455。第1节和第4-7节对服务器实现者特别有意义。第10节讨论了安全性,在暴露服务器之前一定要仔细阅读它。

WebSocket服务器在这里解释得很低。WebSocket服务器通常是独立的专用服务器(用于负载平衡或其他实际原因),因此您经常使用反向代理(例如常规HTTP服务器)来检测WebSocket握手,预处理它们,并将这些客户端发送到一个真正的WebSocket服务器。这意味着您不必使用cookie和身份验证处理程序(例如)膨胀您的服务器代码。

WebSocket握手部分

首先,服务器必须使用标准TCP套接字侦听传入的套接字连接。根据您的平台,这可能会自动为您处理。例如,假设您的服务器正在侦听example.com,端口8000,并且您的套接字服务器响应GET请求example.com/chat

警告:服务器可以侦听它选择的任何端口,但如果它选择80或443以外的任何端口,它可能会出现防火墙和/或代理问题。浏览器通常需要WebSockets的安全连接,尽管它们可能为本地设备提供例外。

握手是WebSockets中的“Web”。它是从HTTP到WebSockets的桥梁。在握手中,协商的细节是协商的,如果条款不利,任何一方都可以在完成之前退出。服务器必须小心了解客户端要求的所有内容,否则将引入安全问题。

提示: request-uri(/chat此处)在规范中没有明确的含义。很多人巧妙地使用它让一台服务器处理多个WebSocket应用程序。例如,example.com/chat 可以调用多用户聊天应用程序,而/game在同一台服务器上则可以调用多人游戏。

客户握手请求部分

客户握手请求客户握手请求

即使您正在构建服务器,客户端仍然必须通过联系服务器并请求WebSocket连接来启动WebSocket握手过程。所以你必须知道如何解释客户的请求。该客户端将发送标题相当标准的HTTP请求,看起来像这样(HTTP版本必须是1.1或更高,并且该方法必须GET):

<span style="color:#333333"><code class="language-html">GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13</code></span>

客户可以在此处征求扩展和/或子协议; 有关详细信息,请参阅其他 此外,常见的标题一样User-AgentRefererCookie,或者认证头,可能是有作为。做任何你想要的东西; 它们与WebSocket没有直接关系。忽略它们也是安全的。在许多常见设置中,反向代理已经处理过它们。

提示:所有浏览器都会发送Origin标头。您可以使用此标题进行安全性检查(检查相同的来源,白名单/黑名单等),如果您不喜欢所看到的内容,请发送403 Forbidden。但是,请注意,非浏览器代理只能发送伪造的Origin。大多数应用程序将拒绝没有此标头的请求

如果任何标头未被理解或具有不正确的值,则服务器应发送400(“错误请求”)}响应并立即关闭套接字。像往常一样,它也可能给出HTTP响应体中握手失败的原因,但是消息可能永远不会显示(浏览器不显示它)。如果服务器不理解该版本的WebSockets,它应该发Sec-WebSocket-Version回一个包含它理解的版本的头。在上面的示例中,它表示WebSocket协议的版本13。

这里最有趣的标题是Sec-WebSocket-Key。让我们看看下一步。

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

服务器握手响应部分

服务器收到握手请求时,它应该发回一个特殊的响应,指示协议将从HTTP更改为WebSocket。该标题看起来类似于以下内容(请记住每个标题行结束\r\n\r\n在最后一个标题后添加一个额外标题以指示标题的结尾):

<span style="color:#333333"><code class="language-html">HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

</code></span>

此外,服务器可以在此决定扩展/子协议请求; 有关详细信息,请参阅其他 Sec-WebSocket-Accept标题是很重要的,服务器必须从产生它Sec-WebSocket-Key的客户端发送给它。为了得到它,c oncatenate客户端Sec-WebSocket-Key和字符串“ 258EAFA5-E914-47DA-95CA-C5AB0DC85B11”(它是一个“ 魔术字符串”),  获取结果的SHA-1哈希值,并  返回该哈希值的base64 编码。

注意:这个看似过于复杂的过程存在,因此对于客户端来说,服务器是否支持WebSockets是显而易见的。这很重要,因为如果服务器接受WebSockets连接但将数据解释为HTTP请求,则可能会出现安全问题。

因此,如果Key为“ dGhlIHNhbXBsZSBub25jZQ==”,则Sec-WebSocket-Accept标题的值为“ s3pPLMBiTxaQ9kYGzzhZRbK+xOo=”。一旦服务器发送了这些头文件,握手就完成了,您就可以开始交换数据了!

注意:Set-Cookie在发送回复握手之前,服务器可以发送其他标头,或者通过其他状态代码请求身份验证或重定向。

跟踪客户部分

这与WebSocket协议没有直接关系,但值得一提的是:您的服务器必须跟踪客户端的套接字,这样您就不会再与已经完成握手的客户握手。相同的客户端IP地址可以尝试多次连接(但是如果服务器尝试太多连接以便从拒绝服务攻击中保存自己,则服务器可以拒绝它们)。

例如,您可以保留一个用户名或ID号表以及与该WebSocket连接关联所需的相应数据和其他数据。

交换数据帧部分

客户端或服务器可以随时选择发送消息 - 这就是WebSockets的神奇之处。然而,从这些所谓的“数据帧”中提取信息是一种不那么神奇的体验。尽管所有帧都遵循相同的特定格式,但使用XOR加密(使用32位密钥)会屏蔽从客户端到服务器的数据。说明书的第5节详细描述了这一点。

格式部分

每个数据框(从客户端到服务器,反之亦然)遵循相同的格式:

<span style="color:#333333"><code class="language-html">Frame format:  
​​
      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 ...                |
     +---------------------------------------------------------------+</code></span>

MASK位只是告诉消息是否被编码。必须屏蔽来自客户端的消息,因此您的服务器应该期望它为1.(实际上,规范的第5.1节说明,如果客户端发送未屏蔽的消息,您的服务器必须与客户端断开连接。)当发送帧时对客户端,不要屏蔽它,也不要设置掩码位。我们稍后会解释掩蔽。注意:即使使用安全套接字,也必须屏蔽消息。 RSV1-3可以忽略,它们用于扩展。

操作码字段定义了如何解释有效载荷数据:0x0  用于连续,用于文本(总是以UTF-8编码),用于二进制,以及其他所谓的“控制代码”,稍后将对此进行讨论。在这个版本中的WebSockets的,要和以没有任何意义。 0x10x20x30x70xB0xF

FIN位告诉这是否是系列中的最后一条消息。如果它为0,那么服务器将继续监听消息的更多部分; 否则,服务器应该考虑传递的消息。稍后会详细介绍。

解码有效载荷长度部分

要读取有效负载数据,您必须知道何时停止读取。这就是有效载荷长度很重要的原因。不幸的是,这有点复杂。要阅读它,请按照下列步骤操作:

  1. 读取位9-15(包括)并将其解释为无符号整数。如果它是125或更小,那就是长度; 你做了。如果是126,请转到步骤2.如果是127,请转到步骤3。
  2. 读取下16位并将其解释为无符号整数。你做了
  3. 读取下一个64位并将其解释为无符号整数(最高有效位必须为0)。你做了

读取和取消屏蔽数据部分

如果设置了MASK位(并且它应该是,对于客户端到服务器的消息),读取接下来的4个八位位组(32位); 这是掩蔽键。 解码有效负载长度和屏蔽密钥后,您可以继续从套接字读取该字节数。让我们调用数据ENCODED和密钥MASK。为了得到DECODED,通过八位组循环(对于文本数据字节又名字符)ENCODED并与异或该八位位组第(i模4)个MASK的八位位组。在伪代码中(恰好是有效的JavaScript):

<span style="color:#333333"><code class="language-html">var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
    DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}</code></span>

现在,您可以根据应用程序找出DECODED的含义。

消息碎片部分

FIN和操作码字段一起工作以将消息拆分为单独的帧。这称为消息碎片。碎片是只对操作码可0x00x2

回想一下,操作码会告诉帧的意图。如果是  0x1,则有效载荷是文本。如果是  0x2,则有效载荷是二进制数据。 但是,如果它  0x0, 是一个连续帧。这意味着服务器应该将帧的有效负载连接到从该客户端接收的最后一帧。 这是一个粗略的草图,其中服务器对发送文本消息的客​​户端做出反应。第一条消息在一个帧中发送,而第二条消息在三个帧中发送。FIN和操作码详细信息仅针对客户端显示:

<span style="color:#333333"><code class="language-html">Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!</code></span>

请注意,第一帧包含整个消息(has  FIN=1 和  opcode!=0x0),因此服务器可以根据需要进行处理或响应。客户端发送的第二帧有一个文本payload(opcode=0x1),但整个消息尚未到达(FIN=0)。该消息的所有剩余部分都使用continuation frames(opcode=0x0)发送,并且消息的最后一帧标记为  FIN=1规范的第5.4节  描述了消息碎片。

Pings and Pongs:WebSockets 部分的心跳

在握手后的任何时刻,客户端或服务器都可以选择向另一方发送ping。收到ping后,收件人必须尽快发回pong。例如,您可以使用它来确保客户端仍然连接。

ping或pong只是一个常规帧,但它是一个控制帧。Pings有一个操作码  0x9,而pongs的操作码是0xA。当您获得ping时,请发送一个乒乓,其中包含与ping完全相同的有效负载数据(对于ping和pongs,最大有效负载长度为125)。你也可能在没有发送ping的情况下得到一个乒乓球; 如果它发生,请忽略它。

如果您在有机会发送乒乓球之前获得了多次ping,则只发送一个乒乓球。

关闭连接部分

要关闭连接,客户端或服务器可以发送包含指定控制序列的数据的控制帧,以开始关闭握手(详见第5.5.1节)。在接收到这样的帧时,另一个对等体响应地发送关闭帧。然后第一个对等体关闭连接。然后丢弃在关闭连接之后接收的任何其他数据。 

其他

WebSocket代码,扩展,子协议等在IANA WebSocket协议注册表中注册

WebSocket扩展和子协议在握手期间通过标头协商。有时扩展和子协议看起来太相似而不是不同的东西,但有一个明显的区别。扩展控制WebSocket 框架修改有效负载,而子协议构造WebSocket 有效负载从不修改任何内容。扩展是可选的和通用的(如压缩); 子协议是强制性的和本地化的(如用于聊天和MMORPG游戏的子协议)。

扩展部分

本节需要扩展。如果你有能力,请编辑。

将扩展名视为压缩文件,然后通过电子邮件发送给某人。无论你做什么,你都会以不同的形式发送相同的数据。收件人最终将能够获得与本地副本相同的数据,但发送方式不同。这就是扩展的作用。WebSockets定义了一种协议和一种发送数据的简单方法,但是诸如压缩之类的扩展可以允许以较短的格式发送相同的数据。

扩展在规范的5.8,9,11.3.2和11.4节中解释。

去做

子协议部分

将子协议视为自定义XML架构doctype声明。您仍在使用XML及其语法,但您还受到您同意的结构的限制。WebSocket子协议就是这样的。他们不介绍任何花哨的东西,他们只是建立结构。像文档类型或模式一样,双方必须就子协议达成一致; 与doctype或schema不同,子协议在服务器上实现,不能由客户端外部引用。

子规程在规范的1.9,4.2,11.3.4和11.5节中解释。

客户必须要求特定的子协议。为此,它将发送这样的内容作为原始握手的一部分

<span style="color:#333333"><code class="language-html">GET /chat HTTP/1.1
...
Sec-WebSocket-Protocol: soap, wamp</code></span>

或者,等效地:

<span style="color:#333333"><code class="language-html">...
Sec-WebSocket-Protocol: soap
Sec-WebSocket-Protocol: wamp</code></span>

现在,服务器必须选择客户端建议并支持的协议之一。如果有多个,请发送客户端发送的第一个。想象一下,我们的服务器可以使用soapwamp。然后,在响应握手中,它将发送:

<span style="color:#333333"><code class="language-html">Sec-WebSocket-Protocol: soap</code></span>

服务器无法发送多个Sec-Websocket-Protocol标头。
如果服务器不想使用ny子协议,则不应发送任何Sec-WebSocket-Protocol标头。发送空白标题不正确。
如果客户端没有获得它想要的子协议,则客户端可以关闭连接。

如果您希望服务器遵守某些子协议,那么您自然需要在服务器上使用额外的代码。我们假设我们正在使用子协议json。在此子协议中,所有数据都以JSON格式传递。如果客户端请求此协议并且服务器想要使用它,则服务器将需要具有JSON解析器。实际上,这将是库的一部分,但服务器需要传递数据。

提示:为避免名称冲突,建议将子协议名称作为域字符串的一部分。如果您要构建使用Example Inc.独有的专有格式的自定义聊天应用程序,那么您可以使用:  Sec-WebSocket-Protocol: chat.example.com。请注意,这不是必需的,它只是一个可选的约定,您可以使用任何您想要的字符串。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值