使用轮询、iframe、eventsource、websocket实现服务器推送

337 阅读4分钟

前言

由于http半双工的特性, 同一时刻只允客户端要先向服务器发送请求,然后服务器进行响应, 服务器不能主动推送数据给服务器。 为了解决这个问题, 通常情况下我们可以采取以下的几种方式:

1.轮询

就是客户端过一段时间就调用后台接口发出请求 , 然后服务器进行响应, 造成了服务器推送的假象。 缺点很明显:耗费流量耗费cpu的利用率。 具体的实现代码是: server:

let express = require("express");
let app = express();
const cors = require("cors");
app.use(cors());
app.get("/", (req, res) => {
  res.end(new Date().toLocaleTimeString());
});

app.listen(8080, function () {
  console.log("reaedy");
});

客户端:

    <script>
            setInterval(function () {
                let xhr = new XMLHttpRequest();
                xhr.open('GET', 'http://localhost:8080', true);
                xhr.onreadystatechange = function () {
                    if (xhr.readyState == 4 && xhr.status == 200) {
                        console.log( xhr.responseText)
                       
                    }
                }
                xhr.send();
            }, 1000);
    </script>

2.长轮询

长轮询是对轮询的一个改进, 具体的流程是:

  1. 客户端发送请求
  2. 服务端接收到请求之后如果没有消息就不返回,连接不断开
  3. 当服务端有消息返回的时候, 服务端断开,同时浏览器发出一个新的请求。 可以看出相对于短轮询来说, 减少了请求次数 , 也就减少了资源的浪费, 但是一个http header还是很大 , 无可避免的还是会造成浪费。 实现代码是: 服务端:
let express = require("express");
let app = express();
const cors = require("cors");
app.use(cors());
app.get("/", (req, res) => {
  setTimeout(() => {
    res.end(new Date().toLocaleTimeString());
  }, 10000);
});

app.listen(8080, function () {
  console.log("reaedy");
});

客户端:

    (function getRes  () {
            let xhr = new XMLHttpRequest();
                xhr.open('GET', 'http://localhost:8080', true);
                xhr.onreadystatechange = function () {
                    if (xhr.readyState == 4 && xhr.status == 200) {
                        console.log( xhr.responseText)
                        getRes()
                       
                    }
                }
                xhr.send();
         })()

3.server-sent-events

上面两种方式都是一问一答式的 , sse是一次请求, 多次推送。一个客户端去从服务器端订阅一条,之后服务端可以发送消息给客户端直到服务端或者客户端关闭该“流”。 上代码: sse是支持重连的 , 短时间断开的话 , 会重新连接,比如我下面代码改成count++==10的时候res.end(),然后再浏览器的network上又会看到重新连接了。 服务端: 一定要注意res.write 的格式一定要加上data: ,最后以两个换行结束 , 不然会报错。 支持自定义其他事件, 把event对应的name改一下就好了res.write(event:test\nid:${++count}\ndata:hello${count}\n\n); 客户端监听事件是 :

    eventSource.addEventListener('test', function (res){
        console.log(1111, res)
       })
    
let express = require("express");
let app = express();
const cors = require("cors");
app.use(cors());
app.get("/hello", (req, res) => {
  res.header("Content-Type", "text/event-stream");
  let count = 0;
  setInterval(() => {
    res.write(`event:message\nid:${count++}\ndata:hello${Date.now()}\n\n`);
      //  res.write(`data:hello${Date.now()}\n\n`);
  }, 1000);
});

app.listen(8080, function () {
  console.log("reaedy");
});

客户端: 使用h5自带的eventsource

    const eventSource  = new EventSource('http://localhost:8080/hello')
       eventSource.onmessage = function (res) {
        console.log(res)
       }
       eventSource.onerror = function (){}
    

4.iframe 实现服务器推送

iframe的src会设置长连接,具体步骤如下:

  1. iframe设置display:none
  2. src设置为请求地址
  3. 通过addEventListener('message')进行监听。 优点是浏览器的兼容性比较好。 缺点是:
  4. 由于连接没有断开 , 所以浏览器标签页的 loading icon会一直的旋转
  5. 服务器维护长连接需要一个资源的浪费 注意点: parent.postMessage是因为出现了跨域 ,第一个参数是value值, 记得加上引号,因为不然就是一个变量,第二个参数是客户端的url,这样做可以解决跨域问题。 服务端:
