WebSocket 与传统 HTTP 有如下好处。

  1. 客户端与服务器端只建立一个 TCP 连接,可以使用更少的连接。
  2. WebSocket 服务器端可以推送数据到客户端,这远比 HTTP 请求响应模式更灵活、更高效。
  3. 有更轻量级的协议头,减少数据传送量。

WebSocket 最早是作为 HTML5 重要特性而出现的,现代浏览器大多都支持 WebSocket 协议,接下来我们用一段代码来展现 WebSocket 在客户端的应用示例:

let ws = new WebSocket('ws://localhost:8888')
ws.onopen = function () {
  console.log('客户端链接成功')
  ws.send('hello')
}
ws.onmessage = function (event) {
  console.log('客户端收到服务端的消息', event.data)
}

上述代码中,浏览器与服务器端创建 WebSocket 协议请求,在请求完成后连接打开,向服务器端发送一次数据,同时可以通过 onmessage() 方法接收服务器端传来的数据。这行为与 TCP 客户端十分相似,相较于 HTTP,它能够双向通信。浏览器一旦能够使用 WebSocket,可以想象应用的使用空间极大。

在 WebSocket 之前,网页客户端与服务器端进行通信最高效的是 Comet 技术。Comet是一种用于web的推送技术,能使服务器能实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有三种实现方式:轮询(polling) 长轮询(long-polling)和iframe流(streaming)。

轮询

轮询是客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。

这种方式连接数会很多,一个接受,一个发送。而且每次发送请求都会有 Http 的 Header,会很耗流量,也会消耗 CPU 的利用率。

let express = require('express')
const app = express()
const path = require('path')

// 每次请求都立刻返回
app.get('/clock', function (req, res) {
  res.send(new Date().toLocaleString())
})

app.use(express.static(path.resolve(__dirname, 'public')))
app.listen(8080)
<body>
  <div id="app"></div>
  <script>
    // 每1秒向服务器请求时间
    setInterval(() => {
      let xhr = new XMLHttpRequest()
      xhr.open('GET', '/clock', true)
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
          app.innerHTML = xhr.responseText
        }
      }
      xhr.send(null)
    }, 1000)
  </script>
</body>

长轮询

长轮询是对轮询的改进版,客户端发送HTTP给服务器之后,看有没有新消息,如果没有新消息,就一直等待。

当有新消息的时候,才会返回给客户端。在某种程度上减小了网络带宽和CPU利用率等问题。

由于 http 数据包的头部数据量往往很大(通常有400多个字节),但是真正被服务器需要的数据却很少(有时只有 10 个字节左右),这样的数据包在网络上周期性的传输,难免对网络带宽是一种浪费

let express = require('express')
const app = express()
const path = require('path')

// 1秒之后更新数据才返回
app.get('/clock', function (req, res) {
    setTimeout(() => {
      res.send(new Date().toLocaleString())
    }, 1000);
})

app.use(express.static(path.resolve(__dirname, 'public')))
app.listen(8080)
<body>
  <div id="app"></div>
  <script>
	// 请求完成之后才发送下次请求
    (function poll () {
      let xhr = new XMLHttpRequest()
      xhr.open('GET', '/clock', true)
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status === 200) {
          app.innerHTML = xhr.responseText
          poll()
        }
      }
      xhr.send(null)
    })()
  </script>
</body>

iframe流

通过在HTML页面里嵌入一个隐藏的iframe,然后将这个iframe的src属性设为对一个长连接的请求,服务器端就能源源不断地往客户推送数据。

let express = require('express')
const app = express()
const path = require('path')
app.get('/clock', function (req, res) {
    // 拿到父窗口,一直往里面写,没有调用end
    setInterval(() => {
      res.write(
        `
        <script>
        parent.document.getElementById('app').innerHTML="${new Date().toLocaleString()}";
        </script>
        `
      )
    }, 1000);
})

app.use(express.static(path.resolve(__dirname, 'public')))
<body>
  <div id="app">
  </div>
  <iframe src="/clock" frameborder="0"></iframe>
</body>

这些通讯方式各有缺点,你都学会了吗?