飞飞龙女 發表於 2021-8-20 10:39:00

HTML5(十二)——一文读懂 WebSocket 原理

<h2 class="pgc-h-arrow-right" data-track="1">一、WebSocket 由来</h2>
<p data-track="3">WebSocket 是一个持久化的协议,通过第一次 HTTP Request 建立连接之后,再把通信协议升级成 websocket,保持连接状态,后续的数据交换不需要再重复请求。websocket 可以看成一种类似 TCP/IP 的 socke t技术,在 web 应用中实现、并获得同 TCP/IP 通信一样的双向通信功能,因此客户端既和服务器可以发送消息也可以接收消息,同时还支持多路复用的功能,由于它借用了 HTTP 协议的一些概念,所以被称为 WebSocket。</p>
<p data-track="2">webSocket API定义了web应用和服务器进行通信的公共接口,具体的构造函数创建对象、对象的属性、方法、事件及它的意义,在上一篇《HTML5(十一)——WebSocket 基础教程》文章中已详细介绍。</p>
<h2 class="pgc-h-arrow-right" data-track="7">二、WebSocket 通信过程</h2>
<p data-track="6">WebSocket 协议可分为两部分:握手阶段和数据通信阶段。</p>
<p data-track="8">WebSocket 为应用层协议,定义在 TCP/IP 协议栈之上,连接服务器的 url 是以 ws 或 wss 开头的。ws 开头的默认TCP端口为80,wss 开头的默认端口为443。</p>
<p data-track="9">ws(websocket)是不安全的,容易被窃听,只要别人知道你的ip和端口号,任何人都可以去连接通讯。</p>
<p data-track="10">wss(web socket secure)是websocket的加密版本。</p>
<h3 data-track="13">2.1、建立连接</h3>
<p data-track="14">客户端去与服务器建立 TCP 连接,客户端生成 websocket 对象,然后使用 API 建立连接,代码如下:</p>
<div class="cnblogs_code">
<pre>let ws= <span style="color: rgba(0, 0, 255, 1)">new</span> WebSocket('ws://localhost:8888'<span style="color: rgba(0, 0, 0, 1)">)
ws.onopen </span>= <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(){
console.log(</span>"连接"<span style="color: rgba(0, 0, 0, 1)">)
}</span></pre>
</div>
<h3 data-track="11">2.2、握手阶段</h3>
<p data-track="12">客户端与服务器建立连接之后,客户端发送握手请求,随后服务器发送握手响应即完成握手阶段。</p>
<p data-track="16">客户端握手请求如下:</p>
<pre class="syl-page-code hljs sql"><code>'GET / HTTP/1.1',
'Host: localhost:8888',
'Connection: Upgrade',
'<span class="hljs-keyword">Pragma: <span class="hljs-keyword">no-<span class="hljs-keyword">cache<span class="hljs-string">',
'<span class="hljs-keyword">Cache-Control: <span class="hljs-keyword">no-<span class="hljs-keyword">cache<span class="hljs-string">',
'<span class="hljs-keyword">Upgrade: websocket<span class="hljs-string">',
'Origin: <span class="hljs-keyword">file://<span class="hljs-string">',
'Sec-WebSocket-<span class="hljs-keyword">Version: <span class="hljs-number">13<span class="hljs-string">',
'<span class="hljs-keyword">Accept-<span class="hljs-keyword">Encoding: gzip, deflate, br<span class="hljs-string">',
'<span class="hljs-keyword">Accept-<span class="hljs-keyword">Language: zh-CN,zh;q=0.9',
'Sec-WebSocket-Key: In1aAp/ya9Lkv+tsUtXLXQ==',
'Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits',</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></code></pre>
<p data-track="33">服务器握手响应如下:</p>
<pre class="syl-page-code hljs yaml"><code><span class="hljs-attr">Status Code: <span class="hljs-number">101 <span class="hljs-string">Switching <span class="hljs-string">Protocols
<span class="hljs-attr">Connection: <span class="hljs-string">Upgrade
<span class="hljs-attr">sec-websocket-Accept: <span class="hljs-string">HBMDBbZMiS59r3aAITpGtJ64Mfc=
<span class="hljs-attr">Upgrade: <span class="hljs-string">websocket</span></span></span></span></span></span></span></span></span></span></code></pre>
<h3 data-track="35">2.3、数据通讯</h3>
<p data-track="36">WebSocket 握手连接成功之后。可以使用 send 进行发送数据,onmessage 接收数据,如下发送“你好”:</p>
<div class="cnblogs_code">
<pre>let ws= <span style="color: rgba(0, 0, 255, 1)">new</span> WebSocket('ws://localhost:8888'<span style="color: rgba(0, 0, 0, 1)">)
ws.onopen </span>= <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(){
console.log(</span>"连接成功"<span style="color: rgba(0, 0, 0, 1)">)
ws.send(</span>"你好"<span style="color: rgba(0, 0, 0, 1)">)
}
ws.onmessage </span>= <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(res){
console.log(</span>'接收到的消息'<span style="color: rgba(0, 0, 0, 1)">,res)
}</span></pre>
</div>
<p>&nbsp;</p>
<p data-track="37">服务器打印接收到的数据,如:&lt;Buffer 81 86 af 87 53 b4 4b 3a f3 51 0a 3a&gt;。</p>
<p data-track="41">websocket 在发送数据时,被组织为一串数据帧,然后进行发送。传送的帧包含两部分:数据帧和控制帧。数据帧可以携带文本数据或者二进制数据,控制帧包含关闭帧和 Ping/Pong 帧。</p>
<div class="pgc-img"><img alt="HTML5(十二)——一文读懂 WebSocket 原理" class="syl-page-img lazyload" data-src="https://p3-tt.byteimg.com/origin/pgc-image/1748bcb7e91646faba47ad2bbf100335?from=pc">
<p class="pgc-img-caption">&nbsp;</p>
</div>
<ul>
<li>FIN :1bit ,表示是消息的最后一帧,如果消息只有一帧那么第一帧也就是最后一帧。</li>
<li>RSV1,RSV2,RSV3:每个1bit,必须是0,除非扩展定义为非零。如果接受到的是非零值但是扩展没有定义,则需要关闭连接。</li>
<li>Opcode:4bit,解释Payload数据,规定有以下不同的状态,如果是未知的,接收方必须马上关闭连接。状态如下:0x0(附加数据帧) 0x1(文本数据帧) 0x2(二进制数据帧) 0x3-7(保留为之后非控制帧使用) 0xB-F(保留为后面的控制帧使用) 0x8(关闭连接帧) 0x9(ping) 0xA(pong)</li>
<li>&nbsp;Mask:1bit,掩码,定义payload数据是否进行了掩码处理,如果是1表示进行了掩码处理。Masking-key域的数据即是掩码密钥,用于解码PayloadData。客户端发出的数据帧需要进行掩码处理,所以此位是1。</li>
<li>Payload length:7位,7 + 16位,7+64位,payload数据的长度,如果是0-125,就是真实的payload长度,如果是126,那么接着后面的2个字节对应的16位无符号整数就是payload数据长度;如果是127,那么接着后面的8个字节对应的64位无符号整数就是payload数据的长度。</li>
<li>Masking-key:0到4字节,如果MASK位设为1则有4个字节的掩码解密密钥,否则就没有。</li>
<li>Payload data:任意长度数据。包含有扩展定义数据和应用数据,如果没有定义扩展则没有此项,仅含有应用数据。</li>
</ul>
<p data-track="83">把接收到的buffer十六进制数据转成二进制数据,控制帧与上述各个类型帧进行对比解析其意义。</p>
<h3 data-track="42">2.4、关闭连接</h3>
<p data-track="38">任何一端可以关闭连接。客户端关闭连接如下:</p>
<pre class="syl-page-code hljs css"><code><span class="hljs-selector-tag">ws<span class="hljs-selector-class">.close()</span></span></code></pre>
<p data-track="39">然后发送关闭帧给对方,通常会带有关闭连接的状态码,常见的状态码如下:</p>
<ul>
<li>1000 连接正常关闭</li>
<li>1001 端点离线,例如服务器down,或者浏览器已经离开此页面</li>
<li>1002 端点因为协议错误而中断连接</li>
<li>1003 端点因为受到不能接受的数据类型而中断连接</li>
<li>1004 保留</li>
<li>1005 保留, 用于提示应用未收到连接关闭的状态码</li>
<li>1006 端点异常关闭</li>
<li>1007 端点收到的数据帧类型不一致而导致连接关闭</li>
<li>1008 数据违例而关闭连接</li>
<li>1009 收到的消息数据太大而关闭连接</li>
<li>1010 客户端因为服务器未协商扩展而关闭</li>
<li>1011 服务器因为遭遇异常而关闭连接</li>
<li>1015 TLS握手失败关闭连接</li>
</ul>
<h2 class="pgc-h-arrow-right" data-track="78">三、websocket 实例</h2>
<p data-track="79">3.1、客户端创建websocket对象,并建立连接之后发送数据。其中 ws 地址根据后台服务端口对应。</p>
<div class="cnblogs_code">
<pre>&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
&lt;meta charset="UTF-8"&gt;
&lt;meta http-equiv="X-UA-Compatible" content="IE=edge"&gt;
&lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
&lt;title&gt;Document&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;script&gt;<span style="color: rgba(0, 0, 0, 1)">
let ws</span>= <span style="color: rgba(0, 0, 255, 1)">new</span> WebSocket('ws://localhost:8888'<span style="color: rgba(0, 0, 0, 1)">)
ws.onopen </span>= <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(){
   console.log(</span>"连接"<span style="color: rgba(0, 0, 0, 1)">)
   ws.send(</span>"你好"<span style="color: rgba(0, 0, 0, 1)">)
   }
   ws.onmessage </span>= <span style="color: rgba(0, 0, 255, 1)">function</span><span style="color: rgba(0, 0, 0, 1)">(res){
    console.log(</span>'res'<span style="color: rgba(0, 0, 0, 1)">,res)
}
</span>&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;</pre>
</div>
<p data-track="81">3.2、使用 node.js 创建一个 websocket 服务,如创建一个serve.js文件,代码如下:</p>
<div class="cnblogs_code">
<pre>const http = require("http"<span style="color: rgba(0, 0, 0, 1)">)
const net </span>= require("net") <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">原生的websocket</span>
const crypto = require('crypto') <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 安全性校验</span>
let serve = net.createServer(sock=&gt;<span style="color: rgba(0, 0, 0, 1)">{
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">只握手一次</span>
sock.once('data',(data)=&gt;<span style="color: rgba(0, 0, 0, 1)">{
console.log(</span>"hand shake start") <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 开始握手</span>
let str =<span style="color: rgba(0, 0, 0, 1)"> data.toString();
let lines </span>= str.split('\r\n'<span style="color: rgba(0, 0, 0, 1)">)
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">舍弃第一行和最后两行</span>
lines = lines.slice(1,lines.length-2<span style="color: rgba(0, 0, 0, 1)">)
let headers </span>=<span style="color: rgba(0, 0, 0, 1)"> {}
lines.forEach(line</span>=&gt;<span style="color: rgba(0, 0, 0, 1)">{
   let </span>= line.split(': '<span style="color: rgba(0, 0, 0, 1)">)
   headers </span>=<span style="color: rgba(0, 0, 0, 1)"> val
})
</span><span style="color: rgba(0, 0, 255, 1)">if</span>( headers['upgrade']!= 'websocket'<span style="color: rgba(0, 0, 0, 1)"> ){
    console.log(</span>"其他协议"<span style="color: rgba(0, 0, 0, 1)">)
    sock.end()
}</span><span style="color: rgba(0, 0, 255, 1)">else</span> <span style="color: rgba(0, 0, 255, 1)">if</span>(headers['sec-websocket-version']!=13<span style="color: rgba(0, 0, 0, 1)">){
   console.log(</span>"版本不对"<span style="color: rgba(0, 0, 0, 1)">)
   sock.end()
}</span><span style="color: rgba(0, 0, 255, 1)">else</span><span style="color: rgba(0, 0, 0, 1)">{
   let key </span>= headers['sec-websocket-key'<span style="color: rgba(0, 0, 0, 1)">]
   let mask </span>= '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
   <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">sha1(key+mask) -&gt; base64 =&gt;client</span>
   let hash = crypto.createHash('sha1'<span style="color: rgba(0, 0, 0, 1)">)
   hash.update(key</span>+<span style="color: rgba(0, 0, 0, 1)">mask)
   let key2 </span>= hash.digest('base64'<span style="color: rgba(0, 0, 0, 1)">)
   sock.write(`HTTP</span>/1.1 101 Switching Protocols\r\nUpgrade:websocket\r\nConnection:Upgrade\r\nsec-websocket-Accept:${key2}\r\n\r\n` )
   console.log("hand shake end") <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)"> 握手结束</span>
   <span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">真正的数据</span>
   sock.on('data',res=&gt;<span style="color: rgba(0, 0, 0, 1)">{
   console.log(</span>"真正接收数据"<span style="color: rgba(0, 0, 0, 1)">,res)
   </span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">数据解析</span>
   let FIN = res&amp;0x001<span style="color: rgba(0, 0, 0, 1)">;
   let opcode </span>= data&amp;0x0F0<span style="color: rgba(0, 0, 0, 1)">;
   let msak </span>= data&amp;0x001<span style="color: rgba(0, 0, 0, 1)">;
   let payload </span>= data&amp;0x0FE<span style="color: rgba(0, 0, 0, 1)">;         
})
}
})
</span><span style="color: rgba(0, 128, 0, 1)">//</span><span style="color: rgba(0, 128, 0, 1)">断开</span>
sock.on('end',()=&gt;<span style="color: rgba(0, 0, 0, 1)">{
console.log(</span>"连接已断开"<span style="color: rgba(0, 0, 0, 1)">)
})
})
serve.listen(</span>"8888")</pre>
</div>
<p>&nbsp;</p>
<p data-track="88">使用命令 node serve.js 或node serve 启动服务,服务启动成功之后可以使用localhost:8888访问服务。</p>
<p data-track="89">启动服务之后,访问前边创建的html文件访问websocket服务。</p>
<h2 class="pgc-h-arrow-right" data-track="86">四、websocket的优点</h2>
<ul>
<li data-track="90">第一次通过http建立连接之后,数据交互不用发送http请求,节省了带宽资源。</li>
<li data-track="91">websocket连接是双向通信,服务器和客户端既可接受也可发送消息。</li>
<li data-track="92">websocket多路复用,几个不同url可以复用一个websocket服务。</li>
<li data-track="93">是HTML5的技术之一,有巨大应用前景。</li>
</ul><br><br>
来源:https://www.cnblogs.com/web-learn/p/15165380.html
頁: [1]
查看完整版本: HTML5(十二)——一文读懂 WebSocket 原理