• websocket学习笔记1


    1. 知识模块一

    1.1. websocket与http对比

    1.1.1. http协议

    主要关注:客户端->服务器(获取资源)

    特点:

    • 无状态协议,每个请求都是独立的,请求应答模式,服务端无法主动给客户端推送消息,半双工(同一刻数据传输只能是单项的,还有单工和全双工)。
    • http受浏览器同源策略影响,需要保证协议、主机名、端口号一致,否则会出现跨域问题(为了安全)。
    • 适合获取资源、下载文件,但不适合实时性要求高的需求。

    1.1.2.websocket协议

    双向通信(全双工协议),每次不需要重新建立连接,可以一致相互通信,适合长通信。

    1.1.3.关系

    都是通信协议,websocket是建立在http基础之上的,第一次websocket握手是基于http的,底层传输都依靠TCP。

    1.2.不用websocket以前是如何实现双向通信的

    Comet,这个技术主要是为了实现服务端可以向客户端推送数据,为了解决实时性比较高的情况。

    import express from "express";
    import cors from "cors";
    
    const app = express();
    // 解决跨域问题
    app.use(cors());
    
    // 轮询,短轮询()
    
    
    // 接口
    app.get('/clock',function(req,res){
        res.send(new Date().toLocaleDateString());
    })
    
    // 通过node命令启动时,修改后并不会重新执行
    // 通过nodeman启动可以在改变后自动执行
    app.listen(3000,function(){
        console.log('server start 3000');
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    1. 轮询

      clock-1.html

      DOCTYPE html>
      <html lang="en">
      
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>Documenttitle>
      head>
      
      <body>
          <div id="clock">div>
      
          <script>
              setInterval(() => {
               	// 创建请求
                  const xhr = new XMLHttpRequest();
                  
                  // 访问请求,异步
                  xhr.open('GET','http://localhost:3000/clock',true);
                  xhr.onload = function () {
                      console.log(xhr.responseText);
                      clock.innerHTML = xhr.responseText;
                  }
                  // 发送请求
                  xhr.send();
              }, 1000)//每隔一秒
          script>
      body>
      
      html>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      • 30

      存在问题:

      • 竞速问题:无法保证请求的先后顺序,可能会出现多个请求返回的时候同时修改资源,会导致一些不可预测的问题。
      • 频繁的网络请求,请求数目过多,会导致网络带宽的消耗,增加服务端和客户端的消耗。
      • http在发送请求的时候,会增加http报文(鉴权、内容类型),增加额外的数据消耗
      • 实时性比较低,如果服务端1s内变了三次,而客户端每隔1s发送一次请求。

      优点:

      • 容易实现,适合轻量级、低并发。
    2. 长轮询

      DOCTYPE html>
      <html lang="en">
      
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>Documenttitle>
      head>
      
      <body>
          <div id="clock">div>
      
          <script>
              // 客户端发送请求后,服务端相应后,我就发下一个请求
              function longPolling() {
                  const xhr = new XMLHttpRequest();
                  xhr.open('GET', 'http://localhost:3000/clock', true);
                  xhr.onload = function () {
                      console.log(xhr.responseText);
                      clock.innerHTML = xhr.responseText;
                      longPolling();
                  }
                  xhr.send();
              }
              longPolling()
          script>
      body>
      
      html>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28
      • 29
      1. 想解决短轮询的问题,希望实时性更强,但是实时性强了的同时,也会造成频繁的网络请求(实时性强了,但是要求服务端的并发能力必须强)。
      2. 连接堆叠问题,这些链接都在服务端中保持打开,会占用服务端资源。
    3. iframe流(以前用的挺多的)

      DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <meta name="viewport" content="width=device-width, initial-scale=1.0">
          <title>Documenttitle>
      head>
      <body>
          <div id="clock">div>\
          
          <script>
              document.domain = 'localhost'
          script>
          <iframe src="http://localhost:3000/clock" frameborder="0">iframe>
      body>
      html>
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      import express from "express";
      import cors from "cors";
      
      const app = express();
      // 解决跨域问题
      app.use(cors());
      
      // 接口
      app.get('/clock', function (req, res) {
          // res.end或者res.send请求结束后会断开
          // res.write方法不会结束本次的响应
          setInterval(() => {
              res.write(`
              
              `);
          })
      })
      
      // 通过node命令启动时,修改后并不会重新执行
      // 通过nodeman启动可以在改变后自动执行
      app.listen(3000, function () {
          console.log('server start 3000');
      })
      
      
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24
      • 25
      • 26
      • 27
      • 28

      创建之后一直保持链接,会出现跨域问题

      可以保证实时性,而且不用客户频繁发送请求 。

      缺点:单向通信。

    4. sse EventSource(写法已经比较接近websocket了)

      html提供的,单向通信,客户端可以监控服务端推送的事件,只能推送文本类型的数据,适合小数据,需要做额外的处理。

      缺点:单向,客户端无法给服务端传递数据。

    5. websocket

      优势:

      1. 双向绑定
      2. 持久链接,可以一直握手
      3. 发送的消息增加帧非常小
      4. 支持多种数据格式
      5. 天生支持跨域

    2. 知识模块二

    2.1.基础内容

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Documenttitle>
    head>
    <body>
    
        
        <script>
            // 与服务端提供的一个websocket服务相关联
            const ws = new WebSocket('ws://localhost:3000');
            // 给服务端发送消息
            ws.onopen = function(){
                console.log('Connection opend');
                ws.send('hello server');// 给服务端发送消息
            }
    
            // 监控服务端的数据
            ws.onmessage = function(e){
                console.log('服务端相应的数据:' + e.data);
            }
    
            // http各种header的使用
    
            // websocket怎么实现握手、数据长什么样的、怎么通信的
            // 协议的表示方式
        
            // 请求行:GET ws://localhost:3000 HTTP/1.1
            // Connection:Upgrade
            // Sec-Websocket-Key:用于保证是安全的websocket链接,防止恶意连接,用于握手
            // Sec-Websoeckt-Version:版本
    
            // 握手成功后服务端会返回一个Sec-Websocket-Accept,是根据key算出来的
            // Upgrade:websocket,表示升级成什么协议
    
        script>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    import express, { response } from 'express';
    import http from 'http';
    import { WebSocketServer } from 'ws';
    
    const app = express();
    const server = http.createServer(app); // http服务
    
    const wss = new WebSocketServer({server});
    
    // 监控连接成功
    wss.on('connection',(ws)=>{
        console.log('Connection opend');
    
        // 给客户端发送消息
        ws.send('hello client');
    
        // 第一个参数可以为
        // close、error、message、open、ping、pong、upgrade、unexpected-response
        ws.on('message', function(message){
            console.log("客户端数据:"+message);
        })
    })
    
    // 监控端口
    server.listen(3000)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    2.2. key和accept的换算

    // 可以使用wireshark抓包软件,分析协议信息
    // key-> P2P2F9kEf/wg18RKzXM8eA== ,握手的时候创建一个随机的key
    // 服务端通过key加上
    // const number = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
    // 然后经历sha1算法计算生成accept,
    // accept-> adAEOXRx506qcgqahbjvIHPI1Sk= ,服务端要相应一个值
    import crypto from 'crypto'
    
    const number = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'const WebsocketKey = 'P2P2F9kEf/wg18RKzXM8eA=='; // key是随机值
    
    const WebsocketAccept = crtpto
    		 .createHash('sha1')
    		 .update(websocketKey + number)
    		 .digest('base64');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    2.3.具体握手过程

    2.3.1.三次握手:

    1. 第一次握手:建立连接,客户端A发送SYN=1、随机产生Seq=client_isn的数据包到服务器B,等待服务器确认。
    2. 第二次握手:服务器B收到请求后确认联机(可以接受数据),发起第二次握手请求,ACK=(A的Seq+1)、SYN=1,随机产生Seq=client_isn的数据包到A。
    3. 第三次握手:A收到后检查ACK是否正确,若正确,A会在发送确认包ACK=服务器B的Seq+1、ACK=1,服务器B收到后确认Seq值与ACK值,若正确,则建立连接。

    通俗点,客户端跟服务端说我们结婚吧,服务端给客户端说好的我们结婚吧,然后服务端和客户端结婚了。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    2.3.2.websocket数据帧格式:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    • FIN:1个比特,如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是消息(message)的最后一个分片(fragment)。

    • RSV1、RSV2、RSV1:各占1个比特,一般情况全为0.当客户端、服务端协商采用websocket扩展时,这三个标志位可以非0,且值的含义由拓展进行定义。如果出现非零的值,且并没有采用websocket拓展,连接出错。

    • Opcode:4个比特。操作代码,决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接

      • %x1:表示这是一个文本帧。
      • %x2:表示这是一个二进制帧。
      • %x2:表示这是一个二进制帧。
      • %x3-7:保留的操作代码,用于后续定义的非控制帧。
      • %x8:表示连接断开
      • %x9:表示这是一个ping操作
      • %xA:表示这是一个pong操作
      • %xB-F:保留的操作代码,用于后续定义的控制帧
    • Mask:1个比特。表示是否要对数据载荷进行掩码操作

      • 从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作,如果服务端接收到的数据没有进行掩码操作,服务端需要断开连接。

        在这里插入图片描述

      • 如果Mask是1,那么在 Masking-key中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask都是1。

        在这里插入图片描述

    • Payload length:表示数据载荷的长度,单位是字节,由7位/7+16位/7+64位

      • Payload length=x为0~125:数据的长度为x字节。
      • Payload length=x为126:后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。
      • Payload length=x为127:后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。
      • 如果Payload length占用了多个字节的话,Payload length的二进制表达采用网络序(big endian,重要的位在前)
    • Masking-key:0或4字节(32位),所有从客户端传到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。载荷数据的长度不包括mask key的长度

    • Payload data:(x+y)字节

      • 载荷数据:包括了拓展数据、应用数据。其中拓展数据x字节,应用数据y字节

    2.3.3.具体代码模拟

    // 引入node内的tcp模块,可以接收原始的tcp消息
    import net from 'net';
    import crypto from 'crypto';
    const server = net.createServer(function (socket) { //每个人都会产生一个socket
        // 接收二进制信息
        socket.once('data', function (data) {
            // 将二进制信息转化为字符串
            data = data.toString();
            // 如果升级为websocket协议
            // console.log(data);
            // GET / HTTP/1.1
    		// Host: localhost:3000
    		// Connection: Upgrade
    		// Pragma: no-cache
    		// Cache-Control: no-cache
    		// User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) 						//AppleWebKit/537.36 (KHTML, like Gecko) 
            //Chrome/117.0.0.0 Safari/537.36
    		// Upgrade: websocket
    		// Origin: http://127.0.0.1:5500
    		// Sec-WebSocket-Version: 13
    		// Accept-Encoding: gzip, deflate, br
    		// Accept-Language: zh-CN,zh;q=0.9
    		// Sec-WebSocket-Key: 1tIB0I01z9xlRZt89EDUxw==
    		// Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
            if (data.match(/Upgrade: websocket/)){
                // 报文是以换行来分割的
                let rows = data.split('\r\n');
                // 解析出请求头
                const headers = rows.slice(1,-2).reduce((memo,row)=>{
                    let [key,value] = row.split(': ')
                    // 改成小写
                    memo[key.toLowerCase()] = value;
                    return memo;
                },{});
                const number = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
                let websocketKey = headers['sec-websocket-key'];
                let websocketAccept = crypto.createHash('sha1').update(websocketKey + number).digest('base64');
    
                // 相应报文
                let response = [
                    'HTTP/1.1 101 Switching Protocols',
                    'Upgrade: websocket',
                    `Sec-Websocket-Accept: ${websocketAccept}`,
                    'Connection: Upgrade',
                    '\r\n'
                ].join('\r\n');
    
                // 表示websocket建立连接成功
                socket.write(response);
    
                // 继续解析 后续发来的websocket数据
                socket.on('data', function(buffers) {
                    // 解析websocket的格式
    
                    // 一、客户端发消息过来,先判断消息是否结束了
                    // 第一个字节(1个字节是8个位,如何获取第一位是不是1)
                    // 位运算:
                    // 1、按位或,有一个为1即为1
                    // 0000 1111
                    // 1111 0000
                    //--------------
                    // 1111 1111
                    // 2、按位与,都是1才是1
                    // 0000 1111
                    // 1111 1111
                    // -------------
                    // 0000 1111
                    // 3、异或,相同为0不同为1
                    // 0000 0111
                    // 1000 0110
                    //--------------
                    // 1000 0001
                    const FIN = ((buffers[0] & 0b10000000) === 0b10000000); //表示完成了
                    console.log(FIN); //true
    
                    // 二、判断发送数据的格式
                    // 1表示的是文本,由于前四位不需要所以为0000 1111
                    const OPCOED = (buffers[0] & 0b00001111);
                    console.log(OPCOED); // 1
    
                    // 三、计算masked,由于第一位数已经使用完,这里开始使用第二位
                    const MASKED = ((buffers[1] & 0b10000000) === 0b10000000);
                    console.log(MASKED); //true
    
                    // 四、计算payload_len
                    const PAYLOAD_LEN = ((buffers[1] & 0b01111111));
                    console.log(PAYLOAD_LEN); // 12
    
                    // 五、获取掩码,掩码的长度是4个字节
                    const MASK_KEY = buffers.slice(2,6);
    
                    // 六、获取真正的数据内容,这个内容是被掩码过的,需要用掩码做异或操作(相同为0不同为1)
                    const PAYLOAD = buffers.slice(6);
                    for (let i = 0 ; i<PAYLOAD.length; i++){
                        // 如果数据有多个字节但是掩码是4个字节时
                        PAYLOAD[i] = PAYLOAD[i]^MASK_KEY[i%4];
                    }
                    console.log(PAYLOAD.toString()); // hello server
    
                    // 以上内容为客户端给服务端发送消息流程。
                    // 服务端如果想给客户端发送消息,按照一样的格式发送即可(服务端给客户端发送消息是不用加掩码的)
                     
    
                })
            }
        })
    })
    
    
    server.listen(3000, function() {
        console.log('server start 3000');
    })
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
  • 相关阅读:
    【Python基础知识点总结】
    【MATLAB源码-第48期】基于matlab的16QAM信号盲解调仿真。
    零资源的大语言模型幻觉预防
    鸿鹄电子招投标系统:基于Spring Boot、Mybatis、Redis和Layui的企业电子招采平台源码与立项流程
    Java.lang.Class asSubclass()方法有什么功能呢?
    Pr:添加字幕轨道
    [附源码]java毕业设计壹家吃货店网站
    新生必看:如何选择适合自己的自考专业?
    java教育机构管理计算机毕业设计MyBatis+系统+LW文档+源码+调试部署
    Angular-01:基本架构
  • 原文地址:https://blog.csdn.net/qq_45952745/article/details/133585759