NodeJS简易博客系统(六)NodeJS入门学习(下)
一、网络编程
1、小试牛刀
NodeJS本来的用途是编写高性能Web服务器。首先在这里重复一下官方文档里的例子,使用NodeJS内置的http
模块简单实现一个HTTP服务器。
var http = require('http'); http.createServer(function (request, response) { |
以上程序创建了一个HTTP服务器并监听8080
端口,打开浏览器访问该端口http://127.0.0.1:8080/
就能够看到效果。
2、常用
-
HTTP
'http'模块提供两种使用方式: 作为服务端使用时,创建一个HTTP服务器,监听HTTP客户端请求并返回响应。 作为客户端使用时,发起一个HTTP客户端请求,获取服务端响应。 首先来看看服务端模式下如何工作。如小试牛刀中的例子所示,首先需要使用.createServer方法创建一个服务器,然后调用.listen方法监听端口。之后,每当来了一个客户端请求,创建服务器时传入的回调函数就被调用一次。可以看出,这是一种事件机制。 HTTP请求本质上是一个数据流,由请求头(headers)和请求体(body)组成。例如以下是一个完整的HTTP请求数据内容。 POST / HTTP/1.1 Hello World http.createServer(function (request, response) { console.log(request.method); request.on('data', function (chunk) { request.on('end', function () { ------------------------------------ HTTP/1.1 200 OK Hello World http.createServer(function (request, response) { request.on('data', function (chunk) { request.on('end', function () { var options = { var request = http.request(options, function (response) {}); request.write('Hello World'); http.get('http://www.example.com/', function (response) {}); http.get('http://www.example.com/', function (response) { console.log(response.statusCode); response.on('data', function (chunk) { response.on('end', function () { ------------------------------------
|
-
HTTPS
https模块与http模块极为类似,区别在于https模块需要额外处理SSL证书。 在服务端模式下,创建一个HTTPS服务器的示例如下。 var options = { var server = https.createServer(options, function (request, response) { 另外,NodeJS支持SNI技术,可以根据HTTPS客户端请求使用的域名动态使用不同的证书,因此同一个HTTPS服务器可以使用多个域名提供服务。接着上例,可以使用以下方法为HTTPS服务器添加多组证书。 server.addContext('foo.com', { server.addContext('bar.com', { var options = { var request = https.request(options, function (response) {}); request.end(); |
-
URL
处理HTTP请求时url模块使用率超高,因为该模块允许解析URL、生成URL,以及拼接URL。首先我们来看看一个完整的URL的各组成部分。 href url.parse('http://user:pass@host.com:8080/p/a/t/h?query=string#hash'); http.createServer(function (request, response) { 反过来,format方法允许将一个URL对象转换为URL字符串,示例如下。 url.format({ url.resolve('http://www.example.com/foo/bar', '../baz'); |
-
Query String
querystring模块用于实现URL参数字符串与参数对象的互相转换,示例如下。 querystring.parse('foo=bar&baz=qux&baz=quux&corge'); querystring.stringify({ foo: 'bar', baz: ['qux', 'quux'], corge: '' }); |
-
Zlib
zlib模块提供了数据压缩和解压的功能。当我们处理HTTP请求和响应时,可能需要用到这个模块。 首先我们看一个使用zlib模块压缩HTTP响应体数据的例子。这个例子中,判断了客户端是否支持gzip,并在支持的情况下使用zlib模块返回gzip之后的响应体数据。 http.createServer(function (request, response) { while (i--) { if ((request.headers['accept-encoding'] || '').indexOf('gzip') !== -1) { var options = { http.request(options, function (response) { response.on('data', function (chunk) { response.on('end', function () { if (response.headers['content-encoding'] === 'gzip') { |
-
Net
net模块可用于创建Socket服务器或Socket客户端。由于Socket在前端领域的使用范围还不是很广,这里先不涉及到WebSocket的介绍,仅仅简单演示一下如何从Socket层面来实现HTTP请求和响应。 首先我们来看一个使用Socket搭建一个很不严谨的HTTP服务器的例子。这个HTTP服务器不管收到啥请求,都固定返回相同的响应。 net.createServer(function (conn) { var options = { var client = net.connect(options, function () { client.on('data', function (data) { |
二、进程管理
NodeJS可以感知和控制自身进程的运行环境和状态,也可以创建子进程并与其协同工作,这使得NodeJS可以把多个程序组合在一起共同完成某项工作,并在其中充当胶水和调度器的作用。
1、小试牛刀
nodejs利用终端命令搞定目录拷贝,示例代码:
var child_process = require('child_process'); function copy(source, target, callback) { copy('a', 'b', function (err) { |
从以上代码中可以看到,子进程是异步运行的,通过回调函数返回执行结果。
2、常用
-
Process
任何一个进程都有启动进程时使用的命令行参数,有标准输入标准输出,有运行权限,有运行环境和运行状态。在NodeJS中,可以通过process对象感知和控制NodeJS自身进程的方方面面。另外需要注意的是,process不是内置模块,而是一个全局对象,因此在任何地方都可以直接使用。 |
-
Child Process
使用child_process模块可以创建和控制子进程。该模块提供的API中最核心的是.spawn,其余API都是针对特定使用场景对它的进一步封装,算是一种语法糖。 |
-
Cluster
cluster模块是对child_process模块的进一步封装,专用于解决单进程NodeJS Web服务器无法充分利用多核CPU的问题。使用该模块可以简化多进程服务器程序的开发,让每个核上运行一个工作进程,并统一通过主进程监听端口和分发请求。 |
-
如何获取命令行参数
在NodeJS中可以通过process.argv获取命令行参数。但是比较意外的是,node执行程序路径和主模块文件路径固定占据了argv[0]和argv[1]两个位置,而第一个命令行参数从argv[2]开始。为了让argv使用起来更加自然,可以按照以下方式处理。 function main(argv) { main(process.argv.slice(2)); |
-
如何退出程序
通常一个程序做完所有事情后就正常退出了,这时程序的退出状态码为0。或者一个程序运行时发生了异常后就挂了,这时程序的退出状态码不等于0。如果我们在代码中捕获了某个异常,但是觉得程序不应该继续运行下去,需要立即退出,并且需要把退出状态码设置为指定数字,比如1,就可以按照以下方式: try { |
-
如何控制输入输出
NodeJS程序的标准输入流(stdin)、一个标准输出流(stdout)、一个标准错误流(stderr)分别对应process.stdin、process.stdout和process.stderr,第一个是只读数据流,后边两个是只写数据流,对它们的操作按照对数据流的操作方式即可。例如,console.log可以按照以下方式实现。 function log() { |
-
如何降权
在Linux系统下,我们知道需要使用root权限才能监听1024以下端口。但是一旦完成端口监听后,继续让程序运行在root权限下存在安全隐患,因此最好能把权限降下来。以下是这样一个例子。 http.createServer(callback).listen(80, function () { process.setgid(gid); 如果是通过sudo获取root权限的,运行程序的用户的UID和GID保存在环境变量SUDO_UID和SUDO_GID里边。如果是通过chmod +s方式获取root权限的,运行程序的用户的UID和GID可直接通过process.getuid和process.getgid方法获取。 process.setuid和process.setgid方法只接受number类型的参数。 降权时必须先降GID再降UID,否则顺序反过来的话就没权限更改程序的GID了。 |
-
如何创建子进程
以下是一个创建NodeJS子进程的例子。 var child = child_process.spawn('node', [ 'xxx.js' ]); child.stdout.on('data', function (data) { child.stderr.on('data', function (data) { child.on('close', function (code) { 另外,上例中虽然通过子进程对象的.stdout和.stderr访问子进程的输出,但通过options.stdio字段的不同配置,可以将子进程的输入输出重定向到任何数据流上,或者让子进程共享父进程的标准输入输出流,或者直接忽略子进程的输入输出。 |
-
进程间如何通讯
在Linux系统下,进程之间可以通过信号互相通信。以下是一个例子。 /* parent.js */ child.kill('SIGTERM'); /* child.js */ 另外,如果父子进程都是NodeJS进程,就可以通过IPC(进程间通讯)双向传递数据。以下是一个例子。 /* parent.js */ child.on('message', function (msg) { child.send({ hello: 'hello' }); /* child.js */ |
-
如何守护子进程
守护进程一般用于监控工作进程的运行状态,在工作进程不正常退出时重启工作进程,保障工作进程不间断运行。以下是一种实现方式。 /* daemon.js */ worker.on('exit', function (code) { spawn('worker.js'); |
三、异步编程
NodeJS最大的卖点——事件机制和异步IO,对开发者并不是透明的。开发者需要按异步方式编写代码才用得上这个卖点,而这一点也遭到了一些NodeJS反对者的抨击。但不管怎样,异步编程确实是NodeJS最大的特点,没有掌握异步编程就不能说是真正学会了NodeJS。本章将介绍与异步编程相关的各种知识。
1、回调
在代码中,异步编程的直接体现就是回调。异步编程依托于回调来实现,但不能说使用了回调后程序就异步化了。我们首先可以看看以下代码。
function heavyCompute(n, callback) { for (i = n; i > 0; --i) { callback(count); heavyCompute(10000, function (count) { console.log('hello'); -- Console ------------------------------ |
可以看到,以上代码中的回调函数仍然先于后续代码执行。JS本身是单线程运行的,不可能在一段代码还未结束运行时去运行别的代码,因此也就不存在异步执行的概念。
但是,如果某个函数做的事情是创建一个别的线程或进程,并与JS主线程并行地做一些事情,并在事情做完后通知JS主线程,那情况又不一样了。我们接着看看以下代码。
setTimeout(function () { console.log('hello'); -- Console ------------------------------ |
这次可以看到,回调函数后于后续代码执行了。如同上边所说,JS本身是单线程的,无法异步执行,因此我们可以认为setTimeout这类JS规范之外的由运行环境提供的特殊函数做的事情是创建一个平行线程后立即返回,让JS主进程可以接着执行后续代码,并在收到平行进程的通知后再执行回调函数。除了setTimeout、setInterval这些常见的,这类函数还包括NodeJS提供的诸如fs.readFile之类的异步API。
另外,我们仍然回到JS是单线程运行的这个事实上,这决定了JS在执行完一段代码之前无法执行包括回调函数在内的别的代码。也就是说,即使平行线程完成工作了,通知JS主线程执行回调函数了,回调函数也要等到JS主线程空闲时才能开始执行。以下就是这么一个例子。
function heavyCompute(n) { for (i = n; i > 0; --i) { var t = new Date(); setTimeout(function () { heavyCompute(50000); -- Console ------------------------------ |
可以看到,本来应该在1秒后被调用的回调函数因为JS主线程忙于运行其它代码,实际执行时间被大幅延迟。
2、代码设计模式
-
函数返回值
使用一个函数的输出作为另一个函数的输入是很常见的需求,在同步方式下一般按以下方式编写代码: var output = fn1(fn2('input')); fn2('input', function (output2) { |
-
遍历数组
在遍历数组时,使用某个函数依次对数据成员做一些处理也是常见的需求。如果函数是同步执行的,一般就会写出以下代码: var len = arr.length, for (; i < len; ++i) { // All array items have processed. (function next(i, len, callback) { 如果数组成员可以并行处理,但后续代码仍然需要所有数组成员处理完毕后才能执行的话,则异步代码会调整成以下形式: (function (i, len, count, callback) { |
-
异常处理
JS自身提供的异常捕获和处理机制——try..catch..,只能用于同步执行的代码。以下是一个例子。 function sync(fn) { try { -- Console ------------------------------ function async(fn, callback) { try { -- Console ------------------------------ function async(fn, callback) { async(null, function (err, data) { -- Console ------------------------------ 有了异常处理方式后,我们接着可以想一想一般我们是怎么写代码的。基本上,我们的代码都是做一些事情,然后调用一个函数,然后再做一些事情,然后再调用一个函数,如此循环。如果我们写的是同步代码,只需要在代码入口点写一个try语句就能捕获所有冒泡上来的异常,示例如下。 function main() { try { function main(callback) { main(function (err) { |
3、域(Domain)
NodeJS提供了domain模块,可以简化异步代码的异常处理。在介绍该模块之前,我们需要首先理解“域”的概念。简单的讲,一个域就是一个JS运行环境,在一个运行环境中,如果一个异常没有被捕获,将作为一个全局异常被抛出。NodeJS通过process对象提供了捕获全局异常的方法,示例代码如下
process.on('uncaughtException', function (err) { setTimeout(function (fn) { -- Console ------------------------------ |
虽然全局异常有个地方可以捕获了,但是对于大多数异常,我们希望尽早捕获,并根据结果决定代码的执行路径。我们用以下HTTP服务器代码作为例子:
function async(request, callback) { http.createServer(function (request, response) { |
以上代码将请求对象交给异步函数处理后,再根据处理结果返回响应。这里采用了使用回调函数传递异常的方案,因此async函数内部如果再多几个异步函数调用的话,代码就变成上边这副鬼样子了。为了让代码好看点,我们可以在每处理一个请求时,使用domain模块创建一个子域(JS子运行环境)。在子域内运行的代码可以随意抛出异常,而这些异常可以通过子域对象的error事件统一捕获。于是以上代码可以做如下改造:
function async(request, callback) { http.createServer(function (request, response) { d.on('error', function () { d.run(function () { |
可以看到,我们使用.create方法创建了一个子域对象,并通过.run方法进入需要在子域中运行的代码的入口点。而位于子域中的异步函数回调函数由于不再需要捕获异常,代码一下子瘦身很多。
注意
无论是通过process对象的uncaughtException事件捕获到全局异常,还是通过子域对象的error事件捕获到了子域异常,在NodeJS官方文档里都强烈建议处理完异常后立即重启程序,而不是让程序继续运行。按照官方文档的说法,发生异常后的程序处于一个不确定的运行状态,如果不立即退出的话,程序可能会发生严重内存泄漏,也可能表现得很奇怪。
但这里需要澄清一些事实。JS本身的throw..try..catch异常处理机制并不会导致内存泄漏,也不会让程序的执行结果出乎意料,但NodeJS并不是存粹的JS。NodeJS里大量的API内部是用C/C++实现的,因此NodeJS程序的运行过程中,代码执行路径穿梭于JS引擎内部和外部,而JS的异常抛出机制可能会打断正常的代码执行流程,导致C/C++部分的代码表现异常,进而导致内存泄漏等问题。
因此,使用uncaughtException或domain捕获异常,代码执行路径里涉及到了C/C++部分的代码时,如果不能确定是否会导致内存泄漏等问题,最好在处理完异常后重启程序比较妥当。而使用try语句捕获异常时一般捕获到的都是JS本身的异常,不用担心上诉问题。
参考原文:http://nqdeng.github.io/7-days-nodejs/#4.2.2,尊重原创,感谢原文作者