中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

從Chrome源碼看WebSocket

2018-07-20    來(lái)源:編程學(xué)習(xí)網(wǎng)

容器云強(qiáng)勢(shì)上線!快速搭建集群,上萬(wàn)Linux鏡像隨意使用

WebSocket是為了解決雙向通信的問(wèn)題,因?yàn)橐环矫鍴TTP的設(shè)計(jì)是單向的,只能是一邊發(fā)另一邊收。而另一方面,HTTP等都是建立在TCP連接之上的,HTTP請(qǐng)求完就會(huì)把TCP給關(guān)了,而TCP連接本身就是一個(gè)長(zhǎng)連接嗎,只要連接雙方不斷關(guān)閉連接它就會(huì)一直連接態(tài),所以有必要再搞一個(gè)WebSocket的東西嗎?

我們可以考慮一下,如果不搞WebSocket怎么實(shí)現(xiàn)長(zhǎng)連接:

(1)HTTP有一個(gè)keep-alive的字段,這個(gè)字段的作用是復(fù)用TCP連接,可以讓一個(gè)TCP連接用來(lái)發(fā)多個(gè)http請(qǐng)求,重復(fù)利用,避免新的TCP連接又得三次握手。這個(gè)keep-alive的時(shí)間服務(wù)器如 Apache 的時(shí)間是5s,而 nginx 默認(rèn)是75s,超過(guò)這個(gè)時(shí)間服務(wù)器就會(huì)主動(dòng)把TCP連接關(guān)閉了,因?yàn)椴魂P(guān)閉的話會(huì)有大量的TCP連接占用系統(tǒng)資源。所以這個(gè)keep-alive也不是為了長(zhǎng)連接設(shè)計(jì)的,只是為了提高h(yuǎn)ttp請(qǐng)求的效率,而http請(qǐng)求上面已經(jīng)提到它是面向單向的,要么是服務(wù)端下發(fā)數(shù)據(jù),要么是客戶(hù)端上傳數(shù)據(jù)。

(2)使用HTTP的輪詢(xún),這也是一種很常用的方法,沒(méi)有websocket之前,基本上網(wǎng)頁(yè)的聊天功能都是這么實(shí)現(xiàn)的,每隔幾秒就向服器發(fā)個(gè)請(qǐng)求拉取新消息。這個(gè)方法的問(wèn)題就在于它也是需要不斷地建立TCP連接,同時(shí)HTTP頭部是很大的,效率低下。

(3)直接和服務(wù)器建立一個(gè)TCP連接,保持這個(gè)連接不中斷。這個(gè)至少在瀏覽器端是做不到的,因?yàn)闆](méi)有相關(guān)的API。所以就有了WebSocket直接和服務(wù)器建立一個(gè)TCP連接。

TCP連接是使用套接字建立的,如果你寫(xiě)過(guò)Linux服務(wù)的話,就知道怎么用系統(tǒng)底層的API(C語(yǔ)言)建立一個(gè)TCP連接,它是使用的套接字socket,這個(gè)過(guò)程大概如下,服務(wù)端使用socket創(chuàng)建一個(gè)TCP監(jiān)聽(tīng):

// 先創(chuàng)建一個(gè)套接字,返回一個(gè)句柄,類(lèi)似于setTimout返回的tId
// AF_INET是指使用IPv4地址,SOCK_STREAM表示建立TCP連接(相對(duì)于UDP)
int sockfd = socket(AF_INET, SOCK_STREAM, 0));
// 把這個(gè)套接字句柄綁定到一個(gè)地址,如localhost:9000
bind(sockfd, servaddr, sizeof(servaddr));
// 開(kāi)始使用這個(gè)套接字監(jiān)聽(tīng),最大pending的連接數(shù)為100
listen(sockfd, 100);

客戶(hù)端也使用的套接字進(jìn)行連接:

