【nodejs原理&原始碼賞析(5)】net模組與通訊的實現

大史不說話發表於2019-06-04

【nodejs原理&原始碼賞析(5)】net模組與通訊的實現

示例程式碼託管在:http://www.github.com/dashnowords/blogs

部落格園地址:《大史住在大前端》原創博文目錄

華為雲社群地址:【你要的前端打怪升級指南】

一. net模組簡介

net模組是nodejs通訊功能實現的基礎,nodejs中最常用的功能就是作為WebServer使用,建立伺服器時使用的http.createServer就是在net.createServer方法的基礎上建立的。前端最熟悉的http協議屬於應用層協議,應用層的內容想要傳送出去,還需要將訊息逐層下發,通過傳輸層(tcp,udp),網際層(ip)和更底層的網路介面後才能被傳輸出去。net模組就是對分層通訊模型的實現。

net模組中有兩大主要抽象概念——net.Servernet.Socket。《deep-into-node》一書中對Socket概念進行了解釋:

Socket 是對 TCP/IP 協議族的一種封裝,是應用層與TCP/IP協議族通訊的中間軟體抽象層。它把複雜的TCP/IP協議族隱藏在Socket介面後面,對使用者來說,一組簡單的介面就是全部,讓Socket去組織資料,以符合指定的協議。

Socket 還可以認為是一種網路間不同計算機上的程式通訊的一種方法,利用三元組(ip地址,協議,埠)就可以唯一標識網路中的程式,網路中的程式通訊可以利用這個標誌與其它程式進行互動。

簡單地說,net.Server例項可以監聽一個埠(用於實現客戶端TCP連線通訊)或者地址(用於實現IPC跨程式通訊),net.Socket例項可以建立一個套接字例項,它可以用來和server建立連線,連線建立後,就可以實現通訊了。你可以將socket想象成手機,把server想象成基站,雖然不是很貼切,但可以降低理解難度。net相關API可以直接檢視中文文件【net模組文件】

二. Client-Server的通訊

2.1 server的建立

Server類的定義非常精簡,也很容易看懂:

【nodejs原理&原始碼賞析(5)】net模組與通訊的實現

可以看到建構函式基本上只是初始化了一些屬性,然後新增了對connection事件的響應。伺服器是net.Server類的例項,通過net.createServer([options][,onConnection] )方法建立,如果傳入一個函式,則這個函式會作為connection事件的回撥函式,當一個socket例項連線到server時,connection事件就會觸發,回撥函式中的形參就指向了發起連線的socket例項。server例項並不能獨立工作,作為網路伺服器使用時需要需要呼叫listen方法來監聽一個地址,示例如下:

const net = require('net');
const { StringDecoder } = require('string_decoder');
let decoder = new StringDecoder('utf8');

let server = net.createServer(socket=>{
    console.log('接收連線');
    socket.on('data',data=>{
        console.log('收到來自客戶端的訊息:',decoder.write(data));
    });

    socket.on('end',function(){
       console.log('socket從客戶端被關閉了');
    });
});

server.listen(12315);

socket上以流的形式傳送資料,所以需要呼叫string_decoder模組進行解碼才能夠看到內容,否則看到的就是原始的位元組資訊。上面的例項監聽了12315埠。

2.2 Socket的建立

前文已經提及Socket是對TCP/IP協議族的一種封裝。客戶端通訊套接字是net.Socket的例項,通過呼叫例項方法socket.connect(args)來和伺服器建立連線,作為客戶端通訊套接字時需要監聽埠號,建立連線後,客戶端server通過connection事件的回撥函式就可以拿到發起連線的socket例項,這樣客戶端和伺服器就可以通訊了,其中一方通過socket.write()方法寫入資料,另一方註冊的監聽器socket.on('data',onData)回撥函式就會收到資訊。socket例項化示例如下:

const net = require('net');

let socket = new net.Socket();
socket.connect(12315);
//連線伺服器
socket.on('connect',c=>{
    console.log('成功建立和12315的連線')
    setTimeout(()=>{
        console.log('建立連線1s後傳送訊息');
        socket.write('SN:1231512315','utf8',function(){
            console.log('訊息已傳送');
        });
    },1000);
});

socket.on('data',function(resp){
    console.log('收到伺服器返回訊息:',resp);
});

socket.on('end',function(){
    console.log('socket從客戶端被關閉了');
})

客戶端connect連線伺服器的動作,就好比打電話前要先撥號一樣,等接通以後,你說的話(也就是socket.write( )寫入的data)才能被髮送過去。【程式碼倉的示例DEMO】中提供了相對完整的示例,分別放在server.jsclient.js中,你可以通過控制檯列印的資訊來觀察每條語句執行的先後順序,熟悉從通訊建立到訊息收到再到伺服器關閉的整個過程,記得要先起伺服器,後起客戶端。

【nodejs原理&原始碼賞析(5)】net模組與通訊的實現

Tips:你可以使用postman向這個server發一個GET請求,看看是什麼樣子,對理解httptcp/ip的關係有很大幫助,它非常直觀,反正我是第一次見。

三. IPC通訊

