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 -vnpm -v应该能看到相应的版本号。

  1. node -v
  2. v0.10.26
  3. npm -v
  4. 1.4.6

搭建WebSocket服务端

这个环节我们尽可能的考虑真实生产环境,把WebSocket后端服务搭建成一个线上可以用域名访问的服务,如果你是在本地开发环境,可以换成本地ip地址,或者使用一个虚拟域名指向本地ip。

进入到你的工作目录,如/workspace/wwwroot/plhwin/realtime.plhwin.com,新建一个名为 package.json的文件,内容如下:

  1. {
  2. "name": "realtime-server",
  3. "version": "0.0.1",
  4. "description": "my first realtime server",
  5. "dependencies": {}
  6. }

接下来使用npm命令安装expresssocket.io

  1. npm install --save express
  2. npm install --save socket.io

安装成功后,应该可以看到工作目录下生成了一个名为node_modules的文件夹,里面分别是expresssocket.io,接下来可以开始编写服务端的代码了,新建一个文件:index.js

  1. var app = require('express')();
  2. var http = require('http').Server(app);
  3. var io = require('socket.io')(http);
  4.  
  5. app.get('/', function(req, res){
  6. res.send('<h1>Welcome Realtime Server</h1>');
  7. });
  8.  
  9. http.listen(3000, function(){
  10. console.log('listening on *:3000');
  11. });

命令行运行node index.js,如果一切顺利,你应该会看到返回的listening on *:3000字样,这说明服务已经成功搭建了。此时浏览器中打开http://localhost:3000应该可以看到正常的欢迎页面。

如果你想要让服务运行在线上服务器,并且可以通过域名访问的话,可以使用Nginx做代理,在nginx.conf中添加如下配置,然后将域名(比如:realtime.plhwin.com)解析到服务器IP即可。

  1. server
  2. {
  3. listen 80;
  4. server_name realtime.plhwin.com;
  5. location / {
  6. proxy_pass http://127.0.0.1:3000;
  7. }
  8. }

完成以上步骤,http://realtime.plhwin.com:3000的后端服务就正常搭建了。

服务端代码实现

前面讲到的index.js运行在服务端,之前的代码只是一个简单的WebServer欢迎内容,让我们把WebSocket服务端完整的实现代码加入进去,整个服务端就可以处理客户端的请求了。完整的index.js代码如下:

  1. var app = require('express')();
  2. var http = require('http').Server(app);
  3. var io = require('socket.io')(http);
  4.  
  5. app.get('/', function(req, res){
  6. res.send('<h1>Welcome Realtime Server</h1>');
  7. });
  8.  
  9. //在线用户
  10. var onlineUsers = {};
  11. //当前在线人数
  12. var onlineCount = 0;
  13.  
  14. io.on('connection', function(socket){
  15. console.log('a user connected');
  16.  
  17. //监听新用户加入
  18. socket.on('login', function(obj){
  19. //将新加入用户的唯一标识当作socket的名称,后面退出的时候会用到
  20. socket.name = obj.userid;
  21.  
  22. //检查在线列表,如果不在里面就加入
  23. if(!onlineUsers.hasOwnProperty(obj.userid)) {
  24. onlineUsers[obj.userid] = obj.username;
  25. //在线人数+1
  26. onlineCount++;
  27. }
  28.  
  29. //向所有客户端广播用户加入
  30. io.emit('login', {onlineUsers:onlineUsers, onlineCount:onlineCount, user:obj});
  31. console.log(obj.username+'加入了聊天室');
  32. });
  33.  
  34. //监听用户退出
  35. socket.on('disconnect', function(){
  36. //将退出的用户从在线列表中删除
  37. if(onlineUsers.hasOwnProperty(socket.name)) {
  38. //退出用户的信息
  39. var obj = {userid:socket.name, username:onlineUsers[socket.name]};
  40.  
  41. //删除
  42. delete onlineUsers[socket.name];
  43. //在线人数-1
  44. onlineCount--;
  45.  
  46. //向所有客户端广播用户退出
  47. io.emit('logout', {onlineUsers:onlineUsers, onlineCount:onlineCount, user:obj});
  48. console.log(obj.username+'退出了聊天室');
  49. }
  50. });
  51.  
  52. //监听用户发布聊天内容
  53. socket.on('message', function(obj){
  54. //向所有客户端广播发布的消息
  55. io.emit('message', obj);
  56. console.log(obj.username+'说:'+obj.content);
  57. });
  58.  
  59. });
  60.  
  61. http.listen(3000, function(){
  62. console.log('listening on *:3000');
  63. });
  64.  

客户端代码实现

