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客户端和服务端的通信。