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

WebSocket服务器可以用任何实现了Berkeley sockets的服务器端编程语言编写,如C(++)或Python甚至PHP服务器端JavaScript。 这不是任何特定语言的教程,而是作为指导,以方便编写自己的服务器。

您需要知道HTTP的工作原理,并具有中级编程经验。 根据语言帮助(Depending on language support),可能需要TCP套接字的知识。 本指南的范围是介绍编写WebSocket服务器所需的最低知识。

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

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

WebSocket 握手

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

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

握手是WebSockets中的“Web”。 这是从HTTP到WS的桥梁。 在握手过程中,有关连接的详细信息正在初始化中,如果条件不利,任何一方可以在完成之前退出。 服务器必须小心了解客户要求的一切,否则会产生安全问题。

客户端握手请求

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

GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

客户可以在这里请求扩展和/或子协议;详情请见杂项。当然,你也可以在这里加上你所需要的一般请求头如User-Agent, Referer, Cookie或者认证头。WebSocket没有作要求,忽略它们也是安全的。在大多数情况下,反向代理已经做了这些处理。

如果任何请求头信息不被理解或者具有不正确的值,则服务器应该发送“400 Bad Request”并立即关闭套接字。 像往常一样,它也可能会给出HTTP响应正文中握手失败的原因,但可能永远不会显示消息(浏览器不显示它)。 如果服务器不理解该版本的WebSocket,则应该发送一个Sec-WebSocket-Version头,其中包含它理解的版本。 (本指南解释了最新的v13)。 下面我们来看看奇妙的请求头Sec-WebSocket-Key

提示: 所有浏览器将会发送一个 Origin请求头。 你可以将这个请求头用于安全方面(检查是否是同一个域,白名单/ 黑名单等),如果你不喜欢这个请求发起源,你可以发送一个403 Forbidden。需要注意的是非浏览器只能发送一个模拟的 Origin。大多数应用会拒绝不含这个请求头的请求.。

提示: 请求URI(这里的是/chat)在规范里没有定义。很多开发者聪明地把这点用于控制多功能WebSocket应用。例如example.com/chat会请求一个多方会话应用,而在相同服务器上example.com/game则会请求一个多玩家游戏应用。

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

服务器握手响应

当它收到这个请求时,服务器应该会一个看上去非常奇怪的(但仍然是HTTP)响应,看起来像这样(记住每个请求头以 \r\n结尾,并在最后一个之后放置一个额外的 \r\n):

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

另外,服务器可以在这时候决定插件或子协议,详情参见杂项。  Sec-WebSocket-Accept 参数很有趣,它需要服务器通过客户端发送的Sec-WebSocket-Key 计算出来。 怎样计算呢, 把客户发送的 Sec-WebSocket-Key 和 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (这个叫做 "魔法值")连接起来,把结果用SHA-1编码,再用base64编码一次,就可以了。 

参考:这看起来繁复的处理使得客户端明确服务端是否支持WebSocket。这是十分重要的,如果服务端接收到一个WebSocket连接但是把数据作为HTTP请求理解可能会导致安全问题。

所以如果Sec-WebSocket-Key是“dGhlIHNhbXBsZSBub25jZQ==”,Sec-WebSocket-Accept将是“s3pPLMBiTxaQ9kYGzzhZRbK+xOo=”。 一旦服务器发送这个请求头,握手就完成了,你可以开始交换数据!

服务端可以在发送握手回复前发送其他请求头,诸如Set-Cookie,请求认证或通过状态码重定向。

跟踪客户端

这并不直接与WebSocket协议相关,但是在这里值得一提的是:你的服务器将不得不跟踪客户的套接字,所以你不会再和已经完成握手的客户握手。 同一个客户端IP地址可以尝试连接多次(但是如果客户端尝试过多的连接,服务器可以拒绝它们以免遭拒绝服务攻击)。

交换数据帧

客户端或服务端都可以在任何时间点发送数据——这就是WebSocket的魅力。然而,从这些被称为“帧”的数据中提取信息就不是十分愉快的体验了。尽管所有的帧都遵从相同的格式规范,从客户端发送到服务端的数据都被 异或加密(用一个32位的key)格式化。详情请参见规范的第5节。

格式

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

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 ...                |
     +---------------------------------------------------------------+

掩码明确告知我们消息是否经过格式化。从客户端来的消息必须经过格式化,所以你的服务器必须要求这个掩码是1(事实上,规范5.1节规定了如果客户端发送了没有格式化的消息,你的服务器应该断开连接)

当向客户端发送帧时,不要对其进行掩码,也不要设置掩码位。稍后我们将解释屏蔽。注意:即使使用安全套接字,也必须屏蔽消息。RSV1-3可以忽略,它们是用于扩展的。

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

FIN位告诉我们这是不是系列的最后一条消息。如果是0,那么服务器将继续侦听消息的更多部分;否则,服务器应该考虑传递的消息。不仅仅是这样。

解码有效载荷长度

