Node-網(wǎng)絡(luò)編程

構(gòu)建TCP服務(wù)

TCP是面向連接的協(xié)議,其顯著的特征是在傳輸之前需要3次握手形成會(huì)話,只有會(huì)話形成之后,服務(wù)端和客戶(hù)端之間才能互相發(fā)送數(shù)據(jù),在創(chuàng)建會(huì)話的過(guò)程中,服務(wù)端和客戶(hù)端分別提供一個(gè)套接字,這兩個(gè)套接字共同形成一個(gè)連接,服務(wù)端與客戶(hù)端則通過(guò)套接字實(shí)現(xiàn)兩者之間連接的操作。

創(chuàng)建TCP服務(wù)器端

var net = require('net');
var server = net.createServer(function (socket) {
  // 新的連接
  socket.on('data', function (data) {
    socket.write("你好");
  });

  socket.on('end', function () {
    console.log('連接斷開(kāi)');
  });
  socket.write("歡迎光臨深入淺出Node.js");
});

server.listen(8124, function () {
  console.log('server bound');
});

然后可以通過(guò)net模塊自行構(gòu)造客戶(hù)端進(jìn)行會(huì)話,測(cè)試上面構(gòu)建的TCP服務(wù)的代碼。

// client.js
var net = require('net');
var client = net.connect({port: 8124}, function () { //'connect' listener
  console.log('client connected');
  client.write('world!\r\n');
});

client.on('data', function (data) {
  console.log(data.toString());
  client.end(); // 主動(dòng)關(guān)閉連接
});

client.on('end', function () {
  console.log('client disconnected');
});
$ node client.js
client connected
歡迎光臨深入淺出Node.js

你好
client disconnected

TCP服務(wù)的事件

服務(wù)器事件

對(duì)于通過(guò)net.createServer()創(chuàng)建的服務(wù)器而言,它是一個(gè)EventEmitter實(shí)例,它的自定義事件有如下幾種:

  • listening: 在調(diào)用server.listen()綁定端口觸發(fā),對(duì)這個(gè)事件的監(jiān)聽(tīng),簡(jiǎn)潔寫(xiě)法:可以當(dāng)做server.listen()的第二個(gè)參數(shù)傳入,server.listen(port,listeningListener)

  • connection: 每個(gè)客戶(hù)端套接字連接到服務(wù)器端時(shí)觸發(fā),同樣這個(gè)事件的監(jiān)聽(tīng)也有簡(jiǎn)潔寫(xiě)法,當(dāng)做net.createServer()最后一個(gè)參數(shù)傳遞。

  • close: 當(dāng)服務(wù)器關(guān)閉時(shí)觸發(fā),在調(diào)用sever.close()后,服務(wù)器將停止接受新的套接字連接,但保持當(dāng)前存在的連接,等待所有連接都斷開(kāi)后,會(huì)觸發(fā)該事件。

  • error: 當(dāng)服務(wù)器發(fā)生異常時(shí),將會(huì)觸發(fā)該事件,比如偵聽(tīng)一個(gè)使用中的端口,將會(huì)觸發(fā)一個(gè)異常,如果不偵聽(tīng)error事件,服務(wù)器將會(huì)拋出異常。

連接事件

服務(wù)器可以同時(shí)與多個(gè)客戶(hù)端保持連接,對(duì)于每個(gè)連接而言是典型的可寫(xiě)可讀Stream對(duì)象,就是上面代碼示例中的socket,用于服務(wù)端和客戶(hù)端之間的通信。它的事件有:

  • data: 當(dāng)一端調(diào)用write()事件發(fā)送數(shù)據(jù)時(shí),另一端接收到數(shù)據(jù)就會(huì)觸發(fā)data事件,傳遞的數(shù)據(jù)就是write()發(fā)送的。

  • end: 當(dāng)連接中的任意一端發(fā)送了FIN數(shù)據(jù)時(shí),將會(huì)觸發(fā)該事件。

  • connect: 該事件用于客戶(hù)端,當(dāng)套接字與服務(wù)器端連接成功時(shí)會(huì)被觸發(fā),就是上面示例代碼client.js中的'connect' listener,也是一種簡(jiǎn)寫(xiě)的方式。

  • drain: 當(dāng)任意一端調(diào)用write()發(fā)送時(shí),當(dāng)前這段會(huì)觸發(fā)該事件。

  • error: 當(dāng)異常發(fā)生時(shí),觸發(fā)該事件。

  • close: 當(dāng)套接字完全關(guān)閉時(shí),觸發(fā)該事件。

  • timeout: 當(dāng)一定時(shí)間后連接不再活躍時(shí),該事件將會(huì)被觸發(fā),通知用戶(hù)當(dāng)前該連接已經(jīng)被閑置了。