进入客户端工作目录/workspace/wwwroot/plhwin/demo.plhwin.com/chat,新建一个index.html:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <meta name="format-detection" content="telephone=no"/>
  6. <meta name="format-detection" content="email=no"/>
  7. <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=0" name="viewport">
  8. <title>多人聊天室</title>
  9. <link rel="stylesheet" type="text/css" href="./style.css" />
  10. <!--[if lt IE 8]><script src="./json3.min.js"></script><![endif]-->
  11. <script src="http://realtime.plhwin.com:3000/socket.io/socket.io.js"></script>
  12. </head>
  13. <body>
  14. <div id="loginbox">
  15. <div style="width:260px;margin:200px auto;">
  16. 请先输入你在聊天室的昵称
  17. <br/>
  18. <br/>
  19. <input type="text" style="width:180px;" placeholder="请输入用户名" id="username" name="username" />
  20. <input type="button" style="width:50px;" value="提交" onclick="CHAT.usernameSubmit();"/>
  21. </div>
  22. </div>
  23. <div id="chatbox" style="display:none;">
  24. <div style="background:#3d3d3d;height: 28px; width: 100%;font-size:12px;">
  25. <div style="line-height: 28px;color:#fff;">
  26. <span style="text-align:left;margin-left:10px;">Websocket多人聊天室</span>
  27. <span style="float:right; margin-right:10px;"><span id="showusername"></span> |
  28. <a href="javascript:;" onclick="CHAT.logout()" style="color:#fff;">退出</a></span>
  29. </div>
  30. </div>
  31. <div id="doc">
  32. <div id="chat">
  33. <div id="message" class="message">
  34. <div id="onlinecount" style="background:#EFEFF4; font-size:12px; margin-top:10px; margin-left:10px; color:#666;">
  35. </div>
  36. </div>
  37. <div class="input-box">
  38. <div class="input">
  39. <input type="text" maxlength="140" placeholder="请输入聊天内容,按Ctrl提交" id="content" name="content">
  40. </div>
  41. <div class="action">
  42. <button type="button" id="mjr_send" onclick="CHAT.submit();">提交</button>
  43. </div>
  44. </div>
  45. </div>
  46. </div>
  47. </div>
  48. <script type="text/javascript" src="./client.js"></script>
  49. </body>
  50. </html>
  51.  

