一 序
在读<<netty权威指南>>第11章websocket协议开发的时候,觉得作者对于这块介绍相对简单,对于相关知识不熟悉的,不适合上来就用netty来实现websocket。打算分为三步:1websocket入门介绍。2.Tomcat实现websocket。3.netty实现websocket
主要包含两部分:1websocket协议介绍,2 Tomcat实现简单websocket.
二 websocket
2.1 背景
由于历史原因,在创建一个具有双向通信机制的 web 应用程序时,需要利用到 HTTP 轮询的方式。围绕轮询产生了 “短轮询” 和 “长轮询”。在特定的时间间隔(比如1秒钟)由浏览器自动发出请求,将服务器的消息主动的拉回来,在这种情况下,我们需要不断的向服务器发送请求,然而HTTP request 的header是非常长的,里面包含的数据可能只是一个很小的值,这样会占用很多的带宽和服务器资源。
而最比较新的技术去做轮询的效果是Comet – 用了AJAX。但这种技术虽然可达到全双工通信,但依然需要发出请求(reuqest)。
2.2 websocket简介
Websocket是html5提出的一个协议规范,参考rfc6455。
websocket约定了一个通信的规范,通过一个握手的机制,客户端(浏览器)和服务器(webserver)之间能建立一个类似tcp的连接,从而方便c-s之间的通信。在websocket出现之前,web交互一般是基于http协议的短连接或者长连接。
WebSocket是为解决客户端与服务端实时通信而产生的技术。websocket协议本质上是一个基于tcp的协议,是先通过HTTP/HTTPS协议发起一条特殊的http请求进行握手后创建一个用于交换数据的TCP连接,此后服务端与客户端通过此TCP连接进行实时通信。
websocket协议参见https://tools.ietf.org/html/rfc6455
2.3 协议概览
- 打开握手
- 数据传递
- 关闭握手
在websocket协议发展过程中前前后后就出现了多个版本的握手协议,这里列举下。
基于flash的握手协议
使用场景是IE的多数版本,因为IE的多数版本不都不支持WebSocket协议,以及FF、CHROME等浏览器的低版本,还没有原生的支持WebSocket。此处,server唯一要做的,就是准备一个WebSocket-Location域给client,没有加密,可靠性很差。
客户端请求:
GET /demo HTTP/1.1
Host: example.com
Connection: Upgrade
Sec-WebSocket-Key2:
Upgrade: WebSocket
Sec-WebSocket-Key1:
Origin: http://www.qixing318.com
[8-byte security key]
服务端返回:
HTTP/1.1 101 WebSocket Protocol Handshake
Upgrade: WebSocket
Connection: Upgrade
WebSocket-Origin: http://www.qixing318.com
WebSocket-Location: ws://example.com/demo
[16-byte hash response]
其中 Sec-WebSocket-Key1,Sec-WebSocket-Key2 和 [8-byte security key] 这几个头信息是web server用来生成应答信息的来源,依据 draft-hixie-thewebsocketprotocol-76 草案的定义。
基于sha加密方式的握手协议
也是目前见的最多的一种方式,这里的版本号目前是需要13以上的版本。其中 server就是把客户端上报的key拼上一段GUID( “258EAFA5-E914-47DA-95CA-C5AB0DC85B11″),拿这个字符串做SHA-1 hash计算,然后再把得到的结果通过base64加密,最后再返回给客户端。
客户端发出的握手信息类似:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13
服务端回应的握手信息类式:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
客户端的握手请求由 请求行(Request-Line) 开始。客户端的回应由 状态行(Status-Line) 开始。请求行和状态行的生产式见 RFC2616。
首行之后的部分,都是没有顺序要求的 HTTP Headers。
一旦客户端和服务端都发送了它们的握手信息,握手过程就完成了,随后就开始数据传输部分。因为这是一个双向的通信,所以客户端和服务端都可以首先发出信息。
在数据传输时,客户端和服务器都使用 “消息 Message” 的概念去表示一个个数据单元,而消息又由一个个 “帧 frame” 组成。这里的帧并不是对应到具体的网络层上的帧。
和 TCP 以及 HTTP 之间的关系
WebSocket 是一个独立的基于 TCP 的协议,它与 HTTP 之间的唯一关系就是它的握手请求可以作为一个升级请求(Upgrade request)经由 HTTP 服务器解释(也就是可以使用 Nginx 反向代理一个 WebSocket。
2.4 握手过程
- Upgrade:WebSocket
表示这是一个特殊的 HTTP 请求,请求的目的就是要将客户端和服务器端的通讯协议从 HTTP 协议升级到 WebSocket 协议。 - 源标识 Origin可以预防在浏览器中运行的脚本,在未经 WebSocket 服务器允许的情况下,对其发送跨域的请求。
- Sec-WebSocket-Key
是一段浏览器base64加密的密钥,server端收到后需要提取Sec-WebSocket-Key 信息,然后加密。 Sec-WebSocket-Accept
服务器端在接收到的Sec-WebSocket-Key密钥后追加一段神奇字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,并将结果进行sha-1哈希,然后再进行base64加密返回给客户端(就是Sec-WebSocket-Key)。如果加密算法错误,客户端在进行校检的时候会直接报错。如果握手成功,则客户端侧会出发onopen事件。function encry($req) { $key = $this->getKey($req); $mask = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; # 将 SHA-1 加密后的字符串再进行一次 base64 加密 return base64_encode(sha1($key . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true)); }
- Sec-WebSocket-Protocol
表示客户端请求提供的可供选择的子协议,及服务器端选中的支持的子协议。 - Sec-WebSocket-Version: 13
客户端在握手时的请求中携带,这样的版本标识,表示这个是一个升级版本,现在的浏览器都是使用的这个版本。 HTTP/1.1 101 Switching Protocols
101为服务器返回的状态码,所有非101的状态码都表示handshake并未完成。
连接初始的初始状态被定义为 “连接中 CONNECTING”。
一旦客户端的握手请求发送完成后,客户端必须等待服务端的握手响应,在此期间不可以向服务器传输任何数据。
客户端也必须按照服务端生成 |Sec-WebSocket-Accept| 头字段值的方式也生成一个字符串,与服务端回传的进行对比,如果不同就标记连接为失败的。
如果服务端的响应符合上述的描述的话,那么就说明 WebSocket 的连接已经建立了,并且连接的状态变为 “OPEN 状态”。
2.5 数据帧
Websocket协议通过序列化的数据帧传输数据。数据封包协议中定义了opcode、payload length、Payload data等字段。其中要求:
- 客户端向服务器传输的数据帧必须进行掩码处理:服务器若接收到未经过掩码处理的数据帧,则必须主动关闭连接。
- 服务器向客户端传输的数据帧一定不能进行掩码处理。客户端若接收到经过掩码处理的数据帧,则必须主动关闭连接。
针对上情况,发现错误的一方可向对方发送close帧(状态码是1002,表示协议错误),以关闭连接。
具体数据帧格式如下图所示:
- FIN
标识是否为此消息的最后一个数据包,占 1 bit - RSV1, RSV2, RSV3: 用于扩展协议,一般为0,各占1bit
- Opcode
数据包类型(frame type),占4bits
0x0:标识一个中间数据包
0x1:标识一个text类型数据包
0x2:标识一个binary类型数据包
0x3-7:保留
0x8:标识一个断开连接类型数据包
0x9:标识一个ping类型数据包
0xA:表示一个pong类型数据包
0xB-F:保留 - MASK:占1bits
用于标识PayloadData是否经过掩码处理。如果是1,Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。 - Payload length
Payload data的长度,占7bits,7+16bits,7+64bits:- 如果其值在0-125,则是payload的真实长度。
- 如果值是126,则后面2个字节形成的16bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
- 如果值是127,则后面8个字节形成的64bits无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
这里的长度表示遵循一个原则,用最少的字节表示长度(尽量减少不必要的传输)。举例说,payload真实长度是124,在0-125之间,必须用前7位表示;不允许长度1是126或127,然后长度2是124,这样违反原则。
Payload data
应用层数据server解析client端的数据
接收到客户端数据后的解析规则如下:
- 1byte
- 1bit: frame-fin,x0表示该message后续还有frame;x1表示是message的最后一个frame
- 3bit: 分别是frame-rsv1、frame-rsv2和frame-rsv3,通常都是x0
- 4bit: frame-opcode,x0表示是延续frame;x1表示文本frame;x2表示二进制frame;x3-7保留给非控制frame;x8表示关 闭连接;x9表示ping;xA表示pong;xB-F保留给控制frame
- 2byte
- 1bit: Mask,1表示该frame包含掩码;0表示无掩码
- 7bit、7bit+2byte、7bit+8byte: 7bit取整数值,若在0-125之间,则是负载数据长度;若是126表示,后两个byte取无符号16位整数值,是负载长度;127表示后8个 byte,取64位无符号整数值,是负载长度
- 3-6byte: 这里假定负载长度在0-125之间,并且Mask为1,则这4个byte是掩码
- 7-end byte: 长度是上面取出的负载长度,包括扩展数据和应用数据两部分,通常没有扩展数据;若Mask为1,则此数据需要解码,解码规则为- 1-4byte掩码循环和数据byte做异或操作。
2.6 关闭握手
关闭握手的操作也很简单。任意一端都可以选择关闭握手过程。主动关闭的一方向另一方发送一个关闭类型的数据包,对方收到此数据包之后,再回复一个相同类型的数据包,关闭完成。关闭类型数据包遵守封包协议,Opcode为0x8,Payload data可以用于携带关闭原因或消息。2.7websocket的事件响应
以上的Opening Handshake、Data Framing、Closing Handshake三个步骤其实分别对应了websocket的三个事件:
- onopen 当接口打开时响应
- onmessage 当收到信息时响应
- onclose 当接口关闭时响应
任何程序语言的websocket api都至少要提供上面三个事件的api接口, 有的可能还提供的有onerror事件的处理机制。
websocket 在任何时候都会处于下面4种状态中的其中一种:
- CONNECTING (0):表示还没建立连接;
- OPEN (1): 已经建立连接,可以进行通讯;
- CLOSING (2):通过关闭握手,正在关闭连接;
- CLOSED (3):连接已经关闭或无法打开;
参考:
http://www.jianshu.com/p/867274a5e054
https://www.cnblogs.com/lizhenghn/p/5155933.html
https://www.cnblogs.com/tinywan/p/5894403.html