{{% notice info %}}
值得注意的是,TCP針對(duì)網(wǎng)絡(luò)中的小數(shù)據(jù)包有一定的優(yōu)化策略:Nagle算法。TCP/IP協(xié)議中,無(wú)論發(fā)送多少數(shù)據(jù),總要在數(shù)據(jù)前面加上協(xié)議頭,同時(shí),對(duì)方接到數(shù)據(jù),也需要發(fā)送ACK表示確認(rèn),為了盡可能的利用網(wǎng)絡(luò)帶寬,TCP總是希望盡可能的發(fā)送足夠大的數(shù)據(jù)(一個(gè)連接會(huì)設(shè)置MSS參數(shù),因此,TCP/IP希望每次能夠以MSS尺寸的數(shù)據(jù)塊來(lái)發(fā)送數(shù)據(jù)),Nagle算法就是為了盡可能發(fā)送大塊數(shù)據(jù),避免網(wǎng)絡(luò)中充斥著許多小數(shù)據(jù)塊。




如果每次只發(fā)送1字節(jié)的數(shù)據(jù),會(huì)在傳輸上造成41字節(jié)的包,其中包括1字節(jié)的有用信息和40字節(jié)的首部數(shù)據(jù),這種情況轉(zhuǎn)變成了4000%的消耗,對(duì)于輕負(fù)載的網(wǎng)絡(luò)還是可以接受的,但是重負(fù)載的就受不了了。Nagle算法通常會(huì)在未確認(rèn)數(shù)據(jù)發(fā)送的時(shí)候讓發(fā)送器把數(shù)據(jù)送到緩存里,任何數(shù)據(jù)隨后繼續(xù)直到得到明顯的數(shù)據(jù)確認(rèn)或者直到攢到了一定數(shù)量的數(shù)據(jù)了再發(fā)包。

{{% /notice %}}

在Node中,TCP默認(rèn)開(kāi)啟Nagle算法,可調(diào)用socket.setNoDelay(true)去掉Nagle算法,使得write()可以立即發(fā)送數(shù)據(jù)到網(wǎng)絡(luò)中。

另一個(gè)需要注意的是,盡管在網(wǎng)絡(luò)的一段調(diào)用writre()會(huì)觸發(fā)另一端得到data事件,但是并不意味著每次write()都會(huì)觸發(fā)一次data事件,在關(guān)閉掉Nagle算法后,接受端可能會(huì)將接受到的多個(gè)小數(shù)據(jù)包合并,然后只觸發(fā)一次data事件。

構(gòu)建UDP服務(wù)

UDP又稱(chēng)用戶(hù)數(shù)據(jù)包協(xié)議,與TCP一樣同屬于網(wǎng)絡(luò)傳輸層,UDP和TCP最大的同是UDP不是面向連接的,在UDP中,一個(gè)套接字可以與多個(gè)UDP服務(wù)通信,它雖然提供面向事務(wù)的簡(jiǎn)單不可靠信息傳輸服務(wù),在網(wǎng)絡(luò)差的情況下存在丟到嚴(yán)重的情況,但是它無(wú)須連接、資源消耗低、處理快速且靈活,所以常常應(yīng)用在那種偶爾丟一兩個(gè)數(shù)據(jù)包也不會(huì)產(chǎn)生重大影響的場(chǎng)景,比如音頻、視頻等,DNS服務(wù)也是基于它實(shí)現(xiàn)的。

創(chuàng)建UDP套接字

UDP套接字一旦創(chuàng)建,既可以作為客戶(hù)端發(fā)送數(shù)據(jù),也可以作為服務(wù)端接受數(shù)據(jù),

var dgram = require('dgram');
var socket = dgram.createSocket("udp4"); // 套接字

創(chuàng)建UDP服務(wù)端

var dgram = require('dgram');
var server = dgram.createSocket("udp4"); // 套接字

// 套接字事件
// 當(dāng)UDP套接字偵聽(tīng)網(wǎng)卡端口后,接受到消息后觸發(fā),觸發(fā)攜帶的數(shù)據(jù)為消息的Buffer對(duì)象和一個(gè)遠(yuǎn)程地址信息
server.on("message", function (msg, rinfo) {
  console.log("server got: " + msg + " from " + rinfo.address + ":" + rinfo.port);
});

// 當(dāng)UDP套接字開(kāi)始偵聽(tīng)時(shí)觸發(fā)
server.on("listening", function () {
  var address = server.address();
  console.log("server listening " + address.address + ":" + address.port);
});

// 還有close事件 和error事件
// close:調(diào)用close()方式時(shí)觸發(fā)該事件,并不再觸發(fā)message事件,如需繼續(xù)觸發(fā)message 重新bind即可
// error:當(dāng)異常發(fā)生時(shí)觸發(fā)該事件,如果不偵聽(tīng),異常將直接拋出,使進(jìn)程退出

server.bind(41234); // 接受網(wǎng)卡上所有41234端口上的消息,綁定完成后,觸發(fā)listening事件

創(chuàng)建UDP客戶(hù)端

var dgram = require('dgram');
var message = new Buffer("深入淺出Node.js");
var client = dgram.createSocket("udp4");

client.send(message, 0, message.length, 41234, "localhost", function(err, bytes) {
  client.close();
});

當(dāng)套接字對(duì)象用在客戶(hù)端時(shí),可以調(diào)用send方法發(fā)送消息到網(wǎng)絡(luò)中,send的參數(shù)如下socket.send(buf, offset, length, port, address, [callback])

分別為要發(fā)送的Buffer、Buffer的偏移、Buffer的長(zhǎng)度、目標(biāo)端口、目標(biāo)地址、發(fā)送完成后的回調(diào)。雖然參數(shù)列表相對(duì)復(fù)雜,但是它更靈活的地方在于可以隨意發(fā)送數(shù)據(jù)到網(wǎng)絡(luò)中的服務(wù)器端,而TCP如果要發(fā)送數(shù)據(jù)給另一個(gè)服務(wù)器端,則需要重新通過(guò)套接字構(gòu)造新的連接。

構(gòu)建HTTP服務(wù)

HTTP是應(yīng)用層協(xié)議,Node提供了http和https模塊用于HTTP和HTTPS的封裝。

HTTP協(xié)議構(gòu)建在請(qǐng)求和響應(yīng)的概念上,對(duì)應(yīng)在Node.js中就是由http.ServerRequest和http.ServerResponse這兩個(gè)構(gòu)造器構(gòu)造出來(lái)的對(duì)象。

當(dāng)用戶(hù)瀏覽一個(gè)網(wǎng)站時(shí),用戶(hù)代理(瀏覽器)會(huì)創(chuàng)建一個(gè)請(qǐng)求,該請(qǐng)求通過(guò)TCP發(fā)送給Web服務(wù)器,隨后服務(wù)器會(huì)給出響應(yīng)。

在構(gòu)建TCP服務(wù)器時(shí),createServer()中接受的回調(diào)的參數(shù)是一個(gè)連接對(duì)象(connection)對(duì)象,而在HTTP服務(wù)器中則是請(qǐng)求和響應(yīng)對(duì)象。

盡管我們可以通過(guò)req.connection獲取TCP連接對(duì)象,但大多數(shù)情況下你還是與請(qǐng)求和響應(yīng)的抽象打交道,默認(rèn)情況下,Node會(huì)告訴瀏覽器始終保持連接(請(qǐng)求頭:connection: keep-alive),通過(guò)它發(fā)送更多的請(qǐng)求,這是為了提高性能,因?yàn)椴幌肜速M(fèi)時(shí)間去重新建立和關(guān)閉TCP連接,當(dāng)然我們可以使用writeHead()傳遞一個(gè)不同的值,如Close,將連接關(guān)閉。

HTTP

HTTP全稱(chēng)是超文本傳輸協(xié)議(HyperText Transfer Protocol),在其兩端是服務(wù)器和瀏覽器,即B/S模式,Web即是HTTP應(yīng)用。

HTTP是基于請(qǐng)求響應(yīng)式的,以一問(wèn)一答的方式實(shí)現(xiàn)服務(wù),雖然基于TCP會(huì)話,但是本身并無(wú)會(huì)話的特點(diǎn),從協(xié)議的的角度來(lái)說(shuō),瀏覽器其實(shí)是一個(gè)HTTP的代理,用戶(hù)的行為會(huì)通過(guò)它轉(zhuǎn)化為HTTP請(qǐng)求報(bào)文發(fā)送給服務(wù)端,服務(wù)器端在處理請(qǐng)求后,發(fā)送響應(yīng)報(bào)文給代理,代理在解析報(bào)文后,將用戶(hù)需要的內(nèi)容呈現(xiàn)在界面上。簡(jiǎn)而言之。HTTP服務(wù)只做兩件事,處理HTTP請(qǐng)求發(fā)送和發(fā)送HTTP響應(yīng)。

無(wú)論是請(qǐng)求報(bào)文還是響應(yīng)報(bào)文,報(bào)文內(nèi)容都包含兩個(gè)部分報(bào)文頭和報(bào)文體,但是GET的請(qǐng)求報(bào)文中沒(méi)有包含報(bào)文體,傳遞的消息包含在報(bào)文頭中。

http模塊

在Node中,HTTP服務(wù)繼承自TCP服務(wù)器(net模塊),它能夠與多個(gè)客戶(hù)端保持連接,由于其采用事件驅(qū)動(dòng)的形式,并不為每一個(gè)連接創(chuàng)建額外的線程或進(jìn)程,保持很低的內(nèi)存占用,所以能實(shí)現(xiàn)高并發(fā)。

HTTP服務(wù)于TCP服務(wù)模型有區(qū)別的地方在于,在開(kāi)啟keepalive后,一個(gè)TCP會(huì)話可以用于多次請(qǐng)求和響應(yīng),TCP服務(wù)以connection為單位進(jìn)行服務(wù),HTTP服務(wù)以request進(jìn)行服務(wù)。

http模塊將連接所用套接字的讀寫(xiě)抽象為ServerRequest和ServerResponse對(duì)象,它們分別對(duì)應(yīng)請(qǐng)求和響應(yīng)操作(http服務(wù)回調(diào)中常用的req, res),在請(qǐng)求產(chǎn)生的過(guò)程中,http模塊拿到連接中傳來(lái)的數(shù)據(jù),調(diào)用二進(jìn)制模塊http_parser進(jìn)行解析,在解析完請(qǐng)求報(bào)文的報(bào)頭后,觸發(fā)request事件,調(diào)用用戶(hù)的業(yè)務(wù)邏輯。

http請(qǐng)求

對(duì)于TCP連接的讀操作,http模塊將其封裝為ServerRequest對(duì)象,報(bào)文頭通過(guò)http_parser進(jìn)行解析。

報(bào)文頭解析為如下屬性。

  • req.method:請(qǐng)求方法

  • req.url:請(qǐng)求的url

  • req.httpVersion:使用的http的版本

  • req.headers: 其余報(bào)文,包含'user-agent'、'host'、'accept'等。

報(bào)文體部分則抽象為一個(gè)只讀流對(duì)象,如果業(yè)務(wù)邏輯需要讀取報(bào)文體中的數(shù)據(jù),則需要在數(shù)據(jù)流結(jié)束后才能進(jìn)行操作,即req對(duì)象上的end事件觸發(fā)后。

http響應(yīng)

http模塊將其封裝為ServerResponse對(duì)象,可以將其看成一個(gè)可寫(xiě)的流對(duì)象,它影響響應(yīng)報(bào)文頭部信息的API為res.setHeader()和res.writeHead()

我們可以調(diào)用setHeader進(jìn)行多次設(shè)置,但只有調(diào)用writeHead后,報(bào)頭才會(huì)寫(xiě)入到連接中,除此之外,http模塊會(huì)自動(dòng)幫你設(shè)置一些頭信息。

報(bào)文體部分則是調(diào)用res.write()和res.end()方法實(shí)現(xiàn),res.end()會(huì)調(diào)用write()發(fā)送數(shù)據(jù),然后發(fā)送信號(hào)告知服務(wù)器這次響應(yīng)結(jié)束。

值得注意的是,報(bào)頭是在報(bào)文體發(fā)送前發(fā)送的,一旦開(kāi)始了數(shù)據(jù)的發(fā)送,writeHead()和setHeader()將不再生效。

{{% notice tip %}}
無(wú)論服務(wù)端在處理業(yè)務(wù)邏輯時(shí)是否發(fā)生異常,務(wù)必在結(jié)束時(shí)調(diào)用res.end()結(jié)束請(qǐng)求,否則客戶(hù)端將一直處于等待的狀態(tài)。
{{% /notice %}}

http服務(wù)的事件

  • connection事件:在開(kāi)始http請(qǐng)求和響應(yīng)前,客戶(hù)端和服務(wù)器端需要建立底層的TCP連接,這個(gè)連接可能因?yàn)殚_(kāi)啟了keep-alive,可以在多次請(qǐng)求和響應(yīng)之間使用,當(dāng)這個(gè)連接建立時(shí)服務(wù)器觸發(fā)一次connection事件。

  • request事件:建立TCP連接后,http模塊底層將在數(shù)據(jù)流中抽象出HTTP請(qǐng)求和HTTP響應(yīng),當(dāng)請(qǐng)求數(shù)據(jù)發(fā)送到服務(wù)端,并解析出http請(qǐng)求頭后,會(huì)觸發(fā)該事件。

  • close事件:與TCP服務(wù)器的行為一致,調(diào)用server.close()方法停止接受新的連接,當(dāng)已有的連接都段開(kāi)始,觸發(fā)該事件。

  • checkContinue事件:某些客戶(hù)端在發(fā)送較大的數(shù)據(jù)時(shí),并不會(huì)將數(shù)據(jù)直接發(fā)送,而是先發(fā)送頭部帶Expect: 100-continue的請(qǐng)求到服務(wù)器,服務(wù)器觸發(fā)checkContinue觸發(fā),如果服務(wù)器沒(méi)有監(jiān)聽(tīng)這個(gè)事件,將自動(dòng)響應(yīng)客戶(hù)帶100 Continue的狀態(tài)碼表示接受數(shù)據(jù)上傳,如果不接受的數(shù)據(jù)較多時(shí),則響應(yīng)400 Bad Request拒絕客戶(hù)端繼續(xù)發(fā)送,需要注意的是,當(dāng)該事件發(fā)送時(shí)不會(huì)觸發(fā)request事件,兩個(gè)事件之間互斥,當(dāng)客戶(hù)端收到100 Continue后重新發(fā)起請(qǐng)求時(shí),才會(huì)觸發(fā)request事件。

  • connect事件:當(dāng)客戶(hù)端發(fā)起CONNECT請(qǐng)求時(shí)觸發(fā),而發(fā)起CONNECT請(qǐng)求通常在HTTP代理時(shí)出現(xiàn),如果不監(jiān)聽(tīng)該事件,發(fā)起該請(qǐng)求的連接將會(huì)關(guān)閉。

  • upgradd事件:當(dāng)客戶(hù)端要求升級(jí)連接的協(xié)議時(shí),需要和服務(wù)器端協(xié)商,客戶(hù)端會(huì)在請(qǐng)求頭中帶上Upgrade字段,服務(wù)器會(huì)在接受到這樣的請(qǐng)求時(shí)觸發(fā)該事件,如果不監(jiān)聽(tīng)該事件,發(fā)起該請(qǐng)求的連接將會(huì)關(guān)閉。

  • clientError事件:連接的客戶(hù)端觸發(fā)error事件時(shí),這個(gè)錯(cuò)誤會(huì)傳遞到服務(wù)器端,此時(shí)觸發(fā)該事件。

HTTP客戶(hù)端

http模塊提供了一個(gè)底層API:http.request(options, connect),用于構(gòu)造HTTP客戶(hù)端

var options = { 
  hostname: '127.0.0.1', // 服務(wù)器名稱(chēng)
  port: 1334, // 服務(wù)器端口
  path: '/', // 具體請(qǐng)求的路由
  method: 'GET' // 請(qǐng)求的方法
};

// 其他選項(xiàng)
// host: 服務(wù)器的域名或IP地址,默認(rèn)為localhost
// localAddress: 建立網(wǎng)絡(luò)連接的本地網(wǎng)卡
// socketPath: Domain套接字路徑
// headers: 請(qǐng)求頭對(duì)象
// auth: Basic認(rèn)證,這個(gè)值計(jì)算成請(qǐng)求頭中Authorization部分

var req = http.request(options, function(res) { // response listener
  console.log('STATUS: ' + res.statusCode); 
  console.log('HEADERS: ' + JSON.stringify(res.headers)); 
  res.setEncoding('utf8');
  res.on('data', function (chunk) { 
    console.log(chunk);
  }); 
});

req.end();

HTTP代理

為了重用TCP連接,http模塊包含一個(gè)默認(rèn)的客戶(hù)端代理對(duì)象http.globalAgent,它對(duì)每個(gè)服務(wù)器端(host + port)創(chuàng)建的連接進(jìn)行管理,默認(rèn)情況下,通過(guò)ClientRequest對(duì)象對(duì)同一個(gè)服務(wù)器端發(fā)起的HTTP請(qǐng)求最多創(chuàng)建5個(gè)連接,如果調(diào)用HTTP客戶(hù)端同時(shí)對(duì)一個(gè)服務(wù)器發(fā)送10次HTTP請(qǐng)求時(shí),其實(shí)質(zhì)只有5個(gè)請(qǐng)求處于并發(fā)狀態(tài),后續(xù)的請(qǐng)求需要等待某個(gè)請(qǐng)求完成服務(wù)后才真正發(fā)出。

可以通過(guò)在options中傳遞agent選項(xiàng)來(lái)改變這個(gè)限制。

var agent = new http.Agent({
  maxSockets: 10
});

var options = {
  hostname: '127.0.0.1', 
  port: 1334,
  path: '/',
  method: 'GET',
  agent: agent  // 直接設(shè)為false,可以使請(qǐng)求不受并發(fā)的控制
};

// Agent對(duì)象的sockets和requests屬性分別表示當(dāng)前連接池中使用中的連接數(shù)和處于等待狀態(tài)的請(qǐng)求數(shù)
// 在業(yè)務(wù)中監(jiān)視這兩個(gè)值有助于發(fā)現(xiàn)業(yè)務(wù)狀態(tài)的繁忙程度

http客戶(hù)端事件

  • response:請(qǐng)求發(fā)出后得到服務(wù)端的響應(yīng)時(shí)觸發(fā)該事件。

  • socket:當(dāng)?shù)讓舆B接池中建立的連接分配給當(dāng)前請(qǐng)求對(duì)象時(shí),觸發(fā)該事件。

  • connect:當(dāng)客戶(hù)端向服務(wù)端發(fā)送CONNECT請(qǐng)求時(shí),如果服務(wù)器響應(yīng)了200狀態(tài)碼,客戶(hù)端觸發(fā)該事件。

  • upgrade:客戶(hù)端向服務(wù)端發(fā)起Upgrade請(qǐng)求時(shí),如果服務(wù)器響應(yīng)101 Switching Protocols狀態(tài),客戶(hù)端觸發(fā)該事件。

  • continue: 客戶(hù)端向服務(wù)端發(fā)起Expect:100-continue頭信息,以試圖發(fā)送較大數(shù)據(jù)量,如果服務(wù)端響應(yīng)了100 continue狀態(tài),客戶(hù)端觸發(fā)該事件。

