K 的博客

记录精彩的程序人生 开始使用

Node.js 入门教程

 

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。

双击虚拟机桌面的Xfce终端,进入Linux终端:

输入nodenodejs进入Node.js的交互式运行环境(老版本的Node.js,不支持node命令,只能使用nodejs命令),Ctrl+d组合键可以退出此环境。

查看系统中安装的Node.js版本:

$ node -v
$ node
> console.log('shiyanlou');

通过交互式环境也可以运行JavaScript文件。

在虚拟机桌面新建一个JavaScript文件,命名为test.js,打开gvim,输入如下代码并保存:

var num = 100;

console.log(num);
console.log(num + 100);

通过Node.js交互式环境运行文件:

$ cd Desktop
Desktop$ node test.js

##三、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 模块

Node.js 模块和 Node.js 包介绍。

##一、Node.js模块

每一个Node.js都是一个Node.js模块,包括JavaScript文件(.js)、JSON文本文件(.json)和二进制模块文件(.node)。

###1. 模块的使用

编写一个模块:

在虚拟机桌面新建一个文件mymodule.js,输入如下代码并保存:

function hello() {
    console.log('Hello');
}

function world() {
console.log('World');
}

这就是一个Node.js模块,但是怎么在其他模块中引入并使用这个模块呢?我们需要为模块提供对外的接口,这就要用到module.exportsexports

我们可以这样写mymodul.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');
    };
<span class="hljs-keyword">this</span>.world = <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'World'</span>);
};

}

module.exports = Hello;

此时,index.js就要改成这样:

var Hello = require('./mymodule');

var hello = new Hello();

hello.hello(); // >> Hello
hello.world(); // >> World

###2. module.exports和exports

module是一个对象,每个模块中都有一个module对象,module是当前模块的一个引用。module.exports对象是Module系统创建的,而exports可以看作是对module.exports对象的一个引用。在模块中require另一个模块时,以module.exports的值为准,因为有的情况下,module.exportsexports它们的值是不同的。module.exportsexports的关系可以表示成这样:

// 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 }

##二、Node.js包

###1. 包

包用于管理多个模块及其依赖关系,可以对多个模块进行封装,包的根目录必须包含package.json文件,package.json文件是CommonJS规范用于描述包的文件,符合CommonJS规范的package.json文件应该包含以下字段:

  1. name:包名。包名是唯一的,只能包含小写字母、数字和下划线。
  2. version:包版本号。
  3. description:包说明。
  4. keywords:关键字数组。用于搜索。
  5. homepage:项目主页。
  6. bugs:提交bug的地址。
  7. license:许可证。
  8. maintainers:维护者数组。
  9. contributors:贡献者数组。
  10. repositories:项目仓库托管地址数组。
  11. 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"
        }
    }
}

###2. npm包管理工具

由于实验楼环境网络限制,所以npm命令会连接taobao的源,而不会直接连接官方源。

npm可以从第三方网站(http://www.npmjs.org/)上下载第三方Node.js包。

搜索第三方包:

shiyanlou@cqqg0heZ:~$ sudo npm search express

安装包:

shiyanlou@cqqg0heZ:~$ sudo npm install -g express

更新包:

shiyanlou@cqqg0heZ:~$ sudo npm update express

卸载包:

shiyanlou@cqqg0heZ:~$ sudo npm uninstall express
 
 

Node.js Events模块

在Node.js中,很多对象都会发出事件。比如,fs.readStream打开文件时会发出一个事件。所有发出事件的对象都是events.EventEmitter的实例,可以通过require("events");获得event模块。

通常,事件名采用“驼峰式”命名方式,但是,并没有严格规定。这只是推荐的命名方法。

函数可以添加给对象,对象发出事件时,对应函数就会被执行。这些函数被称作监听器(listeners)。在监听器函数中,this引用的是它(指此监听器函数)添加到的EventEmitter对象。

##Class: events.EventEmitter

通过require('events').EventEmitter得到EventEmitter类。

当EventEmitter对象遇到错误时,通常会触发error事件。error事件在Node.js中是一种特殊情况,如果没有监听器,那么默认会打印出栈跟踪器并退出程序。

##添加监听器

为事件绑定事件处理程序,可以使用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) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('shiyanlou');
console.log('shiyanlou');
res.end();
});

server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

运行代码:

