Comet,SSE,WebSocket前后端的实现

邱秋 • 2018年08月11日 • 阅读:1188 • javascript nodejs

Comet(服务器推送)的两种方式

短轮询

页面定时向服务器发送请求, 步骤为:建立连接——数据传输——关闭连接...建立连接——数据传输——关闭连接

//前端js
  var xhr = new XMLHttpRequest();
  setInterval(()=>{
    xhr.onreadystatechange = function () {
      if (xhr.readyState == 4) {
        let result = xhr.responseText
        console.log(result);
      }
    }
    xhr.open('get', '/front/test');
    xhr.send(null);
  },3000)
}

//后端 node
const Koa = require('koa');
const app = new Koa();
const router = require('koa-router')()
const koaBody = require('koa-body');

router.get('/front/test', koaBody(), ctx=>{
  // ctx.set('Connection', 'close');  不设置response.header,默认长连接,否则为短链接
  ctx.body = { title: 'chuchur'}
});
app.use(router.routes());

app.listen(7005, () => {
  console.log('server run in http://localhost:7005');
});

长轮询

长轮询的方式是,页面向服务器发起一个请求,服务器一直保持 tcp 连接打开,知道有数据可发送。发送完数据后,页面关闭该连接,随即又发起一个新的服务器请求,在这一过程中循环,步骤为:建立连接——数据传输...(保存连接)...数据传输——关闭连接

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
  if (xhr.readyState == 4) {
    let result = xhr.responseText;
    console.log(result);
    xhr.open("get", "/front/test"); //在获得数据后重新向服务器发起请求
    xhr.send(null);
  }
};
xhr.open("get", "/front/test");
xhr.send(null);

短轮询和长轮询的区别是:短轮询中服务器对请求立即响应,而长轮询中服务器等待新的数据到来才响应,因此实现了服务器向页面推送实时,并减少了页面的请求次数。

http 流

//前端
var xhr = new XMLHttpRequest();
var received = 0; //最新消息在响应消息的位置
xhr.onreadystatechange = function () {
  if (xhr.readyState == 3) {
    let result = xhr.responseText.substring(received);
    console.log(result);
    received += result.length;
  } else if (xhr.readyState == 4) {
    console.log("完成消息推送");
  }
};
xhr.open("get", "/front/test");
xhr.send(null);

//node
router.get("/front/test", koaBody(), (ctx) => {
  ctx.set({
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });
  ctx.body = { title: "chuchur" };
});

http 流不同于上述两种轮询,因为它在页面整个生命周期内只使用一个 HTTP 连接,具体使用方法即页面向浏览器发送一个请求,而服务器保持 tcp 连接打开,然后不断向浏览器发送数据。

SSE eventSource

eventSource 是用来解决 web 上服务器端向客户端推送消息的问题的。不同于 ajax 轮询的复杂和 websocket 的资源占用过大,eventSource(sse)是一个轻量级的,易使用的消息推送 API ,大多数浏览器实现了 SSE(Server-Sent Events,服务器发送事件) API,SSE 支持短轮询、长轮询和 HTTP

前端实现

//生成EventSource对象,url必须同源
var evtSource = new EventSource("/front/test");
//收到服务器发生的事件时触发,如果连接断开,还会自动重新连接
evtSource.onmessage = function (e) {
  console.log(e.data);
};
//成功与服务器发生连接时触发
evtSource.onopen = function () {
  console.log("Server open");
};
//出现错误时触发
evtSource.onerror = function (e) {
  console.log("Error");
};

//自定义事件
evtSource.addEventListener(
  "test",
  function (e) {
    console.log(e.data);
  },
  false
);

服务端实现

//node
router.get("/front/test", koaBody(), (ctx) => {
  ctx.set({
    "Content-Type": "text/event-stream", //事件流的对应MIME格式为text/event-stream
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });
  ctx.body =
    "id:11\n" + "retry: 100\n" + "event:test\n" + "data: hello word\n\n";
});

服务端返回数据需要特殊的格式,它分为四种消息类型:event, data, id, retry

  • event 指定自定义消息的名称
  • data 指定具体的消息体,可以是对象或者字符串
  • id 为当前消息的标识符,可以不设置
  • retry 设置当前 http 连接失败后,重新连接的间隔。EventSource 规范规定,客户端在 http 连接失败后默认进行重新连接,重连间隔为 3s,通过设置 retry 字段可指定重连间隔;

每个字段都有名称,紧接着有个":"。当出现一个没有名称的字段而只有”:“时,这就会被服务端理解为”注释“,并不会被发送至浏览器端,如: commision

WebSocket 全双工通讯

WebSocketHTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。 浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。

前端实现

let socket = new WebSocket("ws://localhost:7005/ws/test");

// 建立连接
socket.onopen = () => {
  console.log("connection");
  //发送消息,告诉服务器,我上线了.
  socket.send(JSON.stringify({ name: "chuchur" + Date.now(), msg: "我来了" }));
};
//接收消息
socket.onmessage = (evt) => {
  let data = JSON.parse(evt.data);
  console.log(data);
};

//socket 断开
socket.onclose = () => {
  console.log("close");
};
//socket 发生错误
socket.onerror = () => {
  console.log("error");
};

后端的实现

//引入相关ws库
....
const WebSocketServer = require('ws').Server;
//此处定义变量
const server = app.listen(7005, () => {
  console.log('server run in http://localhost:7005');
});
//拿到ws对象
const wss = new WebSocketServer({ server });

//链接成功
wss.on('connection', ws => {
  console.log('有人来了') //当客户端连接之后
  //接收消息
  ws.on('message', data => {
    data = JSON.parse(data)
    console.log(data)

    if (data.name) {
      //广播,告诉大家,有人来了
      wss.broadcast(JSON.stringify({ msg: data.name + '来了,大家欢迎' }))

      //记录ID,可以实现客户端一对一,或一对多通讯,ID为唯一值
      ws.id = data.name
    }
    //实现一对一通话
    if (data.toID) {
      wss.clients.forEach(client => {
        if (data.toID == client.id) {
          client.send(JSON.stringify({ msg: ws.name + '对你说:' + data.msg }))
          return;
        }
      })
    }
  })

  //发送消息
  ws.send(JSON.stringify({ msg: '你来了, 奖励一颗糖' }));
})

//广播
wss.broadcast = data => {
  wss.clients.forEach(client => {
    client.send(data);
  });
};

setInterval(() => {
  console.log('当前在线人数:' + wss.clients.size)
}, 1000);

ws 转发配置

//webpack 开发配置
devServer: {
 ...
 proxy: {
      '/ws': {
          target: 'http://120.0.0.1:7005', //转发到node
          changeOrigin: true,
      }
  }
}
//nginx 部署
server {
    listen      80;
    server_name localhost;

    # 处理WebSocket连接:
    location ^~ /ws/ {
        proxy_pass         http://127.0.0.1:7005;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection "upgrade";
    }

    # 其他所有请求:
    location / {
        proxy_pass       http://127.0.0.1:7005;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

关于更多的 ws 相关配置,可以参考 WS 开发过程中需要自行做转发

总结

  • ajax 短轮询:省事,最简单,不论服务端是否返回数据,埋头苦干,任劳任怨
  • 长轮询:也是埋头,只不过是,拿到数据才做出反应. 服务器有个阻塞的过程.
  • SSE:可以接收服务端推送.接收 http 流,双向可控
  • Socket:全双工通讯,功能强大, 耗资源

各有优缺点, 主要是看什么场景用什么.

我,秦始皇,打钱!