请选择 进入手机版 | 继续访问电脑版

默认
打赏 发表评论 14
一个WebSocket实时聊天室Demo:基于node.js+socket.io [附件下载]

前言


在HTML5的WebSocket出现之前,为了实现Web端的即时通讯能力,主流的实现所用的技术基本都是轮询(polling)。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给客户端的浏览器。这种传统的HTTP request 的模式带来很明显的缺点:浏览器需要不断的向服务器发出请求,然而HTTP request 的header是非常长的,里面包含的有用数据可能只是一个很小的值,这样会占用很多的带宽。

而比较新的技术去做轮询的效果是Comet:用了AJAX(具体请参见:《Comet技术详解:基于HTTP长连接的Web端实时通信技术》)。但这种技术虽然可达到全双工通信,但依然每次都需要客户端主动发出请求。

新一代HTML5规范在传统的web交互基础上为我们带来了众多的新特性,随着web技术被广泛用于web APP的开发,这些新特性得以推广和使用,而WebSocket作为一种新的web通信技术具有巨大意义。

基于WebSocket的实时通信,浏览器和服务器只需要做一个握手的动作,然后浏览器和服务器之间就形成了一条快速通道,两者之间就直接可以数据互相传送。

在此WebSocket 协议中,为我们实现Web端即时通信服务带来了两大好处:

  • 极大的减小了Header的大小:互相沟通的Header是很小的-大概只有 2 Bytes
  • Server Push(服务端主动推的能力):服务器不再被动的接收到浏览器的request之后才返回数据,而是在有新数据时就主动推送给浏览器。

本文将基于HTML5规范中的WebSocket技术,使用Node.js和Socket.io(关于Socket.io介绍,请参见Socket.IO介绍:支持WebSocket、用于WEB端的即时通讯的框架》)来实现一个可用于Web端的简易实时聊天室,源码可从文末附件中下载到。

运行效果截图


52im.net.png

WebSocket简介


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

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

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

一个典型WebSocket客户端请求头:

a.jpg

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

WebSocket实战


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

1需求分析


1、兼容不支持WebSocket的低版本浏览器。
2、允许客户端有相同的用户名。
3、进入聊天室后可以看到当前在线的用户和在线人数。
4、用户上线或退出,所有在线的客户端应该实时更新。
5、用户发送消息,所有客户端实时收取。

在实际的开发过程中,为了使用WebSocket接口构建Web应用,我们首先需要构建一个实现了 WebSocket规范的服务端,服务端的实现不受平台和开发语言的限制,只需要遵从WebSocket规范即可,目前已经出现了一些比较成熟的WebSocket服务端实现,比如本文使用的Node.js+Socket.IO。为什么选用这个方案呢?先来简单介绍下他们两。

2Node.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的成长的速度有目共睹,其发展前景获得了技术社区的充分肯定。

3Socket.IO


Socket.IO是一个开源的WebSocket库,它通过Node.js实现WebSocket服务端,同时也提供客户端JS库。Socket.IO支持以事件为基础的实时双向通讯,它可以工作在任何平台、浏览器或移动设备。

Socket.IO支持4种协议:WebSocket、htmlfile、xhr-polling、jsonp-polling,它会自动根据浏览器选择适合的通讯方式,从而让开发者可以聚焦到功能的实现而不是平台的兼容性,同时Socket.IO具有不错的稳定性和性能。(更多Socket.io的文章,请参见:http://www.52im.net/forum.php?mod=collection&action=view&ctid=15

编码实现


本文一开始的的插图就是效果演示图:可以这里查看在线演示,整个开发过程非常简单,下面简单记录了开发步骤。

1安装Node.js


根据自己的操作系统,去Node.js官网下载安装即可。如果成功安装,在命令行输入node -v和npm -v应该能看到相应的版本号。
node -v  
v0.10.26  
npm -v  
1.4.6

2搭建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.domain.com)解析到服务器IP即可。
server
{
  listen       80;
  server_name  realtime.plhwin.com;
  location / {
    proxy_pass [url=http://127.0.0.1:3000;]http://127.0.0.1:3000;[/url]
  }
}

完成以上步骤,后端服务就正常搭建完成了。

服务端代码实现


前面讲到的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="提交"/>
            </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:;" 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">提交</button>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <script type="text/javascript" src="./client.js"></script>
    </body>
</html>

上面的html内容本身没有什么好说的,我们主要看看里面的4个文件请求:

  • /socket.io/socket.io.js
  • style.css
  • json3.min.js
  • 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 += '<div class="msg-system">';
                        html += user.username;
                        html += (action == 'login') ? ' 加入了聊天室' : ' 退出了聊天室';
                        html += '</div>';
                        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 = '<div>'+obj.content+'</div>';
                                var usernameDiv = '<span>'+obj.username+'</span>';
                                
                                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/

源码下载


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

附件下载:
nodejs socketio聊天室Demo源码(52im.net).zip (13.44 KB , 下载次数: 107 , 售价: 2 金币)

即时通讯网 - 即时通讯开发者社区! 来源: - 即时通讯开发者社区!

上一篇:独家发布《TCP/IP详解 卷1:协议》CHM版 [附件下载]下一篇:钉钉——基于IM技术的新一代企业OA平台的技术挑战(视频+PPT) [附件下载]

本帖已收录至以下技术专辑

推荐方案
评论 14
网上能找到的socket.io的都是群聊的例子。。。
引用:PonyZhao 发表于 2016-09-05 12:57
网上能找到的socket.io的都是群聊的例子。。。

是的,我之前研究过。socket.io对群聊封装的很简单,但要想真正实现生产上用的聊天:一对一单聊、登陆认证这些都得自已去处理,如果对nodejs或JavaScript不熟练的话,要掌握这些至少还得一周时间。
引用:IMDeveloper 发表于 2016-09-05 12:59
是的,我之前研究过。socket.io对群聊封装的很简单,但要想真正实现生产上用的聊天:一对一单聊、登陆认 ...

网上的demo多是基于socket.io v1.0以下版本,而socket.io 1.0以后要实现登陆认证跟原先v0.9版有很大区别,改天有空我来写一个更完整的能用于生产的系列教程。
签名: 《保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)》http://www.52im.net/thread-1138-1-1.html
学习了
学习啦
签名: 遇见
好东西,下载学习学习
学习啦
必须用//连接websocket后端服务器this.socket = io.connect('ws://realtime.plhwin.com');这个服务器么?改成本地本机可以么??如果改的话怎么改啊??菜鸟求指点啊
引用:悲伤逆流成河 发表于 2016-09-22 15:26
必须用//连接websocket后端服务器this.socket = io.connect('ws://realtime.plhwin.com');这个服务器么?改 ...

本机就用 ws://127.0.0.1
签名: 《保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)》http://www.52im.net/thread-1138-1-1.html
提示: 该帖被管理员或版主屏蔽
签名: 来过
Node+Express+MongoDB+Socket.io的方式做IM开发靠谱么?有没有人试过坑?
签名: 该会员没有填写今日想说内容.
引用:ashura 发表于 2017-05-18 10:29
Node+Express+MongoDB+Socket.io的方式做IM开发靠谱么?有没有人试过坑?

nodejs+socket.io是WebSocket很好的实现方案,网易云信的Web版、号称国产的Zendesk的逸创客服云都是用的这种。见下图:
图片1.png
图片2.png
签名: 《保活终极总结(二):Android6.0及以上的保活实践(进程防杀篇)》http://www.52im.net/thread-1138-1-1.html
引用:JackJiang 发表于 2017-05-18 10:40
nodejs+socket.io是WebSocket很好的实现方案,网易云信的Web版、号称国产的Zendesk的逸创客服云都是用的 ...

多谢楼主,看来有进一步深入学习的价值!
签名: 该会员没有填写今日想说内容.
我在写一个基于swoole的
签名: 该会员没有填写今日想说内容.
打赏楼主 ×
使用微信打赏! 使用支付宝打赏!

Processed in 0.156249 second(s), 38 queries , Gzip On.

返回顶部