shiyanlou@sdf234jh4:~$ cd Desktop
shiyanlou@sdf234jh4:~/Desktop$ node test1.js

然后打开虚拟机桌面的Firefox浏览器,在地址栏输入127.0.0.1:1337,即可看到页面上打印出了“shiyanlou”字样,同时console界面也会输出'shiyanlou'字样。

##只执行一次的监听器

使用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('shiyanlou');
console.log('shiyanlou');
res.end();
});

server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

运行代码:

shiyanlou@sdf234jh4:~/Desktop$ node test2.js

打开虚拟机桌面的Firefox浏览器,在地址栏输入127.0.0.1:1337,即可看到页面上打印出了“shiyanlou”字样,再次刷新此页面,就不会再显示,因为此事件只会执行一次。

##移除监听器

移除监听器使用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('shiyanlou');
console.log('shiyanlou');
res.end();
});

server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

运行代码:

shiyanlou@sdf234jh4:~/Desktop$ node test3.js

打开虚拟机桌面的Firefox浏览器,在地址栏输入127.0.0.1:1337,即可看到页面上打印出了“shiyanlou”字样,为什么没有显示“Hello World”呢?因为显示“Hello World”的监听器被移除了。

##移除所有监听器

移除所有监听器使用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('shiyanlou,111');
console.log('shiyanlou,111');
res.end();
});

server.on('request', function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('shiyanlou,222');
console.log('shiyanlou,222');
res.end();
});

// 移除绑定的所有监听器
server.removeAllListeners('request');

server.on('request', function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('shiyanlou');
console.log('shiyanlou');
res.end();
});

server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

运行代码:

shiyanlou@sdf234jh4:~/Desktop$ node test4.js

打开虚拟机桌面的Firefox浏览器,在地址栏输入127.0.0.1:1337,即可看到页面上打印出了“shiyanlou”字样,说明前面的监听器被移除了,都没有执行,所以没有显示,同时console界面也只会输出'shiyanlou'字样。

##设置监听器最大绑定数

emitter.setMaxListeners(n)可以设置同一事件的监听器最大绑定数,默认情况下,超过10个就会警告提示,这能帮我们快速找到类存泄露的地方。显然,不是所有的事件触发器都限制在10个监听器,通过这个方法可以设置,如果设置为0就是无限制。

##自定义事件

使用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', 'shiyanlou');

server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

运行代码:

shiyanlou@sdf234jh4:~/Desktop$ node test5.js

可以看到console界面输出了'shiyanlou'字样,说明触发自定义事件成功。

##查看事件绑定的监听器个数

使用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('shiyanlou,111');
console.log('shiyanlou,111');
res.end();
});

server.on('request', function(req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.write('shiyanlou,222');
console.log('shiyanlou,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);

运行代码:

shiyanlou@sdf234jh4:~/Desktop$ node test6.js

可以看到console界面输出了数字“2”,因为server绑定了两个监听器到'request'事件。

 

Node.js fs 模块

fs模块用于对系统文件及目录进行读写操作。

参考链接:http://nodejs.org/api/fs.html

##一、同步和异步

使用require('fs')载入fs模块,模块中所有方法都有同步和异步两种形式。

异步方法中回调函数的第一个参数总是留给异常参数(exception),如果方法成功完成,那么这个参数为null或者undefined

异步方法实例代码(无需在虚拟机中编写):

var fs = require('fs'); // 载入fs模块

fs.unlink('/tmp/shiyanlou', function(err) {
if (err) {
throw err;
}
console.log('成功删除了 /tmp/shiyanlou');
});

同步方法实例代码(无需在虚拟机中编写):

var fs = require('fs');

fs.unlinkSync('/tmp/shiyanlou'); // Sync 表示是同步方法
console.log('成功删除了 /tmp/shiyanlou');

同步方法执行完并返回结果后,才能执行后续的代码。而异步方法采用回调函数接收返回结果,可以立即执行后续的代码。

##二、readFile读取文件

使用fs.readFile(filename, [options], callback)方法读取文件。

readFile接收三个参数,filename是文件名;[options]是可选的参数,为一个对象,用于指定文件编码(encoding)及操作方式(flag);callback是回调函数。

在虚拟机家目录(/home/shiyanlou)下新建一个文件text.txt,文件中的内容如下:

line one
line two

使用readFile读取此文件,虚拟机家目录下新建文件readfile.js,输入如下代码并保存:

var fs = require('fs'); // 引入fs模块

fs.readFile('./test.txt', function(err, data) {
// 读取文件失败/错误
if (err) {
throw err;
}
// 读取文件成功
console.log(data);
});

readFile的回调函数接收两个参数,err是读取文件出错时触发的错误对象,data是从文件读取的数据。

运行程序:

shiyanlou:~/ $ 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('./test.txt', function(err, data) {
// 读取文件失败/错误
if (err) {
throw err;
}
// 读取文件成功
console.log('toString: ', data.toString());
});

// 设置编码格式
fs.readFile('./test.txt', 'utf-8', function(err, data) {
// 读取文件失败/错误
if (err) {
throw err;
}
// 读取文件成功
console.log('utf-8: ', data.toString());
});

这样再运行程序就能正常显示出文件中的内容了。

fs.readFileSync(filename, [options])是readFile的同步方法。

##三、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;
}

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Saved.'</span>);

<span class="hljs-comment">// 写入成功后读取测试</span>
fs.readFile(<span class="hljs-string">'./test2.txt'</span>, <span class="hljs-string">'utf-8'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(err, data)</span> </span>{
    <span class="hljs-keyword">if</span> (err) {
        <span class="hljs-keyword">throw</span> err;
    }
    <span class="hljs-built_in">console</span>.log(data);
});

});

