標籤:style blog http color io os 使用 ar java
一,利用Node搭建靜態伺服器
這個是這個項目的底層支撐部分。用來支援靜態資源檔案像html, css, gif, jpg, png, javascript, json, plain text等等靜態資源的訪問。這裡面是有一個mime類型的檔案對應。
mime.js
/** * 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請求對應一個資源檔的路徑。知道了這個路徑後,我們就可以採用檔案讀寫的方式擷取指定路徑下的資源,然後返回給用戶端。
我們知道Node裡面的檔案讀寫的API有readFile和readFileSync,但是更好的方式是採用流的方式去讀取檔案,採用流的方式的優點是可以採用緩衝和gzip壓縮。
OK,那麼如何?緩衝呢?通常情況下,用戶端第一次去請求的時候,服務端會讀取資源檔,返回給用戶端。但是第二次再去請求同樣的檔案時,這個時候還是需要發送一次請求到服務端。服務端會根據Expires, cache-control, If-Modified-Since等Http頭資訊判斷這個資源是否已經緩衝過。如果有緩衝,服務端則不會再次訪問資源檔的實際路徑。直接返回緩衝的資源。
server.js
/** * 聊天室服務端 * 功能:實現了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構建聊天室服務端
為什麼採用websocket?
我們知道現在主流的聊天室還是採用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); });});
需要下載原始碼的可以訪問這個地址: https://github.com/Geek-Coder/Geek-Coder-Chat
使用Angular和Nodejs搭建聊天室