WebSocket协议还很年轻,RFC文档相比HTTP的发布时间也很短,它的诞生是为了建立一种「双向通讯」的协议,来做为HTTP协议的一个替代者。那么首先看一下它和HTTP(或者HTTP的长链接)的区别。html
上一篇中提到WebSocket的目的就是解决网络传输中的双向通讯的问题,HTTP1.1默认使用持久链接(persistent connection),在一个TCP链接上也能够传输多个Request/Response消息对,可是HTTP的基本模型仍是一个Request对应一个Response。这在双向通讯(客户端要向服务器传送数据,同时服务器也须要实时的向客户端传送信息,一个聊天系统就是典型的双向通讯)时通常会使用这样几种解决方案:web
轮询(polling),轮询就会形成对网络和通讯双方的资源的浪费,且非实时。浏览器
长轮询,客户端发送一个超时时间很长的Request,服务器hold住这个链接,在有新数据到达时返回Response,相比#1,占用的网络带宽少了,其余相似。安全
长链接,其实有些人对长链接的概念是模糊不清的,我这里讲的实际上是HTTP的长链接(1)。若是你使用Socket来创建TCP的长链接(2),那么,这个长链接(2)跟咱们这里要讨论的WebSocket是同样的,实际上TCP长链接就是WebSocket的基础,可是若是是HTTP的长链接,本质上仍是Request/Response消息对,仍然会形成资源的浪费、实时性不强等问题。ruby
HTTP的长链接模型服务器
WebSocket的目的是取代HTTP在双向通讯场景下的使用,并且它的实现方式有些也是基于HTTP的(WS的默认端口是80
和443
)。现有的网络环境(客户端、服务器、网络中间人、代理等)对HTTP都有很好的支持,因此这样作能够充分利用现有的HTTP的基础设施,有点向下兼容的意味。websocket
简单来说,WS协议有两部分组成:握手和数据传输。网络
出于兼容性的考虑,WS的握手使用HTTP来实现(此文档中提到将来有可能会使用专用的端口和方法来实现握手),客户端的握手消息就是一个「普通的,带有Upgrade头的,HTTP Request消息」。因此这一个小节到内容大部分都来自于RFC2616,这里只是它的一种应用形式,下面是RFC6455文档中给出的一个客户端握手消息示例:socket
GET /chat HTTP/1.1 //1 Host: server.example.com //2 Upgrade: websocket //3 Connection: Upgrade //4 Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== //5 Origin: http://example.com //6 Sec-WebSocket-Protocol: chat, superchat //7 Sec-WebSocket-Version: 13 //8
能够看到,前两行跟HTTP的Request的起始行如出一辙,而真正在WS的握手过程当中起到做用的是下面几个header域。ide
Upgrade:upgrade是HTTP1.1中用于定义转换协议的header域。它表示,若是服务器支持的话,客户端但愿使用现有的「网络层」已经创建好的这个「链接(此处是TCP链接)」,切换到另一个「应用层」(此处是WebSocket)协议。
Connection:HTTP1.1中规定Upgrade只能应用在「直接链接」中,因此带有Upgrade头的HTTP1.1消息必须含有Connection头,由于Connection头的意义就是,任何接收到此消息的人(每每是代理服务器)都要在转发此消息以前处理掉Connection中指定的域(不转发Upgrade域)。
若是客户端和服务器之间是经过代理链接的,那么在发送这个握手消息以前首先要发送CONNECT消息来创建直接链接。
Sec-WebSocket-*:第7行标识了客户端支持的子协议的列表(关于子协议会在下面介绍),第8行标识了客户端支持的WS协议的版本列表,第5行用来发送给服务器使用(服务器会使用此字段组装成另外一个key值放在握手返回信息里发送客户端)。
Origin:做安全使用,防止跨站***,浏览器通常会使用这个来标识原始域。
若是服务器接受了这个请求,可能会发送以下这样的返回信息,这是一个标准的HTTP的Response消息。101
表示服务器收到了客户端切换协议的请求,而且赞成切换到此协议。RFC2616规定只有切换到的协议「比HTTP1.1更好」的时候才能赞成切换。
HTTP/1.1 101 Switching Protocols //1 Upgrade: websocket. //2 Connection: Upgrade. //3 Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= //4 Sec-WebSocket-Protocol: chat. //5
ws协议默认使用80
端口,wss协议默认使用443
端口。
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ] wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ] host = <host, defined in [RFC3986], Section 3.2.2> port = <port, defined in [RFC3986], Section 3.2.3> path = <path-abempty, defined in [RFC3986], Section 3.3> query = <query, defined in [RFC3986], Section 3.4>
在握手以前,客户端首先要先创建链接,一个客户端对于一个相同的目标地址(一般是域名或者IP地址,不是资源地址)同一时刻只能有一个处于CONNECTING状态(就是正在创建链接)的链接。从创建链接到发送握手消息这个过程大体是这样的:
客户端检查输入的Uri是否合法。
客户端判断,若是当前已有指向此目标地址(IP地址)的链接(A)仍处于CONNECTING状态,须要等待这个链接(A)创建成功,或者创建失败以后才能继续创建新的链接。
PS:若是当前链接是处于代理的网络环境中,没法判断IP地址是否相同,则认为每个Host地址为一个单独的目标地址,同时客户端应当限制同时处于CONNECTING状态的链接数。
PPS:这样能够防止一部分的DDOS***。
PPPS:客户端并不限制同时处于「已成功」状态的链接数,可是若是一个客户端「持有大量已成功状态的链接的」,服务器或许会拒绝此客户端请求的新链接。
若是客户端处于一个代理环境中,它首先要请求它的代理来创建一个到达目标地址的TCP链接。
例如,若是客户端处于代理环境中,它想要链接某目标地址的80
端口,它可能要收现发送如下消息:
CONNECT example.com:80 HTTP/1.1 Host: example.com
若是客户端没有处于代理环境中,它就要首先创建一个到达目标地址的直接的TCP链接。
若是上一步中的TCP链接创建失败,则此WebSocket链接失败。
若是协议是wss,则在上一步创建的TCP链接之上,使用TSL发送握手信息。若是失败,则此WebSocket链接失败;若是成功,则之后的全部数据都要经过此TSL通道进行发送。
握手必须是RFC2616中定义的Request消息
此Request消息的方法必须是GET,HTTP版本必须大于1.1 。
如下是某WS的Uri对应的Request消息:
ws://example.com/chat GET /chat HTTP/1.1
此Request消息中Request-URI部分(RFC2616中的概念)所定义的资型必须和WS协议的Uri中定义的资源相同。
此Request消息中必须含有Host头域,其内容必须和WS的Uri中定义的相同。
此Request消息必须包含Upgrade头域,其内容必须包含websocket关键字。
此Request消息必须包含Connection头域,其内容必须包含Upgrade指令。
此Request消息必须包含Sec-WebSocket-Key头域,其内容是一个Base64编码的16位随机字符。
若是客户端是浏览器,此Request消息必须包含Origin头域,其内容是参考RFC6454。
此Request消息必须包含Sec-WebSocket-Version头域,在此协议中定义的版本号是13。
此Request消息可能包含Sec-WebSocket-Protocol头域,其意义如上文中所述。
此Request消息可能包含Sec-WebSocket-Extensions头域,客户端和服务器可使用此header来进行一些功能的扩展。
此Request消息可能包含任何合法的头域。如RFC2616中定义的那些。
若是返回的返回码不是101
,则按照RFC2616进行处理。若是是101
,进行下一步,开始解析header域,全部header域的值不区分大小写。
判断是否含有Upgrade头,且内容包含websocket。
判断是否含有Connection头,且内容包含Upgrade
判断是否含有Sec-WebSocket-Accept头,其内容在下面介绍。
若是含有Sec-WebSocket-Extensions头,要判断是否以前的Request握手带有此内容,若是没有,则链接失败。
若是含有Sec-WebSocket-Protocol头,要判断是否以前的Request握手带有此协议,若是没有,则链接失败。
服务端指的是全部参与处理WebSocket消息的基础设施,好比若是某服务器使用Nginx(A)来处理WebSocket,而后把处理后的消息传给响应的服务器(B),那么A和B都是这里要讨论的服务端的范畴。
若是请求是HTTPS,则首先要使用TLS进行握手,若是失败,则关闭链接,若是成功,则以后的数据都经过此通道进行发送。
以后服务端能够进行一些客户端验证步骤(包括对客户端header域的验证),若是须要,则按照RFC2616来进行错误码的返回。
若是一切都成功,则返回成功的Response握手消息。
此握手消息是一个标准的HTTP Response消息,同时它包含了如下几个部分:
状态行(如上一篇RFC2616中所述)
Upgrade头域,内容为websocket
Connection头域,内容为Upgrade
Sec-WebSocket-Accept头域,其内容的生成步骤:
首先将Sec-WebSocket-Key的内容加上字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11
(一个UUID)。
将#1中生成的字符串进行SHA1编码。
将#2中生成的字符串进行Base64编码。
Sec-WebSocket-Protocol头域(可选)
Sec-WebSocket-Extensions头域(可选)
一旦这个握手发出去,服务端就认为此WebSocket链接已经创建成功,处于OPEN状态。它就能够开始发送数据了。
Sec-WebSocket-Version能够被通讯双方用来支持更多的协议的扩展,RFC6455中定义的值为13
,WebSocket的客户端和服务端可能回自定义更多的版本号来支持更多的功能。其使用方法如上文所述。
WebSocket中全部发送的数据使用帧的形式发送。客户端发送的数据帧都要通过掩码处理,服务端发送的全部数据帧都不能通过掩码处理。不然对方须要发送关闭帧。
一个帧包含一个帧类型的标识码,一个负载长度,和负载。负载包括扩展内容和应用内容。
帧类型是由一个4位长的叫Opcode的值表示,任何WebSocket的通讯方收到一个位置的帧类型,都要以链接失败的方式断开此链接。
RFC6455中定义的帧类型以下所示:
Opcode == 0 继续
表示此帧是一个继续帧,须要拼接在上一个收到的帧以后,来组成一个完整的消息。因为这种解析特性,非控制帧的发送和接收必须是相同的顺序。
Opcode == 1 文本帧
Opcode == 2 二进制帧
Opcode == 3 - 7 将来使用(非控制帧)
Opcode == 8 关闭链接(控制帧)
此帧可能会包含内容,以表示关闭链接的缘由。
通讯的某一方发送此帧来关闭WebSocket链接,收到此帧的一方若是以前没有发送此帧,则须要发送一个一样的关闭帧以确认关闭。若是双方同时发送此帧,则双方都须要发送回应的关闭帧。
理想状况服务端在确认WebSocket链接关闭后,关闭相应的TCP链接,而客户端须要等待服务端关闭此TCP链接,但客户端在某些状况下也能够关闭TCP链接。
Opcode == 9 Ping
相似于心跳,一方收到Ping,应当当即发送Pong做为响应。
Opcode == 10 Pong
若是通讯一方并无发送Ping,可是收到了Pong,并不要求它返回任何信息。Pong帧的内容应当和收到的Ping相同。可能会出现一方收到不少的Ping,可是只须要响应最近的那一次就能够了。
Opcode == 11 - 15 将来使用(控制帧)
具体的每一项表明什么意思在这里就不作详细的阐述了。
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 ... | +---------------------------------------------------------------+
一样做为应用层的协议,WebSocket在现代的软件开发中被愈来愈多的实践,和HTTP有不少类似的地方,这里将它们简单的作一个纯我的、非权威的比较:
都是基于TCP的应用层协议。
都使用Request/Response模型进行链接的创建。
在链接的创建过程当中对错误的处理方式相同,在这个阶段WS可能返回和HTTP相同的返回码。
均可以在网络中传输数据。
WS使用HTTP来创建链接,可是定义了一系列新的header域,这些域在HTTP中并不会使用。
WS的链接不能经过中间人来转发,它必须是一个直接链接。
WS链接创建以后,通讯双方均可以在任什么时候刻向另外一方发送数据。
WS链接创建以后,数据的传输使用帧来传递,再也不须要Request消息。
WS的数据帧有序。
这一篇简单地将WebSocket协议介绍了一遍,篇幅有点长了,数据帧也没有来得及详述。下篇会继续深扒WebSocket帧传输,另外将经过实例探讨一些WebSocket协议实际使用中的问题。
刨根问底HTTP和WebSocket协议(一)
WebSocket和Socket的区别(WebSocket外传)
文/TheAlchemist(简书做者)原文连接:http://www.jianshu.com/p/f666da1b1835著做权归做者全部,转载请联系做者得到受权,并标注“简书做者”。