运行程序:

shiyanlou:~/ $ 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;
}

<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Saved.'</span>);

<span class="hljs-comment">// 写入成功后读取测试</span>
fs.readFile(<span class="hljs-string">'./test2.txt'</span>, <span class="hljs-string">'utf-8'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(err, data)</span> </span>{
    <span class="hljs-keyword">if</span> (err) {
        <span class="hljs-keyword">throw</span> err;
    }
    <span class="hljs-built_in">console</span>.log(data);
});

});

运行程序:

shiyanlou:~/ $ node writeFile.js

flag传递的值,r代表读取文件,,w代表写入文件,a代表追加写入文件,还有其他的值不作详细介绍。

##四、使用fs.read和fs.write读写文件

使用fs.readfs.write读写文件需要使用fs.open打开文件和fs.close关闭文件。

###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());

    <span class="hljs-comment">// 关闭文件</span>
    fs.close(fd);
});

});

运行程序:

shiyanlou:~/ $ node read.js

###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, writeten, buffer) {
if (err) {
throw err;
}

    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'write success.'</span>);
    <span class="hljs-comment">// 打印出buffer中存入的数据</span>
    <span class="hljs-built_in">console</span>.log(bytesRead, buffer.slice(<span class="hljs-number">0</span>, bytesRead).toString());

    <span class="hljs-comment">// 关闭文件</span>
    fs.close(fd);
});

});

运行程序:

shiyanlou:~/ $ node write.js

##五、目录操作

###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.');
});

运行代码:

shiyanlou:~/ $ node mkdir.js

运行程序后会发现在当前目录下已经创建了newdir目录,删除目录可以使用fs.rmdir(path, callback),但是只能删除空目录。

###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);
});

运行代码:

shiyanlou:~/ $ node readdir.js

##六、结束

fs模块中还有很多其他方法,其使用与前面介绍的方法类似,在此不一一介绍,可自行查阅官方API文档。

 
 

Node.js 的 http 模块

http模块主要用于创建http server服务,此次实验还会讲到url模块和path模块,同时也会用到前面讲过的fs模块。url模块用于解析url,path模块用于处理和转换文件路径。

通过前面的实验,相信大家对Node.js模块的使用已经比较熟悉。在这个实验中,我们就通过编写一个简单的http server来学习http模块。

##一、创建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件夹,在文件夹中新建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 请求

<span class="hljs-comment">// 处理request请求</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">onRequest</span><span class="hljs-params">(req, res)</span> </span>{
    <span class="hljs-comment">// 使用url.parse()方法解析url</span>
    <span class="hljs-comment">// 它会把url string转化为一个object</span>
    <span class="hljs-comment">// 这样我们就可以很方便的获取url中的host、port、pathname等值了</span>
    <span class="hljs-keyword">var</span> pathname = url.parse(request.url).pathname;
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Request for '</span> + pathname + <span class="hljs-string">' received.'</span>);

    <span class="hljs-comment">// 判断并处理不同url请求</span>
    <span class="hljs-comment">// 后面介绍此方法</span>
    route(handle, pathname, res, req);
}

