node之Buffer--以websocket不徹底實現爲例

Buffer

定義

Buffernode提供的一個操做二進制流的類。它可以讓開發者以相似操做數組的方式去操做二進制數據。事實上,es6也提供了定型數組來操做二進制數據,後文會簡單對比下二者區別。javascript

Buffer的使用場景

回顧二進制操做

假設html

a=0001 0101(2)
b=0101 1010(2)

1.| 位或, 對應位有一個爲1則結果爲1java

a | b = 0101 1111(2)

2.& 位與,對應位都爲1結果才爲1node

a & b = 0001 0000(2)

3.~ 位非, 對應位1變0,0變1es6

~a = 1110 1010(2)

console.log('~a',(~a).toString(2));    
js中運行結果-10110 也就是1001 0110;
這個過程多是,求位非後獲得111 01010,發現是個負數,存它的補碼即1001 0110,讀到的就是這個值了

4.>> 向右移位 全部位像左移指定的位數,左邊超出的捨棄,右邊不足的補0web

a >> 2 = 0000 0101(2)

5.<< 向左移位 全部位像左移指定的位數,左邊超出的捨棄,右邊不足的補0segmentfault

a << 2 = 0101 0100(2)
java 裏有帶符號右移和帶符號左移,js裏是沒有這種操做的

6.^ 異或 對應位相同則爲0,不然爲1api

a ^ b = 01001111

7.一些操做數組

  • 取出a的低4位
a & 0xf
  • 取出a的高4位
a >> 4
  • 取出a的高第3位
(a & 0x20) >> 5

其餘的方式本身探索去吧
關於位運算,能夠參看https://blog.csdn.net/foreverling_ling/article/details/61417649瀏覽器

Buffer的建立

node v6.0以前能夠用new Buffer()建立, 之後則用以下幾個方法建立

  1. Buffer.alloc(size[, fill[, encoding]])
  2. Buffer.allocUnsafe(size)
  3. Buffer.allocUnsafeSlow(size)
  4. Buffer.from(array) // array 必須是八位字節數組,不然有問題
  5. Buffer.from(arrayBuffer[, byteOffset[, length]])
  6. Buffer.from(buffer)
  7. Buffer.from(string[, encoding])

關於這些方法的具體用法可參考文檔,這裏簡單演示一些基礎用法. 須要注意的是,
Buffer中的一項是一個字節(8位)。log出來每一個項用兩個16進制數表示。

let buf1 = Buffer.alloc(8);
let buf2 = Buffer.alloc(8,1);
let buf3 = Buffer.alloc(8,'a');
let buf4 = Buffer.alloc(8, 'a', 'utf8');

let buf5 = Buffer.allocUnsafe(8);
let buf6 = Buffer.allocUnsafeSlow(8);


console.log('buf1', buf1);  // buf1 <Buffer 00 00 00 00 00 00 00 00>
console.log('buf2', buf2);  // buf2 <Buffer 01 01 01 01 01 01 01 01>
console.log('buf3', buf3);  // buf3 <Buffer 61 61 61 61 61 61 61 61>
console.log('buf4', buf4);  // buf4 <Buffer 61 61 61 61 61 61 61 61>


console.log('buf5', buf5);  // buf5 <Buffer c0 1d e0 03 01 00 00 00>
console.log('buf6', buf6);  // buf6 <Buffer 00 00 00 00 00 00 00 00>

// 注意buf6與buf1對比


let buf7 = Buffer.from([10,0x61,0b10,'a']);
console.log('buf7', buf7);  // buf7 <Buffer 0a 61 02 00>

let arrayBuffer = new Uint16Array(2);
arrayBuffer[0] = 10;
arrayBuffer[1] = 12;
console.log('arrayBuffer.byteLength', arrayBuffer.byteLength);  // arrayBuffer.byteLength 4

// 這樣寫是不共享內存的, 新開闢了一塊內存, 並且去掉了爲0的字節
let buf8 = Buffer.from(arrayBuffer);
console.log('buf8', buf8);  // buf8 <Buffer 0a 0c>
arrayBuffer[0] = 11;
console.log('buf8', buf8);  // buf8 <Buffer 0a 0c>

// 這樣作跟ArrayBuffer是共享內存的
let buf9 = Buffer.from(arrayBuffer.buffer);
console.log('buf9', buf9);  // buf9 <Buffer 0b 00 0c 00>

