使用Angular和Nodejs、socket.io搭建聊天室及多人聊天室
一,利用Node搭建靜態(tài)服務(wù)器
這個(gè)是這個(gè)項(xiàng)目的底層支撐部分。用來(lái)支持靜態(tài)資源文件像html, css, gif, jpg, png, javascript, json, plain text等等靜態(tài)資源的訪問(wèn)。這里面是有一個(gè)mime類(lèi)型的文件映射。
mime.js
/**
* mime類(lèi)型的 map
* @ author Cheng Liufeng
* @ date 2014/8/30
* 當(dāng)請(qǐng)求靜態(tài)服務(wù)器文件的類(lèi)型 html, css, gif, jpg, png, javascript, json, plain text, 我們會(huì)在此文件進(jìn)行映射
*/
exports.types = {
"css": "text/css",
"gif": "image/gif",
"html": "text/html",
"ico": "image/x-icon",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"js": "text/javascript",
"json": "application/json",
"pdf": "application/pdf",
"png": "image/png",
"svg": "image/svg+xml",
"swf": "application/x-shockwave-flash",
"tiff": "image/tiff",
"txt": "text/plain",
"wav": "audio/x-wav",
"wma": "audio/x-ms-wma",
"wmv": "video/x-ms-wmv",
"xml": "text/xml"
};
這里面我先解釋一下從輸入網(wǎng)址到頁(yè)面出現(xiàn)的過(guò)程。 當(dāng)用戶在瀏覽器地址欄里面輸入一個(gè)url的時(shí)候。
接下來(lái)會(huì)發(fā)生一系列的過(guò)程。首先是DNS解析, 將域名轉(zhuǎn)換成對(duì)應(yīng)的IP地址,之后瀏覽器與遠(yuǎn)程Web服務(wù)器通過(guò)TCP三次握手協(xié)商來(lái)建立一個(gè)TCP/IP連接。該握手包括一個(gè)同步報(bào)文,開(kāi)一個(gè)同步-應(yīng)答報(bào)文和一個(gè)應(yīng)答報(bào)文,這三個(gè)報(bào)文在瀏覽器和服務(wù)器之間傳遞。該握手首先由客戶端嘗試建立起通信,而后服務(wù)器應(yīng)答并接受客戶端的請(qǐng)求,最后由客戶端發(fā)出該請(qǐng)求已經(jīng)被接受的報(bào)文。一旦TCP/IP連接建立,瀏覽器會(huì)通過(guò)該連接向遠(yuǎn)程服務(wù)器發(fā)送HTTP的GET請(qǐng)求。
遠(yuǎn)程服務(wù)器找到資源并使用HTTP響應(yīng)返回該資源,值為200的HTTP響應(yīng)狀態(tài)表示一個(gè)正確的響應(yīng)。此時(shí),Web服務(wù)器提供資源服務(wù),客戶端開(kāi)始下載資源。下載的資源包括了html文件,css文件,javascript文件,image文件。然后開(kāi)始構(gòu)建一顆渲染樹(shù)和一顆DOM樹(shù),期間會(huì)有css阻塞和js阻塞。所以底層是需要一個(gè)靜態(tài)服務(wù)器支撐。這里面我原生構(gòu)造一個(gè)靜態(tài)服務(wù)器,不采用express框架。
事實(shí)上每一次資源文件請(qǐng)求的過(guò)程是一次次GET請(qǐng)求。下面我解釋一下客戶端(瀏覽器端或者采用linux下采用curl方式)的GET請(qǐng)求所對(duì)應(yīng)的服務(wù)端處理過(guò)程。一次Get請(qǐng)求發(fā)送到服務(wù)端后,服務(wù)端可以根據(jù)GET請(qǐng)求對(duì)應(yīng)一個(gè)資源文件的路徑。知道了這個(gè)路徑后,我們就可以采用文件讀寫(xiě)的方式獲取指定路徑下的資源,然后返回給客戶端。
我們知道Node里面的文件讀寫(xiě)的API有readFile和readFileSync,但是更好的方式是采用流的方式去讀取文件,采用流的方式的優(yōu)點(diǎn)是可以采用緩存和gzip壓縮。
OK,那么如何實(shí)現(xiàn)緩存呢?通常情況下,客戶端第一次去請(qǐng)求的時(shí)候,服務(wù)端會(huì)讀取資源文件,返回給客戶端。但是第二次再去請(qǐng)求同樣的文件時(shí),這個(gè)時(shí)候還是需要發(fā)送一次請(qǐng)求到服務(wù)端。服務(wù)端會(huì)根據(jù)Expires, cache-control, If-Modified-Since等Http頭信息判斷這個(gè)資源是否已經(jīng)緩存過(guò)。如果有緩存,服務(wù)端則不會(huì)再次訪問(wèn)資源文件的實(shí)際路徑。直接返回緩存的資源。
server.js
/**
* 聊天室服務(wù)端
* 功能:實(shí)現(xiàn)了Node版的靜態(tài)服務(wù)器
* 實(shí)現(xiàn)了緩存,gzip壓縮等
* @ author Cheng Liufeng
* @ date 2014/8/30
*/
// 設(shè)置端口號(hào)
var PORT = 3000;
// 引入模塊
var http = require('http');
var url = require('url');
var fs = require('fs');
var path = require('path');
var zlib = require('zlib');
// 引入文件
var mime = require('./mime').types;
var config = require('./config');
var chatServer = require('./utils/chat_server');
var server = http.createServer(function (req, res) {
res.setHeader("Server","Node/V8");
// 獲取文件路徑
var pathName = url.parse(req.url).pathname;
if(pathName.slice(-1) === "/"){
pathName = pathName + "index.html"; //默認(rèn)取當(dāng)前默認(rèn)下的index.html
}
// 安全處理(當(dāng)使用Linux 的 curl命令訪問(wèn)時(shí),存在安全隱患)
var realPath = path.join("client", path.normalize(pathName.replace(/\.\./g, "")));
// 檢查文件路徑是否存在
path.exists(realPath, function(exists) {
// 當(dāng)文件不存在時(shí)的情況, 輸出一個(gè)404錯(cuò)誤
if (!exists) {
res.writeHead(404, "Not Found", {'Content-Type': 'text/plain'});
res.write("The request url" + pathName +" is not found!");
res.end();
} else { // 當(dāng)文件存在時(shí)的處理邏輯
fs.stat(realPath, function(err, stat) {
// 獲取文件擴(kuò)展名
var ext = path.extname(realPath);
ext = ext ? ext.slice(1) : "unknown";
var contentType = mime[ext] || "text/plain";
// 設(shè)置 Content-Type
res.setHeader("Content-Type", contentType);
var lastModified = stat.mtime.toUTCString();
var ifModifiedSince = "If-Modified-Since".toLowerCase();
res.setHeader("Last-Modified", lastModified);
if (ext.match(config.Expires.fileMatch)) {
var expires = new Date();
expires.setTime(expires.getTime() + config.Expires.maxAge * 1000);
res.setHeader("Expires", expires.toUTCString());
res.setHeader("Cache-Control", "max-age=" + config.Expires.maxAge);
}
if (req.headers[ifModifiedSince] && lastModified == req.headers[ifModifiedSince]) {
res.writeHead(304, "Not Modified");
res.end();
} else {
// 使用流的方式去讀取文件
var raw = fs.createReadStream(realPath);
var acceptEncoding = req.headers['accept-encoding'] || "";
var matched = ext.match(config.Compress.match);
if (matched && acceptEncoding.match(/\bgzip\b/)) {
res.writeHead(200, "Ok", {'Content-Encoding': 'gzip'});
raw.pipe(zlib.createGzip()).pipe(res);
} else if (matched && acceptEncoding.match(/\bdeflate\b/)) {
res.writeHead(200, "Ok", {'Content-Encoding': 'deflate'});
raw.pipe(zlib.createDeflate()).pipe(res);
} else {
res.writeHead(200, "Ok");
raw.pipe(res);
}
//下面是普通的讀取文件的方式,不推薦
// fs.readFile(realPath, "binary", function(err, data) {
// if(err) {
// // file exists, but have some error while read
// res.writeHead(500, {'Content-Type': 'text/plain'});
// res.end(err);
// } else {
// // file exists, can success return
// res.writeHead(200, {'Content-Type': contentType});
// res.write(data, "binary");
// res.end();
// }
// });
}
});
}
});
});
//監(jiān)聽(tīng)3000端口
server.listen(PORT, function() {
console.log("Server is listening on port " + PORT + "!");
});
// 讓socket.io服務(wù)器和http服務(wù)器共享一個(gè)端口
chatServer.listen(server);
二,服務(wù)端利用WebSocket構(gòu)建聊天室服務(wù)端
為什么采用websocket?
我們知道現(xiàn)在主流的聊天室還是采用ajax去實(shí)現(xiàn)客戶端和服務(wù)端的通信。采用的是一種輪詢的機(jī)制。所謂輪詢,就是客戶端每隔一段時(shí)間就去發(fā)送一次請(qǐng)求,詢問(wèn)服務(wù)端,看看服務(wù)端有沒(méi)有新的聊天數(shù)據(jù),如果有新的數(shù)據(jù),就返回給客戶端。
Websocket則完全不同。 websocket是基于長(zhǎng)鏈接。就是客戶端和服務(wù)端一旦建立鏈接之后,這個(gè)鏈接就會(huì)一直存在。 是一種全雙工的通信。 這個(gè)時(shí)候的機(jī)制有點(diǎn)類(lèi)似發(fā)布-訂閱模式。 客戶端會(huì)訂閱一些事件,一旦服務(wù)端有新的數(shù)據(jù)出現(xiàn),會(huì)主動(dòng)推送給客戶端。
websocket采用的是ws協(xié)議,不是http協(xié)議或者h(yuǎn)ttps協(xié)議。另外采用websocket的另一個(gè)好處就是可以減少很多數(shù)據(jù)流量。文章開(kāi)頭,我已經(jīng)介紹了傳統(tǒng)的一次資源請(qǐng)求過(guò)程,需要三次握手協(xié)議,而且每次請(qǐng)求頭所占空間比較大,這樣會(huì)很費(fèi)流量。而Websocket里面的互相溝通的Header是很小的-大概只有 2 Bytes。
/**
* 聊天服務(wù)。
*/
var socketio = require('socket.io');
var io;
var guestNumber = 1; //初始用戶名編號(hào)
var nickNames = {}; // 昵稱(chēng)列表
var namesUsed = []; //使用過(guò)的用戶名
var currentRoom = {}; //當(dāng)前聊天室
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;
}
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 + '.'
});
}
function handleMessageBroadcasting(socket) {
socket.on('message', function(message) {
socket.broadcast.to(message.room).emit('message', {
text: nickNames[socket.id] + ':' + message.text
});
});
}
exports.listen = function(server) {
io = socketio.listen(server);
io.set('log level', 1);
// 定義每個(gè)用戶的連接處理
io.sockets.on('connection', function(socket) {
// 分配一個(gè)用戶名
guestNumber = assignGuestName(socket, guestNumber, nickNames, namesUsed);
// 將用戶加入聊天室Lobby里
joinRoom(socket, 'Lobby');
//處理聊天信息
handleMessageBroadcasting(socket, nickNames);
//handleNameChangeAttempts(socket, nickNames, namesUsed);
//handleRoomJoining(socket);
//handleClientDisconnection(socket, nickNames, namesUsed);
});
};
三,利用Angular搭建聊天室客戶端
為什么使用Angular?
作為一款前端MVC框架,Angular.js無(wú)疑是引人注目的。模塊化,雙向數(shù)據(jù)綁定,指令系統(tǒng),依賴注入。而且Angular內(nèi)置jquerylite,這讓熟悉jQuery語(yǔ)法的同學(xué)很容易上手。
當(dāng)然,個(gè)人認(rèn)為, angular在構(gòu)建一個(gè)單頁(yè)應(yīng)用和crud項(xiàng)目方面有很大的優(yōu)勢(shì)。 我們這個(gè)聊天室就是基于SPA(single page application)的目的。
index.html
<!DOCTYPE html> <html ng-app="chatApp"> <head> <meta name="viewport" content="width=device-width, user-scalable=no"> </head> <body ng-controller="InitCtrl"> <div ng-view></div> <script src="lib/angular.js"></script> <script src="lib/angular-route.js"></script> <script src="lib/socket.io.js"></script> <script src="app.js"></script> <script src="controllers/InitCtrl.js"></script> </body> </html>
怎樣構(gòu)建一個(gè)單頁(yè)應(yīng)用?單頁(yè)應(yīng)用的原理?
先談?wù)剢雾?yè)應(yīng)用的原理。所謂單頁(yè),并不是整個(gè)頁(yè)面無(wú)刷新。當(dāng)你審查一下google chrome的console控制臺(tái)的時(shí)候,你會(huì)發(fā)現(xiàn),angular內(nèi)部還是采用了ajax去異步請(qǐng)求資源。所以只是局部刷新。但是這種方式相對(duì)于以前的DOM節(jié)點(diǎn)的刪除和修改已經(jīng)有很大的進(jìn)步了。
構(gòu)建單頁(yè)應(yīng)用,我們需要借助于angular-route.js。這個(gè)angular子項(xiàng)目可以幫助我們定義路由和對(duì)應(yīng)的邏輯處理控制器。利用它,我們可以實(shí)現(xiàn)一個(gè)單頁(yè)應(yīng)用。
app.js
/**
* 客戶端(目前只支持瀏覽器,將來(lái)會(huì)擴(kuò)展到移動(dòng)端)程序入口文件
* 創(chuàng)建一個(gè)模塊,并且命名為chatApp
* 配置路由,實(shí)現(xiàn)單頁(yè)應(yīng)用(single page application)
*/
var chatApp = angular.module("chatApp", ['ngRoute']);
// 路由配置
chatApp.config(function($routeProvider) {
$routeProvider.when('/', {
templateUrl : 'views/init.html',
controller: 'InitCtrl'
})
.when('/init', {
templateUrl : 'views/init.html',
controller: 'InitCtrl'
});
});
客戶端聊天界面的代碼邏輯如下
InitCtrl.js
/**
* # InitCtrl
*/
angular.module('chatApp').controller('InitCtrl', function($scope) {
var socket = io.connect('http://127.0.0.1:3000');
socket.on('nameResult', function(result) {
var message;
if (result.success) {
message = 'you are now known as ' + result.name + '.';
console.log('message=', message);
document.getElementById('guestname').innerHTML = message;
} else {
message = result.message;
}
});
socket.on('joinResult', function(result) {
document.getElementById('room').innerHTML = result.room;
});
$scope.sendMessage = function() {
var message = {
room: 'Lobby',
text: document.getElementById('user_input').value
};
socket.emit('message', message);
};
socket.on('message', function(message) {
var p = document.createElement('p');
p.innerHTML = message.text;
document.getElementById('message').appendChild(p);
});
});
基于node.js和socket.io搭建多人聊天室
剛學(xué)node.js,想著做點(diǎn)東西練練手。網(wǎng)上的東西多而雜,走了不少?gòu)澛罚艘惶鞎r(shí)間在調(diào)代碼上。參考網(wǎng)上的一篇文章,重寫(xiě)了部分代碼,原來(lái)的是基于基于node-websocket-server框架的,我沒(méi)用框架,單單是socket.io。
一、基本功能
1、用戶隨意輸入一個(gè)昵稱(chēng)即可登錄
2、登錄成功后
1) 對(duì)正在登錄用戶來(lái)說(shuō),羅列所有在線用戶列表,羅列最近的歷史聊天記錄
2) 對(duì)已登錄的用戶來(lái)說(shuō),通知有新用戶進(jìn)入房間,更新在線用戶列表
3、退出登錄
1)支持直接退出
2) 當(dāng)有用戶退出,其他所有在線用戶會(huì)收到信息,通知又用戶退出房間,同時(shí)更新在線用戶列表
4、聊天
1) 聊天就是廣播,把信息廣播給所有連接在線的用戶
5、一些出錯(cuò)處理
1) 暫時(shí)簡(jiǎn)單處理了系統(tǒng)邏輯錯(cuò)誤、網(wǎng)絡(luò)出錯(cuò)等特殊情況的出錯(cuò)提示
問(wèn)題:功能不完善,有bug(退出后,新用戶重新登錄,還是原來(lái)的用戶) 。抽空完善吧
二、技術(shù)介紹
socket.io(官網(wǎng):http://socket.io/)是一個(gè)跨平臺(tái),多種連接方式自動(dòng)切換,做即時(shí)通訊方面的開(kāi)發(fā)很方便,而且能和expressjs提供的傳統(tǒng)請(qǐng)求方式很好的結(jié)合,即可以在同一個(gè)域名,同一個(gè)端口提供兩種連接方式:request/response, websocket(flashsocket,ajax…)。
這篇文章對(duì)socket.io的使用做了詳細(xì)介紹:http://www.dbjr.com.cn/article/71361.htm
《用node.js和Websocket做個(gè)多人聊天室吧》http://www.html5china.com/HTML5features/WebSocket/20111206_3096.html
三、注意事項(xiàng)
(1)客戶端這樣引用socket.io.js:
<script src="/socket.io/socket.io.js"></script>
可能會(huì)加載失?。ㄎ以谶@里耗了不少時(shí)間)
可以改為:
<script src="http://ip:port/socket.io/socket.io.js"></script>
(對(duì)應(yīng)服務(wù)器的ip地址和端口號(hào),比如說(shuō)localhost和80端口)
(2)實(shí)現(xiàn)廣播的時(shí)候,參考官網(wǎng)的寫(xiě)法,竟然不起作用,如:
var io = require('socket.io').listen(80);
io.sockets.on('connection', function (socket) {
socket.broadcast.emit('user connected');
socket.broadcast.json.send({ a: 'message' });
});
后來(lái)看了這個(gè):http://stackoverflow.com/questions/7352164/update-all-clients-using-socket-io
改為以下才起作用:
io.sockets.emit('users_count', clients);
四、效果圖




