【摘要】 Node.js net模塊的原理及使用html
示例代碼託管在:http://www.github.com/dashnowords/blogs前端
net
模塊是nodejs
通信功能實現的基礎,nodejs
中最經常使用的功能就是做爲WebServer使用,創建服務器時使用的http.createServer
就是在net.createServer
方法的基礎上創建的。前端最熟悉的http
協議屬於應用層協議,應用層的內容想要發送出去,還須要將消息逐層下發,經過傳輸層(tcp
,udp
),網際層(ip
)和更底層的網絡接口後才能被傳輸出去。net
模塊就是對分層通信模型的實現。node
net
模塊中有兩大主要抽象概念——net.Server
和net.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
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
前文已經說起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.js
和client.js
中,你能夠經過控制檯打印的信息來觀察每條語句執行的前後順序,熟悉從通訊創建到消息收到再到服務器關閉的整個過程,記得要先起服務器,後起客戶端。
Tips:你可使用postman向這個server發一個GET請求,看看是什麼樣子,對理解http
和tcp/ip
的關係有很大幫助,它很是直觀,反正我是第一次見。
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中跨進程通信是直接可使用的,不須要本身手動創建,但「造輪子」對於理解集羣通信機制很是有幫助。簡易模型的基本方案以下,邏輯的順序已經標記出來了,在前文的基礎上實際上增長的只是調度相關的功能(也就是橙色背景的部分):
首先主線程和子線程之間創建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
模塊的通信原理可以提供很好的過渡。
做者:大史不說話