arrayBuffer[1] = 14;
console.log('buf9', buf9);  // buf9 <Buffer 0b 00 0e 00>

let buf10 = Buffer.alloc(8, 1);
// 從Buffer建立Buffer也是會新開闢內存的, 不共享
let buf11 = Buffer.from(buf10);

console.log('buf11', buf11);    // buf11 <Buffer 01 01 01 01 01 01 01 01>
buf10[0] = 2;
console.log('buf10', buf10);    // buf10 <Buffer 02 01 01 01 01 01 01 01>
console.log('buf11', buf11);    // buf11 <Buffer 01 01 01 01 01 01 01 01>

讀Buffer

  • buf.readDoubleBE(offset)
  • buf.readDoubleLE(offset)
  • buf.readFloatBE(offset)
  • buf.readFloatLE(offset)
  • buf.readInt8(offset)
  • buf.readInt16BE(offset)
  • buf.readInt16LE(offset)
  • buf.readInt32BE(offset)
  • buf.readInt32LE(offset)
  • buf.readIntBE(offset, byteLength)
  • buf.readIntLE(offset, byteLength)
  • buf.readUInt8(offset)
  • buf.readUInt16BE(offset)
  • buf.readUInt16LE(offset)
  • buf.readUInt32BE(offset)
  • buf.readUInt32LE(offset)
  • buf.readUIntBE(offset, byteLength)
  • buf.readUIntLE(offset, byteLength)
具體用法可參考文檔,這裏比較下有U和無U, BE和LE

1.BE和LE

  • BE是按大端序去讀

    認爲低地址存的是高位

  • LE是按小端序去讀

    相反,認爲低地址存的是低位

大端序用的多一點,符合習慣

let buf = Buffer.from([0x01, 0x02, 0x03, 0x01, 0x02]);

 console.log(buf.readInt16BE(0));   // 258
 console.log(buf.readInt16LE(0));   // 513

 console.log(0x0102); // 258
 console.log(0x0201); // 513

2.有U和無U

主要是看第一位算不算符號位,無U的話第一位是算符號位的,有U第一位算數據位

let buf2 = Buffer.from([0xff]);
 console.log(buf2.readInt8(0));     // -1
 console.log(buf2.readUInt8(0));    // 255

注意下上邊的offset是偏移多少字節,單位字節

寫Buffer

  • buf.write(string[, offset[, length]][, encoding])
  • buf.writeDoubleBE(value, offset)
  • buf.writeDoubleLE(value, offset)
  • buf.writeFloatBE(value, offset)
  • buf.writeFloatLE(value, offset)
  • buf.writeInt8(value, offset)
  • buf.writeInt16BE(value, offset)
  • buf.writeInt16LE(value, offset)
  • buf.writeInt32BE(value, offset)
  • buf.writeInt32LE(value, offset)
  • buf.writeIntBE(value, offset, byteLength)
  • buf.writeIntLE(value, offset, byteLength)
  • buf.writeUInt8(value, offset)
  • buf.writeUInt16BE(value, offset)
  • buf.writeUInt16LE(value, offset)
  • buf.writeUInt32BE(value, offset)
  • buf.writeUInt32LE(value, offset)
  • buf.writeUIntBE(value, offset, byteLength)
  • buf.writeUIntLE(value, offset, byteLength)

與讀操做相似,不作演示

類數組操做

1.訪問和寫入

buf[0] // 訪問第一個字節
buf[0] = 1 // 將第一個字節寫爲1

2.slice方法

從一個buffer中截取一段生成一個buffer,新舊buffer是共享內存的,和Array不一樣,Array的這個方法會新開闢內存。

let buf = Buffer.from([0x01, 0x02, 0x03]);

console.log(buf);   // <Buffer 01 02 03>

buf[0] = 0x5;
console.log(buf[0]);    // 5

let buf2 = buf.slice(0,2);
console.log(buf2);      // <Buffer 05 02>
buf[1] = 0x8;
console.log(buf2);      // <Buffer 05 08>


let arr = [0,1,2,3];
let arr2 = arr.slice(0,2);
arr[0] = 10;
console.log(arr2);  // [0, 1]

3.concat方法

鏈接多個buffer生成一個buffer, 不共享內存

let buf3 = Buffer.alloc(4,1);
let buf4 = Buffer.alloc(4,5);
let totalLength = buf3.length + buf4.length;

