一、Node.js介绍
1. 概述
Node.js是基于Chrome JavaScript运行时建立的一个平台,实际上它是对Google Chrome V8引擎进行了封装,它主要用于创建快速的、可扩展的网络应用。Node.js采用事件驱动和非阻塞I/O模型,使其变得轻量和高效,非常适合构建运行在分布式设备的数据密集型的实时应用。
对运行于浏览器的JavaScript来说,浏览器就是JavaScript代码的解析器,而Node.js则是服务器端的JavaScript代码解析器,存在于服务器端的JavaScript代码由Node.js来解析和运行。
JavaScript解析器只是JavaScript代码运行的一种环境,浏览器是JavaScript运行的一种环境,浏览器为JavaScript提供了操作DOM对象和window对象等的接口。Node.js也是JavaScript运行的一种环境,Node.js为JavaScript提供了操作文件、创建HTTP服务、 创建TCP/UDP服务等的接口,所以Node.js可以完成其他后端开发语言(如Python、PHP等)能完成的工作。
2. 交互式运行环境:REPL
Node.js提供了一个交互式运行环境,通过这个环境,可以立即执行JavaScript代码块,使用方法类似于Chrome浏览器中Firebug插件的Console。
进入Linux终端:
输入node或nodejs进入Node.js的交互式运行环境(老版本的Node.js,不支持node命令,只能使用nodejs命令),Ctrl+d组合键可以退出此环境。
查看系统中安装的Node.js版本:
$ node -v
测试Node.js控制台输出:
$ node
> console.log('nodejs test');
通过交互式环境也可以运行JavaScript文件。
3. Node.js模块和包
-
模块
Node.js官方提供了很多模块,这些模块分别实现了一种功能,如操作文件的模块fs,构建http服务的模块http等,每个模块都是一个JavaScript文件,当然你也可以自己编写模块。 -
包
包可以将多个具有依赖关系的模块组织在一起,封装多个模块,以方便管理。Node.js采用了CommonJS规范,根据CommonJS规范规定,一个JavaScript文件就是一个模块,而包是一个文件夹,包内必须包含一个JSON文件,命名为package.json。一般情况下,包内的bin文件夹存放二进制文件,包内的lib文件夹存放JavaScript文件,包内的doc文件夹存放文档,包内的test文件夹存放单元测试。package.json文件中需要包含的字段及包的使用,后面的实验再做详细介绍。 -
npm包管理工具
npm是Node.js的包管理工具,npm定义了包依赖关系标准,我们使用npm来下载第三方包和管理本地下载的第三方包。
二、Node.js模块和包
2.1 Node.js模块(module)
每一个 Node.js 文件都是一个 Node.js 模块,包括JavaScript文件(.js)、JSON文本文件(.json)和二进制模块文件(.node)。
2.1.1 模块的使用
编写一个模块:
新建一个文件mymodule.js,输入如下代码并保存:
function hello() {
console.log('Hello');
}
function world() {
console.log('World');
}
这就是一个Node.js模块,但是怎么在其他模块中引入并使用这个模块呢?我们需要为模块提供对外的接口,这就要用到module.exports和exports。
我们可以这样写mymodule.js:
function hello() {
console.log('Hello');
}
function world() {
console.log('World');
}
exports.hello = hello;
exports.world = world;
在其他模块中,可以使用require(module_name);载入需要的模块,如,在新建index.js,输入如下代码并保存:
var hello = require('./mymodule'); // 也可以写作 var hello = require('./mymodule.js');
// 现在就可以使用mymodule.js中的函数了
hello.hello(); // >> Hello
hello.world(); // >> World
也可以这样写mymodule.js:
function Hello() {
this.hello = function() {
console.log('Hello');
};
this.world = function() {
console.log('World');
};
}
module.exports = Hello;
此时,index.js就要改成这样:
var Hello = require('./mymodule');
var hello = new Hello();
hello.hello(); // >> Hello
hello.world(); // >> World
2.1.2 module.exports和exports
module是一个对象,每个模块中都有一个module对象,module是对当前模块的一个引用。module.exports对象是由Module系统创建的,而exports可以看作是对module.exports对象的一个引用。在模块中require另一个模块时,以module.exports的值为准,因为有的情况下,module.exports和exports它们的值是不同的。module.exports和exports的关系可以表示成这样:
// module.exports和exports相同的情况
var m = {}; // 表示 module
var e = m.e = {}; // e 表示 exports, m.e 表示 module.exports
m.e.a = 5;
e.b = 6;
console.log(m.e); // Object { a: 5, b: 6 }
console.log(e); // Object { a: 5, b: 6 }
// module.exports和exports不同的情况
var m = {}; // 表示 module
var e = m.e = {}; // e 表示 exports, m.e 表示 module.exports
m.e = { c: 9 }; // m.e(module.exports)引用的对象被修改了
e.d = 10;
console.log(m.e); // Object { c: 9 }
console.log(e); // Object { d: 10 }
2.2 Node.js包(package)
2.2.1 包
包用于管理多个模块及其依赖关系,可以对多个模块进行封装,包的根目录必须包含package.json文件,package.json文件是CommonJS规范用于描述包的文件,符合CommonJS规范的package.json文件一般包含以下字段:
- name:包名。包名是唯一的,只能包含小写字母、数字和下划线。
- version:包版本号。
- description:包说明。
- keywords:关键字数组。用于搜索。
- homepage:项目主页。
- bugs:提交bug的地址。
- license:许可证。
- maintainers:维护者数组。
- contributors:贡献者数组。
- repositories:项目仓库托管地址数组。
- dependencies:包依赖。
下面是一个package.json示例:
{
"name": "shiyanlou",
"description": "Shiyanlou test package.",
"version": "0.1.0",
"keywords": [
"shiyanlou",
"nodejs"
],
"maintainers": [{
"name": "test",
"email": "test@shiyanlou.com"
}],
"contributors": [{
"name": "test",
"web": "http://www.shiyanlou.com/"
}],
"bugs": {
"mail": "test@shiyanlou.com",
"web": "http://www.shiyanlou.com/"
},
"licenses": [{
"type": "Apache License v2",
"url": "http://www.apache.org/licenses/apache2.html"
}],
"repositories": [{
"type": "git",
"url": "http://github.com/test/test.git"
}],
"dependencies": {
"webkit": "1.2",
"ssl": {
"gnutls": ["1.0", "2.0"],
"openssl": "0.9.8"
}
}
}
package.json文件可以自己手动编辑,但为了方便,更推荐通过npm init命令进行生成。你可以自己尝试在终端中输入npm init命令来生成一个包含package.json文件的包。
2.2.2 npm包管理工具
npm 也可以从第三方网站(http://www.npmjs.org/)上下载第三方Node.js包。
通过npm安装的包会按包名字母顺序出现在package.json中的dependencies字段中。
先设置为官方源:
$ npm config set registry https://registry.npmjs.org
然后搜索包:
$ npm search express //搜索express包
安装包,如果安装包失败或者比较慢的话可以切换到 taobao 的源:
$ npm config set registry https://registry.npm.taobao.org
$ npm install -g express //安装包,-g即global全局安装
更新包:
$ npm update express
卸载包:
$ npm uninstall express
三、Node.js的Events模块
Node.js采用事件驱动模型,在Node.js中,事件是一个很重要的概念。
很多对象都会发出事件。比如,fs.readStream打开文件时会发出一个事件。所有发出事件的对象都是events.EventEmitter的实例,可以通过require(“events”);获得event模块。
通常,事件名采用“驼峰式”(即单词首字母大写,其他字母小写)命名方式,但是,不采用这种命名方式也不会造成程序功能上的问题。但为了程序风格的规范性和一致性,请尽量使用这种命名方式。
对象发出的事件需要有函数来捕获并处理。为此,需要先使一个事件与一个响应函数建立关系。对象发出事件时,与其建立关系的对应函数就会被执行。这些函数被称作监听器(listeners)。在监听器函数中,this指向发出该事件的EventEmitter对象。
3.1 events.EventEmitter类
通过require(‘events’).EventEmitter得到EventEmitter类。
当EventEmitter对象遇到错误时,通常会触发error事件。error事件在Node.js中是一种特殊情况,如果没有监听器,那么默认会打印出栈跟踪器并退出程序。
3.2 添加监听器
为事件绑定事件处理程序,可以使用emitter.addListener(event, listener)和emitter.on(event, listener),它们的作用是完全一样的。传入的参数是事件(event)和处理函数(listener)。
新建文件test1.js,输入如下代码并保存:
var http = require('http');
var server = http.createServer();
// 为request事件绑定处理函数
// 也可以使用server.addListener
server.on('request', function(req, res) { //为`request`事件添加一个匿名listener函数
res.writeHead(200, { 'Content-Type': 'text/plain' }); //写入http响应头
res.write('respons'); //写入http响应体
console.log('test');
res.end(); //结束,返回响应内容
});
server.listen(1337, '127.0.0.1'); // 在127.0.0.1(即本地回环)1337端口监听http请求
console.log('Server running at http://127.0.0.1:1337/');
运行代码:
$ node test1.js
然后打开浏览器,在地址栏输入127.0.0.1:1337,即可看到页面上打印出了“respons”字样,同时console界面也会输出’test’字样。
3.3 只执行一次的监听器
使用emitter.once(event, listener)绑定的事件监听器只会执行一次,然后就会被删除掉。
新建文件test2.js,输入如下代码并保存:
var http = require('http');
var server = http.createServer();
// 为request事件绑定处理函数,事件只会执行一次
server.once('request', function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('test');
console.log('test');
res.end();
});
server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
运行代码:
$ node test2.js
打开浏览器,在地址栏输入127.0.0.1:1337,即可看到页面上打印出了“test”字样,再次刷新此页面,就不会再显示,因为此事件只会执行一次。
3.4 移除监听器
移除监听器使用emitter.removeListener(event, listener)。
新建文件test3.js,输入如下代码并保存:
var http = require('http');
var server = http.createServer();
function callback(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('Hello World');
console.log('Hello World');
res.end();
}
server.on('request', callback);
// 移除绑定的监听器callback
server.removeListener('request', callback);
server.on('request', function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('hello');
console.log('hello');
res.end();
});
server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
运行代码:
$ node test3.js
打开浏览器,在地址栏输入127.0.0.1:1337,即可看到页面上打印出了“hello”字样,为什么没有显示“Hello World”呢?因为显示“Hello World”的监听器被移除了。
3.5 移除所有监听器
移除所有监听器使用emitter.removeAllListeners([event])。
新建文件test4.js,输入如下代码并保存:
var http = require('http');
var server = http.createServer();
server.on('request', function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('111');
console.log('111');
res.end();
});
server.on('request', function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('222');
console.log('222');
res.end();
});
// 移除绑定的所有监听器
server.removeAllListeners('request');
server.on('request', function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('test');
console.log('test');
res.end();
});
server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
运行代码:
$ node test4.js
打开浏览器,在地址栏输入127.0.0.1:1337,即可看到页面上打印出了“test”字样,说明前面的监听器被移除了,都没有执行,所以没有显示,同时console界面也只会输出’test’字样。
3.6 设置监听器最大绑定数
emitter.setMaxListeners(n)可以设置同一事件的监听器最大绑定数,默认情况下,超过10个就会警告提示,这能帮我们快速找到内存泄露的地方。显然,不是所有的事件触发器都限制在10个监听器,通过这个方法可以设置,如果设置为0就是无限制。
3.7 自定义事件
使用emitter.emit(event, [arg1], [arg2], […])可以触发自定义的事件。
新建文件test5.js,输入如下代码并保存:
var http = require('http');
var server = http.createServer();
// 绑定自定义事件myevent
server.on('myevent', function(arg) {
console.log(arg);
});
// 触发自定义事件
server.emit('myevent', 'test');
server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
运行代码:
$ node test5.js
可以看到console界面输出了’test’字样,说明触发自定义事件成功。
3.8 查看事件绑定的监听器个数
使用EventEmitter.listenerCount(emitter, event)可以查看事件监听器数量。
新建文件test6.js,输入如下代码并保存:
var http = require('http');
var events = require('events'); // 加载events模块
var server = http.createServer();
server.on('request', function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('111');
console.log('111');
});
server.on('request', function(req, res) {
res.write('\n222');
console.log('222');
res.end();
});
server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
// 查看server绑定的'request'事件的监听器个数
var num = events.EventEmitter.listenerCount(server, 'request');
console.log(num);
运行代码:
$ node test6.js
可以看到console界面输出了数字“2”,因为server绑定了两个监听器到’request’事件。
四、Node.js 的 fs 模块
出于安全考虑,浏览器端的JavaScript代码一般无法访问本地文件系统。但对于运行在服务器端的Node.js来说,文件系统是可以直接访问的,Node.js中,fs模块可以对系统文件及目录进行读写操作。
4.1 同步和异步
不同于C++、Java等编程语言,JavaScript语句是异步执行的,Node.js也是如此。异步使得Node.js能够"同时"处理多个请求,提高了服务程序的并发性。异步特性也是Node.js得以流行的原因之一。
fs模块中所有方法都有同步和异步两种形式,我们可以在学习fs模块本身的同时体会异步和同步的差别。
异步方法中回调函数的第一个参数总是留给异常参数(exception),如果方法成功完成,那么这个参数为null或者undefined。
异步方法实例代码:
var fs = require('fs'); // 载入fs模块
fs.unlink('/tmp/test', function(err) {
if (err) {
throw err;
}
console.log('成功删除了 /tmp/test');
});
同步方法实例代码:
var fs = require('fs');
fs.unlinkSync('/tmp/test'); // 函数名最后的Sync 表示是同步方法
console.log('成功删除了 /tmp/test');
同步方法执行完并返回结果后,才能执行后续的代码。而异步方法采用回调函数接收返回结果,可以立即执行后续的代码。
4.2 readFile读取文件
使用fs.readFile(filename, [options], callback)方法读取文件。
readFile接收三个参数,filename是文件名;[options]是可选的参数,为一个对象,用于指定文件编码(encoding)及操作方式(flag);callback是回调函数。
新建一个文件text.txt,文件中的内容如下:
line one
line two
使用readFile读取此文件,新建文件readfile.js,输入如下代码并保存:
var fs = require('fs'); // 引入fs模块
fs.readFile('./text.txt', function(err, data) {
// 读取文件失败/错误
if (err) {
throw err;
}
// 读取文件成功
console.log(data);
});
readFile的回调函数接收两个参数,err是读取文件出错时触发的错误对象,data是从文件读取的数据。
运行程序:
$ node readfile.js
会看到输出的内容类似于这样:
<Buffer 6c 69 6e 65 20 6f 6e 65 0a 6c 69 6e 65 20 74 77 6f 0a>
这是原始二进制数据在缓冲区中的内容。要显示文件内容可以使用toString()或者设置输出编码,readFile.js可以改成这样:
var fs = require('fs'); // 引入fs模块
// 使用toString()
fs.readFile('./text.txt', function(err, data) {
// 读取文件失败/错误
if (err) {
throw err;
}
// 读取文件成功
console.log('toString: ', data.toString());
});
// 设置编码格式
fs.readFile('./text.txt', 'utf-8', function(err, data) {
// 读取文件失败/错误
if (err) {
throw err;
}
// 读取文件成功
console.log('utf-8: ', data);
});
这样再运行程序就能正常显示出文件中的内容了。
fs.readFileSync(filename, [options])是readFile的同步方法。
4.3 writeFile写入文件
使用fs.writeFile(filename, data, [options], callback)写入内容到文件。
writeFile接收四个参数,filename是文件名称;data是要写入文件的数据;[options]是一个对象为可选参数,包含编码格式(encoding),模式(mode)以及操作方式(flag);callback是回调函数。
新建writeFile.js文件,输入如下代码并保存:
var fs = require('fs'); // 引入fs模块
// 写入文件内容(如果文件不存在会创建一个文件)
// 写入时会先清空文件
fs.writeFile('./test2.txt', 'test test', function(err) {
if (err) {
throw err;
}
console.log('Saved.');
// 写入成功后读取测试
fs.readFile('./test2.txt', 'utf-8', function(err, data) {
if (err) {
throw err;
}
console.log(data);
});
});
运行程序:
$ node writeFile.js
如果要追加数据到文件,可以传递一个flag参数,修改代码为如下所示:
var fs = require('fs'); // 引入fs模块
// 写入文件内容(如果文件不存在会创建一个文件)
// 传递了追加参数 { 'flag': 'a' }
fs.writeFile('./test2.txt', 'test test', { 'flag': 'a' }, function(err) {
if (err) {
throw err;
}
console.log('Saved.');
// 写入成功后读取测试
fs.readFile('./test2.txt', 'utf-8', function(err, data) {
if (err) {
throw err;
}
console.log(data);
});
});
flag传递的值,r代表读取(read)文件,,w代表写入(write)文件,a代表追加(append)写入文件,还有其他的值不作详细介绍。
4.4 使用 fs.read 和 fs.write 读写文件
使用fs.read和fs.write读写文件需要使用fs.open打开文件和fs.close关闭文件。
4.4.1 fs.read()
先介绍fs.open(path, flags, [mode], callback)方法,此方法用于打开文件,以便fs.read()读取。path是文件路径,flags是打开文件的方式,[mode]是文件的权限(可选参数,默认值是0666),callback是回调函数。
flags的值:
- r :读取文件,文件不存在时报错;
- r+ :读取并写入文件,文件不存在时报错;
- rs :以同步方式读取文件,文件不存在时报错;
- rs+ :以同步方式读取并写入文件,文件不存在时报错;
- w :写入文件,文件不存在则创建,存在则清空;
- wx :和w一样,但是文件存在时会报错;
- w+ :读取并写入文件,文件不存在则创建,存在则清空;
- wx+ :和w+一样,但是文件存在时会报错;
- a :以追加方式写入文件,文件不存在则创建;
- ax :和a一样,但是文件存在时会报错;
- a+ :读取并追加写入文件,文件不存在则创建;
- ax+ :和a+一样,但是文件存在时会报错。
fs.close(fd, [callback])用于关闭文件,fd是所打开文件的文件描述符。
fs.read(fd, buffer, offset, length, position, callback)方法接收6个参数。
- fd是文件描述符,必须接收fs.open()方法中的回调函数返回的第二个参数;
- buffer是存放读取到的数据的Buffer对象;
- offset指定向buffer中存放数据的起始位置;
- length指定读取文件中数据的字节数;
- position指定在文件中读取文件内容的起始位置;
- callback是回调函数,回调函数的参数:
-
- err用于抛出异常;
-
- bytesRead是从文件中读取内容的实际字节数;
-
- buffer是被读取的缓存区对象。
新建文件testread.txt在文件中随意输入一些内容,然后新建read.js文件,输入如下代码并保存:
var fs = require('fs'); // 引入fs模块
// 打开文件
fs.open('./testread.txt', 'r', function(err, fd) {
if (err) {
throw err;
}
console.log('open file success.');
var buffer = new Buffer(255);
// 读取文件
fs.read(fd, buffer, 0, 10, 0, function(err, bytesRead, buffer) {
if (err) {
throw err;
}
// 打印出buffer中存入的数据
console.log(bytesRead, buffer.slice(0, bytesRead).toString());
// 关闭文件
fs.close(fd);
});
});
4.4.2 fs.write()
fs.write(fd, buffer, offset, length, position, callback)方法的参数和fs.read()相同,buffer是需要写入文件的内容。
新建文件testwrite.txt,然后新建write.js文件,输入如下代码并保存:
var fs = require('fs'); // 引入fs模块
// 打开文件
fs.open('./testwrite.txt', `w`, function(err, fd) {
if (err) {
throw err;
}
console.log('open file success.');
var buffer = new Buffer('shiyanlou');
// 读取文件
fs.write(fd, buffer, 0, 6, 0, function(err, written, buffer) {
if (err) {
throw err;
}
console.log('write success.');
// 打印出buffer中存入的数据
var byteLength = buffer.byteLength;
console.log(byteLength, buffer.slice(0, byteLength).toString());
// 关闭文件
fs.close(fd);
});
});
4.5 目录操作
4.5.1 创建目录
使用fs.mkdir(path, [mode], callback)创建目录,path是需要创建的目录,[mode]是目录的权限(默认值是0777),callback 是回调函数。
创建mkdir.js文件,输入如下代码并保存:
var fs = require('fs'); // 引入fs模块
// 创建 newdir 目录
fs.mkdir('./newdir', function(err) {
if (err) {
throw err;
}
console.log('make dir success.');
});
运行程序后会发现在当前目录下已经创建了newdir目录,删除目录可以使用fs.rmdir(path, callback),但是只能删除空目录。
4.5.2 读取目录
使用fs.readdir(path, callback)读取文件目录。
新建readdir.js文件,输入如下代码并保存:
var fs = require('fs'); // 引入fs模块
fs.readdir('./newdir', function(err, files) {
if (err) {
throw err;
}
// files是一个数组
// 每个元素是此目录下的文件或文件夹的名称
console.log(files);
});
五、Node.js 的 http 模块
5.1 创建http server
通过Node.js创建http server非常简单,示例代码如下:
// 文件名:demo.js
// 引入http模块
var http = require('http');
// 创建http server
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');
运行此文件:
$ node demo.js
然后打开浏览器,访问“http://127.0.0.1:1337/”,就会看到页面上显示了“Hello World”,说明我们的http server创建成功了。
当然,我们在这创建一个简单的http server,所有的代码都放在app这个文件夹中。文件夹结构如下:
app
├── main.js
├── requestHandlers.js
├── router.js
├── server.js
└── views
├── 404.html
├── about.html
└── home.html
首先,新建一个app文件夹,在文件夹中新建server.js文件,输入如下代码(其中的注释为代码解释):
//
// 创建http server
//
// 加载所需模块
var http = require('http');
var url = require('url');
var fs = require('fs');
// 设置ip和端口
// 实际应用中,可以把这些写到配置文件中
var host = '127.0.0.1',
port = 8080;
// 创建http server
function start(route, handle) {
// 参数
// route 判断url是否存在,存在则调用handle处理,不存在则返回404
// handle 处理不同的url请求
// 处理request请求
function onRequest(req, res) {
// 使用url.parse()方法解析url
// 它会把url string转化为一个object
// 这样我们就可以很方便的获取url中的host、port、pathname等值了
var pathname = url.parse(req.url).pathname;
console.log('Request for ' + pathname + ' received.');
// 判断并处理不同url请求
// 后面介绍此方法
route(handle, pathname, res, req);
}
// 使用http.createSserver()方法创建http server
// 并传入onRequest()方法
// 然后使用listen()方法监听指定地址
http.createServer(onRequest).listen(port, host);
console.log('Server has started and listening on ' + host + ':' + port);
}
// 导出 start 方法
exports.start = start;
在文件的最后,我们导出了start方法,以便在主程序中使用。你肯定注意到了,在代码中使用了route()方法,它用于处理判断请求的url是否存在,现在我们就来编写这个方法。
5.2 创建路由
在app文件夹中新建router.js,输入如下代码:
var fs = require('fs');
// 路由函数
// 处理不同url的请求
// 并返回相应内容
function route(handle, pathname, res, req) {
console.log('About to route a request for ' + pathname);
// 判断此url是否存在特定处理函数
// 存在则调用handle处理
// 不存在则返回404页面
if (typeof handle[pathname] === 'function') {
// 后面介绍handle函数
handle[pathname](res, req);
} else {
console.log('No request handler found for ' + pathname);
// 读取404页面
// 所有页面都存放在view文件夹下
var content = fs.readFileSync('./views/404.html');
res.writeHead(404, { 'Content-Type': 'text/html' });
res.write(content);
res.end();
}
}
// 导出 route 方法
exports.route = route;
在此方法中,调用了handle()方法,这个方法用于处理不同的url请求。
在app文件夹中新建requestHandlers.js文件,输入如下代码:
// 处理url请求
var fs = require('fs');
// home.html 主页
function home(res) {
console.log('Request handler "home" was called.');
// 读取home.html文件
var content = fs.readFileSync('./views/home.html');
res.writeHead(200, { 'Content-Type': 'text/html' });
res.write(content);
res.end();
}
// about.html 关于页面
function about(res) {
console.log('Request handler "about" was called.');
// 读取about.html文件
var content = fs.readFileSync('./views/about.html');
res.writeHead(200, { 'Content-Type': 'text/html' });
res.write(content);
res.end();
}
// 导出页面处理函数
exports.home = home;
exports.about = about;
这个方法比较简单,就是读取文件,然后输出到response。
5.3 创建主程序
创建http server,判断url,处理url都写完了,那么我们可以写主程序来运行http server了,在app文件夹新建main.js文件,输入如下代码:
// 主程序
// 引入server,router及requestHandler
var server = require('./server');
var router = require('./router');
var requestHandlers = require('./requestHandlers');
// 保存url处理方法
var handle = {};
handle['/'] = requestHandlers.home;
handle['/about'] = requestHandlers.about;
// 启动http server
server.start(router.route, handle);
到此,所有的服务器代码都写完了,那么我们来添加代码中用到的两个html文件吧。
5.4 创建HTML文件
在app文件夹中新建views文件夹,在views文件夹中,新建home.html文件、about.html文件和404.html文件。
文件中的代码如下所示:
home.html文件:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Home page</title>
</head>
<body>
<p>home page</p>
</body>
</html>
about.html文件:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>About page</title>
</head>
<body>
<p>about page</p>
</body>
</html>
404.html文件:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>404 page</title>
</head>
<body>
<p>404 page not found</p>
</body>
</html>
现在运行程序:
$ node main.js
运行成功后,打开浏览器,访问“http://127.0.0.1:8080”就会看到页面显示“home page”,访问“http://127.0.0.1:8080/about”就会看到页面显示“about page”,访问“http://127.0.0.1:8080”下的其他页面就会看到页面显示“404 page not found”。
六、Node.js中的网络编程
net模块提供了一个异步网络包装器,用于TCP网络编程,它包含了创建服务器和客户端的方法。dgram模块用于UDP网络编程。
6.1 TCP Server
net模块通过net.createServer方法创建TCP服务器,通过net.connect方法创建客户端去连接服务器。
通过net模块创建一个TCP Server:
// server.js
var net = require('net');
// 创建TCP服务器
var server = net.createServer(function(socket) {
console.log('client connected');
// 监听客户端的数据
socket.on('data', function(data) {
console.log('server got data from client: ', data.toString());
});
// 监听客户端断开连接事件
socket.on('end', function(data) {
console.log('connection closed');
});
// 发送数据给客户端
socket.write('Hello\r\n');
});
// 启动服务
server.listen(8080, function() {
console.log('server bound');
});
然后再创建一个客户端:
// client.js
var net = require('net');
// 连接服务器
var client = net.connect({port: 8080}, function() {
console.log('connected to server');
client.write('World!\r\n');
});
// 接收服务端的数据
client.on('data', function(data) {
console.log('client got data from server: ', data.toString());
// 断开连接
client.end();
});
// 断开连接
client.on('end', function() {
console.log('disconnected from server');
});
运行测试:
打开两个终端,先运行TCP服务器代码:
$ node server.js
然后在另一个终端运行TCP客户端代码:
$ node client.js
即可看到两个终端运行结果。
6.2 简易聊天室服务端
了解了基础的东西,下面我们来做一个简单的聊天室吧。
首先创建TCP服务器,和前面一样:
// chatServer.js
var net = require('net');
// 创建TCP服务器
var server = net.createServer();
server.on('error', function(err) {
console.log('Server error: ', err.message);
});
server.on('close', function() {
console.log('Server closed');
});
server.listen(8080);
接收客户端连接请求:
// chatServer.js
var net = require('net');
// 创建TCP服务器
var server = net.createServer();
// 新增的代码
server.on('connection', function(socket) {
console.log('Got a new connection');
});
server.on('error', function(err) {
console.log('Server error: ', err.message);
});
server.on('close', function() {
console.log('Server closed');
});
server.listen(8080);
获取客户端发送过来的数据:
// chatServer.js
var net = require('net');
// 创建TCP服务器
var server = net.createServer();
server.on('connection', function(socket) {
console.log('Got a new connection');
// 新增的代码
socket.on('data', function(data) {
console.log('Got data: ', data.toString());
});
});
server.on('error', function(err) {
console.log('Server error: ', err.message);
});
server.on('close', function() {
console.log('Server closed');
});
server.listen(8080);
既然是聊天室,当然允许多个客户端用户同时连接,所以需要接收所有的用户连接:
// chatServer.js
var net = require('net');
// 创建TCP服务器
var server = net.createServer();
// 新增的代码
var sockets = []; // 存储所有客户端socket
server.on('connection', function(socket) {
console.log('Got a new connection');
// 新增的代码
sockets.push(socket);
socket.on('data', function(data) {
console.log('Got data: ', data);
});
});
server.on('error', function(err) {
console.log('Server error: ', err.message);
});
server.on('close', function() {
console.log('Server closed');
});
server.listen(8080);
服务器广播数据,把来自客户端的数据转发送给其他所有客户端:
// chatServer.js
var net = require('net');
// 创建TCP服务器
var server = net.createServer();
var sockets = []; // 存储所有客户端socket
server.on('connection', function(socket) {
console.log('Got a new connection');
sockets.push(socket);
socket.on('data', function(data) {
console.log('Got data: ', data);
// 新增代码
sockets.forEach(function(otherSocket) {
if (otherSocket !== socket) {
otherSocket.write(data);
}
});
});
});
server.on('error', function(err) {
console.log('Server error: ', err.message);
});
server.on('close', function() {
console.log('Server closed');
});
server.listen(8080);
最后,需要把关闭连接的客户端从服务器广播列表中给删除掉:
// chatServer.js
var net = require('net');
// 创建TCP服务器
var server = net.createServer();
// 存储所有客户端socket
var sockets = [];
server.on('connection', function(socket) {
console.log('Got a new connection');
sockets.push(socket);
socket.on('data', function(data) {
console.log('Got data: ', data);
sockets.forEach(function(otherSocket) {
if (otherSocket !== socket) {
otherSocket.write(data);
}
});
});
// 新增代码
socket.on('close', function() {
console.log('A client connection closed');
var index = sockets.indexOf(socket);
sockets.splice(index, 1);
});
});
server.on('error', function(err) {
console.log('Server error: ', err.message);
});
server.on('close', function() {
console.log('Server closed');
});
server.listen(8080);
6.3 简易聊天室客户端
聊天室的服务端功能基本就实现了,下面我们来写一个简单的客户端吧:
// chatClient.js
var net = require('net');
process.stdin.resume();
process.stdin.setEncoding('utf8');
var client = net.connect({ port: 8080 }, function() {
console.log('Connected to server');
// 获取输入的字符串
console.log('input: ');
process.stdin.on('data', function(data) {
// 发送输入的字符串到服务器
console.log('input: ');
client.write(data);
// 输入 'close' 字符串时关闭连接
if (data === 'close\n') {
client.end();
}
});
});
// 获取服务端发送过来的数据
client.on('data', function(data) {
console.log('Other user\'s input', data.toString());
});
client.on('end', function() {
console.log('Disconnected from server');
// 退出客户端程序
process.exit();
});
客户端中用到了process这个模块,process是一个Node.js的全局模块,可以在任何地方直接使用而无需通过require方法引入。process模块允许你获得或者修改当前Node.js进程的设置。process.stdin用于获取来自标准输入的可读流(Readable Stream)。
现在来运行这个聊天室吧:
$ node chatServer.js
在打开多个终端窗口,运行:
$ node chatClient.js
然后在任意运行的客户端输入字符串并按回车键,会在服务端和其他客户端看到这个客户端的输入内容。这样就实现了一个简单的聊天室了^_^。
6.4 UDP Server
UDP和TCP类似,通过dgram.createSocket创建服务。
UDP服务器:
// udpServer.js
var dgram = require("dgram");
var server = dgram.createSocket("udp4");
server.on("error", function(err) {
console.log("server error:\n" + err.stack);
server.close();
});
// 接收来自客户端的消息
server.on("message", function(msg, rinfo) {
console.log("server got: " + msg.toString() + " from " + rinfo.address + ":" + rinfo.port);
});
// 监听服务
server.on("listening", function() {
var address = server.address();
console.log("server listening on " + address.address + ":" + address.port);
});
server.bind(41234);
// server listening 0.0.0.0:41234
UDP客户端:
// udpClient.js
var dgram = require('dgram');
var client = dgram.createSocket('udp4');
var message = new Buffer('hello');
client.send(message, 0, message.length, 41234, 'localhost', function(err, bytes) {
client.close();
});
发送的消息必须通过Buffer创建。
然后运行服务端:
$ node udpServer.js
再运行客户端:
$ node udpClient.js
此时,服务端就会收到来自客户端的消息:
server got: hello from 127.0.0.1:59393
Node.js基础的网络编程就讲到这里了。
七、Node.js中的进程
7.1 process模块
process模块在前面的网络编程实验中已经用过了,使用时直接通过全局变量process访问,而不需要通过require方法加载。process用来和当前Node.js程序进程互动。
process是EventEmitter的一个实例,process主要包含退出事件、信号事件以及一些属性。
7.1.1 退出事件(exit)
当退出当前进程时,会促发exit事件,exit事件的回调函数中只接受同步操作,并且回调函数只接受一个参数,即退出代码,如:
process.on('exit', function(code) {
setTimeout(function() {
console.log('This will not run');
}, 0);
console.log('exit code: ', code);
});
运行上面的代码,其中setTimeout方法中的回调函数是不会被执行的,因为exit事件中只会运行同步操作,而不会运行异步操作。
在exit事件之前还有一个beforeExit事件会被触发,在beforeExit的回调函数中可以执行异步操作。值得注意的是,通过process.exit()退出程序或者因为发生错误而退出程序是不会触发beforeExit事件的。顺便说一下,当有错误未被捕获时,就会触发uncaughtException事件。
7.1.2 信号事件
信号事件就是接收到某个特定信号才会被触发的事件。
比如SIGINT事件的触发方式是ctrl+c:
// sigint.js
process.stdin.resume();
process.on('SIGINT', function() {
console.log('Got SIGINT. Press Control-D to exit.');
});
运行代码:
$ node sigint.js
然后按住ctrl键,再按c键就会触发SIGINT事件。
7.1.3 属性
process模块提供了很多属性,其中关于IO输入输出的主要有三个:
process.stdin // 标准输入
process.stdout // 标准输出
process.stderr // 标准错误
举例:
// stdin.js
process.stdin.setEncoding('utf8');
process.stdin.on('readable', function() {
var chunk = process.stdin.read();
if (chunk !== null) {
process.stdout.write('data: ' + chunk);
}
});
process.stdin.on('end', function() {
process.stdout.write('end');
});
运行:
node stdin.js
输入任意字符,Node.js会把输入的字符打印出来,输入ctrl+D触发end事件。
process还有其他属性,比如process.argv是一个包含了命令行参数的数组。
7.1.4 方法
process模块还有很多实用的方法,比如:
process.cwd() // 返回脚本运行工作目录
process.chdir() // 切换工作目录
process.exit() // 退出当前进程
process.on() // 添加监听事件
//...
7.2 child_process模块
child_process用于创建子进程。
7.2.1 child_process.spawn()方法
通过当前命令启动一个新的进程。如:
// test_spawn.js
var spawn = require('child_process').spawn,
ls = spawn('ls', ['-lh', '/usr']);
ls.stdout.on('data', function (data) {
console.log('stdout: ' + data);
});
ls.stderr.on('data', function (data) {
console.log('stderr: ' + data);
});
ls.on('close', function (code) {
console.log('child process exited with code ' + code);
});
运行命令:
$ node test_spawn.js
从结果可以看出,子进程成功运行了ls -lh /usr命令。
7.2.2 child_process.exec()方法
在shell中运行一个命令,并缓存其输出。如:
// test_exec.js
var exec = require('child_process').exec,
child;
child = exec('cat *.js bad_file | wc -l',
function (error, stdout, stderr) {
console.log('stdout: ' + stdout);
console.log('stderr: ' + stderr);
if (error !== null) {
console.log('exec error: ' + error);
}
});
运行:
$ node test_exec.js
因为没有bad_file 这个文件,所以会看到终端打印出了相关错误信息。
7.2.3 child_process.execFile()方法
与exec方法类似,执行特定程序文件,参数通过一个数组传送。,如:
// test_execfile.js
var child_process = require('child_process');
// exec: spawns a shell
child_process.exec('ls -lh /usr', function(error, stdout, stderr){
console.log(stdout);
console.log('******************');
});
// execFile: executes a file with the specified arguments
child_process.execFile('/bin/ls', ['-lh', '/usr'], function(error, stdout, stderr){
console.log(stdout);
});
运行:
$ node test_execfile.js
可以看出,execFile成功执行了/bin/ls程序,列出了/usr目录下的所有文件。
7.2.4 child_process.fork()方法
直接创建一个子进程,此进程是node命令的子进程,fork(’./sub.js’)相当于spwan(‘node’, ‘./sub.js’)。fork还会在父进程与子进程之间,建立一个通信管道,通过child.send()发送消息。如:
// main.js
var cp = require('child_process');
var n = cp.fork(__dirname + '/sub.js');
n.on('message', function(m) {
console.log('PARENT got message:', m);
});
n.send({ hello: 'world' });
// sub.js
process.on('message', function(m) {
console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });
运行:
$ node main.js
输出:
PARENT got message: { foo: 'bar' }
CHILD got message: { hello: 'world' }
运行main.js会看到主进程收到了来自子进程的消息,而子进程也收到了来自主进程的消息。
7.3 cluster模块
单个的Node实例运行在单个线程中。要发挥多核系统的能力,需要启动一个Node进程集群来处理负载。cluster模块就用于创建共享服务器端口的子进程。
举个例子:
// test_cluster.js
var cluster = require('cluster');
var http = require('http');
var numCPUs = require('os').cpus().length; // 获取CPU内核数
// master是主进程
// 此处判断是否为master进程
// 是则根据CPU内核数创建worker进程
if (cluster.isMaster) {
// worker是运行节点
// 根据CPU数量启动worker
// Fork workers
for (var i = 0; i < numCPUs; i++) {
cluster.fork();
}
Object.keys(cluster.workers).forEach(function(id) {
console.log('I am running with ID : ' + cluster.workers[id].process.pid);
});
cluster.on('exit', function(worker, code, signal) {
console.log('worker ' + worker.process.pid + ' died');
});
} else {
// cluster.isWorker == true
// 运行到else中的代码
// 说明当前进程是worker进程
// 那么此worker进程就启动一个http服务
http.createServer(function(req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
}
运行程序:
$ node test_cluster.js
在终端会看到根据CPU内核数创建的子进程信息。
每个worker进程都是通过child_process.fork()方法产生的,所以它们可以通过IPC(进程间通信)与master进程通信。
cluster.worker是worker进程对象,其中有 worker.id、worker.process等属性,还有worker.send()等方法。