本文概述了Web套接字的目标,即通过单个持久连接提供全双工、双向通信。Javascript创建WebSocket后,向浏览器发送HTTP请求以发起连接。获得服务器响应后,建立的连接会将HTTP从HTTP协议升级为WebSocket协议。由于WebSocket使用自定义协议,因此URL模式也略有不同。未加密的连接不再是http://,而是ws://;加密连接也不是https://,而是wss://。使用WebSocketURL时,必须带上这个模式,因为以后可能会支持其他模式。使用自定义协议而不是HTTP协议的优点是可以在客户端和服务器之间发送非常少量的数据,而不必担心HTTP的字节级开销。由于传递的数据包体积小,WebSocket非常适合移动应用程序。以上只是对WebSockets的一般描述,后面几页将深入探讨WebSockets的详细实现。本文接下来的四节不会涉及到大量的代码片段,而是将相关API的技术原理进行分析,相信你在看完下文后看到这段描述时会有豁然开朗的感觉。1、WebSocket重用HTTP握手通道“握手通道”是客户端和服务器通过HTTP协议中的“TCP三次握手”建立的连接通道。每次客户端和服务器使用HTTP协议进行交互,都需要先建立这样一个“通道”,然后通过这个通道进行通信。我们熟悉的ajax交互就是在这样一个通道上完成数据传输。下面是HTTP协议中建立“握手通道”的过程示意图:上文我们提到:Javascript创建WebSocket后,会向浏览器发送HTTP请求,发起连接,然后服务器响应。这就是“握手”过程。在握手过程中,客户端和服务端主要做两件事:建立连接“握手通道”进行通信(这个和HTTP协议是一样的,不同的是HTTP协议在完成数据交互后释放握手通道.这就是所谓的“短连接”,它的生命周期是一次数据交互的时间,一般在毫秒级。)将HTTP协议升级为WebSocket协议,复用HTTP协议的握手通道建立持久连接。说到这里,可能有人会问:为什么HTTP协议不复用自己的“握手通道”,而是在每次数据交互时,通过TCP三次握手重新建立“握手通道”呢?答案是这样的:虽然“长连接”省去了客户端与服务器交互时每次都要建立“握手通道”的麻烦步骤,但是维持这样的“长连接”需要服务器资源。在大多数情况下,这种资源消耗是不必要的。可以说HTTP标准的制定是经过深思熟虑的。后面我们讲到WebSocket协议数据帧的时候,你可能会明白要维护一个“长连接”的服务端和客户端要做的事情太多了。说完握手通道,我们再看看HTTP协议是如何升级为WebSocket协议的。2、将HTTP协议升级为WebSocket协议升级协议需要客户端和服务端进行通信。服务器如何知道将HTTP协议升级为WebSocket协议呢?它一定是从客户端接收到某种信号。以下是我从谷歌浏览器截获的“ClientInitiatedProtocolUpgradeRequestMessage”。通过分析这条消息,我们可以得到更多关于WebSocket协议升级的细节。首先,客户端发起协议升级请求。采用标准的HTTP报文格式,只支持GET方式。下面是关键请求头的含义:Connection:Upgrade:表示要升级的协议Upgrade:websocket:表示要升级到websocket的协议Sec-WebSocket-Version:13:表示websocket的版本Sec-WebSocket-Key:UdTUf90CC561cQXn4n5XRg==:与ResponseHeader中的响应头Sec-WebSocket-Accept:GZk41FJZSYY0CmsrZPGpUGRQzkY=匹配,提供基本的保护,如恶意或无意的连接。其中,Connection就是我们前面提到的,客户端向服务端发送的信号,服务端收到信号后才会对HTTP协议进行升级。那么服务端如何确认客户端发送的请求是否合法呢?客户端每次发起协议升级请求都会生成一个唯一的编码:Sec-WebSocket-Key。服务端拿到code后通过算法验证,然后通过Sec-WebSocket-Accept响应给客户端,客户端验证Sec-WebSocket-Accept完成验证。这个算法很简单:1.将Sec-WebSocket-Key与全局唯一的(GUID,[RFC4122])标识符:258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接2.通过SHA1计算摘要,转成base64字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11此字符串也称为“魔术字符串”。至于为什么我们用它作为Websocket握手计算中使用的字符串,我们不用关心。我们只需要知道它是RFC标准规定的就可以了,官方简单分析说这个值不太可能被不懂WebSocket协议的网络终端使用。让我们用世界上最好的语言来描述这个算法。publicfunctiondohandshake($sock,$data,$key){if(preg_match("/Sec-WebSocket-Key:(.*)\r\n/",$data,$match)){$response=base64_encode(sha1($match[1].'258EAFA5-E914-47DA-95CA-C5AB0DC85B11',true));$upgrade="HTTP/1.1101切换协议\r\n".“升级:websocket\r\n”。“连接:升级\r\n”。“Sec-WebSocket-接受:”。$响应。"\r\n\r\n";socket_write($sock,$upgrade,strlen($upgrade));$this->isHand[$key]=true;服务端响应客户端的头信息格式和HTTP协议是一样的,所以这里Sec-WebSocket-Accept字段后面的两个换行符是必不可少的,类似于我们的一个使用curl工具模拟get请求的原因。以这种方式显示结果似乎不直观。我们使用命令行CLI根据上图中的Sec-WebSocket-Key和握手算法计算服务器返回的Sec-WebSocket-Accept是否正确:从图中可以看出,通过base64字符串算法计算出来的和Sec-WebSocket-Accept一样。那么如果服务器在握手过程中返回错误的Sec-WebSocket-Accept字符串怎么办?当然客户端会报错,连接无法建立。你最好试试,比如把全局唯一标识258EAFA5-E914-47DA-95CA-C5AB0DC85B11改成258EAFA5-E914-47DA-95CA-C5AB0DC85B12。3、WebSocket帧和数据分片传输下图是我做的一个测试:把小说《飘》第一章的内容复制成文本数据,通过客户端发送给服务端,服务端响应相同的信息来完成一次通信。可以看到,一篇近15000字节数据的文章只用了150ms就完成了客户端与服务端的通信。我们也可以在浏览器控制台的frame栏中清楚的看到客户端发送,服务器响应的文本数据。你一定对WebSocket通信强大的数据传输能力感到惊讶。数据真的如框架所示,客户端直接向服务器发送一大段文本数据,服务器接收到数据后,返回一大段文本数据给客户端吗?这当然是不可能的。我们都知道HTTP协议是基于TCP实现的,HTTP发送的数据也是以包的形式转发的。即把大数据分成小块按照报文的形式发送给服务器,服务器接收。收到客户端发来的消息后,将小块数据进行拼接组装。关于HTTP的分包策略,可以查阅相关资料进行研究。websocket协议也是通过对数据进行分片打包来进行数据转发,但其策略与HTTP分包不同。帧(frame)是websocket发送数据的基本单位,下面是它的消息格式:消息内容规定了数据标签、操作码、掩码、数据、数据长度等格式。看不懂没关系,只要你明白消息中重要符号的作用,我就给你解释一下。首先,我们了解到客户端和服务端之间的Websocket消息传递是这样的:客户端:将消息切割成多个帧发送给服务端。服务器:接收消息帧并将关联的帧重组为完整的消息。服务器收到客户端发送的帧消息后,将这些帧组装起来。它如何知道数据组装何时完成?这是消息左上角的FIN(占一位)中存储的信息。1表示这是消息的最后一个片段。如果为0,则表示它不是消息的最后一个片段。在websocket通信中,客户端发送的数据片段是有顺序的,这一点与HTTP不同。HTTP将消息分包后,乱序并发发送给服务器。数据包信息的位置在HTTP报告中。它存储在文本中,websocket只需要一个FIN位就可以保证数据完整的发送到服务器。RSV1、RSV2、RSV3接下来的三位的作用是什么?这三个标志是保留给客户端开发者和服务端开发者在开发过程中协商扩展的,默认为0。如何使用扩展必须在握手阶段协商。其实握手本身也是客户端和服务器之间的协商。4、Websocket连接维护和心跳检测Websocket是长连接。为了保持客户端和服务器之间的实时双向通信,需要保证客户端和服务器之间的TCP通道不断开。但是对于一个长时间没有数据交换的连接,如果仍然保持下去,可能会浪费服务器资源。但是,不排除某些情况。客户端和服务器虽然很长时间没有数据交换,但还是需要保持连接。比如你和一个QQ好友几个月没聊天,突然有一天他发QQ消息告诉你他要结婚了。是的,您仍然可以尽快收到它。那是因为,客户端和服务器总是使用心跳来检查连接。client和server之间的心跳连接检测就像打乒乓球:sender->receiver:pingreceiver->sender:pong等,当没有ping或者pong的时候,那么肯定是连接有问题了。说了这么多,那我就用Go语言来实现一个心跳检测。Websocket通信的实现细节是一件比较麻烦的事情。直接使用开源类库是一个不错的选择。我使用:大猩猩/websocket。这个类库很好的封装了websocket的实现细节(握手,数据解码)。下面我直接贴代码:packagemainimport("net/http""time""github.com/gorilla/websocket")var(//完成握手操作upgrade=websocket.Upgrader{//允许跨域(一般来说,websockets是独立部署的)CheckOrigin:func(r*http.Request)bool{returntrue},})funcwsHandler(whttp.ResponseWriter,r*http.Request){var(conn*websocket.Connerrerrordata[]byte)//服务端响应客户端的http请求(升级为websocket协议)。响应后协议升级为websocket,http建立连接时的tcp三次握手会保留。如果conn,err=upgrade.Upgrade(w,r,nil);err!=nil{return}//启动协程,每隔1s向客户端发送心跳消息gofunc(){var(errerror)for{iferr=conn.WriteMessage(websocket.TextMessage,[]byte("心跳"));err!=nil{return}time.Sleep(1*time.Second)}}()//获取websocket的长度链接后,就可以对客户端传过来的数据进行操作for{//读取到的数据websocket长链接可以是文本数据或者二进制Binaryif_,data,err=conn.ReadMessage();err!=nil{gotoERR}如果err=conn.WriteMessage(websocket.TextMessage,data);err!=nil{gotoERR}}ERR://出错后关闭socket连接conn.Close()}funcmain(){http.HandleFunc("/ws",wsHandler)http.ListenAndServe("0.0.0.0:7777",nil)}借助go语言,很容易搭建协程。发送一个消息。打开客户端浏览器可以看到帧中每秒的心跳数据一直在跳动。当长链接断开时,心跳就没有了,就像一个没有心跳的人:
