WebSocket学习(一)——基于socket.io实现简单多人聊天室

前言

什么是Websocket呢?
我们都知道在Http协议中,客户端与服务器端的通信是靠客户端发起请求,然后服务器端收到请求再进行回应,这个过程中,客户端是主动的,服务器端是被动的。Websocket协议就不一样了,它是基于TCP的一种新的网络协议,它与Http协议不同之处就在于Websocket能实现服务器端主动推送消息到客户端,服务器端与客户端都能发起通信,这一次,服务器端终于也拥有了主动权。

什么是socket.io
socket.io封装了Websocket以及其他的一些协议,并且实现了Websocket的服务端代码。同时还有很强的兼容性,兼容各种浏览器以及移动设备。有了它,我们能更方便快捷地实现服务器端与客户端之间的实时通讯。

实现功能简述

要实现多人聊天室的核心就是区分当前用户发送的消息与其他用户发送的消息,在这里我通过用户登录使用的用户名来进行区分。所以用户进入首先展示登录页面。

图1.登录页面

登录成功之后,新用户加入聊天室


图2.登录成功

如果用户重名,会弹出提示,保持吴彦祖的登录状态,我们再打开一个标签,输入“吴彦祖”查看效果


图3.昵称重复

只有当昵称唯一时,才允许登录,我们再登录一个查看效果
图4.登录提示

可以看到,当新用户登录时,其他在线用户会收到提示,接下来就是发送消息了
图5.发送消息展示

发送的消息是实时推送的,当前用户发送的消息与其他用户发送的消息对话框做了区分。
当用户退出时,系统也会给出提示,效果如下


图6.退出登录

怎么样,有没有兴趣继续了解呢?下面就开始着手开发吧。

环境搭建

1.安装node.js

后端服务是用的node.js,所以我们首先要进行安装,安装方法很简单,我在之前一篇文章也提过。首先在node.js官网下载稳定版本,下载完成后点击安装,安装过程也很简单,一直next即可,安装完成会自动添加nodenpm环境变量。

检验是否安装成功,在cmd输入命令node -v,回车 及 npm -v,回车,如出现下图所示版本信息,表示安装成功

2.新建项目文件夹,安装socket.io

新建文件夹chatroom,在这里我把它建到D盘根目录下。打开cmd,定位到刚建的chatroom文件夹下,输入npm install socket.io安装socket.io

安装socket.io

安装完成之后,可以看到文件夹下多了node_modules文件,里面全是刚下载的socket.io依赖包。

3.新建页面

chatroom文件夹下新建页面文件index.html,样式chat.css,后端jsapp.js,前端jschat.js,并下载jquery.min.js,socket.io.js。再下载一张图片作为用户头像,放在images/user/下。
目录结构如下

文件结构

好了,环境搭建完成,开始撸码吧。

项目搭建

1.构建node服务器

在app.js里面构建服务器

/*app.js*/
/*构建http服务*/
var app = require('http').createServer()
/*引入socket.io*/
var io = require('socket.io')(app);
/*定义监听端口,可以自定义,端口不要被占用*/
var PORT = 8081;
/*监听端口*/
app.listen(PORT);

console.log('app listen at'+PORT);

接着启动服务
打开cmd,定位到app.js所在目录,输入node app.js,如图所示,打印出了我们写的内容,表示服务启动成功。

启动服务

2.建立服务端socket连接监听

先给大家简单讲一下服务器端与客户端通信的基本方法
大家可以看一下socket.io的文档
(1)socket.emit
客户端与服务器端之间发送消息是用emit
例如客户端向服务端发送登录请求
socket.emit('login',{username:uname}) login是自定义的事件,后面是带的参数
(2)socket.on
服务器端要接收客户端发送的login事件,就得对该事件进行监听
socket.on('login',function(data){})在回调函数中进行处理
同理,服务器端也可以向客户端发送事件,只要客户端也对该事件进行监听就行
(3)io.sockets.emit
服务器端向连接的所有客户端发送消息得用io.sockets.emit
(4)socket.broadcast.emit
给除了自己以外的客户端广播消息

/*app.js*/
/*构建http服务*/
var app = require('http').createServer()
/*引入socket.io*/
var io = require('socket.io')(app);
/*定义监听端口,可以自定义,端口不要被占用*/
var PORT = 8081;
/*监听端口*/
app.listen(PORT);

/**
*监听客户端连接
*io是我们定义的服务端的socket
*回调函数里面的socket是本次连接的客户端socket
*io与socket是一对多的关系
*/
io.on('connection', function (socket) {
  /*所有的监听on,与发送emit都得写在连接里面,包括断开连接*/
})
console.log('app listen at'+PORT);

3.前端页面

index.html页面中需引入socket.io.jssocket.io.js下载地址

/*index.html*/
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
    <title>聊天室</title>
    <link type="text/css" rel="stylesheet" href="css/chat.css">
    <script type="text/javascript" src="js/jquery.min.js"></script>
    <script type="text/javascript" src="js/socket.io.js"></script>
    <script type="text/javascript" src="js/chat.js"></script>
</head>
<body>
    /*登录界面*/
    <div class="login-wrap">
        <div class="login-con">
            <h3>用户登录</h3>
            <input type="text" placeholder="请输入昵称" id="loginName">
            <button class="login-btn">登录</button>
        </div>
    </div>
    
    /*聊天界面,一开始隐藏,用户登录成功后再显示*/
    <div class="chat-wrap hide">
        <h1>多人聊天室</h1>
        <div class="chat-con clearfix"></div>
        <div class="bottom">
            <input type="text" id="sendtxt">
            <button class="sendBtn">发送</button>
        </div>
    </div>
</body>
</html>

4.样式

样式可以自己编写,我这里随便写了一下

/*公共样式*/
*{padding:0; margin:0;}
html,body{width:100%;height: 100%;}
.clearfix:after{content:".";display:block;height:0;clear:both;visibility:hidden}
.clearfix{*zoom:1}
.cred{color:#f03e3e;}
.cgreen{color:#459d36;}
.hide{display:none;}
.fr{float:right;}
.fl{float: left;}
.rela{position: relative;}
.abs{position:absolute;}
h1{position: fixed; z-index:20; width: 100%; height:50px; line-height:50px; font-size:20px; left: 0; top: 0; background: #000; color: #fff;}

/*登录界面*/
.login-wrap{background:#e7e7e7;width:100%;height:100%; text-align:center;}
.login-con{padding-top: 50px;}
.login-con h3{margin-bottom: 20px;}
.login-con input{width:60%; display:block; margin:0 auto; height: 40px; line-height: 40px; margin-bottom: 20px;}
.login-con button{width:60%;display:block; margin:0 auto; height: 40px; line-height:40px; border:none; background:#459d36; color:#fff; border-radius:5px;}

/*聊天界面*/
.chat-wrap{width: 100%; height: 100%;overflow-y:scroll; background:#e7e7e7; text-align:center;}
.chat-con{padding: 50px 0; background:#e7e7e7;}
.chat-con p{display:inline-block; padding:5px 10px; background:#999;border-radius:5px; color:#fff; margin:5px 0;}
.bottom{position:fixed;bottom:0; left: 0; width:100%; height: 50px; background: #fff;}
.bottom input{width: 78%; height: 50px; line-height: 50px; float:left;border:none;}
.bottom button{width: 20%;height: 50px; float: right; border:none; background:#459d36;color: #fff;}
.chat-item{width:100%; margin-bottom:20px;}
.item-right .message{background: #62b900;}
.item-left .message{background: #fff; margin-top:20px;}
.item-left .img{margin-right:10px;}
.item-left .uname{font-size:12px; left:50px; top:0;}
.chat-item .message{width:60%;display:block; padding:10px;border-radius:5px; margin-right:10px;}
.chat-item .img{display:inline-block; width:40px; height:40px; background:url(../images/user/user.jpg) no-repeat; background-size:100% 100%;}

5.逻辑编写

(1)登录

客户端
浏览器端将获得的用户输入的昵称信息,发送到服务器端,告诉服务器端我要触发login事件。
在客户端chat.js中发送登录事件

/*chat.js*/
$(function(){
    /*建立socket连接,使用websocket协议,端口号是服务器端监听端口号*/
    var socket = io('ws://localhost:8081');
    /*定义用户名*/
    var uname = null;

    /*登录*/
    $('.login-btn').click(function(){
        uname = $.trim($('#loginName').val());
        if(uname){
            /*向服务端发送登录事件*/
            socket.emit('login',{username:uname})
        }else{
            alert('请输入昵称')
        }
    })

})

服务器端
服务器端监听login事件,在后台打印出获取到的昵称信息。
在服务器端app.js中监听登录事件

/*app.js*/
var app = require('http').createServer()
var io = require('socket.io')(app);
var PORT = 8081;

app.listen(PORT);
io.on('connection', function (socket) {
    /*监听登录*/
    socket.on('login',function(data){
        console.log(data)
    })
})

console.log('app listen at'+PORT);

注:更改了app.js,需再次启动服务才能看见效果

打开cmd,按Ctrl+C退出上次服务,再次输入node app.js启动服务,打开浏览器,查看效果


可以看到,点击登录按钮时,服务器端打印出了接收到的用户名信息,没问题,继续往下写登录成功事件。

(2)登录成功与失败

由于没有使用到数据库,所以我们就定义一个用户数组,用户每次登录之后,我们就判断该昵称是否已存在,如果已存在就弹出提示,转到登录失败事件,如果该昵称不存在数组里面,就视为新用户,转到登录成功事件,并且将该昵称存入数组。

服务器端

/*app.js*/
var app = require('http').createServer()
var io = require('socket.io')(app);
var PORT = 8081;
/*定义用户数组*/
var users = [];

app.listen(PORT);
io.on('connection', function (socket) {
    /*是否是新用户标识*/
    var isNewPerson = true; 
    /*当前登录用户*/
    var username = null;
    /*监听登录*/
    socket.on('login',function(data){
        for(var i=0;i<users.length;i++){
            if(users[i].username === data.username){
                isNewPerson = false
                break;
            }else{
                isNewPerson = true
            }
        }
        if(isNewPerson){
            username = data.username
            users.push({
              username:data.username
            })
            /*登录成功*/
            socket.emit('loginSuccess',data)
            /*向所有连接的客户端广播add事件*/
            io.sockets.emit('add',data)
        }else{
            /*登录失败*/
            socket.emit('loginFail','')
        }  
    })
})

console.log('app listen at'+PORT);

客户端

/*chat.js*/
$(function(){
    /*建立socket连接,使用websocket协议,端口号是服务器端监听端口号*/
    var socket = io('ws://localhost:8081');
    /*定义用户名*/
    var uname = null;

    /*登录*/
    $('.login-btn').click(function(){
        uname = $.trim($('#loginName').val());
        if(uname){
            /*向服务端发送登录事件*/
            socket.emit('login',{username:uname})
        }else{
            alert('请输入昵称')
        }
    })

    /*登录成功*/
    socket.on('loginSuccess',function(data){
        if(data.username === uname){
            checkin(data)
        }else{
            alert('用户名不匹配,请重试')
        }
    })

    /*登录失败*/
    socket.on('loginFail',function(){
        alert('昵称重复')
    })

    /*新人加入提示*/
    socket.on('add',function(data){
        var html = '<p>系统消息:'+data.username+'已加入群聊</p>';
        $('.chat-con').append(html);
    })

    /*隐藏登录界面 显示聊天界面*/
    function checkin(data){
        $('.login-wrap').hide('slow');
        $('.chat-wrap').show('slow');
    }
})

再次重启服务,打开浏览器查看效果,登录成功效果如上面图2所示,登录失败效果如上面图3所示。

(3)退出登录

退出登录,只需服务器端在用户数组里面删除退出的用户即可。
服务器端

/*app.js*/
/*退出登录*/
/*写在io.on('connection', function (socket) {})里面*/
socket.on('disconnect',function(){
    /*向所有连接的客户端广播leave事件*/
    io.sockets.emit('leave',username)
    users.map(function(val,index){
        if(val.username === username){
            users.splice(index,1);
        }
    })
 })

客户端

/*chat.js*/
/*退出群聊提示*/
socket.on('leave',function(name){
    if(name != null){
        var html = '<p>FBI warning:'+name+'已退出群聊</p>';
        $('.chat-con').append(html);
    }
})

(4)发送消息

客户端

/*chat.js*/
/*发送消息*/
$('.sendBtn').click(function(){
    sendMessage()
});
$(document).keydown(function(event){
    if(event.keyCode == 13){
        sendMessage()
    }
})
function sendMessage(){
    var txt = $('#sendtxt').val();
    $('#sendtxt').val('');
    if(txt){
        socket.emit('sendMessage',{username:uname,message:txt});
    }
}

服务器端

/*app.js*/
socket.on('sendMessage',function(data){
    io.sockets.emit('receiveMessage',data)
})

客户端

/*chat.js*/
/*接收消息*/
socket.on('receiveMessage',function(data){
    showMessage(data)
})

/*显示消息*/
function showMessage(data){
    var html
    if(data.username === uname){
        html = '<div class="chat-item item-right clearfix"><span class="img fr"></span><span class="message fr">'+data.message+'</span></div>'
    }else{
        html='<div class="chat-item item-left clearfix rela"><span class="abs uname">'+data.username+'</span><span class="img fl"></span><span class="fl message">'+data.message+'</span></div>'
    }
    $('.chat-con').append(html);
}

到这里,一个简单的多人聊天室已基本实现了,先回顾一下准备工作
(1)下载node.js
(2)安装socket.io
npm install socket.io
(3)服务器端构建http服务,引入socket.io,并设置监听端口

var app = require('http').createServer()
var io = require('socket.io')(app);
var PORT = 8081;
app.listen(PORT);

(4)客户端进行socket连接,使用websocket协议
var socket = io('ws://localhost:8081');


再回顾一下整个逻辑流程:
(1)客户端获取用户输入昵称,发送给服务器端;
(2)服务器端接收昵称,判断是否新用户,是则发送登录成功事件,否则发送登录失败事件
(3)客户端收到服务器端发送的登录成功或失败事件,进行相应处理
(4)浏览器端获取登录用户输入的消息,将消息与用户昵称一起发送给服务器端
(5)服务器端接收到用户发送的消息,广播该消息给当前连接的所有客户端
(6)客户端接收服务器端发送来的消息,判断昵称是否是自己,进行相应对话框显示
最后附上github地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容