基於Node的Web聊天室

1 項目名稱

    Web聊天室(《這是NodeJs實戰》第二章的一個案例,把整個開發過程記錄下來)javascript

 

2 項目描述

    該項目是一個簡單的在線聊天程序。打開聊天頁面,程序自動給用戶分配一個暱稱,進入默認的Lobby聊天室。用戶能夠發送消息,也可使用聊天命令(聊天命令以/開頭)修改本身的暱稱或者加入已有的聊天室(聊天室不存在時,建立新的聊天室)。在加入或建立聊天室時,新聊天室的名稱會出如今聊天程序頂端的水平條上,也會出如今聊天消息區域右側的可用房間列表中。在用戶換到新房間後,系統會顯示信息以確認這一變化。css

3 系統設計

    該項目使用Node實現,由於Node用一個端口就能夠輕鬆地提供HTTP和WebSocket兩種服務。使用HTTP處理靜態文件的同時使用WebSocket實現實時數據(聊天消息)。程序的實現能夠劃分如下幾個功能模塊:html

  1. 提供靜態文件(好比HTML、CSS和客戶端JavaScript)
  2. 在服務器上處理與聊天相關的消息
  3. 在用戶的瀏覽器中處理與聊天相關的消息

    爲了提供靜態文件,須要使用Node內置的http模塊。但經過HTTP提供文件時,一般不能只是發送文件中的內容,還應該有所發送文件的類型。也就是說要用正確的MIME類型設置HTTP 頭的Content-Type。爲了查找這些MIME類型,會用到第三方的模塊mime。java

    爲了處理與聊天相關的消息,須要用Ajax輪詢服務器。爲了讓這個程序能儘量快的做出響應,咱們不會用傳統的Ajax發送消息。採用WebSocket,這是一個爲支持實時通信而設計的輕量的雙向通訊協議。由於在大多數狀況下,只有兼容HTML5的瀏覽器才支持WebSocket,因此這個程序會使用流行的Socket.IO庫,他給不能使用WebSocket的瀏覽器提供了一些後備措施。node

4 系統實現

    使用WebStorm開發該項目。WebStorm被稱爲「最強大的HTML5編輯器」、「最智能的JavaScript IDE」。jquery

4.1 建立程序的文件結構

   使用WebStorm,選擇一個目錄,建立一個新的空項目。設計項目結構以下所示:npm

 

4.2 指明依賴項

    程序的依賴項是在package.json文件中指明的。這個文件老是被放在程序的根目錄下。 package.json文件用於描述你的應用程序,它包含一些JSON表達式。在package.json文件中能夠定義不少事情,但最重要的是程序的名稱、版本號、對程序的描述,以及程序的依賴項。 代碼清單1中是一個包描述文件,描述了項目的功能和依賴項。將這個文件保存到項目的根目錄中,命名爲package.json。json

{
  "name": "chatrooms",
  "version": "0.0.1",
  "description":"Minimalist multiroom chat server",
  "dependencies":{
    "socket.io":"~0.9.6",
    "mime":"~1.2.11"
 
}
}數組

 

4.3 安裝依賴項

切換到DOS窗口,在項目的根目錄下輸入如下這條命令瀏覽器

npm install

若是按照失敗,切換到國內的npm鏡像,而後再安裝。鏡像使用方法(三種辦法任意一種都能解決問題,建議使用第三種,將配置寫死,下次用的時候配置還在):

1.經過config命令

npm config set registry https://registry.npm.taobao.org

npm info underscore (若是上面配置正確,這個命令會有字符串response)

2.命令行指定

npm --registry https://registry.npm.taobao.org info underscore

3.編輯 ~/.npmrc 加入下面內容

registry = https://registry.npm.taobao.org

 

    安裝成功後,在根目錄下建立的node_modules目錄,這個目錄中放的就是程序的依賴項。 

 

4.4提供HTML、CSS和客戶端 JavaScript的服務

    程序的邏輯是由一些文件實現的,有些運行在服務器上,有些運行在客戶端。 在客戶端運行的JavaScript須要做爲靜態資源發給瀏覽器,而不是在Node上執行。

