学习 NodeJS 第八天:Socket 通讯实例
前言
一般来讲,HTTP是基于文本的“单向”通讯机制。这里所谓的“单向”,乃相对于“双向”而言,因为HTTP服务器只需根据请求返还恰当的HTML给客户端即可,不涉及客户端向服务端的通讯。这种单向的机制比较简单,对网络质量要求也不高。而更多的场景则是需要可靠、稳定的端到端连接。一般这种服务是实时的、有态的而且是长连接,长连接则暗示两段须达致相向通讯的能力,也就说是服务端客户端两者间能够实时地相互间通信。毫无疑问,能够实时通信的服务器正是我们对服务器基本要求之一。区别于HTTP服务器以HTTP为通讯协议,实时服务器一般采用较为底层的TCP/IP为协议通讯,实现了“套字节Socket”的双向机制。
Socket是根据博克莱(U.C.Berkley)大学早期发展的Socket概念写成的,其设计理念是是将网络传输类比成文件的读取与写入(传送的动作被视为是写入/接收的动作被视为是读取),如此、传送与接收就简化为编程人员比较容易懂的读取与写入,降低了网络编程的学习困难度。
聊天室服务器
聊天室的实时连接基于底层的TCP直接连接,为此我们须调用Node的TCP模块。如果不太熟悉所谓TCP网络编程?太底层了是不是?没关系,我也不熟悉,边学边做嘛,只不过千万不必因为遇到陌生的词汇而害怕,其实这样原理并不深奥,而且下面的例子也十分的简单易懂!咱们就从最简单的开始吧,下面代码仅仅十行,它的作用是服务器向客户端输出一段文本,完成Sever-->Client的单向通讯。
//Sever-->Client的单向通讯 varnet=require('net'); varchatServer=net.createServer(); chatServer.on('connection',function(client){ client.write('Hi!\n');//服务端向客户端输出信息,使用write()方法 client.write('Bye!\n'); client.end();//服务端结束该次会话 }); chatServer.listen(9000);
客户端可以是系统自带的Telnet:
telnet127.0.0.19000
执行telnet后,与服务点连接,反馈Hi!Bye!的字符,并立刻结束服务端程序终止连接。如果我们要服务端接到到客户端的信息?可以监听server.data事件并且不要中止连接(否则会立刻结束无法接受来自客户端的消息):
//在前者的基础上,实现Client-->Sever的通讯,如此一来便是双向通讯 varnet=require('net'); varchatServer=net.createServer(), clientList=[]; chatServer.on('connection',function(client){ //JS可以为对象自由添加属性。这里我们添加一个name的自定义属性,用于表示哪个客户端(客户端的地址+端口为依据) client.name=client.remoteAddress+':'+client.remotePort; client.write('Hi'+client.name+'!\n'); clientList.push(client); client.on('data',function(data){ broadcast(data,client);//接受来自客户端的信息 }); }); functionbroadcast(message,client){ for(vari=0;i<clientList.length;i+=1){ if(client!==clientList[i]){ clientList[i].write(client.name+"says"+message); } } } chatServer.listen(9000);
这里要说明一下的是,不不同操作系统对端口范围的限制不一样,有可能是随机的。
那么上面是不是一个完整功能的代码呢?我们说还有一个问题没有考虑进去:那就是一旦某个客户端退出,却仍保留在clientList里面,这明显是一个空指针(NullPoint)。如果是在这样的话我们写程序太脆弱了,能不能更健壮一些?——请接着看。
首先我们简单地把client从数组clientList中移除掉。完成这工作一点都不困难。NodeTCPAPI已经为我们提供了end事件,即客户端中止与服务端连接的时候发生。移除client对象的代码如下:
chatServer.on('connection',function(client){ client.name=client.remoteAddress+':'+client.remotePort client.write('Hi'+client.name+'!\n'); clientList.push(client) client.on('data',function(data){ broadcast(data,client) }) client.on('end',function(){ clientList.splice(clientList.indexOf(client),1);//删除数组中的制定元素。这是JS基本功哦~ }) })
但是我们还不敢说上述代码很健壮,因为一旦end没有被触发,异常仍然存在着。下面我们看看解决之道:重写broadcast():
functionbroadcast(message,client){ varcleanup=[] for(vari=0;i<clientList.length;i+=1){ if(client!==clientList[i]){ if(clientList[i].writable){//先检查sockets是否可写 clientList[i].write(client.name+"says"+message) }else{ cleanup.push(clientList[i])//如果不可写,收集起来销毁。销毁之前要Socket.destroy()用API的方法销毁。 clientList[i].destroy() } } }//RemovedeadNodesoutofwritelooptoavoidtrashingloopindex for(i=0;i<cleanup.length;i+=1){ clientList.splice(clientList.indexOf(cleanup[i]),1) } }
TCPAPI中还提供一个error事件,用于捕捉客户端的异常:
client.on('error',function(e){ console.log(e); });
Node网络编程的API还丰富,此次仅仅是个入门,更多的内容请接着看,关于浏览器Socket应用。
Socket.IO
前面说到,浏览器虽然也属于客户端的一种,但仅支持“单工”的HTTP通讯。有见及此,HTML5新规范中推出了基于浏览器的WebSocket,开发了底层的接口,允许我们能进行更强大的操作,超越以往的XHR。
如第一个例子那般,我们无须第三方框架就可以直接与NodeTCP服务器进行Socket 通讯。
但我们又要认清一个事实,不是每个浏览器都可以顺利支持WebSocket的。于是Socket.IO(http://socket.io)出现了,它提供了不支持WebSocket时候的降级支持,同时使得一些旧版本的浏览器也可以“全双工”地工作。优先使用的顺序如下:
- WebSocket
- SocketoverFlashAPI
- XHRPolling长连接
- XHRMultipartStreaming
- ForeverIframe
- JSONPPolling
经过封装,我们可以不探究客户端使用上述哪一种技术达致“全双工”;而我们编写代码时,亦无论考虑哪种放法,因为Socket.IO给我们的API只有一套。了解Socket.IO其用法就可以了。
先在浏览器部署Socket.IO的前端代码:
<!DOCTYPEhtml> <html> <body> <scriptsrc="/socket.io/socket.io.js"></script> <script> varsocket=io.connect('http://localhost:8080'); //当服务端发送一条消息到客户端,message事件即被触发。我们把消息在控制台打印出来 socket.on('message',function(data){console.log(data)}) </script> </body> </html>
服务端Node代码:
varhttp=require('http'), io=require('socket.io'), fs=require('fs'); //虽然我们这里使用了同步的方法,那会阻塞Node的事件循环,但是这是合理的,因为readFileSync()在程序周期中只执行一次,而且更重要的是,同步方法能够避免异步方法所带来的“与SocketIO之间额外同步的问题”。当HTML文件读取完毕,而且服务器准备好之后,如此按照顺序去执行就能让客户端马上得到HTML内容。 varsockFile=fs.readFileSync('socket.html'); //Socket服务器还是构建于HTTP服务器之上,因此先调用http.createServer() server=http.createServer(); server.on('request',function(req,res){ //一般HTTP输出的格式 res.writeHead(200,{'content-type':'text/html'}); res.end(sockFile); }); server.listen(8080); varsocket=io.listen(server);//交由Socket.io接管 //Socket.io真正的连接事件 socket.on('connection',function(client){ console.log('Clientconnected'); client.send('Welcomeclient'+client.sessionId);//向客户端发送文本 });
当客户端连接时,服务端会同时出发两个事件:server.onRequest和Socket.onConnection。它们之间有什么区别呢?区别在于Socket的是持久性的。
多个Socket连接,先是客户端代码:
<!DOCTYPEhtml> <html> <body> <scriptsrc="/socket.io/socket.io.js"></script> <script> varupandrunning=io.connect('http://localhost:8080/upandrunning'); varweather=io.connect('http://localhost:8080/weather'); upandrunning.on('message',function(data){ document.write('<br/><br/>Node:UpandRunningUpdate<br/>'); document.write(data); }); weather.on('message',function(data){ document.write('<br/><br/>WeatherUpdate<br/>'); document.write(data); }); </script> </body> </html>
服务端代码:
varsockFile=fs.readFileSync('socket.html'); server=http.createServer(); server.on('request',function(req,res){ res.writeHead(200,{'content-type':'text/html'}); res.end(sockFile); }); server.listen(8080); varsocket=io.listen(server); socket.of('/upandrunning') .on('connection',function(client){ console.log('ClientconnectedtoUpandRunningnamespace.'); client.send("Welcometo'UpandRunning'"); }); socket.of('/weather') .on('connection',function(client){ console.log('ClientconnectedtoWeathernamespace.'); client.send("Welcometo'WeatherUpdates'"); });
如上代码,我们可以划分多个命名空间,分别是upandrunning和weather。
关于Express中使用Soclet.io,可以参考《Node:UpandRuuning》一书的7.2.2小节。
今晚时间的关系,涉及Socket.io许多方面还没有谈,容小弟我日后再了解。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。