Node.js

一、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模块和包

  1. 模块
    Node.js官方提供了很多模块,这些模块分别实现了一种功能,如操作文件的模块fs,构建http服务的模块http等,每个模块都是一个JavaScript文件,当然你也可以自己编写模块。


  2. 包可以将多个具有依赖关系的模块组织在一起,封装多个模块,以方便管理。Node.js采用了CommonJS规范,根据CommonJS规范规定,一个JavaScript文件就是一个模块,而包是一个文件夹,包内必须包含一个JSON文件,命名为package.json。一般情况下,包内的bin文件夹存放二进制文件,包内的lib文件夹存放JavaScript文件,包内的doc文件夹存放文档,包内的test文件夹存放单元测试。package.json文件中需要包含的字段及包的使用,后面的实验再做详细介绍。

  3. 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
新建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()等方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值