学习 NodeJS 第八天:Socket 通讯实例


Posted in NodeJs onDecember 21, 2016

前言

一般来讲,HTTP 是基于文本的“单向”通讯机制。这里所谓的“单向”,乃相对于“双向”而言,因为 HTTP 服务器只需根据请求返还恰当的 HTML 给客户端即可,不涉及客户端向服务端的通讯。这种单向的机制比较简单,对网络质量要求也不高。而更多的场景则是需要可靠、稳定的端到端连接。一般这种服务是实时的、有态的而且是长连接,长连接则暗示两段须达致相向通讯的能力,也就说是服务端客户端两者间能够实时地相互间通信。毫无疑问,能够实时通信的服务器正是我们对服务器基本要求之一。区别于 HTTP 服务器以 HTTP 为通讯协议, 实时服务器一般采用较为底层的 TCP/IP 为协议通讯,实现了“套字节 Socket”的双向机制。

Socket 是根据博克莱 (U.C.Berkley) 大学早期发展的 Socket 概念写成的,其设计理念是是将网络传输类比成文件的读取与写入 (传送的动作被视为是写入/接收的动作被视为是读取),如此、传送与接收就简化为编程人员比较容易懂的 读取与写入,降低了网络编程的学习困难度。

聊天室服务器

聊天室的实时连接基于底层的 TCP 直接连接,为此我们须调用 Node 的 TCP 模块。如果不太熟悉所谓 TCP 网络编程?太底层了是不是?没关系,我也不熟悉,边学边做嘛,只不过千万不必因为遇到陌生的词汇而害怕,其实这样原理并不深奥,而且下面的例子也十分的简单易懂!咱们就从最简单的开始吧,下面代码仅仅十行,它的作用是服务器向客户端输出一段文本,完成 Sever --> Client 的单向通讯。

// Sever --> Client 的单向通讯 
var net = require('net'); 
 
var chatServer = net.createServer(); 
 
chatServer.on('connection', function(client) { 
 client.write('Hi!\n'); // 服务端向客户端输出信息,使用 write() 方法 
 client.write('Bye!\n'); 
 
 client.end(); // 服务端结束该次会话 
}); 
 
chatServer.listen(9000);

客户端可以是系统自带的 Telnet:

telnet 127.0.0.1 9000

执行 telnet 后,与服务点连接,反馈 Hi! Bye! 的字符,并立刻结束服务端程序终止连接。如果我们要服务端接到到客户端的信息?可以监听 server.data 事件并且不要中止连接(否则会立刻结束无法接受来自客户端的消息):

// 在前者的基础上,实现 Client --> Sever 的通讯,如此一来便是双向通讯 
var net = require('net'); 
var chatServer = 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);// 接受来自客户端的信息 
 }); 
}); 
function broadcast(message, client) { 
 for(var i=0;i<clientList.length;i+=1) {  
  if(client !== clientList[i]) {  
  clientList[i].write(client.name + " says " + message);  
  } 
 } 
} 
chatServer.listen(9000);

这里要说明一下的是,不不同操作系统对端口范围的限制不一样,有可能是随机的。

那么上面是不是一个完整功能的代码呢?我们说还有一个问题没有考虑进去:那就是一旦某个客户端退出,却仍保留在 clientList 里面,这明显是一个空指针(NullPoint)。如果是在这样的话我们写程序太脆弱了,能不能更健壮一些?——请接着看。

首先我们简单地把 client 从数组 clientList 中移除掉。完成这工作一点都不困难。Node TCP API 已经为我们提供了 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():

function broadcast(message, client) { 
 var cleanup = [] 
 for(var i=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() 
  } 
 
 } 
 } //Remove dead Nodes out of write loop to avoid trashing loop index 
 for(i=0;i<cleanup.length;i+=1) { 
 clientList.splice(clientList.indexOf(cleanup[i]), 1) 
 } 
}

TCP API 中还提供一个 error 事件,用于捕捉客户端的异常:

client.on('error', function(e) { 
 console.log(e); 
});

Node 网络编程的 API 还丰富,此次仅仅是个入门,更多的内容请接着看,关于浏览器 Socket 应用。

Socket.IO

