Node實戰之聊天室


Node實戰之聊天室

Node如何同時處理Http和WebSocket

  1.只出現在用戶訪問聊天程序網站時:Web瀏覽器->Http請求->Node服務器->Http響應->Web瀏覽器

  2.在用戶聊天時持續發生:Web瀏覽器->WebSocket數據發送->Node服務器->WebSocket數據接收->Web瀏覽器

 

開始搭建

  1.創建程序文檔結構(如下圖所示)

  

  2.指明依賴項

    程序的依賴項是在package.json文件中指明的。這個文件總是被放在程序的根目錄下。package。json文件用於描述你的應用程序,它包含一些JSON表達式,並遵循CommonJS包描述標准。在package.json文件中可以定義很多事情,但最重要的是程序的名稱、版本號、對程序的描述,以及程序的依賴項。

  包描述文件

  

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

 

  3.安裝依賴項

    定義好package.json文件之后,安裝程序的依賴項就是小菜一碟了。Node包管理器(npm)是Node自帶的工具,他有很多功能,可以輕松安裝第三方Node模塊,可以把你自己創建的任何Node模塊向全球發布。它用一行命令就能從package.json文件中讀出依賴項,把他們都裝好。

  在根目錄下(E:\nodeTest\chatrooms)輸入如下命令

    npm install

  在看這個目錄,你應該能看到node_modules目錄,這個目錄中存放的就是程序的依賴項。

 

  4.創建靜態文件服務器 server.js

    (1).發送文件數據及錯誤響應

//(1)請求的文件不存在時發送404錯誤
function send404(response){
    response.writeHead(404,{'Content-Type':'text/plain'});
    response.write('Error 404 :resource not found.');
    response.end();
}

//(2)輔助函數提供文件數據服務。這個函數先寫出正確的HTTP頭,然后發送文件的內容。
function sendFile(response,filePath,fileContents){
    
    response.writeHead(
        200,
        {"Content-Type":mime.lookup(path.basename(filePath))}
    );
    response.end(fileContents);
}
//(3)訪問內存(RAM)要比訪談我呢文件系統快得多,所以Node程序通常會把常用的數據緩存到內存里。
//我們的聊天程序就要把靜態文件緩存到內存中,只有第一次訪問的時候才會從文件系統中讀取。
//下一個輔助函數會確定文件是否緩存了,如果是,就返回它。如果文件還沒被緩存,它會從硬盤中
//讀取並返回它。如果文件不存在,則返回一個HTTP 404錯誤作為響應。
function serveStatic(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);
            }
        });
    }
}

     (2).創建HTTP服務器

      在創建HTTP服務期時,需要給createServer傳入一個匿名函數作為回調函數,由它來處理每個HTTP請求。這個回調函數接受兩個參數:request和response。在這個    回調函數執行時,HTTP服務器會分別組裝這兩個參數對象,以便你可以對請求的細節進行處理,並返回一個響應。

//創建HTTP服務器,用匿名函數定義對每個請求的處理行為
var server = http.createServer(function(request,response){
    var filePath = false;
    if(request.url == '/'){
        filePath = 'public/index.html'; //確定返回的默認HTML文件
    }else{
        filePath = 'public' + request.url; //將url路徑轉換成文件的相對路徑
    }
    
    var absPath = './' + filePath;
    serveStatic(response,cache,absPath); //返回靜態文件
})

    (3).啟動HTTP服務器

  

server.listen(3000,function(){
    console.log("服務器已啟動  端口號3000");
});

   5.創建html和css

  

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>聊天室</title>
        <link rel='stylesheet' href='/css/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='發送' />
                
                <div id='help'>
                    聊天室操作
                    <ul>
                        <li>昵稱:<code>/nick[username]</code></li>
                        <li>進入/創建房間:<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.min.js' type='text/javascript'></script>
        <script src='/js/chat.js' type='text/javascript'></script>
        <script src='/js/chat_ui.js' type='text/javascript'></script>
    </body>
</html>
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;
}

  6.用Socket,IO處理與聊天相關的消息

  Socket.IO為Node及客戶端JavaScript提供了基於WebSocket以及其他傳輸方式的封裝,它提供了一個抽象層。如果瀏覽器沒有實現WebSocket,Socket.IO會自動啟用一個備選方案,而對外提供的API還是一樣的。

  Socket.IO提供了開箱即用的虛擬通道,所以程序不用把每條消息都向已經連接的用戶廣播,而只向那些預訂了某個通道的用戶廣播。用這個功能實現程序里的聊天室非常簡單,很快你就能看到。

  Socket.IO還是事件發射器(Event Emitter)的好例子。事件發射器本質上是組織異步邏輯的一種很方便的設計模式。

  事件發射器是跟某種資源相關聯的,它能向這個資源發送消息,也能從這個資源接收消息。資源可以鏈接遠程服務器,或者更抽象的東西。

    (1)設置Socket.IO服務器