const bufA = Buffer.concat([buf3, buf4], totalLength);

console.log(bufA);  // <Buffer 01 01 01 01 05 05 05 05>
buf3[0] = 3;
console.log(bufA);  // <Buffer 01 01 01 01 05 05 05 05>
關於Buffer更深刻的理解能夠參考

ArrayBuffer

定義

ArrayBuffer在ES6中被引入,能夠認爲是必定大小的內存空間。對這段內存的操做則須要視圖支持,包括通用的視圖DataView和定型數組1TypedArray

ArrayBuffer的基本操做

  1. ArrayBuffer(size: number): ArrayBuffer // 建立size個字節的ArrayBuffer
  2. slice(start: number, end: number): ArrayBuffer // 截取生成新的ArrayBuffer,不共享內存
let buffer1 = new ArrayBuffer(8);

let view1 = new DataView(buffer1);
console.log(buffer1);   // ArrayBuffer { byteLength: 8 }
console.log(view1);
// DataView {
//     byteLength: 8,
//     byteOffset: 0,
//     buffer: ArrayBuffer { byteLength: 8 } }

view1.setInt8(0,3);
console.log(view1.getInt8(0));  // 3

let buffer2 = buffer1.slice(1,2);
let view2 = new DataView(buffer2);
view1.setInt8(1,3);
console.log(view2.getInt8(0));  // 0

DataView

1.屬性

  • buffer
  • byteLength
  • byteOffset

2.寫操做

  • setInt8(byteOffset: number, value: number, [isLittleEndian]): void
  • setUInt8(byteOffset: number, value: number, [isLittleEndian]): void
  • setInt16(byteOffset: number, value: number, [isLittleEndian]): void
  • setUInt16(byteOffset: number, value: number, [isLittleEndian]): void
  • setInt32(byteOffset: number, value: number, [isLittleEndian]): void
  • setUInt32(byteOffset: number, value: number, [isLittleEndian]): void
  • setFloat32(byteOffset: number, value: number, [isLittleEndian]): void
  • setFloat64(byteOffset: number, value: number, [isLittleEndian]): void

3.讀操做

  • getInt8(byteOffset: number, value: number, [isLittleEndian]): void
  • getUInt8(byteOffset: number, value: number, [isLittleEndian]): void
  • getInt16(byteOffset: number, value: number, [isLittleEndian]): void
  • getUInt16(byteOffset: number, value: number, [isLittleEndian]): void
  • getInt32(byteOffset: number, value: number, [isLittleEndian]): void
  • getUInt32(byteOffset: number, value: number, [isLittleEndian]): void
  • getFloat32(byteOffset: number, value: number, [isLittleEndian]): void
  • getFloat64(byteOffset: number, value: number, [isLittleEndian]): void

TypedArray

1.類型

  • Int8Array
  • Uint8Array
  • Uint8ClampedArray
  • Int16Array
  • Uint16Array
  • Int32Array
  • Uint32Array
  • Float32Array
  • Float64Array

建立方法

  • new TypedArray(buf: Buffer):TypedArray
  • new TypedArray(size: number):TypedArray
  • new TypedArray(typedArray: TypedArray): TypedArray
  • new TypedArray(array: Array): TypedArray

2.方法

typedArray[0] = 1
console.log(typedArray[0])

更多類數組方法參考:

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/TypedArray

  1. 屬性
  • byteLength: 佔據的字節數
  • length: 元素個數
  • buffer: 使用的ArrayBuffer
let buffer1 = new ArrayBuffer(10);

let int8Array = new Int8Array(buffer1);

int8Array[0] = 1;

console.log(int8Array.length);          // 10
console.log(int8Array.byteLength);      // 10
console.log(int8Array[0]);              // 1

let int16Array = new Int16Array(buffer1);
console.log(int16Array.length);         // 5
console.log(int16Array.byteLength);     // 10

node Buffer, TypedArray, Array比較

  1. Array是不能去操做位的,可是BufferTypedArray是能夠的
  2. Bufferslice方法是在原來的內存上直接截取的, 會共享一段內存,而ArrayTypedArray是會拷貝一份放到一段新的內存
  3. BufferUint8Array的實例,buf instanceOf Unint8Array=true
  4. BufferArray都沒有byteLength方法,而TypedArray有,byteLength表示TypedArray佔用的字節數,length表示的是有多少個項
  5. Array數據項沒有必定的數據類型,Buffer的每一項都是1字節數字,TypedArray有多種類型