上面的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是完整的客户端的业务逻辑实现代码,它的内容如下:

  1. (function () {
  2. var d = document,
  3. w = window,
  4. p = parseInt,
  5. dd = d.documentElement,
  6. db = d.body,
  7. dc = d.compatMode == 'CSS1Compat',
  8. dx = dc ? dd: db,
  9. ec = encodeURIComponent;
  10.  
  11. w.CHAT = {
  12. msgObj:d.getElementById("message"),
  13. screenheight:w.innerHeight ? w.innerHeight : dx.clientHeight,
  14. username:null,
  15. userid:null,
  16. socket:null,
  17. //让浏览器滚动条保持在最低部
  18. scrollToBottom:function(){
  19. w.scrollTo(0, this.msgObj.clientHeight);
  20. },
  21. //退出,本例只是一个简单的刷新
  22. logout:function(){
  23. //this.socket.disconnect();
  24. location.reload();
  25. },
  26. //提交聊天消息内容
  27. submit:function(){
  28. var content = d.getElementById("content").value;
  29. if(content != ''){
  30. var obj = {
  31. userid: this.userid,
  32. username: this.username,
  33. content: content
  34. };
  35. this.socket.emit('message', obj);
  36. d.getElementById("content").value = '';
  37. }
  38. return false;
  39. },
  40. genUid:function(){
  41. return new Date().getTime()+""+Math.floor(Math.random()*899+100);
  42. },
  43. //更新系统消息,本例中在用户加入、退出的时候调用
  44. updateSysMsg:function(o, action){
  45. //当前在线用户列表
  46. var onlineUsers = o.onlineUsers;
  47. //当前在线人数
  48. var onlineCount = o.onlineCount;
  49. //新加入用户的信息
  50. var user = o.user;
  51.  
  52. //更新在线人数
  53. var userhtml = '';
  54. var separator = '';
  55. for(key in onlineUsers) {
  56. if(onlineUsers.hasOwnProperty(key)){
  57. userhtml += separator+onlineUsers[key];
  58. separator = '、';
  59. }
  60. }
  61. d.getElementById("onlinecount").innerHTML = '当前共有 '+onlineCount+' 人在线,在线列表:'+userhtml;
  62.  
  63. //添加系统消息
  64. var html = '';
  65. html += '<div class="msg-system">';
  66. html += user.username;
  67. html += (action == 'login') ? ' 加入了聊天室' : ' 退出了聊天室';
  68. html += '</div>';
  69. var section = d.createElement('section');
  70. section.className = 'system J-mjrlinkWrap J-cutMsg';
  71. section.innerHTML = html;
  72. this.msgObj.appendChild(section);
  73. this.scrollToBottom();
  74. },
  75. //第一个界面用户提交用户名
  76. usernameSubmit:function(){
  77. var username = d.getElementById("username").value;
  78. if(username != ""){
  79. d.getElementById("username").value = '';
  80. d.getElementById("loginbox").style.display = 'none';
  81. d.getElementById("chatbox").style.display = 'block';
  82. this.init(username);
  83. }
  84. return false;
  85. },
  86. init:function(username){
  87. /*
  88. 客户端根据时间和随机数生成uid,这样使得聊天室用户名称可以重复。
  89. 实际项目中,如果是需要用户登录,那么直接采用用户的uid来做标识就可以
  90. */
  91. this.userid = this.genUid();
  92. this.username = username;
  93.  
  94. d.getElementById("showusername").innerHTML = this.username;
  95. this.msgObj.style.minHeight = (this.screenheight - db.clientHeight + this.msgObj.clientHeight) + "px";
  96. this.scrollToBottom();
  97.  
  98. //连接websocket后端服务器
  99. this.socket = io.connect('ws://realtime.plhwin.com:3000');
  100.  
  101. //告诉服务器端有用户登录
  102. this.socket.emit('login', {userid:this.userid, username:this.username});
  103.  
  104. //监听新用户登录
  105. this.socket.on('login', function(o){
  106. CHAT.updateSysMsg(o, 'login');
  107. });
  108.  
  109. //监听用户退出
  110. this.socket.on('logout', function(o){
  111. CHAT.updateSysMsg(o, 'logout');
  112. });
  113.  
  114. //监听消息发送
  115. this.socket.on('message', function(obj){
  116. var isme = (obj.userid == CHAT.userid) ? true : false;
  117. var contentDiv = '<div>'+obj.content+'</div>';
  118. var usernameDiv = '<span>'+obj.username+'</span>';
  119.  
  120. var section = d.createElement('section');
  121. if(isme){
  122. section.className = 'user';
  123. section.innerHTML = contentDiv + usernameDiv;
  124. } else {
  125. section.className = 'service';
  126. section.innerHTML = usernameDiv + contentDiv;
  127. }
  128. CHAT.msgObj.appendChild(section);
  129. CHAT.scrollToBottom();
  130. });
  131.  
  132. }
  133. };
  134. //通过“回车”提交用户名
  135. d.getElementById("username").onkeydown = function(e) {
  136. e = e || event;
  137. if (e.keyCode === 13) {
  138. CHAT.usernameSubmit();
  139. }
  140. };
  141. //通过“回车”提交信息
  142. d.getElementById("content").onkeydown = function(e) {
  143. e = e || event;
  144. if (e.keyCode === 13) {
  145. CHAT.submit();
  146. }
  147. };
  148. })();

至此所有的编码开发工作全部完成了,在浏览器中打开demo.plhwin.com/chat/就可以看到效果了。上面所有的客户端和服务端的代码可以从Github上获得,点这里跳转到Github项目主页,或者在命令行将代码Clone到本地。

  1. git clone https://github.com/plhwin/nodejs-socketio-chat.git

下载本地后有两个文件夹 clientserverclient文件夹是客户端源码,可以放在Nginx/Apache的WebServer中,也可以放在Node.js的WebServer中。后面的server文件夹里的代码是websocket服务端代码,放在Node.js环境中,使用npm安装完 expresssocket.io 后,node index.js 启动后端服务就可以了。

留给我们的思考

1、假设是一个在线客服系统,里面有许多的公司使用你的服务,每个公司自己的用户可以通过一个专属URL地址进入该公司的聊天室,聊天是一对一的,每个公司可以新建多个客服人员,每个客服人员可以同时和客户端的多个用户聊天。

2、又假设是一个在线WebIM系统,实现类似微信,qq的功能,客户端可以看到好友在线状态,在线列表,添加好友,删除好友,新建群组等,消息的发送除了支持基本的文字外,还能支持表情、图片和文件。

有兴趣的同学可以继续深入研究。

使用Node.js+Socket.IO搭建WebSocket实时应用的更多相关文章

  1. (转)使用Node.js+Socket.IO搭建WebSocket实时应用

    Web领域的实时推送技术,也被称作Realtime技术.这种技术要达到的目的是让用户不需要刷新浏览器就可以获得实时更新.它有着广泛的应用场景,比如在线聊天室.在线客服系统.评论系统.WebIM等. W ...

  2. 使用Node.js+Socket.IO搭建WebSocket实时应用【转载】

    原文:http://www.jianshu.com/p/d9b1273a93fd Web领域的实时推送技术,也被称作Realtime技术.这种技术要达到的目的是让用户不需要刷新浏览器就可以获得实时更新 ...

  3. 用node.js(socket.io)实现数据实时推送

    在做商品拍卖的时候,要求在商品的拍卖页面需要实时的更新当前商品的最高价格.实现的方式有很多,比如: 1.setInterval每隔n秒去异步拉取数据(缺点:更新不够实时) 2. AJAX轮询方式方式推 ...

  4. 转载:node.js socket.io

    本文转自:http://www.xiaocai.name/post/cf1f9_7b6507  学习node.js socket.io 使用 用node.js(socket.io)实现数据实时推送 在 ...

  5. 基于node.js+socket.io+html5实现的斗地主游戏(1)概述

    一.游戏描述 说是斗地主游戏,其实是寝室自创的"捉双A",跟很多地方的捉红10.打红A差不多,大概规则是: 1.基础牌型和斗地主一样,但没有大小王,共52张牌,每人13张,这也是为 ...

  6. node.js+socket.io配置详解

    由于我是在win7的环境下,在这里就以win7系统为例进行讲解了. 首先需要在nodejs官网下载最新版的node.js,下载完毕直接安装即可,安装成功后在cmd命令行中执行node指令,如下结果就说 ...

  7. 基于Node.js+socket.IO创建的Web聊天室

    这段时间进了一个新的项目组,项目是用Appcan来做一个跨平台的移动运维系统,其中前台和后台之间本来是打算用WebSocket来实现的,但写好了示例后发现android不支持WebSocket,大为受 ...

  8. node.js + socket.io实现聊天室一

    前段时间,公司打算在社区做一个聊天室.决定让我来做.本小白第一次做聊天类功能,当时还想着通过ajax请求来实现.经过经理提示,说试试当前流行的node.js 和socket.io来做.于是就上网学习研 ...

  9. node.js+socket.io安装

    最近做安卓遇到一个网络包的bug,服务端使用node做的,通讯用socket.io,但是服务端没法调试,没办法,还是自己搭建一个服务器端吧,索性买了阿里云的ecs测试,之前也配置过node+socke ...

随机推荐

  1. Oracle_创建和管理表

    创建和管理表 常见的数据库对象 Oracle 数据库中的表 查询数据字典 命名规则 CREATE TABLE 语句 数据类型 使用子查询创建表 ALTER TABLE 语句 删除表 清空表 改变对象的 ...

  2. 关于gitignore文件的创建与使用

    在我们使用github提交本地代码时,有些库文件和日志文件是不必要提交的,那如何处理这个问题呢?这个时候我们就会用到.gitignore文件了. 该篇博客我会介绍如何创建.gitignore,以及如何 ...

  3. 监视EntityFramework中的sql流转你需要知道的三种方式Log,SqlServerProfile, EFProfile

    大家在学习entityframework的时候,都知道那linq写的叫一个爽,再也不用区分不同RDMS的sql版本差异了,但是呢,高效率带来了差灵活性,我们 无法控制sql的生成策略,所以必须不要让自 ...

  4. const 与 readonly知多少

    原文地址: http://www.cnblogs.com/royenhome/archive/2010/05/22/1741592.html 尽管你写了很多年的C#的代码,但是可能当别人问到你cons ...

  5. C# WinForm国际化的简单实现

    软件行业发展到今天,国际化问题一直都占据非常重要的位置,而且应该越来越被重视.对于开发人员而言,在编写程序之前,国际化问题是首先要考虑的一个问题,也许有时候这个问题已经在设计者的考虑范围之内,但终归要 ...

  6. C#实现微信开发前奏

    不想废话,直接写了!因为是留给自己做随笔的,所以大神们看到别喷…… 1.必须有微信公众账号 2.你也可以申请测试微信号,链接给你   http://mp.weixin.qq.com/debug/cgi ...

  7. vs2015 已经支持开发asp .net core 1.0 rc2 程序了

    vs2015 已经支持开发asp .net core 1.0 rc2 程序了 http://mp.weixin.qq.com/s?__biz=MzI0MzM1ODczOQ==&mid=2247 ...

  8. Java核心技术点之多线程

    学习Java的同学注意了!!! 学习过程中遇到什么问题或者想获取学习资源的话,欢迎加入Java学习交流群,群号码:279558494 我们一起学Java! 本文主要从整体上介绍Java中的多线程技术, ...

  9. GJM : Unity3D HIAR 目录导航

    感谢您的阅读.喜欢的.有用的就请大哥大嫂们高抬贵手"推荐一下"吧!你的精神支持是博主强大的写作动力以及转载收藏动力.欢迎转载! 版权声明:本文原创发表于 [请点击连接前往] ,未经 ...

  10. jQuery拖动剪裁图片作为头像

    图片上传是许多网站的一个常用的功能,有时需要对上传的图片做初步的选择裁剪,比如上传头像.下面就是一个使用HTML5+jQuery实现的图片上传裁剪特效,可以对选择要上传的图片做缩小.放大.拖动和裁剪, ...