Node.js為javascript語言提供了一個在服務端運行的平台,它以其事件驅動,非阻塞I/O機制使得它本身非常適合開發運行在在分布式設備上的I/O密集型應用,分布式應用要求Node.js必須對網絡通信支持友好,事實上Node.js也提供了非常強大的網絡通信功能,本文就主要探討如何使用Node.js進行網絡編程。
首先,網絡編程的概念是"使用套接字來達到進程間通信的目的"。通常情況下,我們要使用網絡提供的功能,可以有以下幾種方式:
1.使用應用軟件提供的網絡通信功能來獲取網絡服務,最著名的就是瀏覽器,它在應用層上使用http協議,在傳輸層基於TCP協議;
2.在命令行方式下使用shell 命令獲取系統提供的網絡服務,如telnet,ftp等;
3.使用編程的方式通過系統調用獲取操作系統提供給我們的網絡服務。
本文主要目的就是要探討如何在Node.js中通過編程來獲取操作系統提供的網絡服務來達到不同主機進程間通信。現在回過頭來在看看網絡編程的概念,這里涉及到一個套接字(socket)的概念,所謂套接字,實際上是兩個不同進 程間進行通信的端口(這里的端口有別於IP地址中常用的端口),它是對網絡層次模型中網絡層及其下面各層操作的一個封裝,為了讓開發者能夠使用各種語言調用操作系統提供的網絡服務,在不同服務端語言中都使用了套接字這個概念,開發者只要獲得一個套接字(socket),就可以使用套接字(socket)中各種方法來創建不同進程之間的連接進而達到通信目的。通常情況下,我們使用以下網絡層次模型,

所謂的socket(套接字)就是將操作系統中對於傳輸層及其以下各層中對於網絡操作的處理進行了封裝,然后提供一個socket對象,供我們在應用程序中調用這個對象及其方法來達到進程間通信的目的。
基於以上概念,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客戶端和服務端的通信。
