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

  一. net模塊簡介
  
  二. Client-Server的通信
  
  2.1 server的創建
  
  2.2 Socket的創建
  
  三. IPC通信
  
  四. 擼一個簡易的cluster通信模型前端

  博客園地址:《大史住在大前端》原創博文目錄
  
  華爲雲社區地址:【你要的前端打怪升級指南】
  
  一. net模塊簡介
  
  net模塊是nodejs通信功能實現的基礎,nodejs中最經常使用的功能就是做爲WebServer使用,創建服務器時使用的http.createServer就是在net.createServer方法的基礎上創建的。前端最熟悉的http協議屬於應用層協議,應用層的內容想要發送出去,還須要將消息逐層下發,經過傳輸層(tcp,udp),網際層(ip)和更底層的網絡接口後才能被傳輸出去。net模塊就是對分層通信模型的實現。
  
  net模塊中有兩大主要抽象概念——net.Server和net.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類的定義很是精簡,也很容易看懂:
  
  能夠看到構造函數基本上只是初始化了一些屬性,而後添加了對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',www.tcgjgw.com=>{
  
  console.log('成功創建和12315的鏈接')
  
  setTimeout(()=>{
  
  console.log('創建鏈接1s後發送消息');
  
  socket.write('SN:1231512315'www.yuntianyul.com,'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.js和client.js中,你能夠經過控制檯打印的信息來觀察每條語句執行的前後順序,熟悉從通訊創建到消息收到再到服務器關閉的整個過程,記得要先起服務器,後起客戶端。
  
  Tips:你可使用postman向這個server發一個GET請求,看看是什麼樣子,對理解http和tcp/ip的關係有很大幫助,它很是直觀,反正我是第一次見。
  
  三. IPC通信
  
  IPC通信是指Inter Process Communication,也就是跨進程通信,上一節在提到cluster時已經介紹過進程之間是資源隔離的,因此跨進程通信也須要經過net模塊來創建消息管道。它的用法比較簡單,只須要將server.listen( )和socket.connect( )的參數從端口號換成地址字符串就能夠了。示例代碼以下:
  
  const net = require('net');
  
  const cluster = require(www.ztyLegw.cn'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(www.cmyLgw.cn), 'dashipc');
  
  serverForIPC = net.createServer(www.wanhaoptdL.com socket=>{
  
  console.log(`[master]:子進程經過ipcServer鏈接到主進程`);
  
  socket.on('data',data=>{
  
  console.log('[master]:收到來自子進程的消息:'www.cmylept.cn,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 socket和worker socket的對應關係),而後調用socket.write發回給主進程,主進程根據消息的pid屬性在記錄表中找到這個消息應該由哪一個client socket來返回,找到後調用它的end方法將數據返回給客戶端,這樣就完成了一次請求分發。
  
  demo中提供了示例,ipc_http.js是簡易集羣模型的服務端,ipc_http_client.js是客戶端,先後一共發送了3次請求,結果以下:
  
  服務端的日誌:
  
  客戶端的請求:
  
  上面的示例僅僅是爲了幫助理解網絡通訊和跨進程通訊協做的原理,並不表明cluster的源碼,但通訊層面的原理是相似的,實際開發中跨進程通信時不須要本身再構建IPC消息通道,由於子進程返回的process上就已經集成了跨進程通信能力,理解這個簡化的模型對閱讀cluster模塊的通信原理可以提供很好的過渡。node

相關文章
相關標籤/搜索