【nodejs原理&源碼賞析(5)】net模塊與通信的實現

【摘要】 Node.js net模塊的原理及使用html

示例代碼託管在:http://www.github.com/dashnowords/blogs前端

一. net模塊簡介

net模塊是nodejs通信功能實現的基礎,nodejs中最經常使用的功能就是做爲WebServer使用,創建服務器時使用的http.createServer就是在net.createServer方法的基礎上創建的。前端最熟悉的http協議屬於應用層協議,應用層的內容想要發送出去,還須要將消息逐層下發,經過傳輸層(tcp,udp),網際層(ip)和更底層的網絡接口後才能被傳輸出去。net模塊就是對分層通信模型的實現。node

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

Socket 是對 TCP/IP 協議族的一種封裝,是應用層與TCP/IP協議族通訊的中間軟件抽象層。它把複雜的TCP/IP協議族隱藏在Socket接口後面,對用戶來講,一組簡單的接口就是所有,讓Socket去組織數據,以符合指定的協議。github

Socket 還能夠認爲是一種網絡間不一樣計算機上的進程通訊的一種方法,利用三元組(ip地址,協議,端口)就能夠惟一標識網絡中的進程,網絡中的進程通訊能夠利用這個標誌與其它進程進行交互。windows

簡單地說,net.Server實例能夠監聽一個端口(用於實現客戶端TCP鏈接通信)或者地址(用於實現IPC跨進程通信),net.Socket實例能夠創建一個套接字實例,它能夠用來和server創建鏈接,鏈接創建後,就能夠實現通信了。你能夠將socket想象成手機,把server想象成基站,雖然不是很貼切,但能夠下降理解難度。net相關API能夠直接查看中文文檔【net模塊文檔】api

二. Client-Server的通信

2.1 server的創建

Server類的定義很是精簡,也很容易看懂:服務器

能夠看到構造函數基本上只是初始化了一些屬性,而後添加了對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端口。socket

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中,你能夠經過控制檯打印的信息來觀察每條語句執行的前後順序,熟悉從通訊創建到消息收到再到服務器關閉的整個過程,記得要先起服務器,後起客戶端。

 

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中,運行結果以下:

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

既然客戶端通信和跨進程通信都實現了,那麼把它們連起來協調好,其實就能夠復現cluster集羣模塊的功能了,雖然它不能等同於cluster的源碼,cluster中跨進程通信是直接可使用的,不須要本身手動創建,但「造輪子」對於理解集羣通信機制很是有幫助。簡易模型的基本方案以下,邏輯的順序已經標記出來了,在前文的基礎上實際上增長的只是調度相關的功能(也就是橙色背景的部分):

 

首先主線程和子線程之間創建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次請求,結果以下:

服務端的日誌:

客戶端的請求:

 

上面的示例僅僅是爲了幫助理解網絡通訊和跨進程通訊協做的原理,並不表明cluster的源碼,但通訊層面的原理是相似的,實際開發中跨進程通信時不須要本身再構建IPC消息通道,由於子進程返回的process上就已經集成了跨進程通信能力,理解這個簡化的模型對閱讀cluster模塊的通信原理可以提供很好的過渡。

net_demo.rar

做者:大史不說話

相關文章
相關標籤/搜索