// 客戶(hù)端也是創(chuàng)建一個(gè)套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0));
// 用這個(gè)套接字連接到一個(gè)serveraddr
connect(sockfd, servaddr, sizeof(servaddr));
// 向這個(gè)套接字發(fā)送數(shù)據(jù)
send(sockfd, sendline, strlen(sendline), 0);
// 關(guān)閉連接
close(sockfd);

也就是說(shuō)TCP和UDP連接都是使用套接字創(chuàng)建的,所以WebSocket的名字就是這么來(lái)的,本質(zhì)上它就是一個(gè)套接字,并變成了一個(gè)標(biāo)準(zhǔn),瀏覽器器開(kāi)放了API,讓網(wǎng)頁(yè)開(kāi)發(fā)人員也能直接創(chuàng)建套接字和服務(wù)端進(jìn)行通信,并且這個(gè)套接字什么時(shí)候要關(guān)閉了由你們?nèi)Q定,而不像http一樣請(qǐng)求完了瀏覽器或者服務(wù)器就自動(dòng)把TCP的套接字連接關(guān)了。

所以說(shuō)WebSocket并不是一個(gè)什么神奇的東西,它就是一個(gè)套接字。同時(shí),WebSocket得借助于現(xiàn)有的網(wǎng)絡(luò)基礎(chǔ),如果它再?gòu)念^搞一套建立連接的標(biāo)準(zhǔn)代價(jià)就會(huì)很大。在它之前能夠和服務(wù)連接的就只有http請(qǐng)求,所以它得借助于http請(qǐng)求來(lái)建立一個(gè)原生的socket連接,因此才有了協(xié)議轉(zhuǎn)換的那些東西。

瀏覽器建立一個(gè)WebSocket連接非常簡(jiǎn)單,只需要幾行代碼:

// 創(chuàng)建一個(gè)套接字
const socket = new WebSocket('ws://192.168.123.20:9090');
// 連接成功
socket.onopen = function (event) {
    console.log('opened');
    // 發(fā)送數(shù)據(jù)
    socket.send('hello, this is from client');
};

因?yàn)闉g覽器已經(jīng)按照文檔實(shí)現(xiàn)好了,而要?jiǎng)?chuàng)建一個(gè)WebSocket的服務(wù)端應(yīng)該怎么寫(xiě)呢?這里我們先拋開(kāi)Chrome源碼,先研究服務(wù)端的實(shí)現(xiàn),然后再反過(guò)來(lái)看瀏覽器客戶(hù)端的實(shí)現(xiàn)。準(zhǔn)備用Node.js實(shí)現(xiàn)一個(gè)WebSocket的服務(wù)端,來(lái)研究整一個(gè)連接建立和接收發(fā)送數(shù)據(jù)的過(guò)程是怎么樣的。

WebSocket已經(jīng)在 RFC 6455 里面進(jìn)行了標(biāo)準(zhǔn)化,我們只要按照文檔的規(guī)定進(jìn)行實(shí)現(xiàn)就能和瀏覽器進(jìn)行對(duì)接,這個(gè)文檔的說(shuō)明比較有趣,特別是第1部分,有興趣的讀者可以看看,并且我們發(fā)現(xiàn)WebSocket的實(shí)現(xiàn)非常簡(jiǎn)單,讀者如果有時(shí)間的話可以先嘗試自己實(shí)現(xiàn)一個(gè),然后再回過(guò)頭來(lái),對(duì)比本文的實(shí)現(xiàn)。

1. 連接建立

使用Node.js創(chuàng)建一個(gè)hello, world的http服務(wù),如下代碼index.js所示:

let http = require("http");
const hostname = "192.168.123.20"; // 或者是localhost
const port = "9090";
 
// 創(chuàng)建一個(gè)http服務(wù)
let server = http.createServer((req, res) => {
    // 收到請(qǐng)求
    console.log("recv request");
    console.log(req.headers);
    // 進(jìn)行響應(yīng),發(fā)送數(shù)據(jù)
    // res.write('hello, world');
    // res.end();
});
 
