1. Node を使用して静的サーバーを構築します
これは、このプロジェクトの基礎となるサポート部分です。 html、css、gif、jpg、png、javascript、json、プレーン テキスト などの静的リソース ファイルへのアクセスをサポートするために使用されます。ここには 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を入力してページが表示されるまでの流れを説明します。 ユーザーがブラウザのアドレス バーに URL を入力したとき。
次に一連の処理が行われます。 1 つ目は DNS 解決で、ドメイン名を対応する IP アドレスに変換します。次に、ブラウザとリモート Web サーバーは、TCP 3 ウェイ ハンドシェイク ネゴシエーションを通じて TCP/IP 接続を確立します。ハンドシェイクには、同期メッセージ、同期応答メッセージ、および応答メッセージが含まれており、これら 3 つのメッセージがブラウザとサーバーの間で受け渡されます。このハンドシェイクでは、クライアントは最初に通信の確立を試み、次にサーバーが応答してクライアントのリクエストを受け入れ、最後にクライアントはリクエストが受け入れられたことを示すメッセージを送信します。 TCP/IP 接続が確立されると、ブラウザは接続を通じて HTTP GET リクエストをリモート サーバーに送信します。
リモート サーバーはリソースを見つけ、HTTP 応答を使用してそれを返します。HTTP 応答ステータス 200 は、正しい応答を示します。このとき、Web サーバーはリソース サービスを提供し、クライアントはリソースのダウンロードを開始します。ダウンロードされるリソースには、html ファイル、css ファイル、javascript ファイル、画像ファイルが含まれます。次に、レンダリング ツリーと DOM ツリーの構築を開始します。その間、CSS ブロックと JS ブロックが行われます。したがって、最下層には静的サーバーのサポートが必要です。ここでは、Express フレームワークを使用せずに静的サーバーをネイティブに構築します。
実際、各リソース ファイル リクエストの処理は GET リクエストです。次に、クライアントからのGETリクエストに対するサーバー側の処理プロセス(ブラウザ側、またはLinuxではcurlメソッドを使用)について説明します。 Get リクエストがサーバーに送信されると、サーバーは GET リクエストに従ってリソース ファイルのパスを対応付けることができます。このパスがわかったら、ファイルの読み取りと書き込みを使用して、指定されたパスの下のリソースを取得し、それらをクライアントに返すことができます。
Node のファイル読み取りおよび書き込み API には readFile と readFileSync が含まれることはわかっていますが、より良い方法は、ストリームを使用してファイルを読み取ることです。ストリームを使用する利点は、キャッシュと gzip 圧縮を使用できることです。
では、キャッシュを実装するにはどうすればよいでしょうか?通常、クライアントが初めてリクエストを行うと、サーバーはリソース ファイルを読み取り、それをクライアントに返します。ただし、同じファイルを 2 回目にリクエストする場合も、サーバーにリクエストを送信する必要があります。サーバーは、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 は、http プロトコルや https プロトコルではなく、ws プロトコルを使用します。 WebSocket を使用するもう 1 つの利点は、大量のデータ トラフィックを削減できることです。この記事の冒頭で、従来の 1 回限りのリソース要求プロセスを紹介しました。これには 3 ウェイ ハンドシェイク プロトコルが必要で、各要求ヘッダーは比較的大きなスペースを占有し、大量のトラフィックを消費します。 Websocket で相互に通信するために使用されるヘッダーは非常に小さく、わずか約 2 バイトです。
/** * 聊天服务。 */ 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); }); });
基于node.js和socket.io搭建多人聊天室
刚学node.js,想着做点东西练练手。网上的东西多而杂,走了不少弯路,花了一天时间在调代码上。参考网上的一篇文章,重写了部分代码,原来的是基于基于node-websocket-server框架的,我没用框架,单单是socket.io。
一、基本功能
1、用户随意输入一个昵称即可登录
2、登录成功后
1) 对正在登录用户来说,罗列所有在线用户列表,罗列最近的历史聊天记录
2) 对已登录的用户来说,通知有新用户进入房间,更新在线用户列表
3、退出登录
1)支持直接退出
2) 当有用户退出,其他所有在线用户会收到信息,通知又用户退出房间,同时更新在线用户列表
4、聊天
1) 聊天就是广播,把信息广播给所有连接在线的用户
5、一些出错处理
1) 暂时简单处理了系统逻辑错误、网络出错等特殊情况的出错提示
问题:功能不完善,有bug(退出后,新用户重新登录,还是原来的用户) 。抽空完善吧
二、技术介绍
socket.io(官网:http://socket.io/)是一个跨平台,多种连接方式自动切换,做即时通讯方面的开发很方便,而且能和expressjs提供的传统请求方式很好的结合,即可以在同一个域名,同一个端口提供两种连接方式:request/response, websocket(flashsocket,ajax…)。
这篇文章对socket.io的使用做了详细介绍:http://www.jb51.net/article/71361.htm
《用node.js和Websocket做个多人聊天室吧》http://www.html5china.com/HTML5features/WebSocket/20111206_3096.html
三、注意事项
(1)客户端这样引用socket.io.js:
<script src="/socket.io/socket.io.js"></script>
可能会加载失败(我在这里耗了不少时间)
可以改为:
<script src="http://ip:port/socket.io/socket.io.js"></script>
(对应服务器的ip地址和端口号,比如说localhost和80端口)
(2)实现广播的时候,参考官网的写法,竟然不起作用,如:
var io = require('socket.io').listen(80); io.sockets.on('connection', function (socket) { socket.broadcast.emit('user connected'); socket.broadcast.json.send({ a: 'message' }); });
后来看了这个:http://stackoverflow.com/questions/7352164/update-all-clients-using-socket-io
改为以下才起作用:
io.sockets.emit('users_count', clients);
四、效果图
五、源码下载
ps:
1、在命令行运行
node main.js
然后在浏览器中打开index.html,如果浏览器(ff、Chrome)不支持,请升级到支持WebSocket的版本.
2、推荐node.js的IDE WebStorm
以上内容就是本文基于Angular和Nodejs搭建聊天室及多人聊天室的实现,希望大家喜欢。