您的当前位置:首页如何实现websocket服务器-理论篇

如何实现websocket服务器-理论篇

2023-02-26 来源:乌哈旅游
如何实现websocket服务器-理论篇

WebSocket 服务器简单来说就是⼀个遵循特殊协议监听服务器任意端⼝的tcp应⽤。搭建⼀个定制服务器的任务通常会让让⼈们感到害怕。然⽽基于实现⼀个简单的Websocket服务器没有那么⿇烦。

⼀个WebSocket server可以使⽤任意的服务端编程语⾔来实现,只要该语⾔能实现基本的Berkeley sockets(伯克利套接字)。例如c(++)、Python、PHP、服务端JavaScript(node.js)。下⾯不是关于特定语⾔的教程,⽽是⼀个促进我们搭建⾃⼰服务器的指南。

我们需要明⽩http如何⼯作并且有中等编程经验。基于特定语⾔的⽀持,了解TCP sockets 同样也是必要的。该篇教程的范围是介绍开发⼀个WebSocket server需要的最少知识。

该⽂章将会从很底层的观点来解释⼀个 WebSocket server。WebSocket servers 通常是独⽴的专门的servers(因为负载均衡和其他⼀些原因),因此通常使⽤⼀个反向代理(例如⼀个标准的HTTP server)来发现 WebSocket握⼿协议,预处理他们然后将客户端信息发送给真正的WebSocket server。这意味着WebSocket server不必充斥这cookie和签名的处理⽅法。完全可以放在代理中处理。

websocket 握⼿规则

⾸先,服务器必须使⽤标准的TCPsocket来监听即将到来的socket连接。基于我们的平台,这些很可能被我们处理了(成熟的服务端语⾔提供了这些接⼝,使我们不必从头做起)。例如,假设我们的服务器监听example.com的8000端⼝,socket server响应/chat的GET请求。警告:服务器可以选择监听任意端⼝,但是如果在80或443之外,可能会遇到防⽕墙或者代理的问题。443端⼝⼤多数情况下是可以的,当然需要⼀个安全连接(TLS/SSL)。此外,注意这⼀点,⼤多数浏览器不允许从安全的页⾯连接到不安全的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

此处客户端可以发起扩展或者⼦协议,在查看更多细节。同样,公共的headers像User-Agent, Referer, Cookie, or authentication等同样可以包括,⼀句话做你想做的。这些并不直接和WebSocket相关,忽略掉他们也是安全的,在很多公共的设置中,会有⼀个代理服务器来处理这些信息。

如果有的header不被识别或者有⾮法值,服务器应该发送'400 Bad Request'并⽴刻关闭socket,通常也会在HTTP返回体中给出握⼿失败的原因,不过这些信息可能不会被展⽰(因为浏览器不会展⽰他们)。如果服务器不识别WebSockets的版本,应该返回⼀个Sec-WebSocket-Version 消息头,指明可以接受的版本(最好是V13,及最新)。下⾯⼀起看⼀下最神秘的消息头Sec-WebSocket-Key。

提⽰:

所有的浏览器将会发送⼀个Origin header,我们可以使⽤这个header来做安全限制(检查是否相同的origin)如果并不是期望的origin返回⼀个403 Forbidden。然后注意下那些⾮浏览器的客户端可以发送⼀个伪造的origin,很多应⽤将会拒绝没有该消息头的请求。请求资源定位符(这⾥的/chat)在规范中没有明确的定义,所以很多⼈巧妙的使⽤它,让⼀个服务器处理多个WebSocket 应⽤。例如,example.com/chat可以指向⼀个多⽤户聊天app,⽽相同服务器上的/game指向多⽤户的游戏。即。

规范的HTTP code只可以在握⼿之前使⽤,当握⼿成功之后,应该使⽤不同的code集合。请查看规范第7.4节

服务器握⼿返回

当服务器接受到请求时,应该发送⼀个相当奇怪的响应,看起来⼤概这个样⼦,不过仍然遵循HTTP规范。 请注意每⼀个header以\\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 hash获得结果,然后返回该结果的base64编码。

提⽰

因为这个看似复杂的过程存在,所以客户端不⽤关⼼服务器是否⽀持websocket。另外,该过程的重要性还是在于安全性,如果⼀个服务器

将⼀个Websocket连接作为http请求解析的话,将会有不⼩的问题。

因此,如果key是\"dGhlIHNhbXBsZSBub25jZQ==\",Accept将会是\"s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\",⼀旦服务器发送这些消息头,握⼿协议就完成了。

服务器在回复握⼿之前,可以发送其他的header像Set-Cookie、要求签名、重定向等。

跟踪客户端