IPC通訊是指Inter Process Communication,也就是跨程式通訊,上一節在提到cluster時已經介紹過程式之間是資源隔離的,所以跨程式通訊也需要通過net模組來建立訊息管道。它的用法比較簡單,只需要將server.listen( )socket.connect( )的引數從埠號換成地址字串就可以了。示例程式碼如下:

const net = require('net');
const cluster = require('cluster');
const path = require('path');
const { StringDecoder } = require('string_decoder');

let serverForIPC;//作為子程式的server

if (cluster.isMaster) {
    //主程式執行邏輯
    setupMaster();
    cluster.fork();//生成子程式
    cluster.fork();//生成另一個子程式
} else {
    //子程式執行邏輯
    setupWorker();
}

//主程式邏輯
function setupMaster() {
    //作為Server監聽子程式訊息
   let decoder = new StringDecoder('utf8');
    //windows系統中要求的IPC通訊命名規則
   let ipcPath = path.join('\\\\?\\pipe', process.cwd(), 'dashipc');
   serverForIPC = net.createServer(socket=>{
        console.log(`[master]:子程式通過ipcServer連線到主程式`);
        socket.on('data',data=>{
            console.log('[master]:收到來自子程式的訊息:',decoder.write(data));
        });
   });
   //IPC-server端監聽指定地址
   serverForIPC.listen(ipcPath);
}

//子程式邏輯
function setupWorker() {
    let ipcPath = path.join('\\\\?\\pipe', process.cwd(), 'dashipc');
    let socket = new net.Socket();
    //子程式的socket連線主程式中監聽的地址
    socket.connect(ipcPath,c=>{
        console.log(`[child-${process.pid}]:pid為${process.pid}的子程式已經連線到主程式`);
        //過一秒後發個訊息測試一下
        setTimeout(()=>{
           socket.write(`${process.pid}的訊息:SN1231512315`,'utf8',function(){
              console.log(`[child-${process.pid}]:訊息已傳送`);
           });
        },1000);
    });
}

需要注意儘管主程式和子程式執行的是同樣的指令碼,但執行的具體邏輯由cluster.isMaster進行了區分。當主程式的指令碼執行時會建立一個IPC通訊管道的server端並監聽指定地址,然後通過cluster.fork生成子程式,子程式會執行setupWorker( )方法的邏輯,新建一個socket例項並連線主程式監聽的地址,這樣跨程式通訊就建立了。示例程式碼放置在程式碼倉中的ipc.js中,執行結果如下:

【nodejs原理&原始碼賞析(5)】net模組與通訊的實現

四. 擼一個簡易的cluster通訊模型

既然客戶端通訊和跨程式通訊都實現了,那麼把它們連起來協調好,其實就可以復現cluster叢集模組的功能了,雖然它不能等同於cluster的原始碼,cluster中跨程式通訊是直接可以使用的,不需要自己手動建立,但“造輪子”對於理解叢集通訊機制非常有幫助。簡易模型的基本方案如下,邏輯的順序已經標記出來了,在前文的基礎上實際上增加的只是排程相關的功能(也就是橙色背景的部分):

【nodejs原理&原始碼賞析(5)】net模組與通訊的實現

首先主執行緒和子執行緒之間建立IPC通訊,連線建立後,由子程式將自己的pid通過socket發給主程式,這樣主程式就知道連線到IPCserver的socket是哪個子程式連過來的了,demo在內部構建了一個type屬性為internal_init的訊息來完成這個登記動作,然後啟動一個接收客戶端連線的Server,監聽指定的埠。接下來到了第6步,客戶端新建了socket連線到了主執行緒Client Server監聽的埠,clientServer把它發過來的socket傳給排程中心,排程中心根據一定規則(demo中直接就簡單粗暴地輪換使用各個執行緒)決定將這個socket與哪個worker socket相匹配(所謂匹配就是指client socket發來的訊息應該呼叫哪個worker socket的write方法來分發給對應的子程式),然後將這個客戶端socket登記到匹配記錄表中某條記錄的client socket上,這樣通訊通道就建立好了。

當客戶端呼叫socket.write來寫入資料時,主執行緒就會收到這個資料,然後根據已經建立好的socket關係把這條訊息write到子程式,子程式處理完後在訊息體中增加一個pid屬性標明這個訊息是哪個程式處理的(這個標記也可以在主程式中新增,因為主程式中維護的有pid,client socketworker socket的對應關係),然後呼叫socket.write發回給主程式,主程式根據訊息的pid屬性在記錄表中找到這個訊息應該由哪個client socket來返回,找到後呼叫它的end方法將資料返回給客戶端,這樣就完成了一次請求分發。

demo中提供了示例,ipc_http.js是簡易叢集模型的服務端,ipc_http_client.js是客戶端,前後一共傳送了3次請求,結果如下:

服務端的日誌:

【nodejs原理&原始碼賞析(5)】net模組與通訊的實現

客戶端的請求:

【nodejs原理&原始碼賞析(5)】net模組與通訊的實現

上面的示例僅僅是為了幫助理解網路通訊和跨程式通訊協作的原理,並不代表cluster的原始碼,但通訊層面的原理是類似的,實際開發中跨程式通訊時不需要自己再構建IPC訊息通道,因為子程式返回的process上就已經整合了跨程式通訊能力,理解這個簡化的模型對閱讀cluster模組的通訊原理能夠提供很好的過渡。

相關文章