let express = require("express");
let app = express();
const cors = require("cors");
app.use(cors());
let count = 0;
app.get("/hello", (req, res) => {
  setInterval(() => {
    res.write(`
    <script>
    parent.postMessage(
     ' hello${count++}',
      "http://127.0.0.1:7000/" 
  );
    // parent.getElementById("clock").innerHTML(hello${count++})
  </script>
    `);
  }, 1000);
});

app.listen(8080, function () {
  console.log("reaedy");
});

客户端

  <body>
    <iframe src="http://localhost:8080/hello" frameborder="0" style="display:none"></iframe>
    <script>
      window.addEventListener("message", function( event ) {
 console.log(12313123,event)
 });

    </script>

5.websocket

WebSocket是一种全新的协议,随着HTML5草案的不断完善,越来越多的现代浏览器开始全面支持WebSocket技术了,它将TCP的Socket(套接字)应用在了webpage上,从而使通信双方建立起一个保持在活动状态连接通道。 WebSocket协议是借用HTTP协议的101 switchprotocol(服务器根据客户端的指定,将协议转换成为 Upgrade首部所列的协议)来达到协议转换的,从HTTP协议切换成WebSocket通信协议。

如何建立连接

  1. 首先客户端发出了一个ws的请求 ,可以看出采用了http的报文格式
GET ws://localhost:8080/ws HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Origin: http://localhost:8080
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: 8cQjKCYrFciflQ2VcsMZjA==

2.服务器相应 , status切换为101表示协议切换。

服务端使用的是ws这个库

let WebSocketServer = require("ws").Server;
let wsServer = new WebSocketServer({ port: 8888 });
wsServer.on("connection", function (socket) {
  console.log("连接成功");
  socket.on("message", function (message) {
    console.log("接收到客户端消息:" + message);
    socket.send("服务器回应:" + message);
  });
});

客户端使用了websocket

let ws = new WebSocket("ws://localhost:8888");
ws.onopen = function () {
  console.log("客户端连接成功");
  ws.send("hello");
};
ws.onmessage = function (event) {
  console.log("收到服务器的响应 " + event.data);
};

sse跟websocket的区别:

  1. websocket是一种全双工协议 , 但是sse是半双工的
  2. sse使用的是http协议 , websocket是基于websocket协议
  3. sse轻量级的
  4. sse默认支持断线重连 , websocket需要自己去实现
  5. sse一般用来传送文本,mime类型是text/event-stream

socket.io

详见文档

socket.io 是一个websocket库 , 包含了客户端和服务端 既然websocket相对于前面说的那几种已经可以实现相互通信了 , 为什么我们还要使用socket.io呢? 这是因为socket.io有下面几种优点 :

  • 易用性:socket.io封装了服务端和客户端,使用起来非常简单方便。
  • 跨平台:socket.io支持跨平台,这就意味着你有了更多的选择,可以在自己喜欢的平台下开发实时应用。
  • 自适应:它会自动根据浏览器从WebSocket、AJAX长轮询、Iframe流等等各种方式中选择最佳的方式来实现网络实时应用,非常方便和人性化,而且支持的浏览器最低达IE5.5。

服务端:

var express = require("express");
var path = require("path");

var app = express();
const { Server } = require("socket.io");
app.get("/", function (req, res) {
  res.sendFile(path.resolve("index.html"));
});

var server = require("http").createServer(app);
var io = new Server(server, {
  cors: true,
  origins: ["http://localhost:8002/"],
}); //可以解决跨域问题 

io.on("connection", function (socket) {
  console.log("connect1111");
  socket.send("sever send:");
  io.on("message", function (msg) {
    console.log(msg);
  });
});

server.listen(8001);

客户端

import { io } from "socket.io-client";
const socket = io("http://localhost:8001");

socket.on("connect", function () {
  console.log("connect");
  socket.send("hello");
  socket.on("message", function (msg) {
    console.log(1111, msg);
  });
});