構(gòu)建WebSocket服務(wù)

{{% notice tip %}}
WebSocekt前端使用講解

{{% /notice %}}

WebSocket與Node之間的配合堪稱(chēng)完美,其理由有兩條:

  • WebSocket客戶(hù)端基于事件的編程模型與Node中自定義事件相差無(wú)幾

  • WebSocket實(shí)現(xiàn)了客戶(hù)端與服務(wù)器端之間的長(zhǎng)連接,而Node事件驅(qū)動(dòng)的方式十分擅長(zhǎng)與大量的客戶(hù)端保持高并發(fā)連接。

相比于HTTP,WebSocket有如下優(yōu)勢(shì):

  • 客戶(hù)端與服務(wù)端只建立一個(gè)TCP連接即可完成雙向通信,在服務(wù)端和客戶(hù)端頻繁通信時(shí),無(wú)需頻繁斷開(kāi)連接和重發(fā)請(qǐng)求,連接可以得到高效應(yīng)用,編程模型也十分簡(jiǎn)潔。

  • WebSocket服務(wù)器端可以推送數(shù)據(jù)到客戶(hù)端,遠(yuǎn)比HTTP的請(qǐng)求響應(yīng)模式更靈活、更高效。

  • 有更輕量級(jí)的協(xié)議頭,減少數(shù)據(jù)傳送量。

{{% notice tip %}}
相比于HTTP,WebSocket更接近于傳輸層協(xié)議,它并沒(méi)有在HTTP的基礎(chǔ)上模擬服務(wù)器端的推送,而是在TCP上定義獨(dú)立的協(xié)議,讓人迷惑的部分在于WebSocket的握手部分是由HTTP完成的,使人覺(jué)得它可能是基于HTTP實(shí)現(xiàn)的
{{% /notice %}}

WebSocket協(xié)議主要分為兩個(gè)部分:握手和數(shù)據(jù)傳輸。

WebSocket握手

客戶(hù)端建立連接時(shí),通過(guò)HTTP發(fā)起請(qǐng)求報(bào)文:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== 
Sec-WebSocket-Protocol: chat, superchat 
Sec-WebSocket-Version: 13

其中Upgrade、Connection字段代表請(qǐng)求服務(wù)器升級(jí)協(xié)議為WebSocket,其中Sec-WebSocket-ProtocolSec-WebSocket-Version指定子協(xié)議和版本,

Sec-WebSocket-Key字段用于安全校驗(yàn),它的值是客戶(hù)端隨機(jī)生成的Base64編碼的字符串。服務(wù)端接收之后,將其與字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11(固定的)相拼接,然后通過(guò)sha1安全散列算法計(jì)算出結(jié)果后,再進(jìn)行Base64編碼,最后當(dāng)做響應(yīng)頭Sec-WebSocket-Accept的值返回給客戶(hù)端。

// 服務(wù)端的對(duì)Sec-WebSocket-Key的處理
var crypto = require('crypto');
var WS = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

var key = req.headers['sec-websocket-key'];
key = crypto.createHash('sha1').update(key + WS).digest('base64');
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade  
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

服務(wù)器應(yīng)答之后,Client 拿到Sec-WebSocket-Accept ,然后本地做一次驗(yàn)證,如果驗(yàn)證通過(guò)了,就會(huì)觸發(fā) onopen 函數(shù)。

WebSocket數(shù)據(jù)傳輸

在握手順利完成后,當(dāng)前連接不再進(jìn)行HTTP的交互,而是開(kāi)始WebSocket的數(shù)據(jù)幀協(xié)議,實(shí)現(xiàn)客戶(hù)端和服務(wù)器端的數(shù)據(jù)交換。

當(dāng)我們調(diào)用send()發(fā)送一條數(shù)據(jù)時(shí),協(xié)議可能將這個(gè)數(shù)據(jù)封裝為一幀或多幀數(shù)據(jù),然后逐幀發(fā)送。

為了安全考慮,客戶(hù)端需要對(duì)發(fā)送的數(shù)據(jù)進(jìn)行掩碼處理,服務(wù)器一旦收到無(wú)掩碼幀,連接將關(guān)閉。

服務(wù)器發(fā)送到客戶(hù)端的數(shù)據(jù)則需要做無(wú)掩碼處理,客戶(hù)端如果收到帶掩碼的數(shù)據(jù)幀,連接將關(guān)閉。

ws.png
  • FIN:占1位,如果這個(gè)數(shù)據(jù)幀是最后一幀,這個(gè)FIN位是1,其余情況為0,當(dāng)一個(gè)數(shù)據(jù)沒(méi)有被分為多楨時(shí),它既是第一幀也是最后一幀,為1。

  • RSV1、RSV2、RSV3:占3位,保留位。

  • opcode:占4位的操作碼,即表示0-15的二進(jìn)制數(shù)值,用來(lái)解釋當(dāng)前數(shù)據(jù)幀,0表示附加數(shù)據(jù)幀,1表示文本數(shù)據(jù)幀,2表示二進(jìn)制數(shù)據(jù)幀,8表示發(fā)送一個(gè)連接關(guān)閉的數(shù)據(jù)幀,9表示ping數(shù)據(jù)幀,10表示pong數(shù)據(jù)值,其余的值暫時(shí)未定義。ping數(shù)據(jù)幀和pong數(shù)據(jù)幀用于心跳檢測(cè),當(dāng)一端發(fā)送ping數(shù)據(jù)幀時(shí),另一端必須發(fā)送pong數(shù)據(jù)幀作為響應(yīng),告知對(duì)方這一段仍然處于響應(yīng)狀態(tài)

  • MASK: 占1位,表示是否進(jìn)行掩碼處理,為1時(shí)代表是,客戶(hù)端發(fā)送給服務(wù)端為1,反之服務(wù)端發(fā)送給客戶(hù)端時(shí)為0。

  • payload length: 占7、7+16或7+64位,前7位標(biāo)示數(shù)據(jù)的長(zhǎng)度,即表示0127的二進(jìn)制數(shù)值,當(dāng)值在0125之間,那么該值就是數(shù)據(jù)的真實(shí)長(zhǎng)度,如果值是126,則后面16位的值是數(shù)據(jù)的真實(shí)長(zhǎng)度(16位可表示的數(shù)值為0-65535,即在數(shù)據(jù)長(zhǎng)度范圍在126b~8kb,65536代表數(shù)據(jù)轉(zhuǎn)換成二進(jìn)制的長(zhǎng)度,則字節(jié)長(zhǎng)度 65536 / 8 = 8192b),如果值是127,則后面64位的值是數(shù)據(jù)的真實(shí)長(zhǎng)度。

  • Masking key: 當(dāng)MASK為1時(shí)存在,即當(dāng)客戶(hù)端給服務(wù)端發(fā)送數(shù)據(jù)時(shí),是一個(gè)32位長(zhǎng)的數(shù)據(jù)位,用于解密數(shù)據(jù)。

  • Payload Data: 目標(biāo)數(shù)據(jù),位數(shù)為8的整數(shù)。

如果客戶(hù)端發(fā)送hello world!到服務(wù)端,12個(gè)字符,則長(zhǎng)度為12 * 8 = 96位,轉(zhuǎn)換為二進(jìn)制位1100000,則報(bào)文應(yīng)當(dāng)如下:

fin(1) + res(000) + opcode(0001) + masked(1) + payload length(1100000) + masking key(32位) + payload data(hello world!加密后的二進(jìn)制)

服務(wù)器回復(fù)yakexi,報(bào)文則如下,無(wú)需掩碼。

fin(1) + res(000) + opcode(0001) + masked(0) + payload length(0110000) +  + payload data(yakexi加密后的二進(jìn)制)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容