服務器端的文件:

server.js

lib/chat_server.js

發送給客戶端的文件:

public/index.html

public/stylesheets/style.css

public/javascripts/chat.js

public/javascripts/chat_ui.js

4.4.1 在server.js中提供靜態文件服務器

/**
 * Created by Administrator on 2016-05-05.
 */
var http = require('http');
var fs = require('fs');
var path = require('path');
var mime = require('mime');
var cache={};//緩存文件內容的對象

/* 請求的文件不存在時,發送404錯誤*, /
function send404(response){
    response.writeHead(404,{'Content-Type':'text/plain'});
    response.write('Error 404:resource not found.');
    response.end();
}
/*  發送數據文件*/
function sendFile(response,filePath,fileContents){
 response.writeHead(200,{"Content-type":mime.lookup(path.basename(filePath))});
    response.end(fileContents);
}
/*提供靜態文件服務*/
function serverStatic(response,cache,absPath){
    if(cache[absPath]){
        sendFile(response, absPath,cache[absPath]);//從內存中返回數據
    }else{
        fs.exists(absPath,function(exists){//檢查文件是否存在
            if(exists){
                fs.readFile(absPath,function(err,data){
                    if(err){
                        send404(response);
                    }else{
                        cache[absPath] = data;
                        sendFile(response,absPath,data);
                    }
                });
            }else{
                send404(response);
            }
        });
    }
}
/* 1 建立HTTP服務器*,從該句代碼開始閱讀*/
var server= http.createServer(function(request,response){
    var filePath = false;
    if(request.url == '/'){
        filePath = 'public/index.html';
    }else{
        filePath = 'public' + request.url;
    }
    var absPath='./' + filePath;
    serverStatic(response,cache,absPath);
});
server.listen(3000,function(){
    console.log("server listening on port 3000.");
});
/* 2 加載chat_server,建立聊天服務器,chat_server 模塊隨後實現*/
var chatServer = require('./lib/chat_server');
chatServer.listen(server);

 

4.4.2添加HTML和CSS文件

    Index.html文件內容:

<!doctype html>
<html lang='en'>

<head>
    <title>Chat</title>
    <link rel='stylesheet' href='/stylesheets/style.css'></link>
</head>

<body>
<div id='content'>
    <div id='room'></div>
    <div id='room-list'></div>
    <div id='messages'></div>

    <form id='send-form'>
        <input id='send-message' />
        <input id='send-button' type='submit' value='Send'/>

        <div id='help'>
            Chat commands:
            <ul>
                <li>Change nickname: <code>/nick [username]</code></li>
                <li>Join/create room: <code>/join [room name]</code></li>
            </ul>
        </div>
    </form>
</div>

<script src='/socket.io/socket.io.js' type='text/javascript'></script>
<script src='http://code.jquery.com/jquery-1.8.0.min.js' type='text/javascript'></script>
<script src='/javascripts/chat.js' type='text/javascript'></script>
<script src='/javascripts/chat_ui.js' type='text/javascript'></script>
</body>
</html>

 

    style.css文件內容

body {
    padding: 50px;
    font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
    color: #00B7FF;
}

#content {
    width: 800px;
    margin-left: auto;
    margin-right: auto;
}
#room {
    background-color: #ddd;
    margin-bottom: 1em;
}
#messages {
    width: 690px;
    height: 300px;
    overflow: auto;
    background-color: #eee;
    margin-bottom: 1em;
    margin-right: 10px;
}
#room-list {
    float: right;
    width: 100px;
    height: 300px;
    overflow: auto;
}

#room-list div {
    border-bottom: 1px solid #eee;
}
#room-list div:hover {
    background-color: #ddd;
}
#send-message {
    width: 700px;
    margin-bottom: 1em;
    margin-right: 1em;
}
#help {
    font: 10px "Lucida Grande", Helvetica, Arial, sans-serif;
}

 

4.5用Socket.IO處理與聊天相關的功能

4.5.1 chat_server.js實現服務器端功能

var socketio = require('socket.io');
var io;
var guestNumber = 1;
var nickNames = {};
var namesUsed = [];
var currentRoom = {};
exports.listen = function(server){
//啓動socket.io服務器,容許它搭載在已有的http服務器上
    io = socketio.listen(server);
    io.set('log level',1);
//定義每一個用戶鏈接的處理邏輯
    io.sockets.on('connection',function(socket){

        // 1 客戶端鏈接後,分配用戶暱稱
        guestNumber=assignGuestName(socket,guestNumber,nickNames,namesUsed);
        // 2 加入Lobby聊天室
        joinRoom(socket,'Lobby');
        // 3 處理廣播消息
        handleMessageBroadcasting(socket,nickNames);
        // 4處理修改暱稱命令
        handleNameChangeAttempts(socket,nickNames,namesUsed);
        // 5 處理切換/建立聊天室命令
        handleRoomJoining(socket);
        // 6 當收到客戶端請求後,發送給客戶端房間列表
        socket.on('rooms',function(){
            socket.emit('rooms',io.sockets.manager.rooms);
        });
        // 7 處理客戶端斷開鏈接
        handleClientDisconnection(socket,nickNames,namesUsed);
    });
};
/* 分配用戶暱稱*/
function assignGuestName(socket,guestNumber,nickNames,namesUsed){
    var name='Guest'+guestNumber;
//將暱稱保存在暱稱集合nickNames中
    nickNames[socket.id] = name;
//發送給客戶端知悉其暱稱
    socket.emit('nameResult',{
        success:true,
        name:name
    });
//將暱稱保存在另外一個已使用的暱稱數組中
    namesUsed.push(name);
    return guestNumber + 1;
}


/*加入房間*/
function joinRoom(socket, room) {
//用戶進入房間
    socket.join(room);
//記錄用戶的當前房間
    currentRoom[socket.id] = room;
//讓用戶知悉他進入了新的房間
    socket.emit('joinResult', {room: room});
    //讓該房間裏的其餘用戶知悉有新用戶進入了房間
socket.broadcast.to(room).emit('message', {
        text: nickNames[socket.id] + ' has joined ' + room + '.'
    });
    //獲取該房間裏全部的用戶
    var usersInRoom = io.sockets.clients(room);
//若是用戶數量大於1
    if (usersInRoom.length > 1) {
        var usersInRoomSummary = 'Users currently in ' + room + ': ';
        for (var index in usersInRoom) {
            var userSocketId = usersInRoom[index].id;
//判斷非當前用戶
            if (userSocketId != socket.id) {
                if (index > 0) {
                    usersInRoomSummary += ', ';
                }
                usersInRoomSummary += nickNames[userSocketId];
            }
        }
        usersInRoomSummary += '.';
        //彙總該房間裏的其它成員名稱發送給給該用戶
        socket.emit('message', {text: usersInRoomSummary});
    }
}
/* 處理改名請求*/
function handleNameChangeAttempts(socket, nickNames, namesUsed) {
    socket.on('nameAttempt', function(name) {
        if (name.indexOf('Guest') == 0) {//不能以Guest開頭
            socket.emit('nameResult', {
                success: false,
                message: 'Names cannot begin with "Guest".'
            });
        } else {//註冊用戶名稱
            if (namesUsed.indexOf(name) == -1) {
                var previousName = nickNames[socket.id];
                var previousNameIndex = namesUsed.indexOf(previousName);
                namesUsed.push(name);
                nickNames[socket.id] = name;
                delete namesUsed[previousNameIndex];
//當前用戶會收到改名信息
                socket.emit('nameResult', {
                    success: true,
                    name: name
                });
//房間中其餘用戶知悉當前用戶已改名
                socket.broadcast.to(currentRoom[socket.id]).emit('message', {
                    text: previousName + ' is now known as ' + name + '.'
                });
            } else {//該暱稱已經存在
                socket.emit('nameResult', {
                    success: false,
                    message: 'That name is already in use.'
                });
            }
        }
    });
}
/*發送聊天消息,即Node服務器收到客戶端消息,轉發給該房間的其它用戶*/
function handleMessageBroadcasting(socket) {
    socket.on('message', function (message) {
        socket.broadcast.to(message.room).emit('message', {
            text: nickNames[socket.id] + ': ' + message.text
        });
    });
}
/* 切換聊天室,即離開當前房間,加入其餘房間,房間不存在則建立新的房間*/
function handleRoomJoining(socket) {
    socket.on('join', function(room) {
        socket.leave(currentRoom[socket.id]);
        joinRoom(socket, room.newRoom);
    });
}
/*處理客戶端斷開鏈接*/
function handleClientDisconnection(socket) {
    socket.on('disconnect', function() {
        var nameIndex = namesUsed.indexOf(nickNames[socket.id]);
        delete namesUsed[nameIndex];
        delete nickNames[socket.id];
    });
}

 

4.5.2 chat.js以及chat_ui.js實現客戶端功能

    客戶端JavaScript須要實現如下功能:向服務器發送用戶的消息和暱稱/房間變動請求; 顯示其餘用戶的消息,以及可用房間的列表。Chat.js中定義一個原型對象,用於處理聊天消息和命令,該原型對象中的函數在chat_ui.js中調用。

chat.js文件內容:

/**
 * Created by Administrator on 2016-05-05.
 */
/*JavaScript原型對象,處理髮送聊天消息、變動房間、處理聊天命令*/
var Chat = function(socket) {
    this.socket = socket;
};

Chat.prototype.sendMessage = function(room, text) {
    var message = {
        room: room,
        text: text
    };
    this.socket.emit('message', message);
};

Chat.prototype.changeRoom = function(room) {
    this.socket.emit('join', {
        newRoom: room
    });
};

Chat.prototype.processCommand = function(command) {
    var words = command.split(' ');
    var command = words[0]
        .substring(1, words[0].length)
        .toLowerCase();
    var message = false;

    switch(command) {
        case 'join':
            words.shift();
            var room = words.join(' ');
            this.changeRoom(room);//變動房間
            break;
        case 'nick':
            words.shift();
            var name = words.join(' ');
            this.socket.emit('nameAttempt', name);//修改暱稱
            break;
        default:
            message = 'Unrecognized command.';
            break;
    };

    return message;
};

 

chat_ui.js文件內容:

/**
 * Created by Administrator on 2016-05-05.
 */
/*用來顯示可疑的文本。它會淨化文本,將特殊字符轉換 成HTML實體*/
function divEscapedContentElement(message) {
    return $('<div></div>').text(message);
}
/*顯示系統建立的受信內容*/
function divSystemContentElement(message) {
    return $('<div></div>').html('<i>' + message + '</i>');
}
/*顯示用戶輸入的信息*/
function processUserInput(chatApp, socket) {
    var message = $('#send-message').val();
    var systemMessage;

    if (message.charAt(0) == '/') {//顯示系統受信內容
        systemMessage = chatApp.processCommand(message);
        if (systemMessage) {
            $('#messages').append(divSystemContentElement(systemMessage));
        }
    } else {//顯示用戶輸入內容
        chatApp.sendMessage($('#room').text(), message);
        $('#messages').append(divEscapedContentElement(message));
        $('#messages').scrollTop($('#messages').prop('scrollHeight'));
    }
    //清空輸入框
    $('#send-message').val('');
}
/*客戶端程序初始化邏輯*/
var socket = io.connect();

$(document).ready(function() {
    var chatApp = new Chat(socket);
    //顯示用戶改名結果
    socket.on('nameResult', function(result) {
        var message;
        if (result.success) {
            message = 'You are now known as ' + result.name + '.';
        } else {
            message = result.message;
        }
        $('#messages').append(divSystemContentElement(message));
    });
    //顯示用戶切換聊天室結果
    socket.on('joinResult', function(result) {
        $('#room').text(result.room);
        $('#messages').append(divSystemContentElement('Room changed.'));
    });
    //顯示聊天消息
    socket.on('message', function (message) {
        var newElement = $('<div></div>').text(message.text);
        $('#messages').append(newElement);
    });
    //顯示房間列表
    socket.on('rooms', function(rooms) {
        $('#room-list').empty();

        for(var room in rooms) {
            room = room.substring(1, room.length);
            if (room != '') {
                $('#room-list').append(divEscapedContentElement(room));
            }
        }

        $('#room-list div').click(function() {
            chatApp.processCommand('/join ' + $(this).text());
            $('#send-message').focus();
        });
    });
    //每間隔1秒,向服務器從新請求房間列表
    setInterval(function() {
        socket.emit('rooms');
    }, 1000);

    $('#send-message').focus();
    //提交表單,發送聊天消息
    $('#send-form').submit(function() {
        processUserInput(chatApp, socket);
        return false;
    });
});

 

4.6 服務器與客戶端的事件分析

服務器與客戶端的交互主要是經過相互發送事件-處理事件完成的,如下是在整個流程中發生的事件:

4.6.1服務器事件流程

'connection'事件//接收客戶端鏈接

{

    // 1 assignGuestName()函數中的事件

    socket.emit('nameResult',{ //分配默認暱稱

        success:true,

        name:name

    });

 

   // 2 joinRoom()函數中的事件

   socket.emit('joinResult', {room: room});//加入房間

   socket.broadcast.to(room).emit('message', {//廣播消息

        text: nickNames[socket.id] + ' has joined ' + room + '.'

    });

   socket.emit('message', {text: usersInRoomSummary});//發送給每一個用戶包含該房間的其餘用戶的列表

 

   // 3 handleMessageBroadcasting()函數中的事件

   socket.on('message', function (message) {//收到客戶端消息後,發射給同房間的其餘用戶

        socket.broadcast.to(message.room).emit('message', {

            text: nickNames[socket.id] + ': ' + message.text

        });

    });

 

   // 4 handleNameChangeAttempts()函數中的事件

   socket.emit('nameResult', {//改名

                    success: true,

                    name: name

                });

   socket.broadcast.to(currentRoom[socket.id]).emit('message', {//房間中其餘用戶知悉當前用戶已改名

                    text: previousName + ' is now known as ' + name + '.'

                });

    // 5 handleRoomJoining()函數中的事件

    socket.on('join', function(room) {//切換聊天室

        socket.leave(currentRoom[socket.id]);

        joinRoom(socket, room.newRoom);

    });

    // 6 當收到客戶端請求後,發送給客戶端房間列表

        socket.on('rooms',function(){

            socket.emit('rooms',io.sockets.manager.rooms);

        });

    // 7 handleClientDisconnection()函數中的事件

    socket.on('disconnect', function() {

        var nameIndex = namesUsed.indexOf(nickNames[socket.id]);

        delete namesUsed[nameIndex];

        delete nickNames[socket.id];

    });

 

}

4.6.2客戶端事件流程

// 1 鏈接服務器

var socket = io.connect();

// 2 鏈接服務器後,處理服務器發送過來的事件

socket.on('nameResult', function(result) {...});//顯示暱稱

socket.on('joinResult', function(result) {...});//顯示加入房間

socket.on('message', function (message) {...});//顯示該房間的其餘用戶

socket.on('rooms', function(rooms) {...});//顯示房間列表

setInterval(function() {

        socket.emit('rooms');//按期向服務器請求房間列表

    }, 1000);

 

// 3 點擊房間列表

$('#room-list div').click(function() {

       this.socket.emit('join', { newRoom: room});//切換聊天室

     });

 

// 4 點擊提交按鈕調用processUserInput()函數中觸發的客戶端事件

this.socket.emit('message', message);//發送消息

this.socket.emit('nameAttempt', name);//改名

this.socket.emit('join', { newRoom: room});//建立聊天室

相關文章
相關標籤/搜索