websocket協議的簡單實現

協議解讀

1.介紹

WebSocket協議是基於TCP的一種新的網絡協議。它實現了瀏覽器與服務器全雙工(full-duplex)通訊——容許服務器主動發送信息給客戶端。

2.鏈接(握手)

客戶端 -> 發一個特殊的http請求 -> 服務器發一個特殊的http響應 -> 鏈接成功

鏈接請求

下面是個實際的請求頭

GET ws://127.0.0.1:3000/ HTTP/1.1
Host: 127.0.0.1:3000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Upgrade: websocket
Origin: http://localhost:3000
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Sec-WebSocket-Key: HfqW8RyI8GitR89fzjbGgA==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

一個普通的請求頭以下

GET / HTTP/1.1
Host: localhost:3000
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9

關於創建鏈接的請求不詳細說,重點關注的服務端的部分,使用h5的websocket時,瀏覽器已經幫咱們作好了一切。想了解更多細節的同窗能夠參考

鏈接響應

下面是一個實際的響應

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: mG8+Ke3Gs4TDeff7HYfHmoXPkrA=

node中,使用createServer會返回一個http.Server類的實例,當客戶端對這個server發起一個ConnectionUpgrade的請求時,server會觸發一個upgrade事件,咱們能夠在這個事件處理函數中多客戶端發來的信息進行校驗, 校驗成功能夠按協議給出響應即創建鏈接成功了。

const crypto = require('crypto');
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';  // GUID
server.on('upgrade', (req, socket) => {
    let key = req.headers['sec-websocket-key'];
    key = crypto
        .createHash('sha1')
        .update(key + GUID)
        .digest('base64');
    let resHeaders = [
        'HTTP/1.1 101 Switching Protocols',
        'Upgrade: websocket',
        'Connection: Upgrade',
        'Sec-WebSocket-Accept: ' + key,
        '',
        ''
    ];
    resHeaders = resHeaders.join('\r\n');

    socket.write(resHeaders);
});

上述代碼忽略了對請求頭字段的校驗。

Sec-WebSocket-Accept這個字段是須要計算的。

須要將請求頭的sec-websocket-key字段 與一個規定的字符串鏈接,再使用sha1加密,再將這個加密後的字符串轉成base64便可獲得。

3.幀結構

  • FIN: 1bit

一個數據塊可能被拆成多個數據幀發送, FIN標示是否爲最後一個數據幀, 只有一個數據幀也要置爲1

  • RSV1, RSV2, RSV3 1bit x 3

除非兩端協商了非0值的含義, 不然必須都爲0; 否則另外一端要中斷鏈接

  • opcode 4bits

定義數據部分的信息, 無效終止鏈接; 0x8標示是個關閉鏈接的幀

  • mask 1bit

標示數據部分是否使用掩碼

  • payload length: 7bits 7+16 7+64

    0-125: 7bits
    126 2B
    127 8B

  • masking-key: 0 | 4B

從客戶端發送的數據會包含這個掩碼

  • extension data + application data: x+y

x= 0B or xB, 除非已經和客戶端肯定擴展數據含義, 不然不能有

y 應用數據

4.斷開

略了

代碼實現

// 這裏邊還有不少問題
const EventEmitter = require('events');
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';  // GUID
class Websocket extends EventEmitter {

    constructor(p) {
        super();
        if(typeof p === 'number') { // 表示不是掛在http服務上的
            // 略了, 能夠用net 模塊根據指定端口建立tcp鏈接
        }else if(typeof p === 'object') {   // 表示與http服務公用一個服務
            this.server = p;
            this._init();
        } else {
            throw new Error('error');
        }
        this.socketsMap = new Map();
    }
    _handshake(req, socket) {
        this.socket = socket;
        let key = req.headers['sec-websocket-key'];
        key = require('crypto')
            .createHash('sha1')
            .update(key + GUID)
            .digest('base64');
        let resHeaders = [
            'HTTP/1.1 101 Switching Protocols',
            'Upgrade: websocket',
            'Connection: Upgrade',
            'Sec-WebSocket-Accept: ' + key,
            '',
            ''
        ];
        socket.on('data', frame => {   
            const decodedFrame = this._decode(frame);
            if(decodedFrame.opcode === 0x0) {

            } 
            const opcode = decodedFrame.opcode;
            if(opcode === 0x8) {
                this.emit('close', socket);
                socket.write(this.closeFrame());
                this.socketsMap.delete(socket);
            } else {
                decodedFrame.data && this.emit('message', socket, decodedFrame.data.toString('utf8'));
            }
            
        });
        resHeaders = resHeaders.join('\r\n');
        socket.write(resHeaders);
        this.socketsMap.set(socket, 1);
        this.emit('open', socket);
    }