<span class="hljs-comment">// 使用http.createSserver()方法创建http server</span>
<span class="hljs-comment">// 并传入onRequest()方法</span>
<span class="hljs-comment">// 然后使用listen()方法监听指定地址</span>
http.createServer(onRequest).listen(port, host);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Server has started and listening on '</span> + host + <span class="hljs-string">':'</span> + port);

}

// 导出 start 方法
exports.start = start;

在文件的最后,我们导出了start方法,以便在主程序中使用。你肯定注意到了,在代码中使用了route()方法,它用于处理判断请求的url是否存在,现在我们就来编写这个方法。

##二、创建路由

在app文件夹中新建router.js,输入如下代码:

var fs = require('fs');

// 路由函数
// 处理不同 url 的请求
// 并返回相应内容

function route(handle, pathname, res, req) {
console.log('About to route a request for ' + pathname);

<span class="hljs-comment">// 判断此url是否存在特定处理函数</span>
<span class="hljs-comment">// 存在则调用handle处理</span>
<span class="hljs-comment">// 不存在则返回404页面</span>
<span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> handle[pathname] === <span class="hljs-string">'function'</span>) {
    <span class="hljs-comment">// 后面介绍handle函数</span>
    handle[pathname](res, req);
} <span class="hljs-keyword">else</span> {
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'No request handler found for '</span> + pathname);

    <span class="hljs-comment">// 读取404页面</span>
    <span class="hljs-comment">// 所有页面都存放在view文件夹下</span>
    <span class="hljs-keyword">var</span> content = fs.readFileSync(<span class="hljs-string">'./views/404.html'</span>);
    res.writeHead(<span class="hljs-number">404</span>, { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'text/html'</span> });
    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.');

<span class="hljs-comment">// 读取home.html文件</span>
<span class="hljs-keyword">var</span> content = fs.readFileSync(<span class="hljs-string">'./views/home.html'</span>);
res.writeHead(<span class="hljs-number">200</span>, { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'text/html'</span> });
res.write(content);
res.end();

}

// about.html 关于页面
function about(res) {
console.log('Request handler "about" was called.');

<span class="hljs-comment">// 读取about.html文件</span>
<span class="hljs-keyword">var</span> content = fs.readFileSync(<span class="hljs-string">'./views/about.html'</span>);
res.write(<span class="hljs-number">200</span>, { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'text/html'</span> });
res.write(content);
res.end();

}

// 导出页面处理函数
exports.home = home;
exports.about = about;

这个方法比较简单,就是读取文件,然后输出到response。

##三、创建主程序

创建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文件吧。

##四、创建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>

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中的网络编程

主要讲解TCP和UDP的网络编程,net模块提供了一个异步网络包装器,用于TCP网络编程,它包含了创建服务器和客户端的方法。dgram模块用于UDP网络编程。

参考链接:https://nodejs.org/api/net.html, https://nodejs.org/api/dgram.html

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');

<span class="hljs-comment">// 监听客户端的数据</span>
socket.on(<span class="hljs-string">'data'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(data)</span> </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'server got data from client: '</span>, data.toString());
});
<span class="hljs-comment">// 监听客户端断开连接事件</span>
socket.on(<span class="hljs-string">'end'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(data)</span> </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'connection closed'</span>);
});
<span class="hljs-comment">// 发送数据给客户端</span>
socket.write(<span class="hljs-string">'Hello\r\n'</span>);

});

// 启动服务
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

即可看到两个终端运行结果。

简易聊天室服务端

了解了基础的东西,下面我们来做一个简单的聊天室吧。

首先创建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);
]);
});

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');

<span class="hljs-comment">// 新增的代码</span>
sockets.push(socket);

socket.on(<span class="hljs-string">'data'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(data)</span> </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Got data: '</span>, 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(<span class="hljs-string">'data'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(data)</span> </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Got data: '</span>, data);

    <span class="hljs-comment">// 新增代码</span>
    sockets.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(otherSocket)</span> </span>{
        <span class="hljs-keyword">if</span> (otherSoecket !== 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(<span class="hljs-string">'data'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(data)</span> </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Got data: '</span>, data);

    sockets.forEach(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(otherSocket)</span> </span>{
        <span class="hljs-keyword">if</span> (otherSoecket !== socket) {
            otherSocket.write(data);
        }
    });
]);

<span class="hljs-comment">// 新增代码</span>
socket.on(<span class="hljs-string">'close'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">()</span> </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'A client connection closed'</span>);
    <span class="hljs-keyword">var</span> index = sockets.indexOf(socket);
    sockets.splice(index, <span class="hljs-number">1</span>);
});

});