前面说到,浏览器虽然也属于客户端的一种,但仅支持“单工”的 HTTP 通讯。有见及此,HTML5 新规范中推出了基于浏览器的 WebSocket,开发了底层的接口,允许我们能进行 更强大的操作,超越以往的 XHR。

如第一个例子那般,我们无须第三方框架就可以直接与 Node TCP 服务器 进行 Socket  通讯。

但我们又要认清一个事实,不是每个浏览器都可以顺利支持 WebSocket 的。于是 Socket.IO (http://socket.io)出现了,它提供了不支持 WebSocket 时候的降级支持,同时使得一些旧版本的浏览器也可以“全双工”地工作。优先使用的顺序如下:

  • WebSocket
  • Socket over Flash API
  • XHR Polling 长连接
  • XHR Multipart Streaming
  • Forever Iframe
  • JSONP Polling

经过封装,我们可以不探究客户端使用上述哪一种技术达致“全双工”;而我们编写代码时,亦无论考虑哪种放法,因为 Socket.IO 给我们的 API 只有一套。了解 Socket.IO 其用法就可以了。

先在浏览器部署 Socket.IO 的前端代码:

<!DOCTYPE html> 
<html> 
 <body> 
 <script src="/socket.io/socket.io.js"></script> 
 <script> 
  var socket = io.connect('http://localhost:8080'); 
  // 当服务端发送一条消息到客户端,message 事件即被触发。我们把消息在控制台打印出来 
  socket.on('message', function(data){ console.log(data) }) 
 </script> 
 </body> 
</html>

服务端 Node 代码:

var http = require('http'), 
io = require('socket.io'), 
fs = require('fs'); 
 
// 虽然我们这里使用了同步的方法,那会阻塞 Node 的事件循环,但是这是合理的,因为 readFileSync() 在程序周期中只执行一次,而且更重要的是,同步方法能够避免异步方法所带来的“与 SocketIO 之间额外同步的问题”。当 HTML 文件读取完毕,而且服务器准备好之后,如此按照顺序去执行就能让客户端马上得到 HTML 内容。 
var sockFile = 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); 
 
var socket = io.listen(server); // 交由 Socket.io 接管 
 
// Socket.io 真正的连接事件 
socket.on('connection', function(client){ 
 console.log('Client connected'); 
 client.send('Welcome client ' + client.sessionId); // 向客户端发送文本 
});

当客户端连接时,服务端会同时出发两个事件:server.onRequest 和 Socket.onConnection。它们之间有什么区别呢?区别在于 Socket 的是持久性的。

多个 Socket 连接,先是客户端代码:

<!DOCTYPE html> 
<html> 
 <body> 
 <script src="/socket.io/socket.io.js"></script> 
 <script> 
  var upandrunning = io.connect('http://localhost:8080/upandrunning'); 
  var weather = io.connect('http://localhost:8080/weather'); 
  upandrunning.on('message', function(data){ 
  document.write('<br /><br />Node: Up and Running Update<br />'); 
  document.write(data); 
  }); 
  weather.on('message', function(data){ 
  document.write('<br /><br />Weather Update<br />'); 
  document.write(data); 
  }); 
 </script> 
 </body> 
</html>

服务端代码:

var sockFile = 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); 
 
var socket = io.listen(server); 
 
socket.of('/upandrunning') 
 .on('connection', function(client){ 
 console.log('Client connected to Up and Running namespace.'); 
 client.send("Welcome to 'Up and Running'"); 
}); 
 
socket.of('/weather') 
 .on('connection', function(client){ 
 console.log('Client connected to Weather namespace.'); 
 client.send("Welcome to 'Weather Updates'"); 
});

 如上代码,我们可以划分多个命名空间,分别是 upandrunning 和 weather。

关于 Express 中使用 Soclet.io,可以参考《Node:Up and Ruuning》一书的 7.2.2 小节。

今晚时间的关系,涉及 Socket.io 许多方面还没有谈,容小弟我日后再了解。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