// 開(kāi)始監(jiān)聽(tīng)
server.listen(port, hostname, () => {
    // 啟動(dòng)成功
    console.log(`Server running at ${hostname}:${port}`);
});

注意到這里沒(méi)有任何的出錯(cuò)和異常處理,被省略了,在實(shí)際的代碼里面為了提高程序的穩(wěn)健性需要有異常處理,特別是這種server類(lèi)的服務(wù),不能讓一個(gè)請(qǐng)求就把整個(gè)server搞掛了。相關(guān)出錯(cuò)處理可以參考Node.js的文檔。

保存文件,執(zhí)行node index.js啟動(dòng)這個(gè)服務(wù)。

然后寫(xiě)一個(gè)index.html,請(qǐng)求上面寫(xiě)的服務(wù):

<!DOCType html>
<html>
<head>
    <meta charset="utf-8">
</head>
<body>
<script>
!function() {
    const socket = new WebSocket('ws://192.168.123.20:9090');
    socket.onopen = function (event) {
        console.log('opened');
        socket.send('hello, this is from client');
    };
}();
</script>
</body>
</html>

但是我們發(fā)現(xiàn),Node.js代碼里的請(qǐng)求響應(yīng)回調(diào)函數(shù)并不會(huì)執(zhí)行,查了文檔發(fā)現(xiàn)是因?yàn)镹ode.js有另外一個(gè)upgrade的事件:

// 協(xié)議升級(jí)
server.on("upgrade", (request, socket, head) => {
    console.log(request.headers);
});

因?yàn)閃ebSocket需要先協(xié)議升級(jí),在upgrade里面就能收到升級(jí)的請(qǐng)求。把收到的請(qǐng)求頭打印出來(lái),如下所示:

{ host: ‘192.168.123.20:9090’,

connection: ‘ Upgrade ‘,

pragma: ‘no-cache’,

‘cache-control’: ‘no-cache’,

upgrade: ‘websocket’,

origin: ‘http://127.0.0.1:8080’,

‘sec-websocket-version’: ’13’,

‘user-agent’: ‘Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36’,

‘a(chǎn)ccept-encoding’: ‘gzip, deflate’,

‘a(chǎn)ccept-language’: ‘en,zh-CN;q=0.9,zh;q=0.8,zh-TW;q=0.7’,

sec-websocket-key ‘: ‘KR6cP3rhKGrnmIY2iu04Uw==’,

‘sec-websocket-extensions’: ‘permessage-deflate; client_max_window_bits’ }

這是我們建立連接收到的第一個(gè)請(qǐng)求,里面有兩個(gè)關(guān)鍵的字段,一個(gè)是connection: ‘Upgrade’表示它是一個(gè)升級(jí)協(xié)議請(qǐng)求,另外一個(gè)是sec-websocket-key,這是一個(gè)用來(lái)確認(rèn)對(duì)方身份的隨機(jī)的base64字符串,下面將會(huì)用到。

我們需要對(duì)這個(gè)請(qǐng)求進(jìn)行響應(yīng),按照文檔的說(shuō)明,需要包含以下字段:

server.on("upgrade", (request, socket, head) => {
    let base64Value = '';
    // 第一行是響應(yīng)行(Response line),返回狀態(tài)碼101
    socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
        // http響應(yīng)頭部字段用\r\n隔開(kāi)
        'Upgrade: WebSocket\r\n' +
        'Connection: Upgrade\r\n' +
        // 這是一個(gè)給瀏覽器確認(rèn)身份的字符串
        `Sec-WebSocket-Accept: ${base64Value}\r\n` +
        '\r\n');
});

響應(yīng)報(bào)文需要按照http規(guī)定的格式,第一行是響應(yīng)行,包含了http的版本號(hào),狀態(tài)碼101,狀態(tài)碼的解釋。每個(gè)頭部字段用\r\n隔開(kāi),這里面最關(guān)鍵的一個(gè)是Sec-WebSocket-Accept,它需要計(jì)算一下返回瀏覽器。怎么計(jì)算呢?文檔是這么規(guī)定的:

GUID(Globally_Unique_Identifier) = ‘258EAFA5-E914-47DA-95CA-C5AB0DC85B11’
Sec-WebSocket-Accept = base64(sha1(Sec-Websocket-key + GUID))

使用瀏覽器給我的sec-websocket-key值,拼上一個(gè)固定的字符串,這個(gè)字符串叫全局唯一標(biāo)志符,然后取它的sha1值,再進(jìn)行base64編碼,返回給瀏覽器。如果瀏覽器發(fā)現(xiàn)這個(gè)值不對(duì)的話,就會(huì)拋異常,拒絕下一步的連接操作:

因?yàn)樗l(fā)現(xiàn)你是一個(gè)假的WebSocket服務(wù),起碼不是按照文檔實(shí)現(xiàn)的,所以不是同一個(gè)世界,沒(méi)有共同語(yǔ)言,下面的交流就沒(méi)有必要了。

為了計(jì)算這個(gè)值需要引入一個(gè)sha1庫(kù),base64轉(zhuǎn)換可以使用Node.js的Buffer轉(zhuǎn)換,如下代碼所示:

let sha1 = require('sha1');
// 協(xié)議升級(jí)
server.on("upgrade", (request, socket, head) => {
    // 取出瀏覽器發(fā)送的key值
    let secKey = request.headers['sec-websocket-key'];
    // RFC 6455規(guī)定的全局標(biāo)志符(GUID)
    const UNIQUE_IDENTIFIER = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
    // 計(jì)算sha1和base64值
    let shaValue = sha1(secKey + UNIQUE_IDENTIFIER),
        base64Value = Buffer.from(shaValue, 'hex').toString('base64');
    socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
        'Upgrade: WebSocket\r\n' +
        'Connection: Upgrade\r\n' +
        `Sec-WebSocket-Accept: ${base64Value}\r\n` +
        '\r\n');
});

使用上面瀏覽器發(fā)送的key計(jì)算得到的accept值為:

RWMSYL3Zmo91ZR+r39JVM2+PxXc=

把這個(gè)值發(fā)給瀏覽器,Chrome就不會(huì)報(bào)剛剛那個(gè)檢驗(yàn)出錯(cuò)了,確認(rèn)過(guò)眼神,遇上對(duì)的人。這樣WebSocket連接就建立了,沒(méi)錯(cuò)就是這么簡(jiǎn)單。Chrome開(kāi)發(fā)者工具Network面板里的websocket連接將會(huì)從pending狀態(tài)變成101狀態(tài),如果連接關(guān)閉了就會(huì)變成200狀態(tài)。

上面瀏覽器的代碼在建立連接完成之后還send了一個(gè)數(shù)據(jù)過(guò)來(lái):

socket.send('hello, this is from client');

怎么讀取這個(gè)數(shù)據(jù)呢?

2. 接收數(shù)據(jù)

數(shù)據(jù)的傳送,文檔規(guī)定了WebSocket數(shù)據(jù)幀格式,長(zhǎng)這個(gè)樣子:

不要被這個(gè)嚇到,一個(gè)個(gè)拆解來(lái)看的話,還是挺簡(jiǎn)單的?梢苑殖蓛蓚(gè)部分,幀頭字段和有效內(nèi)容或者叫有效負(fù)載(Payload Data),幀頭字段主要的作用是為了解釋這個(gè)幀的,如第1位(bit) FIN 如果置為1就表示它是一個(gè)結(jié)束幀,如果數(shù)據(jù)比較長(zhǎng)就會(huì)被拆成幾個(gè)幀發(fā)送,F(xiàn)IN為1表示它是當(dāng)前數(shù)據(jù)流的最后一個(gè)幀。第4到第7倍的 opcode 是用來(lái)做一些指令控制的,如果值為1話就表示Payload Data是文本格式的,2則表示二進(jìn)制內(nèi)容,8表示連接關(guān)閉。第9位到第15位共7位 Payload Len 表示有效負(fù)載的字節(jié)數(shù),7位二進(jìn)制數(shù)最大表示127,如果有效負(fù)載字節(jié)數(shù)大于127的話就需要用到Extended payload length部分。