server.on('error', function(err) {
console.log('Server error: ', err.message);
});

server.on('close', function() {
console.log('Server closed');
});

server.listen(8080);

简易聊天室客户端

聊天室的服务端功能基本就实现了,下面我们来写一个简单的客户端吧:

// 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');

<span class="hljs-comment">// 获取输入的字符串</span>
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'input: '</span>);
process.stdin.on(<span class="hljs-string">'data'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(data)</span> </span>{
    <span class="hljs-comment">// 发送输入的字符串到服务器</span>
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'input: '</span>);
    client.write(data);

    <span class="hljs-comment">// 输入 'close' 字符串时关闭连接</span>
    <span class="hljs-keyword">if</span> (data === <span class="hljs-string">'close\n'</span>) {
        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

然后在任意运行的客户端输入字符串并按回车键,会在服务端和其他客户端开到这个客户端的输入内容。这样就实现了一个简单的聊天室了^_^。

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 shiyanlou');

client.send(message, 0, message.length, 41234, 'localhost', function(err, bytes) {
client.close();
});

发送的消息必须通过Buffer创建。

然后运行服务端:

$ node server.js

再运行客户端:

$ node client.js

此时,服务端就会收到来自客户端的消息。

Node.js基础的网络编程就讲到这里了。

 

Markdown格式实验文档模板-实验名称

主要学习Node.js中的进程,涉及的模块是Cluster、Porcess和child_process。

Process模块

process模块在前面的实验已经用过了,使用时直接通过全局变量process访问,而不需要通过require方法加载。process用来和当前Node.js程序进程互动。

process是EventEmitter的一个实例,process主要包含退出事件、信号事件以及一些属性。

退出事件(exit)

当退出当前进程时,会促发exit事件,exit事件的回调函数中只接受同步操作,并且回调函数只接受一个参数,即退出代码,如:

process.on('exit', function(code) {
    setTimeout(function() {
        console.log('This will not run');
    }, 0);
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'exit code: '</span>, code);

});

运行上面的代码,其中setTimeout方法中的回调函数是不会被执行的,因为exit事件中只会运行同步操作,而不会运行异步操作。

在exit事件之前还有一个beforeExit事件会被触发,在beforeExit的回调函数中可以执行异步操作。值得注意的是,通过process.exit()退出程序或者因为发生错误而退出程序是不会触发beforeExit事件的。顺便说一下,当有错误未被捕获时,就会触发uncaughtException事件。

信号事件

信号事件就是接收到某个特定信号才会被触发的事件。

比如SIGINT事件的触发方式是ctrl+c

// sigint.js
process.stdin.resume();

process.on('SIGINT', function() {
console.log('Got SIGINT. Press Control-D to exit.');
});

运行代码:

$ node sigint.js

然后按住control键,再按C键就会触发SIGINT事件。

属性

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.argv是一个包含了命令行参数的数组。

方法

process模块还有很多实用的方法,比如:

process.cwd()   // 返回脚本运行工作目录
process.chdir() // 切换工作目录
process.exit()  // 退出当前进程
process.on()    // 添加监听事件
//...

child_process模块

child_process用于创建子进程。

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命令。

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 这个文件,所以会看到终端打印出了相关错误信息。

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

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

运行main.js会看到主进程收到了来自子进程的消息,而子进程也收到了来自主进程的消息。

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();
}

<span class="hljs-built_in">Object</span>.keys(cluster.workers).forEach(<span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(id)</span> </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'I am running with ID : '</span> + cluster.workers[id].process.pid);
});
cluster.on(<span class="hljs-string">'exit'</span>, <span class="hljs-function"><span class="hljs-keyword">function</span><span class="hljs-params">(worker, code, signal)</span> </span>{
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'worker '</span> + worker.process.pid + <span class="hljs-string">' died'</span>);
});

} 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.idworker.process等属性,还有worker.send()等方法。

 
 

 

评论
留下你的脚步
推荐阅读