NodeJs 相关文章推荐
使用forever管理nodejs应用教程
Jun 03 NodeJs
基于NodeJS的前后端分离的思考与实践(三)轻量级的接口配置建模框架
Sep 26 NodeJs
iPhone手机上搭建nodejs服务器步骤方法
Jul 06 NodeJs
nodeJs链接Mysql做增删改查的简单操作
Feb 04 NodeJs
nodejs6下使用koa2框架实例
May 18 NodeJs
详解HTTPS 的原理和 NodeJS 的实现
Jul 04 NodeJs
Windows下快速搭建NodeJS本地服务器的步骤
Aug 09 NodeJs
详解NODEJS基于FFMPEG视频推流测试
Nov 17 NodeJs
nodejs操作mongodb的填删改查模块的制作及引入实例
Jan 02 NodeJs
nodejs微信扫码支付功能实现
Feb 17 NodeJs
nodeJS服务器的创建和重新启动的实现方法
May 12 NodeJs
nodejs更新package.json中的dependencies依赖到最新版本的方法
Oct 10 NodeJs
详解Nodejs基于mongoose模块的增删改查的操作
Dec 21 #NodeJs
nodejs redis 发布订阅机制封装实现方法及实例代码
Dec 15 #NodeJs
解析NodeJs的调试方法
Dec 11 #NodeJs
nodejs连接mongodb数据库实现增删改查
Dec 01 #NodeJs
Nodejs 搭建简单的Web服务器详解及实例
Nov 30 #NodeJs
Nodejs下用submit提交表单提示cannot post错误的解决方法
Nov 21 #NodeJs
Nodejs进阶:核心模块net入门学习与实例讲解
Nov 21 #NodeJs
You might like
php5 apache 2.2 webservice 创建与配置(java)
2011/01/27 PHP
浅析php中jsonp的跨域实例
2013/06/21 PHP
php面向对象中static静态属性和静态方法的调用
2015/02/08 PHP
转一个日期输入控件,支持FF
2007/04/27 Javascript
jQuery 学习第六课 实现一个Ajax的TreeView
2010/05/17 Javascript
JavaScript中两个感叹号的作用说明
2011/12/28 Javascript
jQuery 自定义函数写法分享
2012/03/30 Javascript
js innerHTML 改变div内容的方法
2013/08/03 Javascript
JQuery给元素绑定click事件多次执行的解决方法
2014/05/29 Javascript
Js+php实现异步拖拽上传文件
2015/06/23 Javascript
浅谈Node.js:Buffer模块
2016/12/05 Javascript
详解JavaScript的闭包、IIFE、apply、函数与对象
2016/12/21 Javascript
利用BootStrap的Carousel.js实现轮播图动画效果
2016/12/21 Javascript
JS基于贪心算法解决背包问题示例
2017/11/27 Javascript
bootstrap 弹出框modal添加垂直方向滚轴效果
2018/07/09 Javascript
vue模仿网易云音乐的单页面应用
2019/04/24 Javascript
JS实现水平遍历和嵌套递归操作示例
2019/08/15 Javascript
[01:02:02]DOTA2上海特级锦标赛A组败者赛 EHOME VS CDEC第二局
2016/02/25 DOTA
[35:39]完美世界DOTA2联赛PWL S2 FTD.C vs Rebirth 第二场 11.22
2020/11/24 DOTA
Python聚类算法之DBSACN实例分析
2015/11/20 Python
python数据结构之线性表的顺序存储结构
2018/09/28 Python
详解python中的模块及包导入
2019/08/30 Python
python中的线程threading.Thread()使用详解
2019/12/17 Python
Python实现使用dir获取类的方法列表
2019/12/24 Python
python新手学习使用库
2020/06/11 Python
关于python tushare Tkinter构建的简单股票可视化查询系统(Beta v0.13)
2020/10/19 Python
CSS3 :not()选择器实现最后一行li去除某种css样式
2016/10/19 HTML / CSS
采用怎样的方法保证数据的完整性
2013/12/02 面试题
港湾网络笔试题
2014/04/19 面试题
laravel使用redis队列实例讲解
2021/03/23 PHP
模具数控专业自荐信
2014/01/27 职场文书
高中学生评语大全
2014/04/25 职场文书
领导班子对照检查剖析材料
2014/10/13 职场文书
小学五一劳动节活动总结
2015/02/09 职场文书
个人年终总结范文
2015/03/09 职场文书
百年校庆感言
2015/08/01 职场文书