五、源碼下載
ps:
1、在命令行運(yùn)行
node main.js
然后在瀏覽器中打開(kāi)index.html,如果瀏覽器(ff、Chrome)不支持,請(qǐng)升級(jí)到支持WebSocket的版本.
2、推薦node.js的IDE WebStorm
以上內(nèi)容就是本文基于Angular和Nodejs搭建聊天室及多人聊天室的實(shí)現(xiàn),希望大家喜歡。
- js實(shí)現(xiàn)簡(jiǎn)易聊天對(duì)話框
- javascript和jQuery實(shí)現(xiàn)網(wǎng)頁(yè)實(shí)時(shí)聊天的ajax長(zhǎng)輪詢
- nodejs實(shí)現(xiàn)的一個(gè)簡(jiǎn)單聊天室功能分享
- nw.js實(shí)現(xiàn)類(lèi)似微信的聊天軟件
- Vue.js仿微信聊天窗口展示組件功能
- JavaScript/jQuery、HTML、CSS 構(gòu)建 Web IM 遠(yuǎn)程及時(shí)聊天通信程序
- js編寫(xiě)簡(jiǎn)單的聊天室功能
- nodejs基于WS模塊實(shí)現(xiàn)WebSocket聊天功能的方法
- AngularJS+Node.js實(shí)現(xiàn)在線聊天室
- JavaScript實(shí)現(xiàn)簡(jiǎn)易QQ聊天界面
相關(guān)文章
ionic js 復(fù)選框 與普通的 HTML 復(fù)選框到底有沒(méi)區(qū)別
本文通過(guò)實(shí)例給大家演示ionic js 復(fù)選框 與普通的 HTML 復(fù)選框到底有沒(méi)區(qū)別的相關(guān)知識(shí),非常不錯(cuò)具有參考借鑒價(jià)值,感興趣的朋友一起學(xué)習(xí)吧2016-06-06
JavaScript實(shí)現(xiàn)Flash炫光波動(dòng)特效
JavaScript寫(xiě)的炫光波動(dòng)效果,看到一些Flash效果不錯(cuò),用JS也模擬一下,還有很多不完善的地方,給各位參考參考。2015-05-05
原生js實(shí)現(xiàn)日期聯(lián)動(dòng)
日期聯(lián)動(dòng)算是一個(gè)比較常見(jiàn)的功能了,隨便度娘一下,你就能找到N多代碼,今天給大家介紹的是個(gè)人比較常用,代碼很簡(jiǎn)潔,高效,這里推擠給大家。2015-01-01
用js通過(guò)url傳參把數(shù)據(jù)從一個(gè)頁(yè)面?zhèn)鞯搅硪粋€(gè)頁(yè)面
如果是傳到新頁(yè)面的話,你網(wǎng)站基于什么語(yǔ)言開(kāi)發(fā)直接用get或者post獲取,然后輸出到這個(gè)層2014-09-09
JavaScript中函數(shù)(Function)的apply與call理解
這篇文章主要介紹了JavaScript中函數(shù)(Function)的apply與call理解,本文講解了JavaScript函數(shù)調(diào)用分為4中模式以及通過(guò)apply和call實(shí)現(xiàn)擴(kuò)展和繼承兩方面,需要的朋友可以參考下2015-07-07
詳解ES6之用let聲明變量以及l(fā)et loop機(jī)制
本篇文章主要介紹了詳解ES6之用let聲明變量以及l(fā)et loop機(jī)制,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07
利用JS如何計(jì)算字符串所占字節(jié)數(shù)示例代碼
因?yàn)樽罱?xiàng)目有個(gè)需求要用js計(jì)算一串字符串寫(xiě)入到localStorage里所占的內(nèi)存,所以便有了這篇文章,下面這篇文章主要給大家介紹了關(guān)于利用JS如何計(jì)算字符串所占字節(jié)數(shù)的相關(guān)資料,需要的朋友可以參考下。2017-09-09
微信小程序自定義組件傳值 頁(yè)面和組件相互傳數(shù)據(jù)操作示例
這篇文章主要介紹了微信小程序自定義組件傳值 頁(yè)面和組件相互傳數(shù)據(jù)操作,結(jié)合實(shí)例形式分析了微信小程序常見(jiàn)傳值操作相關(guān)實(shí)現(xiàn)技巧,需要的朋友可以參考下2019-05-05