第8位的 Mask 如果設(shè)置為1就表示這個(gè)幀的有效負(fù)載內(nèi)容被掩碼處理過(guò)了,客戶(hù)端向服務(wù)端發(fā)送的幀需要進(jìn)行掩碼,而服務(wù)端向客戶(hù)端發(fā)送的數(shù)據(jù)幀不需要掩碼。為什么要使用掩碼,這個(gè)掩碼計(jì)算又是怎么進(jìn)行的呢?掩碼計(jì)算很簡(jiǎn)單,就是把要發(fā)送的數(shù)據(jù)和另一個(gè)數(shù)字異或一下再放到Payload Data, 這個(gè)數(shù)字就是上面數(shù)據(jù)幀里的 Masking-key ,它是一個(gè)32位的數(shù)字。接收方把Payload Data再和這個(gè)數(shù)異或一下就能得到原始的數(shù)據(jù),因?yàn)楹屯粋(gè)數(shù)異或兩次等于原本的數(shù),即:

a ^ b ^ b = a

并且每個(gè)幀里的Making-key要求都是隨機(jī)的,不可被(代理)服務(wù)所預(yù)測(cè)的,為什么要這樣呢?文檔里面是這么說(shuō)的:

The unpredictability of the masking key is essential to prevent authors of malicious applications from selecting the bytes that appear on the wire

這個(gè)解釋有點(diǎn)含糊, Stackoverflow 上有人說(shuō)是為了避免代理緩存中毒攻擊,具體可參考 Http Cache Poinsing .

所以我們需要從這個(gè)幀里面取出掩碼的key值,還原原始的paylod數(shù)據(jù)。

數(shù)據(jù)的發(fā)送和傳輸都要靠socket對(duì)象,因?yàn)樗皇亲叩膆ttp請(qǐng)求,所以在http的響應(yīng)函數(shù)里面是收不到數(shù)據(jù)的,在upgrade事件里面可以拿到這個(gè)socket,監(jiān)聽(tīng)這個(gè)socket對(duì)象的data事件,就可以得到接收的數(shù)據(jù):

socket.on('data', buffer => {
    console.log('buffer len = ', buffer.length);
    console.log(buffer);
});

返回的數(shù)據(jù)類(lèi)型是Node.js里的Buffer對(duì)象,把這個(gè)buffer打印出來(lái):

buffer len = 32  <Buffer 81 9a 4c 3f 64 75 24 5a 08 19 23 13 44 01 24 56 17 55 25 4c 44 13 3e 50 09 55 2f 53 0d 10 22 4b>

這個(gè)buffer就是websocket客戶(hù)端給我們發(fā)送的數(shù)據(jù)幀了,總共有32個(gè)字節(jié),上面的打印是用的16進(jìn)制表示,可以改二進(jìn)制0101表示,和上面那個(gè)數(shù)據(jù)幀格式圖一一對(duì)照,就能夠解釋這個(gè)數(shù)據(jù)幀是什么意思,有什么內(nèi)容。把它打印成原始二進(jìn)制表示:

參照?qǐng)?bào)文格式,如下圖所示:

通過(guò)opcode可以知道它是一個(gè)文本數(shù)據(jù)的幀,payload len得到文本長(zhǎng)度為26個(gè)字節(jié),這個(gè)剛好等于上面發(fā)送的內(nèi)容長(zhǎng)度:

同時(shí)掩碼Mask是打開(kāi)的,掩碼key值存放范圍是[16, 16 + 32],因?yàn)檫@里不需要使用擴(kuò)展字段,所以Masking-key就直接跟在Payload len后面了,再往后就是Payload Data,范圍是[48, 48 + 26 * 8].

這就是一個(gè)完整的數(shù)據(jù)幀了,還需要把payload data用掩碼異或一下,還原原始數(shù)據(jù)。在Node.js里面進(jìn)行處理。Node.js里面的Buffer類(lèi)只能操作字節(jié)級(jí)別,如讀取第n個(gè)字節(jié)的內(nèi)容,沒(méi)辦法直接操作位,如讀取第n位的數(shù)據(jù)。所以額外引入一個(gè)庫(kù),網(wǎng)上找了一個(gè)BitBuffer,但是它的實(shí)現(xiàn)好像有問(wèn)題,所以自已實(shí)現(xiàn)了一個(gè)。

如下代碼所示,實(shí)現(xiàn)一個(gè)能夠讀取任意位的BitBuffer:

class BitBuffer {
    // 構(gòu)造函數(shù)傳一個(gè)Buffer對(duì)象
    constructor (buffer) {
        this.buffer = buffer;
    }
    // 獲取第offset個(gè)位的內(nèi)容
    _getBit (offset) {
        let byteIndex = offset / 8 >> 0,
            byteOffset = offset % 8;
        // readUInt8可以讀取第n個(gè)字節(jié)的數(shù)據(jù)
        // 取出這個(gè)數(shù)的第m位即可
        let num = this.buffer.readUInt8(byteIndex) & (1 << (7 - byteOffset));
        return num >> (7 - byteOffset);
    }
}

原理很簡(jiǎn)單,先調(diào)Node.js的Buffer的readUInt8讀取第n個(gè)字節(jié)的數(shù)據(jù),然后計(jì)算一下所要讀取的位數(shù)在這個(gè)字節(jié)的第幾位,通過(guò)與運(yùn)算,把這個(gè)位取出來(lái),更多位運(yùn)算可以參考: 巧用JS位運(yùn)算 。

用這個(gè)代碼取出第8位的Mask Flag是否有設(shè)置,如下代碼:

socket.on('data', buffer => {
    let bitBuffer = new BitBuffer(buffer);
    let maskFlag = bitBuffer._getBit(8);
    console.log('maskFlag = ' + maskFlag);
});

打印maskFlag = 1。那么怎么取出連續(xù)的n位呢,如opcode,是從第4位到7位。這個(gè)也好辦就是把第4位到第7位分別取出來(lái)拼成一個(gè)數(shù)就好了:

getBit (offset, len = 1) {
    let result = 0;
    for (let i = 0; i < len; i++) {
        result += this._getBit(offset + i) << (len - i - 1); 
    }   
    return result;
}

這個(gè)代碼的效率不是很高,但是容易理解。有個(gè)小坑就是JS的位移只支持32位整數(shù)的操作,1 << 31會(huì)變成一個(gè)負(fù)數(shù),具體不展開(kāi)討論。用這個(gè)函數(shù)取32位的掩碼值就會(huì)有問(wèn)題。

可以利用這個(gè)函數(shù)取出opcode和payload len:

socket.on('data', buffer => {
    let bitBuffer = new BitBuffer(buffer);
    let maskFlag = bitBuffer.getBit(8),
        opcode = bitBuffer.getBit(4, 4), 
        payloadLen = bitBuffer.getBit(9, 7);
    console.log('maskFlag = ' + maskFlag);
    console.log('opcode = ' + opcode);
    console.log('payloadLen = ' + payloadLen);
});

打印如下:

maskFlag = 1  opcode = 1  payloadLen = 26

取掩碼值單獨(dú)實(shí)現(xiàn)一下,這個(gè)掩碼是拆成4個(gè)數(shù)使用的,一個(gè)字節(jié)表示一個(gè)數(shù),借助上面的getBit函數(shù),代碼如下:

getMaskingKey (offset) {
    const BYTE_COUNT = 4;
    let masks = []; 
    for (let i = 0; i < BYTE_COUNT; i++) {
        masks.push(this.getBit(offset + i * 8, 8));
    }   
    return masks;
}

這個(gè)例子的掩碼值是從第16位開(kāi)始,所以offset是16:

let maskKeys = bitBuffer.getMaskingKey(16);
console.log('maskKey = ' + maskKeys);

打印出來(lái)的maskKey為:

maskKeys = 76, 63, 100, 117

怎么用這個(gè)Mask Key進(jìn)行異或呢,文檔里面是這么規(guī)定的:

j = i MOD 4  transformed-octet-i = original-octet-i XOR masking-key-octet-j

也就是把Payload Data里面的第n,n + 1,n + 2,n + 3個(gè)字節(jié)內(nèi)容分別與makKey數(shù)組的第0,1,2,3進(jìn)行異或即可,所以這個(gè)實(shí)現(xiàn)也比較簡(jiǎn)單,如下代碼所示:

getXorString (byteOffset, byteCount, maskingKeys) {
    let text = ''; 
    for (let i = 0; i < byteCount; i++) {
        let j = i % 4;
        // 通過(guò)異或得到原始的utf-8編碼
        let transformedByte = this.buffer.readUInt8(byteOffset + i)
                                  ^ maskingKeys[j];
        // 把編碼值轉(zhuǎn)成對(duì)應(yīng)的字符
        text += String.fromCharCode(transformedByte);
    }   
    return text;
}

異或操作之后就可以得到編碼值,再借助String.fromCharCode就能得到對(duì)應(yīng)的文本,如根據(jù)ASCII表,97就會(huì)被還原成字母’a’。

這個(gè)例子的payload data的偏移是第6個(gè)字節(jié)開(kāi)始的,這里我們先直接寫(xiě)死:

let payloadLen = bitBuffer.getBit(9, 7),
    maskKeys = bitBuffer.getMaskingKey(16);
let payloadText = bitBuffer.getXorString(48 / 8, payloadLen, maskKeys);
console.log('payloadText = ' + payloadText);

打印的文本內(nèi)容為:

payloadText = hello, this is from client

到這里,就把接收的數(shù)據(jù)還原出來(lái)了。如果想要發(fā)送數(shù)據(jù),就是把讀取的過(guò)程逆一下,按照幀格式去拼一個(gè)符合規(guī)范的幀發(fā)送給對(duì)方,區(qū)別是服務(wù)端的幀數(shù)據(jù)是不需要Mask的,如果你Mask了,Chrome會(huì)報(bào)一個(gè)異常,說(shuō)數(shù)據(jù)不需要Mask,拒絕解析接收到的數(shù)據(jù)。

我們?cè)購(gòu)腃hrome源碼看Websocket客戶(hù)端的實(shí)現(xiàn),來(lái)補(bǔ)充一些細(xì)節(jié)。

Chrome的websockets代碼是在src/net/websockets,例如Chrome在握手的時(shí)候是怎么生成一個(gè)隨機(jī)的sec-websocket-key?如下代碼所示:

std::string GenerateHandshakeChallenge() {
  std::string raw_challenge(websockets::kRawChallengeLength, '\0');
  crypto::RandBytes(base::string_as_array(&raw_challenge),
                    raw_challenge.length());
  std::string encoded_challenge;
  base::Base64Encode(raw_challenge, &encoded_challenge);
  return encoded_challenge;
}

它是用的一個(gè)crypto::RandBytes生成隨機(jī)字節(jié),而在檢驗(yàn)sec-websocket-accept也是用的同樣的計(jì)算方法:

std::string ComputeSecWebSocketAccept(const std::string& key) {
  std::string accept;
  std::string hash = base::SHA1HashString(key + websockets::kWebSocketGuid);
  base::Base64Encode(hash, &accept);
  return accept;
}

而在使用掩碼計(jì)算的時(shí)候也是用的一樣的方法:

inline void MaskWebSocketFramePayloadByBytes(
    const WebSocketMaskingKey& masking_key,
    size_t masking_key_offset,
    char* const begin,
    char* const end) {
  for (char* masked = begin; masked != end; ++masked) {
    *masked ^= masking_key.key[masking_key_offset++];
    if (masking_key_offset == WebSocketFrameHeader::kMaskingKeyLength)
      masking_key_offset = 0;
  }
}

其它的還有deflate壓縮、cookie、擴(kuò)展extensions等,本文不再展開(kāi)討論。

另外還有一個(gè)問(wèn)題,使用一個(gè)WebSocket就需要操持一個(gè)TCP連接,如果有1000個(gè)用戶(hù)同時(shí)在線,那么服務(wù)端就得保持1000個(gè)TCP連接,而一個(gè)TCP連接通常需要占用一個(gè)獨(dú)立的線程,而線程的開(kāi)銷(xiāo)是很大的,所以WebSocket對(duì)服務(wù)端的壓力特別大?其實(shí)也不見(jiàn)得有那么大,因?yàn)長(zhǎng)inux有一個(gè)epoll的服務(wù)模型,它是一個(gè)事件驅(qū)動(dòng)機(jī)制的,能夠讓一個(gè)核支持并發(fā)的很多個(gè)連接。

最后一個(gè)問(wèn)題,由于連接是一直操持的,如果連接雙方有一方異常退出了,沒(méi)有發(fā)送一個(gè)關(guān)閉連接的包通知對(duì)方,那么對(duì)方就會(huì)傻傻地操持著這個(gè)沒(méi)用的連接,所以WebSocket又引入了一個(gè)ping/pong的消息幀,幀頭里的opcode為0x9就表示是一個(gè)ping幀,0x10表示pong的響應(yīng)幀。所以可以讓客戶(hù)端不斷地ping,如每隔30秒就ping一次,服務(wù)收到了ping就知道當(dāng)前客戶(hù)端還活著,給一個(gè)pong的響應(yīng),如果服務(wù)端太久沒(méi)收到ping了如1分鐘,那么就認(rèn)為這個(gè)客戶(hù)端已經(jīng)走了直接關(guān)閉連接。而客戶(hù)端如果沒(méi)收到pong響應(yīng)那么就認(rèn)為當(dāng)前連接已經(jīng)斷了,需要重連。瀏覽器JS的API沒(méi)有開(kāi)放ping/pong,需要自已實(shí)現(xiàn)一個(gè)消息類(lèi)型。

本篇主要討論了WebSocket存在的意義,給瀏覽器開(kāi)放一個(gè)socket的API,并進(jìn)行標(biāo)準(zhǔn)化,除了瀏覽器,APP等也都可以按照這個(gè)標(biāo)準(zhǔn)實(shí)現(xiàn),彌補(bǔ)了HTTP單向傳輸?shù)娜秉c(diǎn)。還討論了WebSocket報(bào)文幀的格式,以及怎么用Node.js讀取這個(gè)報(bào)文幀,客戶(hù)端會(huì)把它發(fā)送的內(nèi)容進(jìn)行掩碼處理,服務(wù)端收到的也需要進(jìn)行掩碼還原。我們發(fā)現(xiàn)Chrome客戶(hù)端的實(shí)現(xiàn)有很多地方是類(lèi)似的。

怎么保證WebSocket傳輸?shù)姆(wěn)定性可能又是另外一個(gè)話題了,包括出錯(cuò)重連機(jī)制,跨中美地區(qū)的可能需要使用專(zhuān)線等。

Post Views: 8

 

來(lái)自:https://www.yinchengli.com/2018/05/27/chrome-websocket/

 

標(biāo)簽: linux 代碼 服務(wù)器 開(kāi)發(fā)者 時(shí)間服務(wù)器 通信 網(wǎng)絡(luò)

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請(qǐng)聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點(diǎn)!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請(qǐng)與原作者聯(lián)系。

上一篇:JavaScript 的 this 原理

下一篇:正則表達(dá)式基礎(chǔ)知識(shí)