Node.js爲javascript語言提供了一個在服務端運行的平臺,它以其事件驅動,非阻塞I/O機制使得它自己很是適合開發運行在在分佈式設備上的I/O密集型應用,分佈式應用要求Node.js必須對網絡通訊支持友好,事實上Node.js也提供了很是強大的網絡通訊功能,本文就主要探討如何使用Node.js進行網絡編程。javascript
首先,網絡編程的概念是"使用套接字來達到進程間通訊的目的"。一般狀況下,咱們要使用網絡提供的功能,能夠有如下幾種方式:html
1.使用應用軟件提供的網絡通訊功能來獲取網絡服務,最著名的就是瀏覽器,它在應用層上使用http協議,在傳輸層基於TCP協議;java
2.在命令行方式下使用shell 命令獲取系統提供的網絡服務,如telnet,ftp等;node
3.使用編程的方式經過系統調用獲取操做系統提供給咱們的網絡服務。shell
本文主要目的就是要探討如何在Node.js中經過編程來獲取操做系統提供的網絡服務來達到不一樣主機進程間通訊。如今回過頭來在看看網絡編程的概念,這裏涉及到一個套接字(socket)的概念,所謂套接字,其實是兩個不一樣進 程間進行通訊的端口(這裏的端口有別於IP地址中經常使用的端口),它是對網絡層次模型中網絡層及其下面各層操做的一個封裝,爲了讓開發者可以使用各類語言調用操做系統提供的網絡服務,在不一樣服務端語言中都使用了套接字這個概念,開發者只要得到一個套接字(socket),就可使用套接字(socket)中各類方法來建立不一樣進程之間的鏈接進而達到通訊目的。一般狀況下,咱們使用如下網絡層次模型,編程
所謂的socket(套接字)就是將操做系統中對於傳輸層及其如下各層中對於網絡操做的處理進行了封裝,而後提供一個socket對象,供咱們在應用程序中調用這個對象及其方法來達到進程間通訊的目的。api
基於以上概念,Node.js也提供了對socket的支持,它提供了一個net模塊用來處理和TCP相關的操做,提供了dgram模塊用來處理UDP相關操做,關於TCP和UDP的區別這裏就不贅述了,屬於老生常談的話題了。瀏覽器
1.建立TCP服務端。服務器
在Node.js中,能夠很方便地建立一個socket服務端,利用以下函數網絡
var server=net.createServer([options],[listener]);
其中net爲咱們引入的net模塊,options參數爲可選,它是一個對象,其中包含以下兩個屬性:
allowHalfOpen,該屬性默認爲false,這個時候如何TCP客戶端發送一個FIN包的時候,服務端必須回送一個FIN包,這使得這個TCP鏈接兩端同時關閉,這種狀況下關閉後任何一方都不能再發送信息,而若是該屬性爲true,表示TCP客戶端發送一個FIN包的時候,服務端不回發,這將致使TCP客戶端關閉到服務端的通訊,而服務端仍然能夠向客戶端發送信息。這種狀況下若是要徹底關閉這個TCP雙向鏈接,則須要顯式調用服務端socket的end方法。
pauseOnConnect,該屬性默認爲false,當它被設置爲true的時候表示該TCP服務端與之相鏈接的客戶端socket傳輸過來的數據將不被讀取,即不會觸發data事件。若是須要讀取客戶端傳輸的數據,可使用socket的resume方法來設置該socket。
參數listener表示一個建立socket以後的回調函數,它有一個參數,表示當前建立的服務端socket,
function (socket) {
// to do sth..
}
最後這個方法返回的是被建立的服務器對象。
對於這個對象,它是繼承了EventEmitter類,它具備幾個重要的事件和方法,以下:
connection事件,用來監聽客戶端socket鏈接到這個TCP服務器的時候觸發
server.on('connection',function(socket){ // to do sth... });
close事件,TCP服務器被關閉的時候觸發。
server.on('close',function(){ console.log('TCP服務器被關閉'); });
error事件,TCP鏈接出現錯誤的時候觸發
listen方法,用來監聽來自客戶端的TCP鏈接請求。
下面是一個完整的建立TCP服務端的例子。
var net=require('net'); var server=net.createServer(function(socket){ console.log('客戶端和服務端創建鏈接'); server.getConnections(function(err,count){ console.log("當前鏈接數爲%d",count); }); server.maxConnections=2; console.log('tcp最大鏈接數爲%d',server.maxConnections); }); server.on('error',function(e){ if(e.code=='EADDRINUSE'){ console.log('地址和端口被佔用'); } }); server.listen(2000,'localhost',function(){ //console.log('服務器端開始監聽...'); var address=server.address(); console.log(address); });
這段代碼建立了一個TCP服務端並將該服務端指定最多鏈接兩個客戶端,並監聽本地的2000端口等待客戶端鏈接,接着咱們可使用遠程登陸(Telnet基於TCP協議)來測試這個服務端,分別在兩個命令行中輸入
telnet loclhost 2000
結果以下:
當使用第三個命令行窗口進行登錄的時候,發現沒法鏈接到服務端,由於這裏咱們設置了鏈接到服務端的TCP鏈接只能爲最大兩個。
2.建立TCP客戶端並進行服務端與客戶端的通訊
建立一個獨立的客戶端須要調用net模塊的Socket構造方法,以下:
var client=new net.Socket([options]);
這個構造函數接受一個可選的參數,是一個對象,它裏面包含以下幾個屬性:
fd:用來制定一個已經存在的socket文件描述符來建立socket;
allowHalfOpen:做用同上
readable和writeable,當使用fd來建立socket的時候指定該socket是否可讀寫,默認爲false。
實際上該client就是一個獨立的socket對象。這個socket對象一般具備以下比較重要的方法和屬性:
connect方法,用來鏈接指定的TCP服務端。
socket.connect(port,[host],[listener])
write方法,向另一端的socket寫入數據
socket.write(data,[encoding],[callback])
其中data能夠是字符串,也能夠是Buffer數據,若是是字符串須要指定第二個參數用來指定其編碼方式。第三個參數爲回調函數。
如下是一個完整的建立TCP客戶端的代碼:
var net=require('net'); var client=new net.Socket(); client.setEncoding('utf8'); client.connect(2000,'localhost',function(){ console.log('已鏈接到服務端'); client.write('hello!'); setTimeout(function(){ client.end('bye'); },10000); }); client.on('error',function(err){ console.log('與服務端鏈接或通訊發生錯誤,錯誤編碼爲%s',err.code); client.destroy(); }); client.on('data',function(data){ console.log('已接收到服務端發送的數據爲:'+data); });
該段代碼建立了一個TCP客戶端,而且鏈接本地2000端口的服務器,向服務器發送hello數據,而後過十秒以後再發送bye,最後關閉該TCP客戶端的鏈接。而且監聽它的data事件,當收到服務端發送來的數據時打印出來。
與該客戶端對應的一個TCP服務端代碼以下:
var net=require('net'); var server=net.createServer({allowHalfOpen:true}); server.on('connection',function(socket){ console.log('客戶端已經鏈接到服務器'); socket.setEncoding('utf8'); socket.on('data',function(data){ console.log('接收到客戶端發送的數據爲:'+data); socket.write('確認數據:'+data); }); socket.on('error',function(err){ console.log('與客戶端通訊過程當中發生錯誤,錯誤碼爲%s',err.code); socket.destroy(); }); socket.on('end',function(){ console.log('客戶端鏈接被關閉'); socket.end(); //客戶端鏈接所有關閉的時候退出引用程序 server.unref(); }); socket.on('close',function(has_error){ if(has_error){ console.log('因爲一個錯誤致使socket鏈接被關閉'); server.unref(); }else{ console.log('socket鏈接正常關閉'); } }); }); server.getConnections(function(err,count){ if(count==2){ server.close(); } }); server.listen(2000,'localhost'); server.on('close',function(){ console.log('TCP服務器被關閉'); });
該服務端接收到客戶端發送來的數據以後再回發回去,而且當鏈接到該TCP服務端的全部socket鏈接都斷開時,自動退出應用程序。
運行這兩段代碼,結果以下:
服務端:
客戶端
從以上咱們能夠看出,基於TCP鏈接的通訊具備如下特色:
1)面向鏈接,必須創建鏈接後纔可以互相通訊;
2)TCP鏈接是一對一的,就是說在TCP中,一個客戶端socket鏈接一個服務端socket,而且二者能夠相互通訊,通訊是雙向的。
3)TCP鏈接關閉的時候是能夠只關閉一方的鏈接而保留單向通訊;
4)一個特定的IP加端口能夠鏈接多個TCP客戶端,也能夠經過編程指定鏈接上限。
3.建立UDP的客戶端和服務端
在Node.js中,提供了dgram模塊用來處理UDP相關的操做與調用,咱們知道UDP是一種非鏈接不可靠但高效的傳輸協議,因此這裏實際上建立一個TCP客戶端和服務端在函數調用上是沒有區別的,
採用dgram模塊的createSocket方法,以下所示:
var socket=dgram.createSocket(type,[callback])
該方法有兩個參數,分別以下:
type:採用的udp協議類型,能夠是udp4或udp6,該參數必須
callback:建立完成以後的回調函數,該參數可選。回調函數中有兩個參數
function (msg,rinfo) { // 回調函數代碼 }
msg爲一個Buffer對象,表示接收到的數據,rinfo也是一個對象,表示發送者的信息,它含有以下信息:
address:發送者IP
port:發送者端口
family:發送者IP地址類型,如IPV4或IPv6
size:發送者發送信息的字節數
調用建立方法以後返回一個UDP scoket,它 擁有以下幾個重要方法和事件:
message事件,當接收到發送來的信息的時候觸發,以下:
socket.on('message',function (msg,rinfo){ // 回調函數代碼 });
bind方法:爲該socket綁定一個端口和ip,以下:
socket.bind(port,[address],[callback])
listening事件,當第一次接收到一個UDP socket發送來的數據的時候觸發,以下:
socket.on('listening',function (){ // 回調函數代碼 });
send方法,向指定udp socket發送信息。以下:
socket.send(buf,offset,length,port,address,[callback])
該方法有六個參數,buf是一個Buffer對象或者字符串,表示要發送的數據,offset表示從哪一個字節開始發送,length表示發送字節的長度,port表示接收socket的端口,address表示接收socket的IP,callback爲回調函數,其中callback爲可選的以外,其餘參數都是必須的。
如下建立一個UDP客戶端的完整代碼
var dgram=require('dgram'); var message=new Buffer('hello'); var client=dgram.createSocket('udp4'); client.send(message,0,message.length,2001,"localhost",function(err,bytes){ if(err) console.log('數據發送失敗'); else console.log("已發送%d字節數據",bytes); }); client.on("message",function(msg,rinfo){ console.log("已接收到服務端發送的數據%s",msg); console.log("服務器地址信息爲%j",rinfo); client.close(); }); client.on("close",function(){ console.log("socket端口被關閉"); });
這段代碼建立一個客戶端socket並向另一個客戶端發送hello,並將其餘socket發送來的數據打印出來,而後關閉客戶端socket。
下面是相應的服務端socket的代碼:
var dgram=require('dgram'); var server=dgram.createSocket('udp4'); server.on("message",function(msg,rinfo){ console.log('已接收到客戶端發送的數據爲'+msg); console.log("客戶端地址新信息爲%j",rinfo); var buff=new Buffer("確認信息"+msg); server.send(buff,0,buff.length,rinfo.port,rinfo.address); setTimeout(function(){ server.unref(); },10000); }); server.on("listening",function(){ var address=server.address(); console.log("服務器開始監聽,地址信息爲%j",address); }); server.bind(2001,'localhost');
該段代碼建立一個服務端socket,並將它綁定到本地2001端口上,監聽它的listening事件,打印出客戶端信息,並將接收到的客戶端信息打印出來並回送給客戶端,同時在10秒以後若是全部客戶端關閉則退出應用程序。
結果以下,客戶端:
服務端:
從上面咱們能夠看出,與TCP不一樣的是,咱們不須要專門建立一個socket監聽客戶端鏈接,客戶端也不用通過鏈接而是直接向指定服務端socket發送信息,這證實了socket是無鏈接的。
同時,對於udp來說,它的無鏈接特性使得它可以一對一,多對多,一對多和多對一,這和TCP鏈接的一對一是有很大區別的。基於UDP這種特性,咱們可使用UDP來實現數據的廣播和組播。
4.使用UDP來進行數據廣播
在dgram模塊中,使用socket的setBroadcast方法開啓該socket的廣播,以下:
socket.setBroadcast(flag)
其中flag默認爲false,表示不開啓廣播,true表示開啓。
所謂廣播,指的是一個主機向本網絡的其餘主機上發送數據,本網絡內的其餘主機均可以接收到,同時按照對IP地址的分類,對於A,B,C類地址來說,其所在網段的主機號全1的地址就是一個廣播地址,咱們須要將該數據廣播到這個地址上,而不是直接發送給某個指定IP的主機。
基於以上認識,咱們編寫一個廣播服務端以下:
var dgram=require('dgram'); var server=dgram.createSocket("udp4"); server.on("message",function(msg){ var buff=new Buffer("已接收到客戶端數據爲:"+msg); server.setBroadcast(true); server.send(buff,0,buff.length,2002,"192.168.56.255"); }); server.bind(2001,"192.168.56.1");
該段代碼建立一個服務端socket,綁定IP和端口,接受客戶端數據,並將客戶端數據廣播到本網絡的廣播地址上。
客戶端代碼以下:
var dgram=require('dgram'); var client=dgram.createSocket('udp4'); client.bind(2002,'192.168.56.2'); var buf=new Buffer("client"); client.send(buf,0,buf.length,2001,'192.168.56.1'); client.on("message",function(msg,rinfo){ console.log('接收到的服務端數據爲%s',msg); });
這段代碼表示建立一個客戶端socket,併爲該socket綁定IP和端口,同時向服務端發送數據,並將接收到的數據打印出來。
在本地主機上運行服務端代碼,並將客戶端代碼部署在不一樣主機上並修改客戶端socket的IP地址和端口,則任意客戶端發送來的消息都會廣播給全部和該服務器通訊的客戶端。
5.使用UDP進行組播
所謂組播是指任意主機均可以加入到一個組中,這個組的地址是一個特殊的D類IP地址,範圍爲224.0.0.0--239.255.255.255,發送者只須要將發送的數據發送給一個組播地址,那麼全部加入改組的主機均可以收到發送者的數據(注意這裏不是該網絡上的全部主機)。
對於組播地址,一般以下:
•局部組播地址:224.0.0.0~224.0.0.255,這是爲路由協議和其餘用途保留的地址。
•預留組播地址:224.0.1.0~238.255.255.255,可用於全球範圍(如Internet)或網絡協議。
•管理權限組播地址:239.0.0.0~239.255.255.255,可供組織內部使用,相似於私有IP地址,不能用於Internet,可限制組播範圍。
Node.js中使用addMembership來讓主機加入到該組中,從而實現IP組播,以下:
socket.addMembership(multicastAddress, [multicastInterface])
該方法第一個參數是組播地址,第二個參數可選,表示socket須要加入的網絡接口IP地址,若是不指定,則會加入到全部有效的網絡接口中。
一個socket加入組播組以後,可使用dropMembership退出該組播組,以下:
socket.dropMembership(multicastAddress, [multicastInterface])
下面是一個完整的發送組播數據的udp服務端
var dgram=require('dgram'); var server=dgram.createSocket('udp4'); server.on('listening',function(){ server.setMulticastTTL(128); server.addMembership('230.185.192.108'); }); setInterval(broadCast,1000); function broadCast(){ var buf=new Buffer(new Date().toLocaleString()); server.send(buf,0,buf.length,8088,'230.185.192.108'); }
這段代碼建立一個發送組播數據的socket服務端,加入組播組230.185.192.108,並每隔一秒向該組發送服務端時間信息。
對應客戶端代碼以下:
var PORT=8088; var HOST="192.168.56.2"; var dgram=require('dgram'); var client=dgram.createSocket('udp4'); client.on('listening',function(){ client.addMembership('230.185.192.108'); }); client.on('message',function(msg,remote){ console.log(msg.toString()); }); client.bind(PORT,HOST);
客戶端建立一個socket並綁定本身的端口和IP,接收來自服務端發送的數據。在listening事件中將它加入該組播組之中。
在本地主機上運行服務端代碼,在不一樣的網絡主機上運行客戶端代碼並修改其IP和端口爲不一樣主機本身的IP和端口,全部加入到該組播的客戶端都會收到服務端發送的時間信息。
6.總結
綜上所述,在Node.js中,咱們把可使用net模塊來建立基於TCP的服務端和客戶端的鏈接和通訊,同時也可使用dgram模塊來處理基於UDP客戶端和服務端的通訊。