使用Node.js+Socket.IO搭建WebSocket实时应用【转载】
Web领域的实时推送技术,也被称作Realtime技术。这种技术要达到的目的是让用户不需要刷新浏览器就可以获得实时更新。它有着广泛的应用场景,比如在线聊天室、在线客服系统、评论系统、WebIM等。
WebSocket简介
谈到Web实时推送,就不得不说WebSocket。在WebSocket出现之前,很多网站为了实现实时推送技术,通常采用的方案是轮询(Polling)和Comet技术,Comet又可细分为两种实现方式,一种是长轮询机制,一种称为流技术,这两种方式实际上是对轮询技术的改进,这些方案带来很明显的缺点,需要由浏览器对服务器发出HTTP request,大量消耗服务器带宽和资源。面对这种状况,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽并实现真正意义上的实时推送。
WebSocket协议本质上是一个基于TCP的协议,它由通信协议和编程API组成,WebSocket能够在浏览器和服务器之间建立双向连接,以基于事件的方式,赋予浏览器实时通信能力。既然是双向通信,就意味着服务器端和客户端可以同时发送并响应请求,而不再像HTTP的请求和响应。
为了建立一个WebSocket连接,客户端浏览器首先要向服务器发起一个HTTP请求,这个请求和通常的HTTP请求不同,包含了一些附加头信息,其中附加头信息”Upgrade: WebSocket”表明这是一个申请协议升级的HTTP请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的WebSocket连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。
一个典型WebSocket客户端请求头:
前面讲到WebSocket是HTML5中新增的一种通信协议,这意味着一部分老版本浏览器(主要是IE10以下版本)并不具备这个功能, 通过百度统计的公开数据显示,IE8目前仍以33%的市场份额占据榜首,好在chrome浏览器市场份额逐年上升,现在以超过26%的市场份额位居第二,同时微软前不久宣布停止对IE6的技术支持并提示用户更新到新版本浏览器,这个曾经让无数前端工程师为之头疼的浏览器有望退出历史舞台,再加上几乎所有的智能手机浏览器都支持HTML5,所以使得WebSocket的实战意义大增,但是无论如何,我们实际的项目中,仍然要考虑低版本浏览器的兼容方案:在支持WebSocket的浏览器中采用新技术,而在不支持WebSocket的浏览器里启用Comet来接收发送消息。
WebSocket实战
本文将以多人在线聊天应用作为实例场景,我们先来确定这个聊天应用的基本需求。
需求分析
1、兼容不支持WebSocket的低版本浏览器。
2、允许客户端有相同的用户名。
3、进入聊天室后可以看到当前在线的用户和在线人数。
4、用户上线或退出,所有在线的客户端应该实时更新。
5、用户发送消息,所有客户端实时收取。
在实际的开发过程中,为了使用WebSocket接口构建Web应用,我们首先需要构建一个实现了 WebSocket规范的服务端,服务端的实现不受平台和开发语言的限制,只需要遵从WebSocket规范即可,目前已经出现了一些比较成熟的WebSocket服务端实现,比如本文使用的Node.js+Socket.IO。为什么选用这个方案呢?先来简单介绍下他们两。
Node.js
Node.js采用C++语言编写而成,它不是Javascript应用,而是一个Javascript的运行环境,据Node.js创始人Ryan Dahl回忆,他最初希望采用Ruby来写Node.js,但是后来发现Ruby虚拟机的性能不能满足他的要求,后来他尝试采用V8引擎,所以选择了C++语言。
Node.js支持的系统包括*nux、Windows,这意味着程序员可以编写系统级或者服务器端的Javascript代码,交给Node.js来解释执行。Node.js的Web开发框架Express,可以帮助程序员快速建立web站点,从2009年诞生至今,Node.js的成长的速度有目共睹,其发展前景获得了技术社区的充分肯定。
Socket.IO
Socket.IO是一个开源的WebSocket库,它通过Node.js实现WebSocket服务端,同时也提供客户端JS库。Socket.IO支持以事件为基础的实时双向通讯,它可以工作在任何平台、浏览器或移动设备。
Socket.IO支持4种协议:WebSocket、htmlfile、xhr-polling、jsonp-polling,它会自动根据浏览器选择适合的通讯方式,从而让开发者可以聚焦到功能的实现而不是平台的兼容性,同时Socket.IO具有不错的稳定性和性能。
编码实现
先上演示效果图:
可以点击这里查看在线演示。整个开发过程非常简单,下面简单记录了开发步骤:
安装Node.js
根据自己的操作系统,去Node.js官网下载安装即可。如果成功安装。在命令行输入node -v
和npm -v
应该能看到相应的版本号。
node -v
v0.10.26
npm -v
1.4.6
搭建WebSocket服务端
这个环节我们尽可能的考虑真实生产环境,把WebSocket后端服务搭建成一个线上可以用域名访问的服务,如果你是在本地开发环境,可以换成本地ip地址,或者使用一个虚拟域名指向本地ip。
先进入到你的工作目录,比如 /workspace/wwwroot/plhwin/realtime.plhwin.com
,新建一个名为 package.json
的文件,内容如下:
{
"name": "realtime-server",
"version": "0.0.1",
"description": "my first realtime server",
"dependencies": {}
}
接下来使用npm
命令安装express
和socket.io
npm install --save express
npm install --save socket.io
安装成功后,应该可以看到工作目录下生成了一个名为`node_modules`的文件夹,里面分别是`express`和`socket.io`,接下来可以开始编写服务端的代码了,新建一个文件:`index.js`
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http); app.get('/', function(req, res){
res.send('<h1>Welcome Realtime Server</h1>');
}); http.listen(3000, function(){
console.log('listening on *:3000');
});
命令行运行node index.js
,如果一切顺利,你应该会看到返回的listening on *:3000
字样,这说明服务已经成功搭建了。此时浏览器中打开http://localhost:3000
应该可以看到正常的欢迎页面。
如果你想要让服务运行在线上服务器,并且可以通过域名访问的话,可以使用Nginx做代理,再nginx.conf中添加如下配置,然后将域名(比如:realtime.plhwin.com)解析到服务器IP即可。
server
{
listen 80;
server_name realtime.plhwin.com;
location / {
proxy_pass http://127.0.0.1:3000;
}
}
完成以上步骤,http://realtime.plhwin.com:3000
的后端服务就正常搭建了。
服务端代码实现
前面讲到的index.js
运行在服务端,之前的代码只是一个简单的WebServer欢迎内容,让我们把WebSocket服务端完整的实现代码加入进去,整个服务端就可以处理客户端的请求了。完整的index.js
代码如下:
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http); app.get('/', function(req, res){
res.send('<h1>Welcome Realtime Server</h1>');
}); //在线用户
var onlineUsers = {};
//当前在线人数
var onlineCount = 0; io.on('connection', function(socket){
console.log('a user connected'); //监听新用户加入
socket.on('login', function(obj){
//将新加入用户的唯一标识当作socket的名称,后面退出的时候会用到
socket.name = obj.userid; //检查在线列表,如果不在里面就加入
if(!onlineUsers.hasOwnProperty(obj.userid)) {
onlineUsers[obj.userid] = obj.username;
//在线人数+1
onlineCount++;
} //向所有客户端广播用户加入
io.emit('login', {onlineUsers:onlineUsers, onlineCount:onlineCount, user:obj});
console.log(obj.username+'加入了聊天室');
}); //监听用户退出
socket.on('disconnect', function(){
//将退出的用户从在线列表中删除
if(onlineUsers.hasOwnProperty(socket.name)) {
//退出用户的信息
var obj = {userid:socket.name, username:onlineUsers[socket.name]}; //删除
delete onlineUsers[socket.name];
//在线人数-1
onlineCount--; //向所有客户端广播用户退出
io.emit('logout', {onlineUsers:onlineUsers, onlineCount:onlineCount, user:obj});
console.log(obj.username+'退出了聊天室');
}
}); //监听用户发布聊天内容
socket.on('message', function(obj){
//向所有客户端广播发布的消息
io.emit('message', obj);
console.log(obj.username+'说:'+obj.content);
}); }); http.listen(3000, function(){
console.log('listening on *:3000');
});
客户端代码实现
进入客户端工作目录/workspace/wwwroot/plhwin/demo.plhwin.com/chat
,新建一个index.html
:
<!DOCTYPE html> <html>
<head>
<meta charset="utf-8">
<meta name="format-detection" content="telephone=no"/>
<meta name="format-detection" content="email=no"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0" name="viewport">
<title>多人聊天室</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
<!--[if lt IE 8]><script src="./json3.min.js"></script><![endif]-->
<script src="http://realtime.plhwin.com:3000/socket.io/socket.io.js"></script>
</head>
<body> <div id="loginbox">
<div style="width:260px;margin:200px auto;">
请先输入你在聊天室的昵称<br/><br/>
<input type="text" style="width:180px;" placeholder="请输入用户名" id="username" name="username" />
<input type="button" style="width:50px;" value="提交" onclick="CHAT.usernameSubmit();"/>
</div>
</div> <div id="chatbox" style="display:none;">
<div style="background:#3d3d3d;height: 28px; width: 100%;font-size:12px;">
<div style="line-height: 28px;color:#fff;">
<span style="text-align:left;margin-left:10px;">Websocket多人聊天室</span>
<span style="float:right; margin-right:10px;"><span id="showusername"></span>| <a href="javascript:;" onclick="CHAT.logout()" style="color:#fff;">退出</span>
</div>
</div>
<div id="doc">
<div id="chat">
<div id="message" class="message">
<div id="onlinecount" style="width:background:#EFEFF4; font-size:12px; margin-top:10px; margin-left:10px; color:#666;">
</div>
</div>
<div class="input-box">
<div class="input">
<input type="text" maxlength="140" placeholder="请输入聊天内容,按Ctrl提交" id="content" name="content">
</div>
<div class="action">
<button type="button" id="mjr_send" onclick="CHAT.submit();">
提交
</button>
</div>
</div>
</div>
</div>
</div> <script type="text/javascript" src="./client.js"></script>
</body>
</html>
上面的html内容本身没有什么好说的,我们主要看看里面的4个文件请求:
1、realtime.plhwin.com:3000/socket.io/socket.io.js
2、style.css
3、json3.min.js
4、client.js
第1个JS是Socket.IO提供的客户端JS文件,在前面安装服务端的步骤中,当npm安装完socket.io并搭建起WebServer后,这个JS文件就可以正常访问了。
第2个style.css文件没什么好说的,就是样式文件而已。
第3个JS只在IE8以下版本的IE浏览器中加载,目的是让这些低版本的IE浏览器也能处理json,这是一个开源的JS,详见:http://bestiejs.github.io/json3/
第4个client.js
是完整的客户端的业务逻辑实现代码,它的内容如下:
(function () {
var d = document,
w = window,
p = parseInt,
dd = d.documentElement,
db = d.body,
dc = d.compatMode == 'CSS1Compat',
dx = dc ? dd: db,
ec = encodeURIComponent; w.CHAT = {
msgObj:d.getElementById("message"),
screenheight:w.innerHeight ? w.innerHeight : dx.clientHeight,
username:null,
userid:null,
socket:null,
//让浏览器滚动条保持在最低部
scrollToBottom:function(){
w.scrollTo(0, this.msgObj.clientHeight);
},
//退出,本例只是一个简单的刷新
logout:function(){
//this.socket.disconnect();
location.reload();
},
//提交聊天消息内容
submit:function(){
var content = d.getElementById("content").value;
if(content != ''){
var obj = {
userid: this.userid,
username: this.username,
content: content
};
this.socket.emit('message', obj);
d.getElementById("content").value = '';
}
return false;
},
genUid:function(){
return new Date().getTime()+""+Math.floor(Math.random()*899+100);
},
//更新系统消息,本例中在用户加入、退出的时候调用
updateSysMsg:function(o, action){
//当前在线用户列表
var onlineUsers = o.onlineUsers;
//当前在线人数
var onlineCount = o.onlineCount;
//新加入用户的信息
var user = o.user; //更新在线人数
var userhtml = '';
var separator = '';
for(key in onlineUsers) {
if(onlineUsers.hasOwnProperty(key)){
userhtml += separator+onlineUsers[key];
separator = '、';
}
}
d.getElementById("onlinecount").innerHTML = '当前共有 '+onlineCount+' 人在线,在线列表:'+userhtml; //添加系统消息
var html = '';
html += '
'; html += user.username; html += (action == 'login') ? ' 加入了聊天室' : ' 退出了聊天室'; html += '
';
var section = d.createElement('section');
section.className = 'system J-mjrlinkWrap J-cutMsg';
section.innerHTML = html;
this.msgObj.appendChild(section);
this.scrollToBottom();
},
//第一个界面用户提交用户名
usernameSubmit:function(){
var username = d.getElementById("username").value;
if(username != ""){
d.getElementById("username").value = '';
d.getElementById("loginbox").style.display = 'none';
d.getElementById("chatbox").style.display = 'block';
this.init(username);
}
return false;
},
init:function(username){
//客户端根据时间和随机数生成uid,这样使得聊天室用户名称可以重复。实际项目中,如果是需要用户登录,那么直接采用用户的uid来做标识就可以
this.userid = this.genUid();
this.username = username; d.getElementById("showusername").innerHTML = this.username;
this.msgObj.style.minHeight = (this.screenheight - db.clientHeight + this.msgObj.clientHeight) + "px";
this.scrollToBottom(); //连接websocket后端服务器
this.socket = io.connect('ws://realtime.plhwin.com:3000'); //告诉服务器端有用户登录
this.socket.emit('login', {userid:this.userid, username:this.username}); //监听新用户登录
this.socket.on('login', function(o){
CHAT.updateSysMsg(o, 'login');
}); //监听用户退出
this.socket.on('logout', function(o){
CHAT.updateSysMsg(o, 'logout');
}); //监听消息发送
this.socket.on('message', function(obj){
var isme = (obj.userid == CHAT.userid) ? true : false;
var contentDiv = '
'+obj.content+'
';
var usernameDiv = ''+obj.username+''; var section = d.createElement('section');
if(isme){
section.className = 'user';
section.innerHTML = contentDiv + usernameDiv;
} else {
section.className = 'service';
section.innerHTML = usernameDiv + contentDiv;
}
CHAT.msgObj.appendChild(section);
CHAT.scrollToBottom();
}); }
};
//通过“回车”提交用户名
d.getElementById("username").onkeydown = function(e) {
e = e || event;
if (e.keyCode === 13) {
CHAT.usernameSubmit();
}
};
//通过“回车”提交信息
d.getElementById("content").onkeydown = function(e) {
e = e || event;
if (e.keyCode === 13) {
CHAT.submit();
}
};
})();
至此所有的编码开发工作全部完成了,在浏览器中打开http://demo.plhwin.com/chat/就可以看到效果了,后续我会把演示代码提交到Github上。
本例只是一个简单的Demo,留下2个有关项目扩展的思考:
1、假设是一个在线客服系统,里面有许多的公司使用你的服务,每个公司自己的用户可以通过一个专属URL地址进入该公司的聊天室,聊天是一对一的,每个公司可以新建多个客服人员,每个客服人员可以同时和客户端的多个用户聊天。
2、又假设是一个在线WebIM系统,实现类似微信,qq的功能,客户端可以看到好友在线状态,在线列表,添加好友,删除好友,新建群组等,消息的发送除了支持基本的文字外,还能支持表情、图片和文件。
使用Node.js+Socket.IO搭建WebSocket实时应用【转载】的更多相关文章
- 使用Node.js+Socket.IO搭建WebSocket实时应用
Web领域的实时推送技术,也被称作Realtime技术.这种技术要达到的目的是让用户不需要刷新浏览器就可以获得实时更新.它有着广泛的应用场景,比如在线聊天室.在线客服系统.评论系统.WebIM等. W ...
- (转)使用Node.js+Socket.IO搭建WebSocket实时应用
Web领域的实时推送技术,也被称作Realtime技术.这种技术要达到的目的是让用户不需要刷新浏览器就可以获得实时更新.它有着广泛的应用场景,比如在线聊天室.在线客服系统.评论系统.WebIM等. W ...
- 用node.js(socket.io)实现数据实时推送
在做商品拍卖的时候,要求在商品的拍卖页面需要实时的更新当前商品的最高价格.实现的方式有很多,比如: 1.setInterval每隔n秒去异步拉取数据(缺点:更新不够实时) 2. AJAX轮询方式方式推 ...
- 转载:node.js socket.io
本文转自:http://www.xiaocai.name/post/cf1f9_7b6507 学习node.js socket.io 使用 用node.js(socket.io)实现数据实时推送 在 ...
- 基于node.js+socket.io+html5实现的斗地主游戏(1)概述
一.游戏描述 说是斗地主游戏,其实是寝室自创的"捉双A",跟很多地方的捉红10.打红A差不多,大概规则是: 1.基础牌型和斗地主一样,但没有大小王,共52张牌,每人13张,这也是为 ...
- node.js+socket.io配置详解
由于我是在win7的环境下,在这里就以win7系统为例进行讲解了. 首先需要在nodejs官网下载最新版的node.js,下载完毕直接安装即可,安装成功后在cmd命令行中执行node指令,如下结果就说 ...
- 基于Node.js+socket.IO创建的Web聊天室
这段时间进了一个新的项目组,项目是用Appcan来做一个跨平台的移动运维系统,其中前台和后台之间本来是打算用WebSocket来实现的,但写好了示例后发现android不支持WebSocket,大为受 ...
- node.js + socket.io实现聊天室一
前段时间,公司打算在社区做一个聊天室.决定让我来做.本小白第一次做聊天类功能,当时还想着通过ajax请求来实现.经过经理提示,说试试当前流行的node.js 和socket.io来做.于是就上网学习研 ...
- node.js+socket.io安装
最近做安卓遇到一个网络包的bug,服务端使用node做的,通讯用socket.io,但是服务端没法调试,没办法,还是自己搭建一个服务器端吧,索性买了阿里云的ecs测试,之前也配置过node+socke ...
随机推荐
- Storm同步调用之DRPC模型探讨
摘要:Storm的编程模型是一个有向无环图,决定了storm的spout接收到外部系统的请求后,spout并不能得到bolt的处理结果并将结果返回给外部请求.所以也就决定了storm无法提供对外部系统 ...
- 宝藏(树形DP)
这道题目是十分考验思维的,n^2应该还是比较好想的,主要是如何转移根的问题.转移根,在我看来应该是树形dp最难的一部分了, 一般学会如何转移根,也就差不多考验通吃树形dp了. 下面转一转大佬链接: ...
- socket及其相关(续篇)
IO 多路复用 基本概念 IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程.IO多路复用适用如下场合: (1)当客户处理多个描述字时(一般是交互式输入和网络套接口), ...
- JS双击div编辑文本内容
HTML代码: <div class="album"> <div class="image"><a href="java ...
- jdk8与jdk9的共存
以前安装JDK,需要手动配置环境变量.JDK8多了自动配置环境变量,所以可以不用手动配置. 如果我已经装了JDK8,还想再装一个JDK9,安装完,自动配置的环境变量会指向JDK9版本. 解决方法 删除 ...
- C# readonly
当某个字段是引用类型,并且该字段被标记为readonly时,不可改变的是引用,而非字段引用的对象,例如:
- IDL 数组运算
1.求大.求小和求余 IDL> arr=indgen(4) IDL> print,arr 0 1 2 3 IDL> print,arr>3 3 3 3 3 IDL> pr ...
- 在HBulider中如何快速的生成有序(ol)和无序(ul)列表
首先你需要创建一个HTML文件,然后在body里面写入你要创建的类型(有序或者无序),然后列表的个数个人来定: 格式如下: ul > li * 5 代表我要创建一个列表为5个的无序类型 然 ...
- Thinking in React Implemented by Reagent
前言 本文是学习Thinking in React这一章后的记录,并且用Reagent实现其中的示例. 概要 构造恰当的数据结构 从静态非交互版本开始 追加交互代码 一.构造恰当的数据结构 Sinc ...
- win10 uwp 简单MasterDetail
中文 English 本文主要讲实现一个简单的界面,可以在窗口比较大显示列表和内容,窗口比较小时候显示列表或内容.也就是在窗口比较小的时候,点击列表会显示内容,点击返回会显示列表. 先放图,很简单. ...