什么是node?
简单来说,node是JS的一种运行环境。在此之前,我们都知道JS可以在浏览器中运行,可以为网页添加各种交互,因此,浏览器也是JS的运行环境。随着Chrome浏览器的发布,带来了全新的V8引擎。经过多年的发展和优化,性能和安全性都已经达到了相当的高度。而 Node.js 则进一步将 V8 引擎加工成可以在任何操作系统中运行 JavaScript 的平台。
两个运行环境如图所示,他们都包含了JavaScript语言标准ECMAScript,在浏览器环境还有
- 浏览器对象模型(Browser Object Model,简称 BOM),也就是 window 对象
- 文档对象模型(Document Object Model,简称 DOM),也就是 document 对象
在node中也有特有的代表不同含义的对象
- global:和window一样,任何global对象上的属性都可以被全局访问到。
- process:所有全局执行上下文中的内容都在process对象中,提供了与操作系统的简单接口。
- buffer:让 JavaScript 也能够轻松地处理二进制数据流,结合 Node 的流接口(Stream),能够实现高效的二进制文件处理。
- __filename:当前所运行 Node 脚本的文件路径。
- __dirname:当前所运行 Node 脚本所在目录路径。
模块系统
node中的每一个文件都是一个模块,有着自己的作用域。使用Commonjs模块规范,该模块系统有三个核心的全局对象:require、module和exports。
- require
require 用于导入其他 Node 模块,其参数接受一个字符串代表模块的名称或路径,通常被称为模块标识符。
绝对模块:node通过在其内部node_modules查到的模块,或者node内置的例如fs这样的模块,可以直接通过名字来requirevar fs = require('fs')
相对模块将require指向一个相对工作目录中的JavaScript文件,比如我们在同一目录中创建名为module_a.js、module_b.js、main.js三个文件,如果我们想要在main.js中引入module_a.js和module_b.js,则需要写作require('./module_a')
- exports
exports用于导出当前node模块的内容,例如有一个module_a.js文件
function a {
console.log('module_a.js')
}
//导出函数a
exports.a = a
通过将a函数添加到exports对象中,其他模块就可以通过require引入调用。
- module
Node内部有个Module的构造函数,每个文件中的module都是其的一个实例。我们在node文件中可以通过console.log(module)
来查看其包含的属性,export其实就是对module.exports的引用
单线程
举个例子:假设一个用户分别向node服务器和PHP服务器各同时发起两次请求
//node
var books =['node','php'];
function serveBooks(){
var html = '<b>' + books.join('</b><br><b>') + '<b>';
books = [];
return html;
}
//php
$books = array('node','php');
function serveBooks(){
$html = '<b>' . books.join('</b><br><b>') . '<b>';
$books = array()
return $html;
}
在上述的两个serveBooks函数中,都将books数组重置了。
- node会将完整的book返回给第一次请求,而第二次则返回一个空的book.
- PHP两次请求都能返回完整的book
这两者的区别就在于基础架构上。node采用了一个长期运行的线程(单线程),使得第二次请求的book是第一次请求操作过的。而Apache会产生多个线程,每次都会刷新状态,因此不会受到影响。
node为JavaScript引入了一个复杂的概念:共享状态的并发。通俗的说,在node中,要谨慎处理回调函数对当前内存中变量的修改,并且还要注意对错误的处理是否会潜在地修改这些状态。这可能会导致整个进程不可用。
非阻塞IO
既然node执行时只有一个线程,那又是如何做到高并发的,这就要说到node的另一个特点,非阻塞(异步)
//php
print('Hello');
sleep(5);
print('World');
//node
console.log('Hello')
setTimeout(function(){
console.log('stop')
},3000)
console.log('World')
观察上面的两段代码有什么区别
除了语义上的区别(Node.js使用了回调函数)以外,两者最大的区别体现在了阻塞和非阻塞上,在PHP中,sleep()
阻塞了线程的执行,导致World无法输出,而node使用了事件轮询,因此这里的setTimeout是非阻塞的(异步),也就是说在3S之后会正常输出World。
事件轮询:从本质上来说Node会先注册事件,然后不停的询问内核这些事件是否已经分发。当事件分发时,对应的回调函数就会执行。如果没有事件触发,则会继续执行其他代码。
V8引擎执行JavaScript的速度非常快,结合非阻塞IO确保了单线程执行时,不会有因为数据库访问或者是硬盘访问等操作导致操作会被挂起。
事件循环
node中的事件循环分为三个队列,事件循环会按顺序执行三个队列中的事件。
- timer:setTimeout、setInterval 定时器操作
- poll:文件读写、网络请求、数据库操作等I\O操作
- check:setImmediate
如果timer队列和check队列都为空的时候,循环会在poll队列等待(优先处理IO事件)。
timer事件在node中的最小间隔为1ms,因此与setImmediate事件的执行顺序不确定。我们希望是确定的,当存在这种情况时,会将其同时放在一个IO事件的回调中,这样事件循环就从poll阶段开始,总是会先执行setImmediate了
process.nextTick() 属于微任务,并且总是最先执行.
事件模块
fs模块
fs模块是唯一一个同时提供同步和异步的API模块,用于对系统文件及目录进行读写操作。
-
fs.readFile(filename,[option],callback)
方法读取文件。 -
fs.writeFile(filename,data,[options],callback)
写入内容到文件。fs.read
和fs.write
功能类似fs.readFile
和fs.writeFile
,但提供更底层的操作,需要使用fs.open
打开文件和fs.close
关闭文件。实际应用中多用fs.readFile
和fs.writeFile
。 -
fs.open(path,flags,[mode],callback)
方法用于打开文件,以便fs.read()
读取。 -
fs.read(fd,buffer,offset,length,position,callback)
接收6个参数。用于关闭文件
//异步写法
var fs = require('fs');
fs.readdir(__dirname, function(err, files) {
console.log(files);
});
//同步写法
var fs = require('fs');
console.log(fs.readdirSync(__dirname))
TCP
传输控制协议(TCP)是一个面向连接的协议,它保证了两台计算机之间数据传输的可靠性和顺序。
- 面向连接:当在TCP连接内进行数据传递时,发送的IP数据报包含了标识该连接以及数据流顺序的信息。
- 面向字节:TCP对字符以及字符编码是不关心的,对消息格式没有严格的约束,具有很好的灵活性。
- 可靠性:当数据发送之后,发送方在窗口时间内等待“消息送达”的确认消息,否则会重发消息。可以有效解决网络错误或者网络阻塞的情况。
- 流控制:所谓流控制就是让发送发送速率不要过快,让接收方来得及接收。原理这就是运用TCP报文段中的窗口大小字段来控制,发送方的发送窗口不可以大于接收方发回的窗口大小。
- 拥堵控制:TCP会通过控制数据包的传输速率来避免拥堵的情况。
var net = require('net');
var tcp_server = net.createServer(); // 创建 tcp server
var Sockets = {};
var SocketID = 1;
// 监听 端口
tcp_server.listen(5678,function (){
console.log('tcp_server listening 5678');
});
// 处理客户端连接
tcp_server.on('connection',function (socket){
console.log(socket.address());
Sockets[SocketID] =socket;
SocketID++;
DealConnect(socket)
})
tcp_server.on('error', function (){
console.log('tcp_server error!');
})
tcp_server.on('close', function () {
console.log('tcp_server close!');
})
// 处理每个客户端消息
function DealConnect(socket){
socket.on('data',function(data){
data = data.toString();
// 向所有客户端广播消息
for(var i in Sockets){
Sockets[i].write(data);
}
// socket.write(data);
console.log('received data %s',data);
})
// 客户端正常断开时执行
socket.on('close', function () {
console.log('client disconneted!');
})
// 客户端正异断开时执行
socket.on("error", function (err) {
console.log('client error disconneted!');
});
}
HTTP
超文本传输协议(HTTP)是属于TCP的上层协议,构建在请求和相应的概念之上。nodejs中的http模块中封装了一个HTPP服务器和一个简易的HTTP客户端,http.Server是一个基于事件的http服务器,http.request则是一个http客户端工具,用于向http服务器发起请求。而上面的createServer方法中的参数函数中的两个参数req和res则是分别代表了请求对象和响应对象。其中req是http.ServerRequest的实例,res是http.ServerResponse的实例,这可以从nodejs中的源码中获取这个信息
//一个简单的http服务器
var http=require("http");
http.createServer(function(req,res){
res.writeHead(200,{
"content-type":"text/plain"
});
res.write("hello World");
res.end();
}).listen(3000);
打开浏览器,输入localhost:3000,如果看到屏幕上的hello World了,这表明这个最简单的nodejs服务器已经搭建成功了。
Connect
- Node.js为常规的网络应用提供了基本的API。然而,实际情况下,绝大部分网络应用都需要完成一系列类似的操作,这些类似的操作你一定不想每次都重复地基于原始的API去实现。
- Connect是一个基于HTTP服务器的工具集,它提供了一种新的组织代码的方式来与请求、响应对象进行交互,称为中间件(middleware)。
- 中间件具有代码复用的好处。在没有使用中间件的情况下,处理请求的这些代码都放在一个简单的事件处理器中(createServer的回调函数中),这将会是一个非常复杂的处理过程。而使用中间件后会将应用拆分为更小单元(让代码有更强大的表达能力),还能够实现很好的重用性。
var http = require('http'),
fs = require('fs');
/**
* 创建服务器
*/
var server = http.createServer(function(req, res) {
// 检查URL是否和服务器目录下的文件匹配,如果匹配,则读取该文件并展示出来
// 请求.jpg图片
if('GET' == req.method
&& '/images' == req.url.substr(0, 7)
&& '.jpg' == req.url.substr(-4)) {
// 返回对应图片
// ...
fs.stat(__dirname + req.url, function(err, stat) {
// 如果检查文件是否存在时发生错误,则终止进程并发送HTTP 404状态码告知无法找到请求的图片。
// 对于stat成功但是路径所表示的并非是文件时,也要做此处理。
if(err || !stat.isFile()) {
res.writeHead(404);
res.end('Not Found');
return;
}
// 发送图片资源
serve(__dirname + req.url, 'application/jpg');
});
} else if('GET' == req.method && '/' == req.url) {
// 发送html文件,默认请求index.html
// ...
// console.log(__dirname);
serve(__dirname + '/index.html', 'text/html');
} else {
// 处理其他请求,返回404错误码
res.writeHead(404);
res.end('Not found');
}
/**
* 根据文件路径来获取文件内容,并添加'Content-Type'头信息
* @param {[type]} path 文件路径
* @param {[type]} type Content-Type头信息
* @return {[type]} [description]
*/
function serve(path, type) {
res.writeHead(200, {'Content-Type': type});
fs.createReadStream(path).pipe(res);
}
});
server.listen(3000);