要读取有效负载数据,您必须知道何时停止读取。这就是为什么有效载荷长度很重要。不幸的是,这有点复杂。要阅读它,请遵循以下步骤:

  1. 读取9-15(包括)位并将其解析为无符号整型。如果长度小于等于125,那么就是长度;你就完成了。如果是126,到第二步。如果是127,到步骤3。
  2. 读取下面的16位,并将其解释为无符号整型。你就完成了。
  3. 读取接下来的64位,并将其解释为无符号整型(最重要的位必须为0)。

读取和解密数据

如果设置了掩码位(对于客户机到服务器的消息应该是这样),则读取接下来的4个字节(32位);这是掩蔽键。一旦有效负载长度和掩蔽键被解码,您就可以继续从套接字读取字节数。让我们调用已编码的数据和密钥掩码。要获得解码,可以通过编码的八位元(字节,即文本数据的字符)和XOR八位元(i模4)掩码的第四个八位元进行循环。在伪代码中(恰好是有效的JavaScript):

var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
    DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}

现在,您可以根据应用程序了解解码意味着什么。

消息帧

FIN和操作码字段一起工作,以发送分裂为独立帧的消息。这称为消息碎片。片段只能在操作码0x0到0x2上可用。

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

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!

注意,第一个框架包含一个完整的消息(具有FIN=1和opcode!=0x0),因此服务器可以根据需要进行处理或响应。客户机发送的第二帧具有文本有效负载(opcode=0x1),但是整个消息还没有到达(FIN=0)。该消息的所有剩余部分都用延续帧(opcode=0x0)发送,消息的最终帧用FIN=1标记。Section 5.4 of the spec描述了消息帧。

Pings和Pongs:WebSockets的心跳

在经过握手之后的任意时刻里,无论客户端还是服务端都可以选择发送一个ping给另一方。 当ping消息收到的时候,接受的一方必须尽快回复一个pong消息。 例如,可以使用这种方式来确保客户端还是连接状态。

一个ping消息或者一个pong消息都只是一个常规的帧, 是这个帧是一个控制帧。Ping消息的opcode字段值为 0x9,pong消息的opcode值为  0xA 。当你获取到一个ping消息的时候,回复一个跟ping消息有相同载荷数据的pong消息 (对于ping和pong,最大载荷长度位125)。 你也有可能在没有发送ping消息的情况下,获取一个pong消息,当这种情况发生的时候忽略它。

如果在你有机会发送一个pong消息之前,你已经获取了超过一个的ping消息,那么你只发送一个pong消息。

关闭连接

客户端或服务器端都可以通过发送一个带有指定控制序列的控制帧以开始关闭连接握手(参见章节5.5.1)。对端收到这个控制帧会回复一个关闭帧,关闭发起端关闭连接。任何在关闭连接后接收到的数据都会被丢弃。

杂项

WebSocket代码、扩展、子协议等在 IANA WebSocket Protocol Registry.注册。

WebSocket扩展和子协议是在握手过程中通过头信息进行协商的。有时候,扩展和子协议看起来太相似而不可能是不同的东西,但是有一个明显的区别。扩展控制WebSocket框架并修改有效负载,而子协议构造WebSocket有效负载,从不修改任何东西。扩展是可选的和通用的(比如压缩);子协议是强制性的和本地化的(就像聊天和MMORPG游戏一样)。

扩展

本节需要扩张。请编辑如果你有这样做的准备。

Think of an extension as compressing a file before e-mailing it to someone. Whatever you do, you're sending the same data in different forms. The recipient will eventually be able to get the same data as your local copy, but it is sent differently. That's what an extension does. WebSockets defines a protocol and a simple way to send data, but an extension such as compression could allow sending the same data but in a shorter format.

扩展在规范的 5.8, 9, 11.3.2, and 11.4 进行了解释

TODO

子协议

可以把子协议理解成一个自定义XML schema文件类型声明。你仍然使用XML和它的语法,但是还要额外受限于你声明的格式。

WebSocket子协议就是像这样的东西。它们不作任何假设实现,只是确立框架。就像一个文件类型或概要。与文件类型或概要类似,通信双方都需要同意子协议;于文件类型或概要不同的是,子协议在服务端实现,而不能由客户端参考第三方。

子协议在规范的章节1.9,4.2,11.3.4和11.5有做解释。

如果客户端需要指定子协议,需要发送如下消息头作为握手信息的一部分

GET /chat HTTP/1.1
...
Sec-WebSocket-Protocol: soap, wamp

等价于:

...
Sec-WebSocket-Protocol: soap
Sec-WebSocket-Protocol: wamp

现在,服务端需要选择一个客户端建议且服务端支持的子协议。如果有多于一个的话使用客户端发送的第一个。如果我们的服务端可以支持soapwamp,则在握手回复时,它会发送:

Sec-WebSocket-Protocol: soap

服务器不能发送多个Sec-Websocket-Protocol。如果服务器不想使用任何子协议,它就不应该发送任何Sec-WebSocket-Protocol header。发送空白header是不正确的。如果客户端没有得到它想要的子协议,它可以关闭连接。

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

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

关联

文档标签和贡献者

标签: 
此页面的贡献者: xiaoyixiang, Kenenth, philwu, zyt, yuyx91, yydzxz
最后编辑者: xiaoyixiang,