//一.設置Socket.IO服務器
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){
        console.log("a new user connection!");
        //在用戶鏈接上來時,賦予其一個訪客名
        guestNumber = assignGuestName(socket,guestNumber,nickNames,namesUsed);
        //在用戶連接上來時把它放入聊天室Lobby里
        joinRoom(socket,'Lobby');
        
        handleMessageBroadcasting(socket,nickNames);
        
        handleNameChangeAttempts(socket,nickNames,namesUsed);
        
        handleRoomJoining(socket);
        //用戶發出請求時,向其提供已經被占用的聊天室的列表
        socket.on('rooms',function(){
            socket.emit('rooms',io.sockets.manager.rooms);
        });
        //定義用戶斷開連接后的清除邏輯
        handleClientDisconnection(socket,nickNames,namesUsed);
    });
}

//二.處理程序場景及事件
//1.分配用戶昵稱
function assignGuestName(socket,guestNumber,nickNames,namesUsed){
    //生成新昵稱
    var  name = 'Guest' +guestNumber;
    nickNames[socket.id] = name;
    //讓用戶知道他們的昵稱
    socket.emit('nameResult',{
        success : true,
        name : name
    });
    //存放已經被占用的昵稱
    namesUsed.push(name);
    //增加用來生成昵稱的計數器
    return guestNumber + 1;
}

//2.進入聊天室
function joinRoom(socket,room){
    console.log(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);
    //如果不止一個用戶在這個房間里,匯總一下都是誰
    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});
    }
}
//3.處理昵稱變更請求
function handleNameChangeAttempts(socket,nickNames,namesUsed){
    socket.on('nameAttempt',function(name){
        //昵稱不能以Guest開頭
        if(name.indexOf('Guest') ==0){
            socket.emit('nameResult',{
                success: false,
                message: 'Name 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 konwn as ' + name + '.'
                });
            }else{
                //如果用戶名已經被占用,給客戶端發送錯誤消息
                socket.emit('nameResult',{
                    success:false,
                    message:'That name is aleady in use.'
                });
            }
        }
    });
}

//4.發送聊天消息 Socket.IO的broadcase函數是用來轉發消息的.

function handleMessageBroadcasting(socket){
    socket.on('message',function(message){
        socket.broadcast.to(message.room).emit('message',{
            text:nickNames[socket.id] + ': ' + message.text
        });
    });
}

//5.創建房間
function handleRoomJoining(socket){
    socket.on('join',function(room){
        //console.log(room.newRoom);
        //console.log(currentRoom[socket.id]);
        socket.leave(currentRoom[socket.id]);
        joinRoom(socket,room.newRoom);
    });
}
//6.用戶斷開連接
function handleClientDisconnection(socket){
    socket.on('disconnect',function(){
        var nameIndex = namesUsed.indexOf(nickNames[socket.id]);
        delete namesUsed[nameIndex];
        delete nickNames[socket.id];
    });
}

    7.在程序的用戶界面上使用客戶端JavaScript

      (1).向服務器發送用戶的消息和昵稱/房間變更請求;(在js目錄中新建chat.js)

      (2).顯示其他用戶的消息,以及可用房間的列表(在js目錄中新建chat_ui.js)

chat.js

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;
    console.log(command);
    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 = 'Unrecongnized command';
            break;
    }
    return message;
}

chat_ui.js

//顯示可疑的文本數據
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('');
}



//用戶的瀏覽器加載完頁面后執行  對客戶端Socket.IO事件處理進行初始化
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 konw as ' + result.name + '.';
        }else{
            message = result.message;
        }
        $('#messages').append(divSystemContentElement);
    });
    
    //顯示房間變更結果
    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();
        console.log(rooms);
        for(var room in rooms){
            //console.log(rooms.size);
            room = room.substring(1,room.length);
            console.log(room);
            if(room !=''){
                $('#room-list').append(divEscapedContentElement(room));
            }
        }
        $('#room-list div').click(function(){
            chatApp.processCommand('/join' + $(this).text());
            $('#send-message').focus();
        });
    });
    
    setInterval(function(){
        socket.emit('rooms');
    },1000);
    
    $('#send-message').focus();
    
    $('#send-form').submit(function(){
        processUserInput(chatApp,socket);
        return false;
    });
});

 

最后效果圖:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM