实时聊天

实时聊天使用 Node js Socket IO 搭建 WebSocket 实时应用原创 nbsp 2014 05 28 nbsp 彼得潘 nbsp 编程 nbsp nbsp nbsp Web 领域的实时推送技术 也被称作 Realtime 技术 这种技术要达到的目的是让用户不需要刷新浏览器就可以获得实时更新 它有着广泛的应用场景 比如在线聊天室 在线客服系统 评论系统 WebIM 等 WebSocket 简介谈到 Web 实时推送 就

使用Node.js+Socket.IO搭建WebSocket实时应用

实时聊天

WebSocket简介

谈到Web实时推送,就不得不说WebSocket。在WebSocket出现之前,很多网站为了实现实时推送技术,通常采用的方案是轮询(Polling)和Comet技术,Comet又可细分为两种实现方式,一种是长轮询机制,一种称为流技术,这两种方式实际上是对轮询技术的改进,这些方案带来很明显的缺点,需要由浏览器对服务器发出HTTP request,大量消耗服务器带宽和资源。面对这种状况,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽并实现真正意义上的实时推送。

WebSocket协议本质上是一个基于TCP的协议,它由通信协议和编程API组成,WebSocket能够在浏览器和服务器之间建立双向连接,以基于事件的方式,赋予浏览器实时通信能力。既然是双向通信,就意味着服务器端和客户端可以同时发送并响应请求,而不再像HTTP的请求和响应。

为了建立一个WebSocket连接,客户端浏览器首先要向服务器发起一个HTTP请求,这个请求和通常的HTTP请求不同,包含了一些附加头信息,其中附加头信息”Upgrade: WebSocket”表明这是一个申请协议升级的HTTP请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的WebSocket连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。

前面讲到WebSocket是HTML5中新增的一种通信协议,这意味着一部分老版本浏览器(主要是IE10以下版本)并不具备这个功能, 通过百度统计的公开数据显示,IE8目前仍以33%的市场份额占据榜首,好在chrome浏览器市场份额逐年上升,现在以超过26%的市场份额位居第二,同时微软前不久宣布停止对IE6的技术支持并提示用户更新到新版本浏览器,这个曾经让无数前端工程师为之头疼的浏览器有望退出历史舞台,再加上几乎所有的智能手机浏览器都支持HTML5,所以使得WebSocket的实战意义大增,但是无论如何,我们实际的项目中,仍然要考虑低版本浏览器的兼容方案:在支持WebSocket的浏览器中采用新技术,而在不支持WebSocket的浏览器里启用Comet来接收发送消息。

WebSocket实战

本文将以多人在线聊天应用作为实例场景,我们先来确定这个聊天应用的基本需求。

需求分析

在实际的开发过程中,为了使用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 2 3 4 
node -v  v0.10.26  npm -v  1.4.6 

搭建WebSocket服务端

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

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


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

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


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

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

1 2 3 4 5 6 7 8 9 10 11 
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http);  app.get('/', function(req, res){ 
         res.send('

Welcome Realtime Server

'
);
}); 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即可。

1 2 3 4 5 6 7 8 
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代码如下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 
var app = require('express')(); var http = require('http').Server(app); var io = require('socket.io')(http);  app.get('/', function(req, res){ 
         res.send('

Welcome Realtime Server

'
);
}); //在线用户 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:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 
 <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" />   
          <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;">退出 
        a> 
       span>   
        div>   
        div>  <div id="doc">  <div id="chat">  <div id="message" class="message"> <div id="onlinecount" style="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> 

第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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 
(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();
}
};
})();

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

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

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

留给我们的思考

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

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

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/175744.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月26日 下午11:31
下一篇 2026年3月26日 下午11:31


相关推荐

  • SSO单点登录简单实现

    SSO单点登录简单实现通过 cookie 来实现单点登录 1 单点登录的流程 cookie 是用来在客户端存储数据的工具 在其中一个子系统登录 跳转到登录系统 登陆系统登陆完成 完成登录后会向发起登录的子系统写入一个 cookie 保存用于认证用户是否登录的信息 token 其他子系统向服务器发起请求的时候 携带这个 cookie 完成登录 cookie 的域要是所有子系统相同的域 这样所有子系统才能访问到这个 cookie 2 单点登录流程图 2 单点登录的编码 部分核心代码 代码链接 https gitee com yueh

    2025年8月2日
    4
  • ubuntu18 防火墙关闭_Ubuntu 18.04 关闭及开启防火墙

    ubuntu18 防火墙关闭_Ubuntu 18.04 关闭及开启防火墙Ubuntu 内建使用 UFW Uncomplicate 作为防火墙管理工具 一般情况下都会开启防火墙 但有些特殊情况 例如测试环境需要关闭防火墙作测试 或者对网络设定进行除错等 以下是在 Ubuntu18 04 关闭防火墙的方法 首先检查目前防火墙是否已经开启 执行以下指令 sudoufwstatu 防火墙默认是关闭 会输出 Status inac

    2026年3月16日
    2
  • 微信公众平台小程序开发教程

    微信公众平台小程序开发教程本文档将带你一步步创建完成一个微信小程序 并可以在手机上体验该小程序的实际效果 这个小程序的首页将会显示欢迎语以及当前用户的微信头像 点击头像 可以在新开的页面中查看当前小程序的启动日志 下载源码 1 获取微信小程序的 AppID 如果你是收邀请的开发者 我们会提供一个帐号 利用提供的帐号 登录 https mp weixin com 就可以在网站的 设置 开发者设置 中

    2026年3月16日
    2
  • 普通索引与唯一索引的区别_唯一索引怎么设置

    普通索引与唯一索引的区别_唯一索引怎么设置所谓普通索引,就是在创建索引时,不附加任何限制条件(唯一、非空等限制)。该类型的索引可以创建在任何数据类型的字段上。所谓唯一索引,就是在创建索引时,限制索引的值必须是唯一的。通过该类型的索引可以更快速地查询某条记录。普通索引还是唯一索引?假设你在维护一个市民系统,每个人都有一个唯一的身份证号,而且业务代码已经保证了不会写入两个重复的身份证号。如果市民系统需要按照身份证号查姓名,就会…

    2026年2月2日
    5
  • jar命令解压war包_java解压文件

    jar命令解压war包_java解压文件在J2EEWeb开发中,Web应用程序存档(WAR)文件只是一个普通的JAR文件,它包含您的所有Web应用程序组件,例如servlet,Java类,库,资源等。有关详细信息,请阅读Wiki。问题当前的Web应用程序WAR文件是通过Ant或Maven工具生成的,复制到*nix环境进行部署,但是不知道如何提取WAR文件?解WAR文件只是一个JAR文件,要提取它,…

    2022年10月4日
    5
  • 关于fastjson在Object转String时的一个坑

    关于fastjson在Object转String时的一个坑关于fastjson在Object转String时的一个坑背景在公司的业务中有这么一个场景:需要将某个系统的结果对象放进Hbase中,然后被后续系统读取使用,在Hbase存储的时候,需要将对象中每个属性解析成字符串存入,但一开始存入接口的设计者只考虑了普通类型(Integer、Long、String之类)的字段存储,未考虑Map或者POJO对象的存储,因此原始代码如下: … if(O…

    2022年6月5日
    61

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号