WebSocket學習(一)——基於socket.io實現簡單多人聊天室

前言

什麼是Websocket呢?
我們都知道在Http協議中,客戶端與服務器端的通信是靠客戶端發起請求,然後服務器端收到請求再進行迴應,這個過程中,客戶端是主動的,服務器端是被動的。Websocket協議就不一樣了,它是基於TCP的一種新的網絡協議,它與Http協議不同之處就在於Websocket能實現服務器端主動推送消息到客戶端,服務器端與客戶端都能發起通信,這一次,服務器端終於也擁有了主動權。

什麼是socket.io
socket.io封裝了Websocket以及其他的一些協議,並且實現了Websocket的服務端代碼。同時還有很強的兼容性,兼容各種瀏覽器以及移動設備。有了它,我們能更方便快捷地實現服務器端與客戶端之間的實時通訊。

實現功能簡述

要實現多人聊天室的核心就是區分當前用戶發送的消息與其他用戶發送的消息,在這裏我通過用戶登錄使用的用戶名來進行區分。所以用戶進入首先展示登錄頁面。


登錄成功之後,新用戶加入聊天室



如果用戶重名,會彈出提示,保持吳彥祖的登錄狀態,我們再打開一個標籤,輸入“吳彥祖”查看效果



只有當暱稱唯一時,才允許登錄,我們再登錄一個查看效果

可以看到,當新用戶登錄時,其他在線用戶會收到提示,接下來就是發送消息了

發送的消息是實時推送的,當前用戶發送的消息與其他用戶發送的消息對話框做了區分。
當用戶退出時,系統也會給出提示,效果如下



怎麼樣,有沒有興趣繼續瞭解呢?下面就開始着手開發吧。

環境搭建

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


安裝完成之後,可以看到文件夾下多了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地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章