虽然并不直接与Websocket协议相关,但值得我们注意。服务器将会跟踪客户端的sockets,因此我们不必和已经完成握⼿协议的客户端再次进⾏握⼿。相同客户端的IP地址可以尝试多次连接(但是服务器可以选择拒绝,如果他们尝试多次连接以达到保存⾃⼰Denial-of-Service 踪迹的⽬的)

FramesEdit 数据交换

客户端和服务器都可以在任意时间发送消息、这正是websocket的魔⼒所在。然⽽从数据帧中提取信息的过程就不那么充满魔⼒了。尽管所有的帧遵循相同的特定格式,从客户端发到服务器的数据通过X异或加密 (使⽤32位的密钥)进⾏处理,该规范的第五章详细描述了相关内容。

格式

每个从客户端发送到服务器的数据帧遵循下⾯的格式:

帧格式:

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

MASK (掩码:⼀串⼆进制代码对⽬标字段进⾏位与运算,屏蔽当前的输⼊位。)位只表明信息是否已进⾏掩码处理。来⾃客户端的消息必须经过处理,因此我们应该将其置为1(事实上5.1节表明,如果客户端发送未掩码处理的消息,服务器必须断开连接)当发送⼀个帧⾄客户端时,不要处理数据并且不设置mask位。下⾯将会阐述原因。注意:我们必须处理消息即使⽤⼀个安全的socket。RSV1-3可以被忽略,这是待扩展位。

opcode字段定义如何解析有效的数据:

0x0 继续处理

0x1 text(必须是UTF-8编码)

0x2 ⼆进制 和其他叫做控制代码的数据。

0x3-0x7 0xB-0xF 该版本的WebSockets⽆意义

FIN 表明是否是数据集合的最后⼀段消息,如果为0,服务器继续监听消息,以待消息剩余的部分。否则服务器认为消息已经完全发送。

有效编码数据长度

为了解析有效编码数据,我们必须知道何时结束。这是知道有效数据长度的重要所在。不幸的是,有⼀些复杂。让我们分步骤来看。1. 阅读9-15位并且作为⽆符号整数解释,如果是⼩于等于125,这就是数据的长度。如果是126,请继续步骤2,如果是127请阅读,步骤32. 阅读后⾯16位并且作为⽆符号整数解读,结束3. 阅读后⾯64位并且作为⽆符号整数解读,结束

读取并反掩码数据

如果MASK位被设置(当然它应该被设置,对于⼀个从客户端到服务器的消息),读取后4字节(即32位),即加密的key。⼀旦数据长度和加密key被解码,我们可以直接从socket中读取成批的字节。获取编码的数据和掩码key,将其解码,循环遍历加密的字节(octets,text数据的单位)并且将其与第(i%4)位掩码字节(即i除以4取余)进⾏异或运算,如果⽤js就如下所⽰(该规则就是加密解密的规则⽽已,没必要深究,

⼤家知道如何使⽤就好)。

var DECODED = \"\";

for (var i = 0; i < ENCODED.length; i++) {

DECODED[i] = ENCODED[i] ^ MASK[i % 4]; }

现在我们可以知道我们应⽤上解码之后的数据具体含义了。

消息分割

FIN和opcode字段共同⼯作来讲⼀个消息分解为单独的帧,该过程叫做消息分割,只有在opcodes为0x0-0x2时才可⽤(前⾯也提到,当前版本其他数值⽆意义)。

回想⼀下,opcode指明了⼀个帧的将要做什么,如果是0x1,数据是text。如果是0x2,诗句是⼆进制数据。然⽽当其为0x0时,该帧是⼀个继续帧,表⽰服务器应该将该帧的有效数据和服务器收到的最后⼀帧链接起来。这是⼀个草图,指明了当客户端发送text消息时,第⼀个消息在⼀个单独的帧⾥发送,然⽽第⼆个消息却包括三个帧,服务器如何反应。FIN和opcode细节仅仅对客户端展⽰。看⼀下下⾯的例⼦应该会更容易理解。

Client: FIN=1, opcode=0x1, msg=\"hello\"Server: (消息传输过程完成) Hi.

Client: FIN=0, opcode=0x1, msg=\"and a\"Server: (监听,新的消息包含开始的⽂本)

Client: FIN=0, opcode=0x0, msg=\"happy new\"Server: (监听,有效数据与上⾯的消息拼接)Client: FIN=1, opcode=0x0, msg=\"year!\"

Server: (消息传输完成) Happy new year to you too!

注意:第⼀帧包括⼀个完全的消息(FIN=1并且opcode!=0x0),因此当服务器发现结束时可以返回。第⼆帧有效数据为text(opcode=0x1),但是完整的消息没有到达(FIN=0)。该消息所有剩下的部分通过继续帧发送(opcode=0x0),并且最后以帧通过FIN=1表明⾝份。

