1. 结语
Websocket协议作为相当有价值的双工应用层通信协议值得扣一扣细节的,这篇结语我们就来扣下,有兴趣的可以看下没兴趣的也不会影响使用.
Websocket可以分为两个部分:
- 建立连接
- 数据传输
1.1. 建立连接
建立连接的顺序是:
- 客户端向服务端提出一个http请求标明要建立websocket连接
- 服务端收到请求验证通过后返回一个Response握手消息
1.1.1. 复用http建立连接
Websocket复用了http协议用于建立连接,这一过程被称为握手(handshake).客户端的握手消息就是一个"普通的,带有Upgrade头的,HTTP Request消息".所以这一个小节到内容大部分都来自于RFC2616,这里只是它的一种应用形式,下面是RFC6455文档中给出的一个客户端握手消息示例:
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域.
- 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 1.1.2. WebSocket协议Uri
ws协议默认使用80端口,wss协议默认使用443端口.其url形式与http一致,只是协议换成了ws或者wss
1.1.3. 客户端需要做的操作
客户端发送握手之前:
在握手之前客户端首先要先建立连接.一个客户端对于一个相同的目标地址(通常是域名或者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
- 此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中定义的那些
在客户端接收到 Response 握手消息之后
- 如果返回的返回码不是101,则按照RFC2616进行处理.如果是101,进行下一步,开始解析header域,所有header域的值不区分大小写.
- 判断是否含有Upgrade头,且内容包含websocket.
- 判断是否含有Connection头,且内容包含Upgrade
- 判断是否含有Sec-WebSocket-Accept头,其内容在下面介绍
- 如果含有Sec-WebSocket-Extensions头,要判断是否之前的Request握手带有此内容,如果没有,则连接失败
- 如果含有Sec-WebSocket-Protocol头,要判断是否之前的Request握手带有此协议,如果没有,则连接失败
1.1.4. 服务端要做的事
如果请求是HTTPS,则首先要使用TLS进行握手,如果失败,则关闭连接,如果成功,则之后的数据都通过此通道进行发送. 之后服务端可以进行一些客户端验证步骤(包括对客户端header域的验证),如果需要则按照RFC2616来进行错误码的返回. 如果一切都成功,则返回成功的Response握手消息.
如果服务端发送的成功的Response握手,那么这个握手消息是一个标准的HTTP Response消息,它包含了以下几个部分:
- 状态行 
- Upgrade头域,内容为websocket 
- Connection头域,内容为Upgrade 
- Sec-WebSocket-Accept头域,其内容的生成步骤: 1.首先将Sec-WebSocket-Key的内容加上字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11(一个UUID) 2.将#1中生成的字符串进行SHA1编码 3.将#2中生成的字符串进行Base64编码 
- Sec-WebSocket-Protocol头域(可选) 
- Sec-WebSocket-Extensions头域(可选) 
一旦这个握手发出去服务端就认为此WebSocket连接已经建立成功,处于OPEN状态.它就可以开始发送数据了.
1.2. 数据传输
WebSocket中所有发送的数据使用帧的形式发送.客户端发送的数据帧都要经过掩码处理,服务端发送的所有数据帧都不能经过掩码处理.否则对方需要发送关闭帧.
一个帧包含一个帧类型的标识码,一个负载长度,和负载.负载包括扩展内容和应用内容.
1.2.1. 帧类型
帧类型是由一个4位长的叫Opcode的值表示,任何WebSocket的通信方收到一个位置的帧类型,都要以连接失败的方式断开此连接. RFC6455中定义的帧类型如下所示:
- Opcode == 0继续,表示此帧是一个继续帧,需要拼接在上一个收到的帧之后,来组成一个完整的消息.由于这种解析特性,非控制帧的发送和接收必须是相同的顺序.
- Opcode == 1文本帧
- Opcode == 2二进制帧
- Opcode == 3 – 7未来使用(非控制帧)
- Opcode == 8关闭连接(控制帧)此帧可能会包含内容,以表示关闭连接的原因。
通信的某一方发送此帧来关闭WebSocket连接,收到此帧的一方如果之前没有发送此帧,则需要发送一个同样的关闭帧以确认关闭.如果双方同时发送此帧,则双方都需要发送回应的关闭帧.
理想情况服务端在确认WebSocket连接关闭后,关闭相应的TCP连接,而客户端需要等待服务端关闭此TCP连接,但客户端在某些情况下也可以关闭TCP连接.
- Opcode == 9Ping,类似于心跳,一方收到Ping,应当立即发送Pong作为响应.
- Opcode == 10Pong,如果通信一方并没有发送Ping,但是收到了Pong,并不要求它返回任何信息.Pong帧的内容应当和收到的Ping相同.可能会出现一方收到很多的Ping,但是只需要响应最近的那一次就可以了.
- Opcode == 11 – 15未来使用(控制帧)
1.2.2. 帧的格式
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 ...                |  +---------------------------------------------------------------+ 


 
		 
		 
		

还没有评论,来说两句吧...