http长短连接和长短轮询、webSocket

本文主要介绍长连接、短连接、长轮询、短轮询 和 webSocket。
长连接、短连接、长轮询、短轮询是基于http的,是由客户端主动发起通信请求;
webSocket是H5新增的基于单个 TCP 连接的通信方式,主要实现一次握手持久性连接,并能进行双向数据传递的通信方式

一、长短连接

http的长连接和短连接(史上最通俗!)
HTTP协议是基于请求/响应模式的,因此只要服务端给了响应,本次HTTP连接就结束了,或者更准确的说,是本次HTTP请求就结束了,根本没有长连接这一说,那么自然也就没有短连接这一说了;网上所说的长连接、短连接,本质其实说的是TCP连接。TCP连接是一个双向通道,它是可以保持一段时间不关闭,因此TCP才有正真的长连接、短连接。

1.1 短连接

概念:客户端发送请求,服务器接收请求,双方建立连接,服务器响应资源,请求结束。

特性:

  • HTTP1.0协议中使用的是短连接;
  • 客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接;当请求网页中的css、js等静态文件时,如果使用短连接,每次都需要重新建立TCP连接,很浪费资源;

缺点:

  • 浪费资源

1.2 长连接

概念:客户端发出请求,服务端接收请求,双方建立连接,在服务端没有返回之前保持连接,当客户端再发送请求时,它会使用同一个连接。这一直继续到客户端或服务器端认为会话已经结束,其中一方中断连接。

特性:

  • 从HTTP1.1协议以后,连接默认都是长连接;现如今的HTTP协议,大部分都是1.1的,因此我们平时用的基本上都是长连接;
  • 长连接实际指TCP长连接;
  • 长连接是为了复用TCP连接,多个HTTP请求可以复用同一个TCP连接,这就节省了很多TCP连接建立和断开的消耗(节约时间、节省流量);
  • 客户端和服务器都需要设置Connection: keep-alive
图1-1 keep-alive

优点:

  • 减少了连接请求;
  • 降低TCP阻塞;
  • 减少了延迟;
  • 实时性较好;

缺点:

  • 影响性能,因为它在文件被请求之后还保持了不必要的连接很长时间;

1.3 长连接SSE

SSE(Server Sent Events) HTTP服务端推送详解
Html5服务器发送事件(sse)在nodejs中的应用
SSE(Sever-Sent Event)服务器发送事件,是HTML5新增的特性,主要用于服务器向客户端发送数据即单双工通信

单双工通信:数据只能单向传递;
半双工通信:数据能双向传递,但是不能同时双向传递;
全双工通信:数据能够同时双向传递;

所谓的SSE,就是浏览器向服务器发送了一个HTTP请求,保持长连接,服务器不断单向地向浏览器推送“信息”,这么做是为了节省网络资源,不用一直发请求,建立新连接。
支持默认3种事件,连接一旦建立就会触发open事件,客户端收到服务器发来的数据,就会触发message事件,如果发生通讯错误(如断开连接)就会触发error事件。

优点:

  • SSE和WebSocket相比,最大的优势是便利,服务端不需要第三方组件,开发难度低;
  • SSE和轮询相比不需要建立或保持大量客户端发往服务器端的请求,节约了很多资源,提升应用性能。

缺点:

  • 如果客户端有很多需要保持很多长连接,会占用大量内存和连接数;
  • 受同源策略的影响,不能跨域;
  • 有兼容问题,IE上不支持。

实现:
前端:

/**
  * SSE受同源策略的影响,不能跨域,此代码在vue中是可以实现的。/apis是代理地址 、 /sse是接口地址
  * 支持默认3种事件,连接一旦建立就会触发open事件,客户端收到服务器发来的数据,就会触发message事件,如果发生通讯错误(如断开连接)就会触发error事件。
*/
// 判断是否支持EventSource
if (typeof EventSource !== 'undefined') {
  // 为http://localhost:8080/apis/sse
  var source = new EventSource('/apis/sse');

  // 接受服务器发来的数据
  source.addEventListener('message', function (e) {
    console.log(e);
  });

  source.addEventListener('open', function (e) {
    console.log('连接sse');
  });

  source.addEventListener('error', function (e) {
    console.log('连接报错了');
  });
}

nodejs:

const http = require('http');
const SSE = require('sse');

var sseClients = [];
 
var server = http.createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('okay');
});
server.listen(8080, '127.0.0.1', function() {
  var sse = new SSE(server, { path: '/sse', verifyRequest: (req) => {
    return true;
  }});
  sse.on('connection', function(client) {
    client.on('close', function() {
      let index = sseClients.indexOf(client);
      if (index > -1) {
        sseClients.splice(index, 1);
      }
    });
    sseClients.push(client);
    client.send('Hello world');
    client.count = 1;
    setInterval(() => {
      sseClients.forEach(function (item, index) {
        item.send(`[${sseClients.length}]服务端推送给客户端${index} : ${item.count}`);
        item.count++;
      });
    }, 1000);
  });
});

结果:


图1-2 SSE请求头

图1-3 SSE运行截图
二、长短轮询
2.1 短轮询

概念:客户端定时向服务器发送Ajax请求,服务器接到请求后马上返回响应信息并关闭连接;即在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客户端的浏览器(可以理解为TCP连接不复用)。

优点:

  • 前后端程序编写比较容易。

缺点:

  • 请求中有大半是无用,难于维护,浪费带宽和服务器资源;如果客户请求频繁/请求头很大,将在TCP的建立和关闭操作上浪费较多时间和带宽;
  • 响应的结果没有顺序(因为是异步请求,当发送的请求没有返回结果的时候,后面的请求又被发送。而此时如果后面的请求比前面的请 求要先返回结果,那么当前面的请求返回结果数据时已经是过时无效的数据了)。

应用场景
传统的web通信模式。后台处理数据,需要一定时间,前端想要知道后端的处理结果,就要不定时的向后端发出请求以获得最新情况。

实例:
适于小型应用。

实现:

function requestApi(url, methed) {
  let ajax = new XMLHttpRequest();
  ajax.open(methed.toUpperCase(), url);
  ajax.setRequestHeader('Authorization', 'token');
  ajax.onreadystatechange = function () {
    if (this.readyState === 4 && this.status === 200) {
      console.log('接口数据', this.response);
    }
  }
  ajax.send();
}

// 响应的结果没有顺序(因为是异步请求,当发送的请求没有返回结果的时候,后面的请求又被发送。而此时如果后面的请求比前面的请 求要先返回结果,那么当前面的请求返回结果数据时已经是过时无效的数据了)。
setInterval(() => {
  requestApi(`http://localhost:13666/polling?_date=${+new Date()}`, 'get');
}, 3000);

2.2 长轮询(comet)

概念:当服务器收到客户端发来的请求后,服务器端不会直接进行响应,而是先将这个请求挂起,然后判断服务器端数据是否有更新;如果有更新,则进行响应,如果一直没有数据,则到达一定的时间限制(服务器端设置,比如nginx需要设置proxy_read_timeout 或者写个心跳检测的代码)才返回, 客户端在处理完服务器返回的信息后,再次发出请求,重新建立连接。

优点:

  • 在无消息的情况下不会频繁的请求,耗费资源小。

缺点:

  • 请求挂起同样会导致资源的浪费。

应用场景:
数据实时更新。

实例
WebQQ、Hi网页版、Facebook IM。

实现:

function requestApi(url, methed) {
  let ajax = new XMLHttpRequest();
  ajax.open(methed.toUpperCase(), url);
  ajax.setRequestHeader('Authorization', 'token');
  ajax.onreadystatechange = function () {
    requestApi(`http://localhost:13666/polling?_date=${+new Date()}`, 'get');
  }
  ajax.send();
}

requestApi(`http://localhost:13666/polling?_date=${+new Date()}`, 'get');
三、webSocket

webSocket是一种网络通信协议,是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

3.1 为什么需要websocket

websocket深入浅出

  • 因为 HTTP 协议有一个缺陷:通信只能由客户端发起;
  • 我们都知道轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开), 因此websocket应运而生。


    图3-1 websocket简介
3.2 实现

前端:

const webSocket = new WebSocket('ws://localhost:13666/test1/1');
webSocket.onopen = () => {
  alert('open:连接成功');
  webSocket.send('hello world');
};
webSocket.onmessage = (e) => {
  console.log('后端推送', e);
};
webSocket.onclose = (e) => {
  console.log('关闭连接', e);
};
webSocket.onerror = (e) => {
  console.log('连接失败', e);
};

后端(nodejs):
app.js文件:

const express = require("express");
const app = express();
const PORT = 13666;

const webSocket = require('./routes/polling/webSocket');

// 将指定目录下的文件对外开放  http://localhost:13666/test.jpg就可以访问到public下的文件了
app.use(express.static('public'));


// app.all() 用于在所有HTTP 请求方法的路径上加载中间件函数, 所有的路由都会走这
app.all('*', (req, res, next) => {
  // 设置跨域访问
  res.header("Access-Control-Allow-Origin", "*");
  /**
   * 解决跨域
   * 包含自定义header字段的跨域请求,浏览器会先向服务器发送OPTIONS请求,探测该服务器是否允许自定义的跨域字段。如果允许,则继续实际的POST/GET正常请求,否则,返回标题所示错误。
   * 若报跨域:...by CORS policy: Request header field range is not allowed by Access-Control-Allow-Headers in preflight response,只需在响应头中包含该字段即可(加入range)
  */
  res.header("Access-Control-Allow-Headers", "content-type,x-requested-with,Authorization,x-ui-request,lang,accept,access-control-allow-origin,range");
  res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
  res.header("X-Powered-By", ' 3.2.1');
  res.header("Content-Type", "application/json;charset=utf-8");

  // 所有接口都会走这,所以可以添加全局处理方法,比如过滤器
  console.log('哈哈哈哈这里可以添加过滤器哦~');
  next();
});

/**
 * 基本路由,也可以使用router -- https://www.expressjs.com.cn/guide/routing.html
 * app.method(url, cbList)
 * method - 方法:get、post、put、delete
 * cbList - 回调函数,可以是个数组/函数,使用next就会调用下一个回调;参数req、res、next
*/

const httpServe = app.listen(PORT, () => {
  console.log('\033[;32m expressService listening at http://localhost:' + PORT + '\033[0m');
});

webSocket(httpServe);

webSocket.js文件:

/*********************************************************************
 * express 实现 webSocket
 * 注意点:
 * 1、send只能发送 字符串/buffer,由于接收到的是Buffer,所以如果需要传对象,需要先将buffer转字符串,再使用JSON.stringify;
 * 2、使用nodejs-websocket包,webSocket和http不能使用一个端口,会报“Error: listen EADDRINUSE: address already in use :::13666”,所以改用ws;
 * 3、使用ws共用一个端口是根据请求头中 Connection:Upgrade 和 Upgrade:websocket 这两个字段确认是否是webSocket;
 *    webSocket和http共用一个端口: https://blog.csdn.net/qq_44856695/article/details/120250286
 *********************************************************************/

const WS = require('ws');

// 不区分地址 - 即ws://localhost:13666 和 ws://localhost:13666/test都能访问到
const bindWs = (httpServer) => {
  const ws = new WS.Server({server: httpServer});
  ws.on('connection', (connect) => wsConnect(connect, '不区分地址'))
}

// ws连接 - 发送消息/关闭连接
const wsConnect = (connect, type) => {
  connect.on('message', (str) => {
    // send只能发送字符串/buffer,接收到的str是buffer类型,使用toString转成字符串
    connect.send(JSON.stringify({type, data: str.toString()}));
    setTimeout(() => {
      // 服务端主动关闭连接
      connect.close();
    }, 3000);
  });
  connect.on('close', (code, reason) => {
    console.log('关闭连接了', code, reason);
  })
}

// 区分地址 - 即只有 ws://localhost:13666/test/:id  和 ws://localhost:13666/test1/:id 能访问,获取id:req.url.match(/\/\d/g)[0].slice(1)
const bindWss = (httpServer) => {
  // ws://localhost:13666/test/:id 使用的ws
  const ws = new WS.Server({noServer: true});
  ws.on('connection', (connect) => wsConnect(connect, 'first'));

  // ws://localhost:13666/test1/:id 使用的ws
  const ws1 = new WS.Server({noServer: true});
  ws1.on('connection', (connect) => wsConnect(connect, 'second'));

  httpServer.on('upgrade', (req, socket, head) => {
    if (req.url.startsWith('/test/')) {
      ws.handleUpgrade(req, socket, head, (connect) => {
        ws.emit('connection', connect, req);
      });
    } else if (req.url.startsWith('/test1/')) {
      ws1.handleUpgrade(req, socket, head, (connect) => {
        ws1.emit('connection', connect, req);
      });
    } else {
      console.log('ws接口不存在');
      socket.destroy();
    }
  })
}
module.exports = function(httpServer) {
  bindWss(httpServer);
}
3.3 SSE和webSocket区别
    1. WebSocket是全双工通道,可以双向通信,功能更强;SSE是单向通道,只能服务器向客户端发送;
    1. WebSocket是一个新的协议,需要服务器端支持;SSE则是部署在 HTTP协议之上的,现有的服务器软件都支持;
    1. SSE是一个轻量级协议,相对简单;WebSocket是一种较重的协议,相对复杂;
    1. SSE默认支持断线重连,WebSocket则需要额外部署;
    1. SSE支持自定义发送的数据类型,webSocket只能发送字符串/buffer;
    1. SSE不支持CORS,参数url就是服务器网址,必须与当前网页的网址在同一个网域(domain),而且协议和端口都必须相同;WebSocket支持跨域。
3.4 webSocket优点
图3-2 webSocket优点
四、通信技术比较

从兼容性角度考虑,短轮询 > 长轮询 > 长连接SSE > WebSocket
从性能方面考虑,WebSocket > 长连接SSE > 长轮询 > 短轮询

参考文章

http的长连接和短连接(史上最通俗!)
长连接、短连接、长轮询和WebSocket
SSE(Server Sent Events) HTTP服务端推送详解
Html5服务器发送事件(sse)在nodejs中的应用

你可能感兴趣的:(http长短连接和长短轮询、webSocket)