WebSockets 的⼼跳:ping和pong

在握⼿接受之后的任意点,不论是客户端还是服务器都可以选择发送ping给另⼀部分。当ping被接收时,接收⽅必须尽可能的返回⼀个pong。我们可以⽤该⽅式来确保连接依然有效。

⼀个ping或者pong只是⼀个规则的帧,但是是控制帧,Pings的opcode为0x9,pong是0xA。当我们得到ping时,返回具有完全相同有效数据的pong。(对ping和pong⽽⾔,最⼤有效数据长度是125)我们可能在没有发送ping的情况下,得到⼀个pong。这种情况请忽略。在发送pong之前,如果我们接收到不⽌⼀个ping,只需回应⼀个pong即可。

关闭连接

要关闭客户端和服务器之间的连接,我们可以发送⼀个包含特定控制队列的数据的控制帧来开始关闭的握⼿协议。当接收到该帧时,另⼀⽅发送⼀个关闭帧作为回应。然后前者会关闭连接。关闭连接之后接收到的数据都会被丢弃。

更多

WebSocket 扩展和⼦协议在握⼿过程中通过headers进⾏约定。有时扩展和⼦协议太近似了以致于难以分别。最基本的区别是,扩展控制websocket 帧并且修改有效数据。然⽽⼦协议构成websocket有效数据并且从不修改任何事物。扩展是可选的⼴义的,⼦协议是必须的局限性的。

扩展

将扩展看作压缩⼀个⽂件在发送之前,⽆论你如何做,你将发送相同的数据只不过帧不同⽽已。收件⼈最终将会受到与你本地拷贝相同的数据,不过以不同⽅式发送。这就是扩展做的事情。websockets定义了⼀个协议和基本的⽅式去发送数据,然⽽扩展例如压缩可以以更短的帧来阿松相同的数据。

⼦协议

将⼦协议看作定做的xml表或者⽂档类型说明。你在使⽤XML和它的语法,但是你被限制于你同意的结构。WebSocket⼦协议就是如此。他们不介绍其他⼀些华丽的东西,仅仅建⽴结构,像⼀个⽂档类型和表⼀样,两个部分(client & server)都同意该协议,和⽂档类型和表不同,⼦协议由服务器实现并且客户端不能对外引⽤。

⼀个客户端必须请求特定的⼦协议,为了达到⽬的,将会发送⼀些像下⾯的内容作为原始握⼿的⼀部分。

GET /chat HTTP/1.1...

Sec-WebSocket-Protocol: soap, wamp

或者等价的写法

...

Sec-WebSocket-Protocol: soapSec-WebSocket-Protocol: wamp

现在,服务器必须选择客户端建议并且⽀持的⼀种协议。如果多余⼀个,发送客户端发送过来的第⼀个。想象我们的服务器可以使⽤soap和wamp中的⼀个,然后,返回的握⼿中将会发送如下形式。

Sec-WebSocket-Protocol: soap

服务器不能发送超过⼀个的Sec-Websocket-Protocol消息头,如果服务器不想使⽤任⼀个⼦协议,应该不发送Sec-WebSocket-Protocol 消息头。发送⼀个空⽩的消息头是错误的。客户端可能会关闭连接如果不能获得期望的⼦协议。

如果我们希望我们的服务器遵守⼀定的⼦协议,⾃然地在我们的服务器需要额外的代码。想象我们使⽤⼀个⼦协议json,基于该⼦协议,所有的数据将会作为JSON传递,如果⼀个客户端征求⼦协议并且服务器想使⽤它,服务你需要有⼀个JSON解析。实话实说,将会有⼀个⼯具库,但是服务器也要需要传递数据。

为了避免名称冲突,推荐选⽤domain的⼀部分作为⼦协议的名称。如果我们开发⼀个使⽤特定格式的聊天app,我们可能使⽤这样的名字:Sec-WebSocket-Protocol: chat.example.com 注意,这不是必须的。仅仅是⼀个可选的惯例,我们可以使⽤我们想⽤的任意字符。

结束语

翻译这篇⽂档的初衷是看到关于websocket的中⽂⼤部分都是客户端相关的内容,⾃⼰⼜对服务器端的实现感兴趣,没有找到合适的资料,就只好⾃⼰阅读下英⽂,本着提⾼⾃⼰的⽬的将其翻译下来,希望对其他同学有所帮助。 后⾯请期待node实现websocket服务器的实践篇。

源⽂档出处

翻译⾃MDN

因篇幅问题不能全部显示,请点此查看更多更全内容