商业转载请联系作者获得授权,非商业转载请注明出处。
作者:FengqiAsia
链接:http://www.zhihu.com/question/19653241/answer/15993549
来源:知乎
要讲清楚这个问题,先讲讲整个Web应用程序架构(包括流量、处理器速度和内存速度)中的瓶颈。瓶颈在于服务器能够处理的并发连接的最大数量。Node.js解决这个问题的方法是:更改连接到服务器的方式。每个连接发射一个在Node.js引擎的进程中运行的事件,而不是为每个连接生成一个新的OS线程(并为其分配一些配套内存)。Node.js不会死锁,因为它根本不允许使用锁,它不会直接阻塞 I/O 调用。Node.js还宣称,运行它的服务器能支持数万个并发连接。
Node本身运行V8 JavaScript。V8 JavaScript引擎是Google用于其Chrome浏览器的底层JavaScript引擎。Google使用V8创建了一个用C++编写的超快解释器,该解释器拥有另一个独特特征:您可以下载该引擎并将其嵌入任何应用程序。V8 JavaScript引擎并不仅限于在一个浏览器中运行。因此,Node.js实际上会使用Google编写的V8 JavaScript引擎,并将其重建为可在服务器上使用。
Node.js优点:
1、采用事件驱动、异步编程,为网络服务而设计。其实Javascript的匿名函数和闭包特性非常适合事件驱动、异步编程。而且JavaScript也简单易学,很多前端设计人员可以很快上手做后端设计。
2、Node.js非阻塞模式的IO处理给Node.js带来在相对低系统资源耗用下的高性能与出众的负载能力,非常适合用作依赖其它IO资源的中间层服务。3、Node.js轻量高效,可以认为是数据密集型分布式部署环境下的实时应用系统的完美解决方案。Node非常适合如下情况:在响应客户端之前,您预计可能有很高的流量,但所需的服务器端逻辑和处理不一定很多。
Node.js缺点:
1、可靠性低
2、单进程,单线程,只支持单核CPU,不能充分的利用多核CPU服务器。一旦这个进程崩掉,那么整个web服务就崩掉了。
不过以上缺点可以可以通过代码的健壮性来弥补。目前Node.js的网络服务器有以下几种支持多进程的方式:
#1 开启多个进程,每个进程绑定不同的端口,用反向代理服务器如 Nginx 做负载均衡,好处是我们可以借助强大的 Nginx 做一些过滤检查之类的操作,同时能够实现比较好的均衡策略,但坏处也是显而易见——我们引入了一个间接层。
#2 多进程绑定在同一个端口侦听。在Node.js中,提供了进程间发送“文件句柄” 的功能,这个功能实在是太有用了(貌似是yahoo 的工程师提交的一个patch) ,不明真相的群众可以看这里: Unix socket magic
#3 一个进程负责监听、接收连接,然后把接收到的连接平均发送到子进程中去处理。
在Node.js v0.5.10+ 中,内置了cluster 库,官方宣称直接支持多进程运行方式。Node.js 官方为了让API 接口傻瓜化,用了一些比较tricky的方法,代码也比较绕。这种多进程的方式,不可避免的要牵涉到进程通信、进程管理之类的东西。
此外,有两个Node.js的module:multi-node 和 cluster ,采用的策略和以上介绍的类似,但使用这些module往往有一些缺点:
#1 更新不及时
#2 复杂庞大,往往绑定了很多其他的功能,用户往往被绑架
#3 遇到问题难以解决
Node表现出众的典型示例包括:
1、RESTful API
提供RESTful API的Web服务接收几个参数,解析它们,组合一个响应,并返回一个响应(通常是较少的文本)给用户。这是适合Node的理想情况,因为您可以构建它来处理数万条连接。它仍然不需要大量逻辑;它本质上只是从某个数据库中查找一些值并将它们组成一个响应。由于响应是少量文本,入站请求也是少量的文本,因此流量不高,一台机器甚至也可以处理最繁忙的公司的API需求。
2、Twitter队列
想像一下像Twitter这样的公司,它必须接收tweets并将其写入数据库。实际上,每秒几乎有数千条tweet达到,数据库不可能及时处理高峰时段所需的写入数量。Node成为这个问题的解决方案的重要一环。如您所见,Node能处理数万条入站tweet。它能快速而又轻松地将它们写入一个内存排队机制(例如memcached),另一个单独进程可以从那里将它们写入数据库。Node在这里的角色是迅速收集tweet,并将这个信息传递给另一个负责写入的进程。想象一下另一种设计(常规PHP服务器会自己尝试处理对数据库本身的写入):每个tweet都会在写入数据库时导致一个短暂的延迟,因为数据库调用正在阻塞通道。由于数据库延迟,一台这样设计的机器每秒可能只能处理2000条入站tweet。每秒处理100万条tweet则需要500个服务器。相反,Node能处理每个连接而不会阻塞通道,从而能够捕获尽可能多的tweets。一个能处理50000条tweet的Node机器仅需20台服务器即可。
3、电子游戏统计数据
如果您在线玩过《使命召唤》这款游戏,当您查看游戏统计数据时,就会立即意识到一个问题:要生成那种级别的统计数据,必须跟踪海量信息。这样,如果有数百万玩家同时在线玩游戏,而且他们处于游戏中的不同位置,那么很快就会生成海量信息。Node是这种场景的一种很好的解决方案,因为它能采集游戏生成的数据,对数据进行最少的合并,然后对数据进行排队,以便将它们写入数据库。使用整个服务器来跟踪玩家在游戏中发射了多少子弹看起来很愚蠢,如果您使用Apache这样的服务器,可能会有一些有用的限制;但相反,如果您专门使用一个服务器来跟踪一个游戏的所有统计数据,就像使用运行Node的服务器所做的那样,那看起来似乎是一种明智之举。
总的来说,Node.js的应用场景
1) 适合
JSON APIs——构建一个Rest/JSON API服务,Node.js可以充分发挥其非阻塞IO模型以及JavaScript对JSON的功能支持(如JSON.stringfy函数)
单页面、多Ajax请求应用——如Gmail,前端有大量的异步请求,需要服务后端有极高的响应速度
基于Node.js开发Unix命令行工具——Node.js可以大量生产子进程,并以流的方式输出,这使得它非常适合做Unix命令行工具
流式数据——传统的Web应用,通常会将HTTP请求和响应看成是原子事件。而Node.js会充分利用流式数据这个特点,构建非常酷的应用。如实时文件上传系统transloadit
准实时应用系统——如聊天系统、微博系统,但Javascript是有垃圾回收机制的,这就意味着,系统的响应时间是不平滑的(GC垃圾回收会导致系统这一时刻停止工作)。如果想要构建硬实时应用系统,Erlang是个不错的选择
2) 不适合
CPU使用率较重、IO使用率较轻的应用——如视频编码、人工智能等,Node.js的优势无法发挥
简单Web应用——此类应用的特点是,流量低、物理架构简单,Node.js无法提供像Ruby的Rails或者Python的Django这样强大的框架
NoSQL + Node.js——如果仅仅是为了追求时髦,且自己对这两门技术还未深入理解的情况下,不要冒险将业务系统搭建在这两个漂亮的名词上,建议使用MySQL之类的传统数据库
如果系统可以匹配Node.js的适用场景,那么是时候采取具体的措施来说服老板了。
说服自己老板采用Node.js的方式
构建一个简单的原型——花一周时间构建系统某一部分的原型是非常值得的,同时也很容易和老板在某一点达成一致,等到系统真的在某一部分应用了Node.js,就是打开局面的时候
寻找开发者——首先JavaScript语言的普及度很高,一般公司都不乏Web前端工程师,而此类工程师的学习门槛也非常低。这就意味着Node.js很容易招人,或者公司就隐藏了一些高手
强大的社区支持——Node.js社区非常活跃,吸引很多优秀的工程师,这就意味着公司可以很容易从社区得到免费或者付费的支持
系统性能考虑——JavaScript引擎Google V8,加之原生异步IO模型,使得Node.js在性能的表现非常出色,处理数以千计的并发请求非常轻松
专业公司的支持——使用开源技术的最大问题是,原作者不承诺对其产品进行技术支持或者质量保证。现在Node.js已经得到Joyent公司的赞助,这就保证了未来Node.js的发展是可持续性的
前端深入node.js 3 模板引擎原理 事件 文件操作 可读流的实现原
模板引擎是基于new Function + with 实现的。
ejs使用
实现:
思路:借助fs的readFile先读取文件内容,然后使用正则表达式替换掉即可。
打印的结果是一样的。
复杂的情况呢?拼接字符串,拼成想要的代码
主要难点就是在字符串的拼接,第二部分,将全文分为三部分,然后拼接对应的,如
let str = ""
with(obj){
str+= `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
`
arr.forEach(item=>{
str+=`
<li>
${item}
</li>
`
})
str+=`
</body>
</html>`}
return str
登录后复制
这就是拼出来的字符串,然后再new Function,包裹一层函数,将with的obj传入,返回str。
大概长这样。
效果就是:
所以本质就是将获取到的内容,使用正则表达式匹配,拼接字符串成我们想要的内容,用with包裹,改变内部作用域,再通过new Function将str包装成一个函数,传入对应的值给obj。然后运行之后str就能正常通过作用域获取值赋值。
buffer
在服务端,需要一个东西来标识内存,但不能是字符串,因为字符串无法标识图片。node中使用buffer来标识内存的数据。他把内存转换成了16进制来显示(16进制比较短)buffer每个字节的取值范围就是0-0xff(十进制的255).
node中buffer可以和字符串任意的转换(可能出现乱码)
编码规范:ASCII ->GB8030/GBK ->unicode ->UTF8
Buffer代表的是内存,内存是一段固定空间,产生的内存是固定大小,不能随意增加。
扩容:需要动态创建一个新的内存,把内容迁移过去。
创建一个长度为5的buffer,有点像数组,但是数组可以扩展,而buffer不可以扩展。
还有一种声明buffer。
Buffer.form。
一般使用alloc来声明一个buffer,或者把字符串转换成Buffer使用。文件操作也是采用Buffer形式。
buffer使用
无论是二进制还是16进制,表现得东西是一样的。
base64编码:
base64可以放在任何路劲的链接里,可以减少请求次数。但是文件大小会变大。比如webpack中的asset/type,把一些小的文件转换成了Base64编码内嵌到了文件当中,虽然可以减少请求次数,但也增大了文件的大小。
base64的来源就是将每个字节都转化为小于64的值。没有加密功能,因为规则很简单。如
第一步:将buffer中每个位置的值转为二进制。如上。
一个字节有八位,八位的最大值是255,有可能超过64。而base64编码是要将每个字节转化为小于64的值。所以就取每个字节的6位。6位的最大值就是2*6 - 1 = 63。也就是:
第二步:将38的形式转成64的,保证每个字节小于64。将其转为十进制。
第三步,通过特定的编码规则转换即完成。
将我们获取到的十进制传入,因为每个字节都是小于64的,所以不超过。
完成。
buffer的常用方法
除了form和alloc还有
slice
// slice
const a = Buffer.from([1,2,3,4,5])
const d = a.slice(0,2)
d[1] = 4
console.log(d)
console.log(a)
登录后复制
与数组的用法相同,但是他并不是浅复制,而是直接关联在一起。改变d也会改变a。而数组的slice是浅复制。改变原始数据的值不会改变。
copy
将Buffer的数据拷贝到另一个数据上。
const a = Buffer.from([1, 2, 3, 4, 5])
const d = Buffer.alloc(5)
a.copy(d, 1, 2, 3)//四个参数,拷贝到哪里?从d的第一个开始拷贝 a的a[2]->a[3]
console.log(d)
登录后复制
copy四个参数,分别是拷贝的目标d。从d的第几个长度开始。拷贝a的第2到第3位。
所以应该是 <Buffer 00 03 00 00 00 >
concat
用于拼接buffer
Buffer.concat(arr, index)
第二个参数是拼接出来的Buffer的长度,如果大于原本的长度,用00填写。
Buffer.myConcat = function (
bufferList,
length = bufferList.reduce((a, b) =>a + b.length, 0)
) {
let bigBuffer = Buffer.alloc(length)
let offset = 0
bufferList.forEach((item) =>{
//使用copy每次拷贝一份然后offset向下走。
item.copy(bigBuffer, offset)
offset += item.length
})
return bigBuffer
}
登录后复制
借助copy,逐个拷贝一份即可。
文件操作
fs模块有两种基本api,同步,异步。
io操作,input,output,输入输出。
读取时默认是buffer类型。
写入的时候,默认会将内容以utf8格式写入,如果文件不存在则创建。
读取的data是Buffer类型,写入的是utf8格式。
这种读写适合小文件
读取文件某段内容的办法
fs.open用于打开一个文件。fs.read用来读取内容并且写入到buffer中。
fs.write用于将内容写入某个文件之中。如上,打开了b.txt,然后用fs.wirte。五个参数,分别是fd,buffer,从buffer的第0个位置,到buffer的第0个位置,从b.txt的第0位开始写,回调函数。
写入成功。
这种写法也不太美观,每次都需要fs.open然后fs.read或者fs.wirte,容易造成回调地狱。
流 Stream的出现
源码的实现步骤:
fs的CreateReadStrem是new了一个ReadStream,他是基于Stream的Readable类的,然后自己实现了_read方法。供Stream.prototype.read调用。
1 内部会先open文件,然后直接直接继续读取操作,默认是调用了pause暂停。
2 监听用户是否绑定了data事件,resume,是的话开始读取事件
3 调用fs.read 将数据读取出来。
4 调用this.push去emit data事件,然后判断是否可以读取更多再去读取。
第一种: fs.readFile(需要将文件读取到磁盘中,占用内容)=>fs.wirteFile
第二种: fs.open =>fs.read =>fs.write 回调地狱。
实现读取三个字节写入三个字节。采用fs.open fs.read fs.write的方法。
实现copy方法。
看实现:
首先创建一个三字节的Buffer。
然后使用fs.open打开要读取和要写入的文件。
因为我们是每三个每三个读取,所以需要采用递归方式,一直读取文件。
直到读取完毕,调用回调函数。fs.read和fs.write的参数都是类似的,即fd,buffer,buffer的start,buffer的end,读取文件/写入文件的start、回调函数(err,真正读取到的个数/真正写入的个数)
现在基本实现了读一部分,写一部分,但是读写的逻辑写在一起了,需要把他拆开。
流 Stream模块
可读流
不是一下子把文件都读取完毕,而是可以控制读取的个数和读取的速率。
流的概念跟fs没有关系,fs基于stream模块底层扩展了一个文件读写方法。
所以fs.open,fs.read等需要的参数,createReadStream也需要、
返回一个对象,获取需要监听data事件。
close事件在end事件之后触发。
由此可以看出:流的内部基于 fs.open fs.close fs.read fs.write以及事件机制。
暂停是不再触发data事件
rs.resume()是恢复。
实现readStream
从vscode调试源码得知
实现思路:
createReadStream内部new了一个ReadStream的实例,ReadStream是来自于Stream模块。
做一系列参数默认后, 调用this.open方法,这个方法会去调用fs.open去打开文件,打开之后触发事件,从回调的形式发布订阅模式,然后监听事件,当发现有用户注册了data事件之后,调用fs.read,j监听open事件,在open之后再去读取文件等等。
这样我们的读写逻辑就分离开了,从回调的形式变成了发布订阅模式,有利于解耦。
第一步:
第一步:参数初始化,并调用fs.open
open打开之后会触发open事件,注意,这里是异步的
第二步: 监听用户注册的data事件,当用户注册了data事件才去调用fs.read。调用this.read的时候open还没完成。
所以第一次read的时候需要判断,然后只监听一次open事件,重复打开read事件。
这个end和start配合,表示读取文件从哪到哪的位置,但是end是包后的,比如上面的end为4,实际上读取到的是5。
创建buffer存放读取的内容,再判断应该读取多少内容,以哪个小为准。
然后打开fs.read。将读取到的buffer发布出去。再次调用this.read去继续读取。
start=1,end=4,读取到2345的内容,正确。
不给end,每次3个3个的读取。
接着实现暂停,需要一个开关。
这样就基本完成了。
总结:
一开始实现的的copy方法,也是利用fs.open, fs.read, fs.write等,通过回调的形式完成的,这样虽能完成,但是内聚度较高,容易形成回调地狱。
而基于fs模块,和events模块,实现的可读流,可以有效的解耦刚才的代码,通过发布订阅模式,在一开始订阅事件,在每个时间点对应发布事件,然后代码执行,各司其职。
open和close是文件流独有的,
可读流具备:on (‘data’ | ‘end’ | ‘error’), resume, pause这些方法。
相关代码:
// copy
const fs = require("fs")
const path = require("path")
// let buf = Buffer.alloc(3)
// //open打开一个文件,第一个参数是路劲,第二个参数是打开文件用途,第三个是回调函数。
// fs.open(path.resolve(__dirname, "a.txt"), "r", function (err, fd) {
// // fd 是file descriptor文件描述
// console.log("fd", fd)
// //读取a.txt的内容,并且将内容写入buf中的第0个位置到3第三个位置,从a.txt的第六个位置开始
// fs.read(fd, buf, 0, 3, 3, function (err, data) {
// fs.open(path.resolve(__dirname, "./b.txt"), "w", function (err, fd2) {
// fs.write(fd2, buf, 0, 3, 0, function (err, data2) {
// console.log("buf", buf)
// console.log("data2", data2)
// })
// })
// })
// })
function copy(source, target, cb) {
const BUFFER_SIZE = 3
const buffer = Buffer.alloc(BUFFER_SIZE)
//每次读入文件的位置
let r_offset = 0
//每次写入新文件的位置
let w_offset = 0
//读取一部分数据,写入一部分数据
//第三个参数可以是权限 权限有三个组合 rwx 可读可写可执行 r的权限是4,w的权限是2,x的权限是1 421 = 777 可写可读可执行
// 0o666表示最大的权限,默认不用写。
//读取的文件必须要存在。写入文件不存在会创建,如果文件有内容会清空。
fs.open(source, "r", function (err, fd1) {
//打开写的文件
fs.open(target, "w", function (err, fd2) {
//每次读取三个写入三个,回调的方式实现的功能,需要用递归
// 同步代码则可以采用while循环
function next() {
fs.read(
fd1,
buffer,
0,
BUFFER_SIZE,
r_offset,
function (err, bytesRed) {
// bytesRed真正读取到的个数
if (err) {
cb("读取失败")
return
}
if (bytesRed) {
//将读取到的内容写入target目标
fs.write(
fd2,
buffer,
0,
bytesRed,
w_offset,
function (err, written) {
if (err) retrun
// written 真正写入的个数
r_offset += bytesRed//每次写入之后,下一次读取的内容就该往前进
w_offset += written
next()
}
)
} else {
//读取内容为空,结束
fs.close(fd1, () =>{})
fs.close(fd2, () =>{})
cb()
}
}
)
}
next()
})
})
}
copy("./a.txt", "b.txt", (err, data) =>{
console.log("copy success")
})
登录后复制
createStream的实现
const EventMitter = require("events")
const fs = require("fs")
class ReadStream extends EventMitter {
constructor(path, options = {}) {
super()
this.path = path
//操作
this.flags = options.flags || "r"
this.encoding = options.encoding || null
this.autoClose = options.autoClose || true
this.start = options.start || 0
this.end = options.end || Infinity//读取的个数,包后的,如果是1 ,就可能读取到0,1,2
this.highWaterMark = options.highWaterMark || 64 * 1024
this.emitClose = options.emitClose || false
this.offset = this.start// 每次读取文件的位移数
this.flowing = true//暂停继续开关
this.open()// 文件操作,注意这个方法是异步的。
// events可以监听newListener,可以获取注册的所有事件
this.on("newListener", function (type) {
if (type === "data") {
//用户订阅了data事件,才去读取。
this.read()//这时候文件还没open,fd为undefined。
}
})
}
pause() {
//暂停
this.flowing = false
}
resume() {
//继续
if (!this.flowing) {
this.flowing = true
this.read()
}
}
destory(err) {
if (err) {
this.emit("error", err)
}
if (this.autoClose) {
fs.close(this.fd, () =>{
this.emit("close")
})
}
}
read() {
//希望在open之后打开
if (typeof this.fd !== "number") {
this.once("open", (fd) =>{
//之前实现的copy这段逻辑是写在了fs.open里面,换成发布订阅模式之后就可以分离出来。
this.read()//第二次read的时候,fd有值了
})
} else {
//判断每次读取多少。因为this.end是包后的,比如start = 0, end = 1, 那么读取的就是 0 , 1, 2所以需要+1
const howMutchToRead = Math.min(
this.end - this.offset + 1,
this.highWaterMark
)
const buffer = Buffer.alloc(howMutchToRead)
fs.read(
this.fd,
buffer,
0,
howMutchToRead,
this.offset,
(err, byteRead) =>{
if (err) {
this.destory(err)
} else {
if (byteRead) {
//读取到文件,发布data事件,发送真正读取到的内容
this.offset += byteRead
this.emit("data", buffer.slice(0, byteRead))
this.flowing &&this.read()
} else {
this.emit("end")
if (this.autoClose) {
this.destory()
}
}
}
}
)
}
}
open() {
fs.open(this.path, this.flags, (err, fd) =>{
if (err) {
//报错:
this.destory(err)
}
//从回调的形式变成了发布订阅模式
this.fd = fd
this.emit("open", fd)
})
}
}
const rs = new ReadStream("./a.txt", {
flags: "r", //创建可读流。
encoding: null, //默认Buffer
autoClose: true, //自动关闭,相当于读取完毕调用fs.close
emitClose: true, //触发close事件
start: 0, //从文件哪里开始读取
highWaterMark: 3, //每次读取的数据个数,默认是64*1025字节。
//end: 4, // 比如这个就会读取 1到5的内容
})
rs.on("open", () =>{
console.log("文件打开了")
})
// rs.on("data", (data) =>{
// console.log("监听Data事件", data)
// })
// //底层还是 fs.open fs.read fs.close
// const rs = fs.createReadStream("./a.txt", {
// flags: "r", //创建可读流。
// encoding: null, //默认Buffer
// autoClose: true, //自动关闭,相当于读取完毕调用fs.close
// emitClose: true, //触发close事件
// start: 0, //从文件哪里开始读取
// highWaterMark: 3, //每次读取的数据个数,默认是64*1025字节。
// })//返回一个对象
//没监听前,是非流动模式,监听后,是流动模式。
//监听data事件,并且不停触发
rs.on("data", function (chunk) {
console.log(chunk)
//暂停
rs.pause()
})
rs.on("end", function () {
console.log("读取完毕")
})
rs.on("close", function () {
console.log("文件关闭")
})
setInterval(() =>{
console.log("一秒后")
rs.resume()
}, 1000)
2000或2001年以前在美国,是Sun的HotSpot JDK的主力开发之一回到丹麦,2002年创立OOVM。因为他对Smalltalk的热爱,OOVM被写成一个以Smalltalk为开发语言的迷你虚拟机,主打各种资源有限嵌入式设备,代码调试、热部署、热替换都很方便。没记错的话当时宣传是最低128K RAM就可以运行。
OOVM在2004年被当时J2ME VM的主要开发商Esmertec收购,后来改名为OSVM,以突出其能够在bare metal上运行的特性。
Lars Bak的家是个丹麦的农场,有时候高层开会就跑去他家里开...
2006年因为经济不景气,同时OSVM始终没能在市场上取得大的进展(一方面原因是Smalltalk还是太小众了),Esmertec关掉了位于丹麦的OSVM分支。
Lars Bak随后加入Google。这里Wikipedia上的记载有误,他是在06年而不是04年加入Google从事V8开发的。