Web Worker, Web Socket

306 阅读2分钟

1. 操作系统基础概念

  • 程序: Program, 可供 CPU 执行的代码, 存储在外存中, Ex: .exe -- 静止的

  • 进程: Progress, 把程序从外存中调入内存, 分配必需数据和可执行代码段 -> 进程是程序的可执行状态 -- 活动的

  • 线程: Thread, 线程是进程内部执行代码的基本单位

  • image-20220427105250881
  • 试题: 进程和线程间的关系

    • 进程是操作系统分配内存的基本单位

    • 线程是 CPU 执行代码的基本单位

    • 线程处于进程内部, 一个进程中至少有一个线程, 也可以同时存在多个线程

    • 每个线程也需要自己独立的内存空间, 至少 2 MB

    • 操作系统中多个线程在宏观上看是 "同时执行", 微观上看是 "依次循环执行" -- 并发执行

  • 试题: chrome 浏览器中的线程模型是怎样的?

    • 发起 HTTP 请求使用 6 个并发线程 -- 请求线程

    • 执行所有代码( HTML / CSS / JS ...... ) 使用一个线程 -- UI 渲染主线程

  • 页面中执行一段耗时的 JS 任务, 出现的现象

    <button>按钮1</button>
    <script type="text/javascript" src="xxx.js"></script>
    <button>按钮2</button>
    
    • xxx.js 不退出, 则按钮 2 不可见, 按钮 1 可见但没有事件处理
    • 产生原因: 浏览器中执行代码只使用 1 个 UI 主线程
    • 解决方法: 创建一个并发线程, 让它去执行耗时的 JS 任务, UI 主线程仅负责页面渲染和事件监听

2. Web Worker

2.1. 创建新的执行线程的方法
  • 语法: new Worker('xxx.js')
2.2. Worker 线程致命的缺陷
  • 浏览器不允许 Web Worker 线程使用任务 DOM & BOM 对象 -> 浏览器只允许 UI 主线程使用 DOM & BOM 对象
2.3. 让 Worker 线程给 UI 主线程发消息
  • UI 主线程:

    let myWorker = new Worker('xxx.js')
    myWorker.addEventListener('message', function (e) { e.data })
    // 还可以写成
    myWorker.onmessage = function (e) { e.data }
    
  • Worker 线程

    ......
    postMessage('stringMsg')
    
  • Ex:

    <body>
      <h3>Worker 线程给 UI 主线程发消息</h3>
      <button class="btnOne">按钮1</button>
      <script type="text/javascript">
      	let myWorker = new Worker('workerTest.js')
        myWorker.addEventListener('message', function (e) {
          console.log(`UI 主线程接受到消息:${e.data}`)
        })
      </script>
      <button class="btnTwo">按钮2</button>
    </body>
    
    // workTest.js
    /* Worker 线程给 UI 主线程发消息 */
    console.time('质数计算')
    let num = 9999999999999
    let result = isPrime(num)
    console.timeEnd('质数计算')
    postMessage(`${num}是质数吗? ${result}`)
    function isPrime(num) {
      // 模拟出耗时 5 s+ 的效果
      let start = new Date().getTime()
      do {
        var now = new Date().getTime()
      } while(5000 >= now - start)
      // JS 中的最大整数只能到十位
      num = parseInt(num)
      for(var i = 2;i < num; i++) {
        if (0 === num % i) {
          break
        }    
      }
      if (i < num) {
        return false
      } else {
        return true
      }
    }
    
2.4. 让 UI 主线程给 Worker 线程发消息
  • UI 主线程

    let myWorker = new Worker('xxx.js')
    myWorker.postMessage('stringMsg')
    
  • Worker 线程

    ......
    onmessage = function (e) { e.data }
    
  • Ex:

    <style>
      span.isPrimeBtn{
        display: inline-block; padding: 0 15px;
        height: 25px; line-height: 25px;
        background: #2F4056; color: #fff;
        cursor: pointer;
      }
    </style>
    <body>
      <h3>UI 主线程给 Worker 线程发消息</h3>
      <button class="btnOne">按钮1</button>
      <input type="text" name="txtNumber" />
      <span class="isPrimeBtn">开始质数判断</span>  
      <button class="btnTwo">按钮2</button>
      <script type="text/javascript">
      	let isPrimeBtn = document.body.getElementsByClassName('isPrimeBtn')[0]
        isPrimeBtn.addEventListener('click', function () {
          let num = document.getElementsByName('txtNumber')[0].value
          // 创建 Worker 线程执行耗时任务
          let myWorker = new Worker('workerMsg.js')
          myWorker.postMessage(num)
        })
      </script>
    </body>
    
    // workerMsg.js
    /* Worker 线程给 UI 主线程发消息 */
    onmessage = function (e) {
      let num = e.data
      if (!num) {
        console.log('请输入值后在判断!')
        return false
      }
      console.time('质数计算')  
      let result = isPrime(num)
      console.timeEnd('质数计算')
      console.log(`${num}是质数吗? ${result}`)
    }
    function isPrime(num) {
      // 模拟出耗时 5 s+ 的效果
      let start = new Date().getTime()
      do {
        var now = new Date().getTime()
      } while(5000 >= now - start)
      // JS 中的最大整数只能到十位
      num = parseInt(num)
      for(var i = 2;i < num; i++) {
        if (0 === num % i) {
          break
        }    
      }
      if (i < num) {
        return false
      } else {
        return true
      }
    }
    

