一,利用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);
}); });

使用Angular和Nodejs搭建聊天室的更多相关文章

  1. 使用nodejs+express+socketio+mysql搭建聊天室

    使用nodejs+express+socketio+mysql搭建聊天室 nodejs相关的资料已经很多了,我也是学习中吧,于是把socket的教程看了下,学着做了个聊天室,然后加入简单的操作mysq ...

  2. NodeJs小试牛刀--聊天室搭建

    最近研究聊天室功能,准备用nodejs实现.下面是自己的尝试!! nodejs的安装这里就不详细赘述了. 程序创建 引入required模块 var express = require('expres ...

  3. 使用socket.io搭建聊天室

    最近在学习nodejs,需要找一些项目练练手.找来找去发现了一个聊天室的教程,足够简单,也能从中学到一些东西.下面记录我练习过程中待一些笔记. nodeJS模块 共用到了2个模块,express和so ...

  4. NodeJS在线聊天室(NodeJS & SocketIO & Express & EJS & MongoDB & Gulp)

    项目背景 这个项目主要是为了玩玩NodeJS,项目的方向大概是做出类似QQ的在线聊天系统.想要在线体验可以点击在线演示. 项目使用PM2进行部署和管理,功能在不断的迭代开发中.如果你觉得这个项目比较有 ...

  5. SpringBoot--使用socket搭建聊天室

    1.添加依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId> ...

  6. 基于swoole搭建聊天室程序

    1. 创建websocket服务器 swoole从1.7.9版本开始, 内置了websocket服务器功能,我们只需几行简单的PHP代码,就可以创建出一个异步非阻塞多进程的WebSocket服务器. ...

  7. workerman搭建聊天室

    首先,先打开官网手册   http://doc.workerman.net/ 根据手册里安装里的提示,完成环境检测,和安装对应的扩展,并把对应的WorkerMan代码包下载解压至根目录 在根目录下创建 ...

  8. 图形验证插件,百度编辑器拓展功能,NodeJs消息机制以及聊天室

    图形验证插件 网上找了很多图形验证插件,比较推荐verify.js <link rel="stylesheet" type="text/css" href ...

  9. 使用 NIO 搭建一个聊天室

    使用 NIO 搭建一个聊天室 前面刚讲了使用 Socket 搭建了一个 Http Server,在最后我们使用了 NIO 对 Server 进行了优化,然后有小伙伴问到怎么使用 Socket 搭建聊天 ...

随机推荐

  1. ios 消息推送流程 转载

    iOS开发:推送通知简述及开发实践热度 1已有 706 次阅读 2013-10-15 09:23 |个人分类:经验之谈|系统分类:ios| IOS, 推送一.关于推送通知 推送通知,也被叫做远程通知, ...

  2. 《Java程序设计》第五次实验实验报告

    实验封面 一.实验内容 1.阅读理解源码进入07_httpd所在的目录,使用vi编辑器理解源代码. 2.编译应用程序使用gcc编译器,分别对文件夹下的copy.c和httpd.c进行编译,出现copy ...

  3. Jmeter测试webocket协议

    Jmeter本身不支持websocket协议的,所以需要安装第三方的插件还有6个依赖包. 首先,我们需要准备Jmeter的WebSocket协议的支持插件: JMeterWebSocketSample ...

  4. java之Cookie详解

    Cookie是由服务器端生成,发送给User-Agent(一般是浏览器),浏览器会将Cookie的key/value保存到某个目录下的文本文件内,下次请求同一网站时就发送该Cookie给服务器(前提是 ...

  5. 关于hangfire的使用

    hangfire 是一个分布式后台执行服务.用它可以代替ThreadPool.QueunItemWork等原生方法.当然4.5后的 task也是相当好用且功能强大.不过如果想分布式处理并且可监控的话, ...

  6. JAVA中的NIO(二)

    一.内存文件映射 内存文件映射允许我们创建和修改那些因为太大而不能放入内存中的文件.有了内存文件映射,我们就可以假定整个文件都在内存中,而且可以完全把文件当作数组来访问. package com.dy ...

  7. 一头扎进EasyUI

    惯例广告一发,对于初学真,真的很有用www.java1234.com,去试试吧! 一头扎进EasyUI第1讲 .加载库文件和样式 <link rel="stylesheet" ...

  8. “耐撕”团队 2016.3.25 站立会议

    成员: Z 郑蕊 * 组长 (博客:http://www.cnblogs.com/zhengrui0452/), P 濮成林(博客:http://www.cnblogs.com/charliePU/) ...

  9. 使用GIT来管理代码的心得

    使用GIT来管理代码,第一步当然就是下载一个GIT客户端(不知道是不是这么叫,但是觉得和客户端的功能差不多).电脑的操作系统是windows7的,所以下的是对应的GIT. 就是这玩意,安装的时候不停的 ...

  10. JS模式:jq中简单的模式--》采摘自js设计(tomxu_version)

    <!DOCTYPE html> <html> <head> <title></title> </head> <body&g ...