本文主要介绍长连接、短连接、长轮询、短轮询 和 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
;
优点:
- 减少了连接请求;
- 降低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);
});
});
结果:
二、长短轮询
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.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区别
-
- WebSocket是全双工通道,可以双向通信,功能更强;SSE是单向通道,只能服务器向客户端发送;
-
- WebSocket是一个新的协议,需要服务器端支持;SSE则是部署在 HTTP协议之上的,现有的服务器软件都支持;
-
- SSE是一个轻量级协议,相对简单;WebSocket是一种较重的协议,相对复杂;
-
- SSE默认支持断线重连,WebSocket则需要额外部署;
-
- SSE支持自定义发送的数据类型,webSocket只能发送字符串/buffer;
-
- SSE不支持CORS,参数url就是服务器网址,必须与当前网页的网址在同一个网域(domain),而且协议和端口都必须相同;WebSocket支持跨域。
3.4 webSocket优点
四、通信技术比较
从兼容性角度考虑,短轮询 > 长轮询 > 长连接SSE > WebSocket
;
从性能方面考虑,WebSocket > 长连接SSE > 长轮询 > 短轮询
。
参考文章
http的长连接和短连接(史上最通俗!)
长连接、短连接、长轮询和WebSocket
SSE(Server Sent Events) HTTP服务端推送详解
Html5服务器发送事件(sse)在nodejs中的应用