总结: 项目中使用 Worker 的场景

  • 页面中需要进行复杂的运算( Ex: 深度递归, 循环嵌套等 )导致很耗时, 在 UI 主线程中执行会导致页面内容 "卡死", 可以使用 Worker 线程与 UI 主线程并发执行
  • Ex: 在页面中有两个输入框, 一个按钮, 点击按钮后对两个输入框中的数字进行整数运算( 假设此运算很耗时 ), 最后在一个 div 中显示运算结果

    <body>
      <h3>使用 Worker 进行复杂运算</h3>
      数字1: <input type="text" name="txtNumberOne" />
      数字2: <input type="text" name="txtNumberTwo" />
      <input type="button" class="numComputeBtn" value="开始执行复杂运算" />
      <div class="computeResult">0</div>
      
      <script type="text/javascript">
      	let numComputeBtn = document.body.getElementsByClassName('numComputeBtn')[0]
          numComputeBtn.addEventListener('click', function () {
            let numOneEl = document.getElementsByName('txtNumberOne')[0]
            let numTwoEl = document.getElementsByName('txtNumberTwo')[0]
            let numOneVal = numOneEl.value
            let numTwoVal = numTwoEl.value
            if (!numOneVal && numTwoVal) {
              alert('请输入数据后在计算!')
              return false
            }
            // 创建并执行 Worker 线程
            let myWorker = new Worker('numCompute.js')
            // 发送消息
            myWorker.postMessage(`${numOneVal},${numTwoVal}`)
            // 接受消息
            myWorker.addEventListener('message', function (e) {
              let computeResult = document.body.getElementsByClassName('computeResult')[0]
              computeResult.innerHTML = e.data
            })
          })
      </script>
    </body>
    
    // numCompute.js
    // 等待接受 UI 主线程的消息
    onmessage = function (e) {
      let arr = e.data.split(',')
      let numOne = parseInt(arr[0])
      let numTwo = parseInt(arr[1])
      // 执行运算
      let result = numOne + numTwo
      // 将处理结果反传给 UI 主线程
      postMessage(result)
    }
    

3. Web Socket

Socket: 插座, 套接字, 源自于 90+ 年代 C 语言, 所有的网络通讯底层都是基于套接字编程 套接字用于 "接受数据 & 发送数据"

3.1. HTTP 协议特点
  • 属于 "请求-响应" 模型的协议, 必须客户端先发送请求, 服务器才会给出响应; 一个请求, 只能得到一个响应 有些应用场景, 此模型有缺陷: 实时走势, 聊天室 -- 即使客户端不发送请求, 服务器端只要有数据更新也应该立即给客户端
  • 解决办法: 长轮询( Long Polling ) / 心跳请求: 定时器 + XHR
    • 该办法并不完美, 心跳过于频繁 -> 服务器压力太大; 不频繁 -> 数据时效性差
3.2. Web Socket 协议

属于 "广播-收听" 模式的协议, 只要客户端连接到服务器就不再断开( 永久连接 ), 双方建立 "全双工通信通道", 一方面不停的给对方发信息, 同时对方也可以发送 / 不发送信息 可以解决 "实时走势, 聊天室" 应用中的问题

3.3. 基于 Web Socket 协议的服务器端程序
  • java, PHP, Node.js 都可以编写
3.4. 基于 Web Socket 协议的客户端程序
  • java, PHP, Node.js, HTML5 都可以编写
3.5. 使用 Node.js 编写一个 Web Socket 协议的服务器端应用
  1. 下载第三方 Web Socket 协议模块

    npm i ws
    
  2. 编写 Web Socket 服务端程序

    // 此 JS 演示 Wen Socket 服务端应用
    const ws = require('ws')
    let server = new ws.Server({ port: 8090 })
    server.on('connection', socket => {
      console.log('WS 服务器接收一个客户端连接......')
      // 套接字孔1: 从客户端接收消息
      socket.on('message', msg => {
        console.log(`WS 服务器接收到消息: ${msg}`)
      })
      // 套接字孔2: 向客户端发送消息
      let counter = 1
      let timer = null
      clearInterval(timer)
      timer = setInterval(() => {
        socket.send(`你好, 我是 WS 服务器 - ${counter}`)
        counter++
      }, 1000)
      // 若连接已断开, 则不再继续发送消息
      socket.on('close', () => {
        console.log('客户端连接已断开')
        clearInterval(timer)
      })
    })
    
3.6. 使用 HTML5 编写一个 Web Socket 协议的客户端应用
<body>
  <h3>使用 HTML5, 创建 Web Socket 协议的客户端程序</h3>
  <button id="btnOne">连接到 WS 服务器</button>
  <button id="btnTwo">开始接受 WS 服务器的消息</button>
  <button id="btnThree">向服务器发送一条消息</button>
  <button id="btnFour">断开 WS 连接</button>
  
  <script type="text/javascript">
  	// 全局变量: 用于与 WS 服务器通信的套接字对象
     let socket = null
     // 创建到 WS 服务器连接
     let btnOne = document.getElementById('btnOne')
     btnOne.addEventListener('click', () => {
       socket = new WebSocket('ws://127.0.0.1:8090')
       socket.addEventListener('open', function () {
         console.log('WS 客户端成功连接到服务器')
       })
     })
    // 接受 WS 服务器的消息
    let btnTwo = document.getElementById('btnTwo')
    btnTwo.addEventListener('click', () => {
      socket.addEventListener('message', e => {
        console.log(`客户端接收到一个消息: ${e.data}`)
      })
    })
    // WS 客户端想服务器发送消息
    let btnThree = document.getElementById('btnThree')
    btnThree.addEventListener('click', () => {
      socket.send(`你好, 我是客户端: ${new Date().getTime()}`)
    })
    // 断开与 WS 服务器的连接
    let btnFour = document.getElementById('btnFour')
    btnFour.addEventListener('click', () => {
      socket.close()
    })
  </script>
</body>