使用Angular和Nodejs搭建聊天室

一,利用Node搭建靜態服務器javascript

        這個是這個項目的底層支撐部分。用來支持靜態資源文件像html, css, gif, jpg, png, javascript, json, plain text等等靜態資源的訪問。這裏面是有一個mime類型的文件映射。css

mime.jshtml

 

/**
 * mime類型的 map
 * @ author      Cheng Liufeng
 * @ date        2014/8/30
 * 當請求靜態服務器文件的類型 html, css, gif, jpg, png, javascript, json, plain text, 咱們會在此文件進行映射
 */
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"

};

 

  這裏面我先解釋一下從輸入網址到頁面出現的過程。 當用戶在瀏覽器地址欄裏面輸入一個url的時候。接下來會發生一系列的過程。首先是DNS解析, 將域名轉換成對應的IP地址,以後瀏覽器與遠程Web服務器經過TCP三次握手協商來創建一個TCP/IP鏈接。該握手包括一個同步報文,開一個同步-應答報文和一個應答報文,這三個報文在瀏覽器和服務器之間傳遞。該握手首先由客戶端嘗試創建起通訊,然後服務器應答並接受客戶端的請求,最後由客戶端發出該請求已經被接受的報文。一旦TCP/IP鏈接創建,瀏覽器會經過該鏈接向遠程服務器發送HTTP的GET請求。遠程服務器找到資源並使用HTTP響應返回該資源,值爲200的HTTP響應狀態表示一個正確的響應。此時,Web服務器提供資源服務,客戶端開始下載資源。下載的資源包括了html文件,css文件,javascript文件,image文件。而後開始構建一顆渲染樹和一顆DOM樹,期間會有css阻塞和js阻塞。因此底層是須要一個靜態服務器支撐。這裏面我原生構造一個靜態服務器,不採用express框架。前端

        事實上每一次資源文件請求的過程是一次次GET請求。下面我解釋一下客戶端(瀏覽器端或者採用linux下采用curl方式)的GET請求所對應的服務端處理過程。一次Get請求發送到服務端後,服務端能夠根據GET請求對應一個資源文件的路徑。知道了這個路徑後,咱們就能夠採用文件讀寫的方式獲取指定路徑下的資源,而後返回給客戶端。java

        咱們知道Node裏面的文件讀寫的API有readFile和readFileSync,可是更好的方式是採用流的方式去讀取文件,採用流的方式的優勢是能夠採用緩存和gzip壓縮。jquery

        OK,那麼如何實現緩存呢?一般狀況下,客戶端第一次去請求的時候,服務端會讀取資源文件,返回給客戶端。可是第二次再去請求一樣的文件時,這個時候仍是須要發送一次請求到服務端。服務端會根據Expires, cache-control, If-Modified-Since等Http頭信息判斷這個資源是否已經緩存過。若是有緩存,服務端則不會再次訪問資源文件的實際路徑。直接返回緩存的資源。linux

server.jsweb

 

/**
 * 聊天室服務端
 * 功能:實現了Node版的靜態服務器
 * 實現了緩存,gzip壓縮等
 * @ author      Cheng Liufeng
 * @ date        2014/8/30
 */

 // 設置端口號
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";   //默認取當前默認下的index.html
    }
	// 安全處理(當使用Linux 的 curl命令訪問時,存在安全隱患)
	var realPath = path.join("client", path.normalize(pathName.replace(/\.\./g, "")));
	// 檢查文件路徑是否存在
	path.exists(realPath, function(exists) {
		// 當文件不存在時的狀況, 輸出一個404錯誤
		if (!exists) {
			res.writeHead(404, "Not Found", {'Content-Type': 'text/plain'});
			res.write("The request url" + pathName +" is not found!");
			res.end();
		} else {                      // 當文件存在時的處理邏輯
			fs.stat(realPath, function(err, stat) {
                            // 獲取文件擴展名
                var ext = path.extname(realPath);
                ext = ext ? ext.slice(1) : "unknown";
                var contentType = mime[ext] || "text/plain";
                // 設置 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();
//						}
//					});
                }
			});
		}
	});
});
//監聽3000端口
server.listen(PORT, function() {
	console.log("Server is listening on port " + PORT + "!");
});

// 讓socket.io服務器和http服務器共享一個端口
chatServer.listen(server);

  二,服務端利用WebSocket構建聊天室服務端ajax

         爲何採用websocket?chrome

         咱們知道如今主流的聊天室仍是採用ajax去實現客戶端和服務端的通訊。採用的是一種輪詢的機制。所謂輪詢,就是客戶端每隔一段時間就去發送一次請求,詢問服務端,看看服務端有沒有新的聊天數據,若是有新的數據,就返回給客戶端。 

        Websocket則徹底不一樣。 websocket是基於長連接。就是客戶端和服務端一旦創建連接以後,這個連接就會一直存在。 是一種全雙工的通訊。 這個時候的機制有點相似發佈-訂閱模式。 客戶端會訂閱一些事件,一旦服務端有新的數據出現,會主動推送給客戶端。

        websocket採用的是ws協議,不是http協議或者https協議。另外採用websocket的另外一個好處就是能夠減小不少數據流量。文章開頭,我已經介紹了傳統的一次資源請求過程,須要三次握手協議,並且每次請求頭所佔空間比較大,這樣會很費流量。而Websocket裏面的互相溝通的Header是很小的-大概只有 2 Bytes。

/**
 * 聊天服務。
 */

var socketio = require('socket.io');

var io;
var guestNumber = 1;                     //初始用戶名編號
var nickNames = {};                      // 暱稱列表
var namesUsed = [];                      //使用過的用戶名
var currentRoom = {};                    //當前聊天室

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); // 定義每一個用戶的鏈接處理 io.sockets.on('connection', function(socket) { // 分配一個用戶名 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無疑是引人注目的。模塊化,雙向數據綁定,指令系統,依賴注入。並且Angular內置jquerylite,這讓熟悉jQuery語法的同窗很容易上手。

固然,我的認爲, angular在構建一個單頁應用和crud項目方面有很大的優點。 咱們這個聊天室就是基於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>

 

 

怎樣構建一個單頁應用?單頁應用的原理?

        先談談單頁應用的原理。所謂單頁,並非整個頁面無刷新。當你審查一下google chrome的console控制檯的時候,你會發現,angular內部仍是採用了ajax去異步請求資源。因此只是局部刷新。可是這種方式相對於之前的DOM節點的刪除和修改已經有很大的進步了。

        構建單頁應用,咱們須要藉助於angular-route.js。這個angular子項目能夠幫助咱們定義路由和對應的邏輯處理控制器。利用它,咱們能夠實現一個單頁應用。

app.js

/**
 *  客戶端(目前只支持瀏覽器,未來會擴展到移動端)程序入口文件
 *  建立一個模塊,而且命名爲chatApp
 *  配置路由,實現單頁應用(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);
    });

});
相關文章
相關標籤/搜索