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; }); });
最后效果圖: