本文主要介紹長(zhǎng)連接、短連接、長(zhǎng)輪詢、短輪詢 和 webSocket。
長(zhǎng)連接、短連接、長(zhǎng)輪詢、短輪詢是基于http
的,是由客戶端主動(dòng)發(fā)起通信請(qǐng)求;
webSocket是H5新增的基于單個(gè) TCP 連接
的通信方式,主要實(shí)現(xiàn)一次握手持久性連接,并能進(jìn)行雙向數(shù)據(jù)傳遞的通信方式
。
一、長(zhǎng)短連接
http的長(zhǎng)連接和短連接(史上最通俗!)
HTTP協(xié)議是基于請(qǐng)求/響應(yīng)模式的,因此只要服務(wù)端給了響應(yīng),本次HTTP連接就結(jié)束了,或者更準(zhǔn)確的說(shuō),是本次HTTP請(qǐng)求就結(jié)束了,根本沒(méi)有長(zhǎng)連接這一說(shuō),那么自然也就沒(méi)有短連接這一說(shuō)了;網(wǎng)上所說(shuō)的長(zhǎng)連接、短連接,本質(zhì)其實(shí)說(shuō)的是TCP連接。TCP連接是一個(gè)雙向通道,它是可以保持一段時(shí)間不關(guān)閉,因此TCP才有正真的長(zhǎng)連接、短連接。
1.1 短連接
概念:客戶端發(fā)送請(qǐng)求,服務(wù)器接收請(qǐng)求,雙方建立連接,服務(wù)器響應(yīng)資源,請(qǐng)求結(jié)束。
特性:
- HTTP1.0協(xié)議中使用的是短連接;
- 客戶端和服務(wù)器每進(jìn)行一次HTTP操作,就建立一次連接,任務(wù)結(jié)束就中斷連接;當(dāng)請(qǐng)求網(wǎng)頁(yè)中的css、js等靜態(tài)文件時(shí),如果使用短連接,每次都需要重新建立TCP連接,很浪費(fèi)資源;
缺點(diǎn):
- 浪費(fèi)資源
1.2 長(zhǎng)連接
概念:客戶端發(fā)出請(qǐng)求,服務(wù)端接收請(qǐng)求,雙方建立連接,在服務(wù)端沒(méi)有返回之前保持連接,當(dāng)客戶端再發(fā)送請(qǐng)求時(shí),它會(huì)使用同一個(gè)連接。這一直繼續(xù)到客戶端或服務(wù)器端認(rèn)為會(huì)話已經(jīng)結(jié)束,其中一方中斷連接。
特性:
- 從HTTP1.1協(xié)議以后,連接默認(rèn)都是長(zhǎng)連接;現(xiàn)如今的HTTP協(xié)議,大部分都是1.1的,因此我們平時(shí)用的基本上都是長(zhǎng)連接;
- 長(zhǎng)連接實(shí)際指TCP長(zhǎng)連接;
- 長(zhǎng)連接是為了復(fù)用TCP連接,多個(gè)HTTP請(qǐng)求可以復(fù)用同一個(gè)TCP連接,這就節(jié)省了很多TCP連接建立和斷開的消耗(節(jié)約時(shí)間、節(jié)省流量);
- 客戶端和服務(wù)器都需要設(shè)置
Connection: keep-alive
;
優(yōu)點(diǎn):
- 減少了連接請(qǐng)求;
- 降低TCP阻塞;
- 減少了延遲;
- 實(shí)時(shí)性較好;
缺點(diǎn):
- 影響性能,因?yàn)樗谖募徽?qǐng)求之后還保持了不必要的連接很長(zhǎng)時(shí)間;
1.3 長(zhǎng)連接SSE
SSE(Server Sent Events) HTTP服務(wù)端推送詳解
Html5服務(wù)器發(fā)送事件(sse)在nodejs中的應(yīng)用
SSE(Sever-Sent Event)服務(wù)器發(fā)送事件,是HTML5新增的特性,主要用于服務(wù)器向客戶端發(fā)送數(shù)據(jù)即單雙工通信。
單雙工通信:數(shù)據(jù)只能單向傳遞;
半雙工通信:數(shù)據(jù)能雙向傳遞,但是不能同時(shí)雙向傳遞;
全雙工通信:數(shù)據(jù)能夠同時(shí)雙向傳遞;
所謂的SSE,就是瀏覽器向服務(wù)器發(fā)送了一個(gè)HTTP請(qǐng)求,保持長(zhǎng)連接,服務(wù)器不斷單向地向?yàn)g覽器推送“信息”,這么做是為了節(jié)省網(wǎng)絡(luò)資源,不用一直發(fā)請(qǐng)求,建立新連接。
支持默認(rèn)3種事件,連接一旦建立就會(huì)觸發(fā)open
事件,客戶端收到服務(wù)器發(fā)來(lái)的數(shù)據(jù),就會(huì)觸發(fā)message
事件,如果發(fā)生通訊錯(cuò)誤(如斷開連接)就會(huì)觸發(fā)error
事件。
優(yōu)點(diǎn):
- SSE和WebSocket相比,最大的優(yōu)勢(shì)是便利,服務(wù)端不需要第三方組件,開發(fā)難度低;
- SSE和輪詢相比不需要建立或保持大量客戶端發(fā)往服務(wù)器端的請(qǐng)求,節(jié)約了很多資源,提升應(yīng)用性能。
缺點(diǎn):
- 如果客戶端有很多需要保持很多長(zhǎng)連接,會(huì)占用大量?jī)?nèi)存和連接數(shù);
- 受同源策略的影響,不能跨域;
- 有兼容問(wèn)題,IE上不支持。
實(shí)現(xiàn):
前端:
/**
* SSE受同源策略的影響,不能跨域,此代碼在vue中是可以實(shí)現(xiàn)的。/apis是代理地址 、 /sse是接口地址
* 支持默認(rèn)3種事件,連接一旦建立就會(huì)觸發(fā)open事件,客戶端收到服務(wù)器發(fā)來(lái)的數(shù)據(jù),就會(huì)觸發(fā)message事件,如果發(fā)生通訊錯(cuò)誤(如斷開連接)就會(huì)觸發(fā)error事件。
*/
// 判斷是否支持EventSource
if (typeof EventSource !== 'undefined') {
// 為http://localhost:8080/apis/sse
var source = new EventSource('/apis/sse');
// 接受服務(wù)器發(fā)來(lái)的數(shù)據(jù)
source.addEventListener('message', function (e) {
console.log(e);
});
source.addEventListener('open', function (e) {
console.log('連接sse');
});
source.addEventListener('error', function (e) {
console.log('連接報(bào)錯(cuò)了');
});
}
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}]服務(wù)端推送給客戶端${index} : ${item.count}`);
item.count++;
});
}, 1000);
});
});
結(jié)果:
二、長(zhǎng)短輪詢
2.1 短輪詢
概念:客戶端定時(shí)向服務(wù)器發(fā)送Ajax請(qǐng)求,服務(wù)器接到請(qǐng)求后馬上返回響應(yīng)信息并關(guān)閉連接;即在特定的的時(shí)間間隔(如每1秒),由瀏覽器對(duì)服務(wù)器發(fā)出HTTP request,然后由服務(wù)器返回最新的數(shù)據(jù)給客戶端的瀏覽器(可以理解為TCP連接不復(fù)用)。
優(yōu)點(diǎn):
- 前后端程序編寫比較容易。
缺點(diǎn):
- 請(qǐng)求中有大半是無(wú)用,難于維護(hù),浪費(fèi)帶寬和服務(wù)器資源;如果客戶請(qǐng)求頻繁/請(qǐng)求頭很大,將在TCP的建立和關(guān)閉操作上浪費(fèi)較多時(shí)間和帶寬;
- 響應(yīng)的結(jié)果沒(méi)有順序(因?yàn)槭钱惒秸?qǐng)求,當(dāng)發(fā)送的請(qǐng)求沒(méi)有返回結(jié)果的時(shí)候,后面的請(qǐng)求又被發(fā)送。而此時(shí)如果后面的請(qǐng)求比前面的請(qǐng) 求要先返回結(jié)果,那么當(dāng)前面的請(qǐng)求返回結(jié)果數(shù)據(jù)時(shí)已經(jīng)是過(guò)時(shí)無(wú)效的數(shù)據(jù)了)。
應(yīng)用場(chǎng)景:
傳統(tǒng)的web通信模式。后臺(tái)處理數(shù)據(jù),需要一定時(shí)間,前端想要知道后端的處理結(jié)果,就要不定時(shí)的向后端發(fā)出請(qǐng)求以獲得最新情況。
實(shí)例:
適于小型應(yīng)用。
實(shí)現(xiàn):
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('接口數(shù)據(jù)', this.response);
}
}
ajax.send();
}
// 響應(yīng)的結(jié)果沒(méi)有順序(因?yàn)槭钱惒秸?qǐng)求,當(dāng)發(fā)送的請(qǐng)求沒(méi)有返回結(jié)果的時(shí)候,后面的請(qǐng)求又被發(fā)送。而此時(shí)如果后面的請(qǐng)求比前面的請(qǐng) 求要先返回結(jié)果,那么當(dāng)前面的請(qǐng)求返回結(jié)果數(shù)據(jù)時(shí)已經(jīng)是過(guò)時(shí)無(wú)效的數(shù)據(jù)了)。
setInterval(() => {
requestApi(`http://localhost:13666/polling?_date=${+new Date()}`, 'get');
}, 3000);
2.2 長(zhǎng)輪詢(comet)
概念:當(dāng)服務(wù)器收到客戶端發(fā)來(lái)的請(qǐng)求后,服務(wù)器端不會(huì)直接進(jìn)行響應(yīng),而是先將這個(gè)請(qǐng)求掛起,然后判斷服務(wù)器端數(shù)據(jù)是否有更新;如果有更新,則進(jìn)行響應(yīng),如果一直沒(méi)有數(shù)據(jù),則到達(dá)一定的時(shí)間限制(服務(wù)器端設(shè)置,比如nginx需要設(shè)置proxy_read_timeout
或者寫個(gè)心跳檢測(cè)的代碼)才返回, 客戶端在處理完服務(wù)器返回的信息后,再次發(fā)出請(qǐng)求,重新建立連接。
優(yōu)點(diǎn):
- 在無(wú)消息的情況下不會(huì)頻繁的請(qǐng)求,耗費(fèi)資源小。
缺點(diǎn):
- 請(qǐng)求掛起同樣會(huì)導(dǎo)致資源的浪費(fèi)。
應(yīng)用場(chǎng)景:
數(shù)據(jù)實(shí)時(shí)更新。
實(shí)例:
WebQQ、Hi網(wǎng)頁(yè)版、Facebook IM。
實(shí)現(xiàn):
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是一種網(wǎng)絡(luò)通信協(xié)議,是 HTML5 開始提供的一種在單個(gè) TCP 連接上進(jìn)行全雙工通訊的協(xié)議。
瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進(jìn)行雙向數(shù)據(jù)傳輸。
3.1 為什么需要websocket
- 因?yàn)?HTTP 協(xié)議有一個(gè)缺陷:通信只能由客戶端發(fā)起;
-
我們都知道輪詢的效率低,非常浪費(fèi)資源(因?yàn)楸仨毑煌_B接,或者 HTTP 連接始終打開), 因此websocket應(yīng)運(yùn)而生。
圖3-1 websocket簡(jiǎn)介
3.2 實(shí)現(xiàn)
前端:
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('關(guān)閉連接', 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');
// 將指定目錄下的文件對(duì)外開放 http://localhost:13666/test.jpg就可以訪問(wèn)到public下的文件了
app.use(express.static('public'));
// app.all() 用于在所有HTTP 請(qǐng)求方法的路徑上加載中間件函數(shù), 所有的路由都會(huì)走這
app.all('*', (req, res, next) => {
// 設(shè)置跨域訪問(wèn)
res.header("Access-Control-Allow-Origin", "*");
/**
* 解決跨域
* 包含自定義header字段的跨域請(qǐng)求,瀏覽器會(huì)先向服務(wù)器發(fā)送OPTIONS請(qǐng)求,探測(cè)該服務(wù)器是否允許自定義的跨域字段。如果允許,則繼續(xù)實(shí)際的POST/GET正常請(qǐng)求,否則,返回標(biāo)題所示錯(cuò)誤。
* 若報(bào)跨域:...by CORS policy: Request header field range is not allowed by Access-Control-Allow-Headers in preflight response,只需在響應(yīng)頭中包含該字段即可(加入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");
// 所有接口都會(huì)走這,所以可以添加全局處理方法,比如過(guò)濾器
console.log('哈哈哈哈這里可以添加過(guò)濾器哦~');
next();
});
/**
* 基本路由,也可以使用router -- https://www.expressjs.com.cn/guide/routing.html
* app.method(url, cbList)
* method - 方法:get、post、put、delete
* cbList - 回調(diào)函數(shù),可以是個(gè)數(shù)組/函數(shù),使用next就會(huì)調(diào)用下一個(gè)回調(diào);參數(shù)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 實(shí)現(xiàn) webSocket
* 注意點(diǎn):
* 1、send只能發(fā)送 字符串/buffer,由于接收到的是Buffer,所以如果需要傳對(duì)象,需要先將buffer轉(zhuǎn)字符串,再使用JSON.stringify;
* 2、使用nodejs-websocket包,webSocket和http不能使用一個(gè)端口,會(huì)報(bào)“Error: listen EADDRINUSE: address already in use :::13666”,所以改用ws;
* 3、使用ws共用一個(gè)端口是根據(jù)請(qǐng)求頭中 Connection:Upgrade 和 Upgrade:websocket 這兩個(gè)字段確認(rèn)是否是webSocket;
* webSocket和http共用一個(gè)端口: https://blog.csdn.net/qq_44856695/article/details/120250286
*********************************************************************/
const WS = require('ws');
// 不區(qū)分地址 - 即ws://localhost:13666 和 ws://localhost:13666/test都能訪問(wèn)到
const bindWs = (httpServer) => {
const ws = new WS.Server({server: httpServer});
ws.on('connection', (connect) => wsConnect(connect, '不區(qū)分地址'))
}
// ws連接 - 發(fā)送消息/關(guān)閉連接
const wsConnect = (connect, type) => {
connect.on('message', (str) => {
// send只能發(fā)送字符串/buffer,接收到的str是buffer類型,使用toString轉(zhuǎn)成字符串
connect.send(JSON.stringify({type, data: str.toString()}));
setTimeout(() => {
// 服務(wù)端主動(dòng)關(guān)閉連接
connect.close();
}, 3000);
});
connect.on('close', (code, reason) => {
console.log('關(guān)閉連接了', code, reason);
})
}
// 區(qū)分地址 - 即只有 ws://localhost:13666/test/:id 和 ws://localhost:13666/test1/:id 能訪問(wèn),獲取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區(qū)別
- WebSocket是全雙工通道,可以雙向通信,功能更強(qiáng);SSE是單向通道,只能服務(wù)器向客戶端發(fā)送;
- WebSocket是一個(gè)新的協(xié)議,需要服務(wù)器端支持;SSE則是部署在 HTTP協(xié)議之上的,現(xiàn)有的服務(wù)器軟件都支持;
- SSE是一個(gè)輕量級(jí)協(xié)議,相對(duì)簡(jiǎn)單;WebSocket是一種較重的協(xié)議,相對(duì)復(fù)雜;
- SSE默認(rèn)支持?jǐn)嗑€重連,WebSocket則需要額外部署;
- SSE支持自定義發(fā)送的數(shù)據(jù)類型,webSocket只能發(fā)送字符串/buffer;
- SSE不支持CORS,參數(shù)url就是服務(wù)器網(wǎng)址,必須與當(dāng)前網(wǎng)頁(yè)的網(wǎng)址在同一個(gè)網(wǎng)域(domain),而且協(xié)議和端口都必須相同;WebSocket支持跨域。
3.4 webSocket優(yōu)點(diǎn)
四、通信技術(shù)比較
從兼容性角度考慮,短輪詢 > 長(zhǎng)輪詢 > 長(zhǎng)連接SSE > WebSocket
;
從性能方面考慮,WebSocket > 長(zhǎng)連接SSE > 長(zhǎng)輪詢 > 短輪詢
。
參考文章
http的長(zhǎng)連接和短連接(史上最通俗!)
長(zhǎng)連接、短連接、長(zhǎng)輪詢和WebSocket
SSE(Server Sent Events) HTTP服務(wù)端推送詳解
Html5服務(wù)器發(fā)送事件(sse)在nodejs中的應(yīng)用