    _init() {
        this.server.on('upgrade', (req, socket) => {
           this._handshake(req, socket);
        })
    }

    send(socket, msg) {
        try {
            socket.write(this._encode(msg));
        } catch(e) {
            console.log(e);
        }
        
    }

    broadcast(msg) {
        for (const socket of this.socketsMap) {
            this.send(socket[0], msg);
        }
    }
    /**
     * 解碼
     */
    _decode(frame) {
        console.log(frame)
        let frame1 = frame[0];  // 第一個字節
        let FIN = frame1 >> 7;  // 標示是否爲結束幀
        // 擴展
        let RSV1 = frame1 >> 6 & 0b01;
        let RSV2 = frame1 >> 5 & 0b001;
        let RSV3 = frame1 >> 4 & 0b0001;
        let opcode = frame1 & 0x0F;   // 標示數據信息類型
        let MASKING_KEY_buf;    // 掩碼
        let data;    // 數據
        let frame2 = frame[1];  // 第二個字節
        let MASK = frame2 >> 7;
        let payloadLength = frame2 & 0x7F;
        console.log(payloadLength)
        let extendPayloadBytes = 0;
        if (payloadLength === 126) {
            payloadLength = frame.readUInt16BE(2);
            extendPayloadBytes = 2;
        } else if (payloadLength === 127) {
            payloadLength = frame.readUInt32BE(2);
            extendPayloadBytes = 8;
        }

        if (MASK === 1) {
            MASKING_KEY_buf = frame.slice(2 + extendPayloadBytes, 6 + extendPayloadBytes);
            data = Buffer.alloc(payloadLength);
            for (let i = 0; i < payloadLength; i++) {
                var j = i % 4;
                data[i] = frame[2 + extendPayloadBytes + 4 + i] ^ MASKING_KEY_buf[j];
            }
        }

        return {
            FIN,
            RSV1,
            RSV2,
            RSV3,
            opcode,
            MASKING_KEY_buf,
            data
        }
    }

    closeFrame() {
        let f = Buffer.from([0x8, 0x8]);
        console.log(f);
        return f;
    }

    _encode(data) {
        let dataBuf = Buffer.from(data, 'utf8');
        let dataLength = dataBuf.length; // 數據長度,bytes
        let frames = [];
        let preInfoArr = [];
        preInfoArr.push((1 << 7) + 1);   // FIN和opcode
        

        if (dataLength < 126) {
            preInfoArr.push((0 << 7) + dataLength); // mask 和數據長度
            let f = Buffer.from(preInfoArr);
            dataBuf = Buffer.concat([f,dataBuf]);
            frames.push(dataBuf);   // 數據
            dataLength = 0;
        } else if (dataLength < Math.pow(2, 16)) {
            preInfoArr.push((0 << 7) + 126);    // 佔位,表示要用後面兩個字節標示長度
            preInfoArr.push(((dataLength & 0xFF00) >> 8), (dataLength & 0xFF));
            let f = Buffer.from(preInfoArr);
            dataBuf = f.concat(dataBuf)
            frames.push(dataBuf);
        } else if (dataLength < Math.pow(2, 32)) {
            preInfoArr.push((0 << 7) + 126);    // 佔位,表示要用後面兩個字節標示長度
            preInfoArr.push(0x0,0x0);
            preInfoArr.push(
                (dataLength & 0xFF000000) >> 24,
                (dataLength & 0xFF0000) >> 16,
                (dataLength & 0xFF00) >> 8,
                dataLength & 0xFF
            )
            let f = Buffer.from(preInfoArr);
            dataBuf = Buffer.concat([f,dataBuf]);            
            frames.push(dataBuf);
        } else { // 須要分片了
            // 暫不考慮了
        }
        return frames[0];
    }
}

module.exports = Websocket;
相關文章
相關標籤/搜索