javascript chapter 16 Node

16.1 Node Programming 基础

16.1.1 Console输出
服务器端的console和客户端有所不同。
console.error()会向stderr流写。通过重定位流可以实现向文件内写日志。

16.1.2 命令行参数和环境变量
Node程序可以从字符串数组process.argv中读命令行参数。数组的第一个元素是Node执行的路径。其他剩余参数使用空格分开,当你调用Node时传递。
例:avgv.js:console.log(process.argv);
输出:
$ node --trace-uncaught arggv.js --arg1 --arg2 filename
[
'/usr/local/bin/node',
'/private/tmp/argv.js',
'--arg1',
'--arg2',
'filename'
]
process.argv第一个元素和第二个元素是Node运行的完整文件路径,即使你没有输入他们。
那些为了Node运行自用的参数不会出现在process.argv中。比如此例的--trace-uncaught 命令行参数。其他出现在Javascript文件名后的参数都会出现在process.argv中。

Node程序也可以获得Unix-style环境变量。Node通过process.env对象实现。这个对象的属性名是环境变量名,属性值是环境变量值。
例:
$ node -p -e 'process.env'
{
SHELL: '/bin/bash',
USER: 'david',
PATH: '/user/local/bin:/usr/bin:/bin:/usr/sbin:/sbin',
PWD:'/tmp',
LANG: 'en_US'.UTF-8',
HOME: '/Users/david',
}

16.1.3 程序生命循环
node命令行期待一个可运行的Javascript文件。这个文件会包含其他模块,模块都是JavaScript代码,也许也定义了他们自己的类或函数。最基本的,Node执行代码是自上而下的,一些Node程序当他们执行完最后一句代码时退出。但通常,一个Node程序会持续运行,在最初的文件被执行后持续很久的时间。Node程序通常是异步的,基于回调函数和事件处理器。Node程序不会退出,直到最初文件运行完毕,直到所有的回调函数被调用,没有其他更多待解决事件。基于Node的服务器程序监听到来的网络链接,理论上永久运行,因为它一直等待新的事件。

一个程序可以强制退出,通过调用process.exit()。用户可以结束Node程序,通过在程序运行的终端窗口使用ctrl+c。程序可以无视ctrl+c,通过注册信号处理器函数,process.on("SIGINT",()=>{})

如果你程序代码抛出异常并没有catch语句捕获它,程序会打印栈并且退出。因为Node异步的特性,异常出现在回调函数或事件处理器中必须当地处理或者根本没有处理。这意味着处理在异步部分的异常非常的困难。如果你不希望这些异常导致你的程序完全崩溃,注册一个全局处理器函数用它替代崩溃。

process.setUncaughtExceptionCaptureCallback(e=>{
  console.error("Uncaught exception:",e);
});

相似的情形出现在你的程序创建了一个Promise,它被拒绝且没有.catch()去处理它。对于Node 13,这不会导致致命错误从而让你的程序退出。但它会打印一系列错误消息到控制台。对于后来的Node,不处理Promise拒绝会被认为是致命错误。如果你不希望未处理的拒绝打印错误消息或结束你的程序,注册一个全局处理函数

process.on("unhandledRejection",(reason,promise)=>{
});

16.1.4 Node模块
Node 模块系统使用require()函数导入值到模块,用exports对象或module.exports属性导出值到模块。这是最近出的Node程序模式,在10.2已做过说明。

Node 13添加了对标准ES6模块的支持,也有对基于需求模块的支持。这两种模块系统不是完全兼容的,所以有时候处理起来很琐碎。Node需要知道,在它加载一个模块前,这一模块是否使用require()或module.exports,亦或是用import export。当Node加载Javscript代码文件时,如果是通常JS模块(CommonJS module),它会自动的定义require()函数,伴随import export声明。它会禁用import export关键字。另一方面,当Node加载文件,使用ES6模块,它必须启用import export声明,并且禁用require module exports。

对于没有显示拥有后缀.mjs .cjs的文件。Node在同一个目录中查询名为package.json的文件,或是包含的文件目录。当最近的package.json被找到,Node检查最上层的JSON对象的type属性。如果type属性是"module",Node会将它作为ES6 模块文件加载。如果属性为"commonjs",Node会将它作为CommonJS module加载。注意你运行Node程序,不需要有一个package.json文件。如果这样的文件,或文件不含type属性。Node默认情况下会使用commonJS模块。当你想使用ES6模块且不想让他们有.mjs扩展名时,package.json才是必须的。

因为有大量代码是使用CommonJS模块格式书写的,Node允许ES6模块加载CommonJS模块使用import关键字。反过来不行,CommonJS模块不可以使用require加载ES6模块。

16.1.5 Node包管理器
当你安装Node时,你也会获得一个程序名为npm。这是Node的包管理程序,它可以帮你下载和管理库,这些库是你程序依赖的。npm保持追踪这些依赖,通过一个名为package.json的文件,它在你的项目的根路径中。被npm创建的package.json,当你需要使用ES6模块时,需要添加"type":"module"

16.2Node默认状态下是异步的
Javascript是通用编程语言,所以编写CPU密集性程序是完全可能的,密集程序可能是复杂统计分析,或乘以巨大的矩阵。但Node是设计和优化为I/O密集程序工作的,比如网络服务器。并且特别的,Node设计为可以轻易实现高并发服务器,可以同时处理很多的请求。

和许多编程语言不同,Node并不是使用线程实现并发的。多线程程序是很难运行正确的,排错也非常困难。同样,线程是相对的重度抽象的,当你想要写一个可以承担数以百计的并发线程的服务器时。使用税百计的线程需要大量的内存,所以对于网络使用,Node采用了单线程Javascript程序模型,这使得网络服务器创建变得简单而不是晦涩神秘。

Node可以实现高度的并发的同时,维持单线程程序模型通过使它的API异步并默认不阻塞。Node对于不阻塞非常的严格,也许令你感到吃惊。你也许期待函数从网络中读,写是异步的。但是Node走的更远,定义了不阻塞异步函数从本地文件系统中读写文件。这样,当你想到:Node API定义在环绕硬件驱动的标准,需要毫秒级的阻塞进行查找,程序等待碟片绕回在文件操作开始之前。对于现代的数据中心,本地文件系统也许实际上穿越了网络,网络延迟在驱动延迟之上。但即使异步读取文件对于你来说是正常的,Node做的更远,默认的初始网络连接或查询文件修改时间的方法,也是不阻塞的。

一些Node API中的函数时同步的但不阻塞的,他们运行完成并返回不需要阻塞。但是这些有趣函数的大部分会执行一些输入输出操作。这些是异步函数,他们可以避免最微小的阻塞。Node创建于Javascript拥有Promise类之前,所以异步Node API是基于回调的。概括来说,你传递给异步Node函数的最后一个参数是回调函数。Node使用错误第一回调,一般情况下使用两个参数调用。第一个参数是错误第一回调函数,通常在没有错误发生时为Null,第二个参数是数据或回应,它由你调用的原始异步函数产生。把错误参数放在第一位的原因是,你不会忽略它,你应该始终检查这个参数是否为空。如果它是一个错误对象,或是整数错误代码或错误消息字符串,那就说明有的地方出错了。这种情况下,第二个参数也许为null。

Node先于标准的promise,但由于它公平的对待error-first回调函数,创建基于Promise的变量,改变量基于回调函数API,使用util.promisify()包装器。这里我重写了readConfigFile()函数来返回Promise

const util=require("util");
const fs=require("fs");
const pfs={
  readFile:util.promisfify(fs.readFile)
};

function readConfigFile(path){
    return pfs.readFile(path,"utf-8").then(text=>{
        return JSON.parse(text);
  });

我们可以简化基于Promise的函数,使用async await

async funciton readConfigFile(path){
  let text=await pfs.readFile(path,"utf-8");
  return JSON.parse(text);
}

util.promisify()包装器可以产生基于Promise的很多Node函数。在Node 10以后,fs.promises对象有很多预先定义的Promise-based函数,它们为文件系统工作。我们会在此章稍后讨论。但注意我们可以使用fs.promises.readFile()替代pfs.readFile()。

我们之前说了Node程序模型是默认异步的。但是为了编程的便利,Node定义了很多它的函数有阻塞,同步变量。特别是文件系统模块。这些函数通常以Sync结尾。

当服务器第一次启动,读取配置文件时,他们不处理网络请求,并且很少可能是并发的。在这种情况下,没有必要回避阻塞,我们可以安全的使用阻塞函数,类似于fs.readFileSync()。我们可以放弃async await,书写一个完全纯净的同步版本的readConfigFile()函数。我们不在调用回调函数或返回Promise,这个函数简单的返回转换的JSON值或者抛出异常。

const fs=require("fs");
function readConfigFileSync(path){
let text=fs.readFileSync(path,"utf-8");
return JSON.parse(text);
}

在错误第一两个参数的回调函数之外,Node也拥有很多API,他们使用基于事件的异步处理,典型情况是处理流数据。我们稍后讨论。

现在我们讨论过了Node的不阻塞API。让我们返回并发的情况。Node内在的非阻塞函数使用操作系统版本的回调和事件处理器。当你调用了其中一个函数,Node开始了操作,然后向操作系统注册一些事件处理器,这样当操作完成时它会被提醒。你传递给Node函数的回调函数会被存储在内部,这样Node可以调用你的回调函数当操作系统发送适当的事件给Node时。

这种并发被称为基于事件的并发。在它的核心,Node拥有一个单线程运行事件循环。当一个Node程序开始时,它运行任何你告诉它的代码。这个代码假设调用至少一个非阻塞函数,引起回调函数或操作系统的事件处理器注册。(如果不是这种情况,你书写的是同步Node程序,Node会在执行完毕时退出) 当Node运行到程序末尾时,它会阻塞直到事件发生。在此时,操作系统又开始运行了。Node映射OS事件到你注册的Javascript回调函数,并且调用这些函数。你的回调函数或许会调用更多的非阻塞Node函数,引起更多的操作系统事件器注册。一旦你的回调函数运行完毕,Node会回退到睡眠状态,开始重复循环。

对于网络服务器或者其他的I/O密集型应用程序,它们会花费大部分时间在等待输入输出上。这种基于事件的并发是有效的且高效的。一个网络服务器可以同时并发处理50个不同客户端的请求,而不需要50个不同的线程,因为他们使用非阻塞API,并且其中有一些内部映射,从网络套接字到Javascript函数的调用。当活动出现在套接字上时。

16.3 缓冲Buffer
Buffer类是一种Node中常用的数据类型,特别是从文件或网络中读数据时。Buffer非常像字符串,除了它是字节的序列而不是字符的序列。Node出现在核心Javascript支持typed array之前,并且当时没有Uint8Array代表无符号字节数组。Node定义了Buffer类来针对这种需求。
现在Uint8Array是Javascript的一部分,Node的Buffer class是Uint8Array的子类。

Buffer和它的超类Uint8Array的区别是,它是设计用于和Javascript字符串相互操作的。buffer中的字节可以被字符串初始化,或转换为字符串。字符编码映射每个字符(他属于某个字符集)到一个整数。当提供一个文本字符串和编码方式时,我们将字符串的字符编码为一系列的字节。当提供一个编码好的字符串和字符串编码方式时,我们可以解码这些字节到字符串。Node Buffer class拥有执行编码或解码的方法,你可以识别这些方法因为他们要求encoding参数,用以说明编码类型。

Node中的编码使用字符串命名
"utf8"默认编码方式
"utf16le" 两字节的Unicode字符,小端排序。
"latin1": ISO-8859-1
"ascii" ASCII码
"hex" 16进制
"base64" 转换3个字节到4个ascii码

16.4 事件和事件触发器
所有的Node API默认情况下是异步的,他们中的很多都是两个参数,第一个参数是错误回调函数,第二个参数当响应操作完成时调用。但是其他函数有一些是基于事件的,这些API是围绕对象而不是函数的。或者是一个回调函数需要被调用很多次,或者是需要多种类型的回调函数。拿net.Server类举例,它是一种服务器套接字类型,用于接收从客户端发起的连接。它派发listening事件当它首次开始监听连接时。当客户端链接时会有connection事件,当链接关闭时有close事件。

在Node中,对象派发的事件是EventEmitter的实例或是它的子类。

const EventEmitter=require("events");
const net=require("net");
let server=new net.Server();
server instanceof EventEmitter//true
EventEmitter的主要特征是他们允许你注册事件处理器函数通过使用on()方法。EventEmitters可以派发多种类型的事件,事件类型取决于名称。为了注册一个事件处理器,调用on()方法,传递事件类型名,和当该类型事件发生时的处理函数。EventMitter可以调用拥有任意参数的处理器函数,你需要阅读文档,了解特定的EventMitter对于特定的事件要传递什么参数。

const net=require("net");
let server=new net.server();
server.on("connection",socket=>{
  //Server Connection events are passed a socket object for the client that just connected,Here we sent some data to the client and   //disconnect
});

如果你倾向于选择明确的方法名去注册事件监听器,你可以使用addListener(),你可以使用off() removeListener()来移除以前注册的事件监听器。他们按顺序调用,从第一个注册的到最后一个注册的。如果有超过一个的处理器函数,他们按序在单线程中调用。Node并不是并行的,记住这点。重要的是,事件处理函数时同步调用的而不是异步调用的。这意味着emit()方法并不将稍后要用的处理器函数排在队列中。emit()方法会一个接一个的调用注册过的处理器,当最后一个处理器返回后才返回。

这意味着,实际上,当一个内置的Node API派发事件时,这个APi会被你的事件处理器阻塞。如果你写了一个事件处理器调用阻塞函数如fs.readFileSync(),在你的同步文件读取完成前不会发生更多的事件处理。如果你的程序是一个网络服务器,需要相应,保持你的事件处理器函数非阻塞和快速是非常重要的。当一个事件发生时你需要作很多计算工作,使用处理器异步调度计算,通过使用setTimeout()通常是最佳选择。Node也定义了setImmediate(),当所有悬挂的回调函数和事件处理后会立即执行函数。

EventEmitter类也定义了emit()方法,会导致注册的事件处理器函数被调用。如果你定义了自己的基于事件的API时,它非常有用。但当你只是用已有的API编程时不会广泛使用它。emit()必须使用事件类型名作为第一个参数去调用。任何传递给emit()的附加的参数会成为注册的处理器函数的参数。处理器函数通常会被调用,其中的this值指向EventEmitter对象自身,这很多时候是很便利的。

任何被事件处理器函数返回的值都会被忽略,如果事件处理器函数抛出异常,它会从emit()调用开始传播,并阻止后面的处理器函数的执行。

回想Node 基于回调函数的API,使用错误第一的回调函数。你要经常检测回调函数的第一个参数,看看是否发生了错误。对于基于事件的API,对应的是error事件,由于基于事件的API经常被用于网络或者其他形式的流I/O,他们对于不可预测的异步错误是脆弱的,大部分EventEmitters定义了error事件,当他们发送时遇到错误时。无论你何时用基于事件的API,你都应该养成注册error事件的处理器的习惯。EventEmitter类的Error事件会被特殊对待。当emit()调用后发送了一个error事件,或没有对应于某事件的处理器被注册,会抛出异常。由于这通常是异步发生的,你无法使用catch()语句处理此异常,这种类型的错误通常会引起你的程序退出。

16.5 流
当实现算法处理数据时,通常会把所有的数据读入内存,处理,再写出数据。比如,你想要写一个拷贝函数使用Node。

const fs=require("fs");

function copyFile(sourceFilename,destnationFilename,callback){
  fs.readFile(sourceFilename,(err,buffer)=>{
      if(err){
          callback(err);
    }else{
      fs.writeFile(destinationFilename,buffer,callback);
  }
  });
}

这个copyFile()使用了异步的函数和回调函数。所以它不会阻塞,且适合用于并发程序,比如说服务器端。但是需要注意必须有足够的内存来保存所有的内容。也许啊在一些使用场景可以工作,但当文件非常大时会失败。如果你的程序是高度并发的,有很多的文件在同一时刻被复制,另一个copyFile()函数的缺点是,在旧文件没有被读完时新文件无法被写入。

这些问题的解决方式是使用流算法,数据是流入你的程序的。处理它,并让它流出你的程序。这个主意是你的算法处理数据在非常小的块内,所有的数据并不都在内存中。当流处理方案是可能时,它们的空间效率很高并且也非常快速。Node网络API是基于流的,Node文件系统模块也定义了流API来读和写文件。所以你可以使用流API在你写的很多Node程序中。

Node支持的基本流类型
Readable
可读流是数据源,这个流是fs.createReadStream()的返回值,举个例子,它是一个内容可被读取的流,process.stdin是另一个可读流,它是从标准输入中返回的值。

Writable
可写流是数据的目的地。fs.createWriteStream()的返回值就是一个可写流。它允许数据被以块写入,输出到特定文件中。

Duplex
即可读又可写流,被net.connect()或其他Node网络API返回的Socket对象就是一个可读可写流。如果你向socket内写,你写的数据就会通过网络连接写到网络连接的另一端,如果你从socket中读,就会读到另一端的计算机向你发送的数据。

Transform
Transform流也是可读可写的流,但他们和Duplex流有很大的不同。向Transform流中写入的数据会是只读的,通常用于一些变换形式。zlib.creatGZip()函数,返回一个Transform流,将写入它的数据压缩。相似的,crypto.createCipheriv()函数返回一个Transform流将写入的数据加密解密。

默认情况下,流会读写缓存,如果你调用可读流的setEncoding()方法,它会返回一个解码字符串而不是Buffer对象。如果你写入字符串到可写Buffer,它会自动的按buffer默认的编码模式或你指定的编码模式进行编码。Node流API也支持对象模式,流读和写对象比buffer,strings更加复杂。Node核心API都不使用对象模式,但你在其他库中也许可以看到它。

可读流必须从某地读入数据,可写流必须向某地写入数据。所以所有流都有两端,输入和输出,或源和目的地。一件棘手的事是基于流的API,它的两端速度是不一致的。也许,代码希望读和处理数据的速度比写入流的速度要快,或者相反。数据写的速度比读和处理的速度要快。流的实现通常包含一个内部的缓冲区来存储数据,它是写入的数据,但还未被读。缓冲有助于确保数据需要读时是可用的,同时保证有空间允许数据写入。但这些并不是百分百可靠的。所以,等待数据写入或者等待数据读出是很正常的现象。

在程序环境使用基于线程的并发时,流API通常有阻塞调用。当数据到达时才调用读,当有足够的空间存储数据时才调用写。伴随使用基于事件的并发模型。但阻塞调用是没有道理的,Node流API是基于事件的和基于回调函数的。不像其他API,它没有同步版本。

16.5.1 Pipes
有时,你需要从流中读取数据,然后简单的向另一个流中写数据。比如,你要写一个http服务器,提供静态文件目录服务。在这种情况下,你需要从文件输入流中读取数据,向网络socket中写入数据。但是,取代你自己写代码处理读入和写的是,你可以链接socket,让他们形成Pipe,然后让Node处理复杂的细节。简单的传递可写流到可读流的pipe()方法。

const fs=require("fs");
function pipeFileToSocket(filename,socket){
  fs.createReadStream(filename).pipe(socket);
}

下述代码使用Pipe连接两个流,当完成或者有错误时调用callback

function pipe(readable,writable,callback){
  function handleError(err){
      readable.close();
      wriable.close();
      callback(err);
  }

  reable.on("error",handleError)
        .pipe(writable)
        .on("error",handleError)
        .on("finish",callback);
}

转换流使用pipe非常有用,创建管道涉及两个流。下面实现压缩文件:

const fs=require("fs");
const zlib=require("zlib");

function gzip(filename,callback){
    let source=fs.createReadStream(filename);
    let destination=fs.createWriteSteram(filename+".gz");
    let gzipper=zlib.crateGZip();


  source
    .on("error",callback)
     .pipe(gzipper)
      .pipe(destination)
      .on("error",callback)
      .on("finish",callback);
}

使用pipe()方法从可读流中复制数据到可写流非常简单。但实际上,你经常需要处理从你的程序中流过的数据。一种方法是实现自己的转换流,这样允许你不需要手工处理读,写。下面的函数实现了从流中读取数据,只有当数据匹配正则表达式时才输出。

const stream=require("stream");

class GrepStream extends stream.Transform{
    constructor(pattern){
        super({decodeStrings:false});
        this.pattern=pattern;
        this.incompleteLine="";
  }
  
  _transform(chunk ,encoding,callback){
            if(typeof chunk!="String"){
                callback(new Error("Expected a string but got a buffer"));
                return;
          }

          let lines=(this.incompleteLine+chunck).split("\n");
          
          this.incompleteLine=lines.pop();
          
          let output =lines
                      .filter(l=>this.pattern.test(l))
                      .join("\n");


        if(output){
            output+="\n";
        }

        callback(null,output);
  }

    //This is called right before the stream is closed
    //It is our chance to write out any last data

    _flush(callback){
      //If we still have an incomplete line ,and it matches ass it to the callback
        if(this.pattern.test(this.incompleteLine)){
          callback(null,this.incompleteLine+"\n");
      }
    }
}

let pattern=new RegExp(process.argv[2]);
process.stdin
      .setEncoding("utf8")
      .pipe(new GrepStream(patern))
      .pipe(process.stdout)
      .on("error",()=>process.exit());

16.5.2 异步迭代
Node 12之后,可读流是异步迭代器的,意味着用async函数,你可以使用for/await循环从流中读取字符串和缓冲块。

使用异步迭代器和使用pipe()方法一样简单,当你需要处理读取的块时更为简单。这里我们重写了grep程序·1,使用异步函数和for/await循环

async function grep(source,destination,pattern,encoding="utf8"){
    source.setEncoding(encoding);
    
  destination.on("error",err=>process.exit());

  let incompleteLine="";
    
  for await(let chunck of source){
     let lines=(incompleteLine+chunk).split("\n");
    
    incompleteLine=lines.pop();

    for(let line of lines){
        if(pattern.test(line)){
          destination.write(line +"\n",encoding);
        }
    }
  }

let pattern=new RegExp(process.argv[2]):
     grep(process.stdin,process.stdout,patter)
      .catch(err=>{
        console.error(err);
        process.exit();  
    });
}

16.5.3 写流和处理反压力

异步的grep()函数作为例子,演示了如何使用可读流作为异步的迭代器,且也演示了你可以向可写流中写入数据简单的传递数据到write()方法。write()方法使用buffer或者字符串作为第一个参数,对象流期待其他种类的对象,但是超过了本章的讨论范围。如果你传递了一个buffer,buffer中的字节会被直接写入。如果你传递了字符串,它会在写入前被编码成buffer中的字节。可写流当你只传递一个参数时,有默认的编码方式。默认的编码是"utf8"。但你可以明确设置它,通过setDefaultEncoding()到可写流。可以选择的是,你传递字符串作为write()的第一参数时,可以传递编码名作为第二参数。

write()使用callback函数作为它的第三个参数。当数据已被写入,在写入流内部没有数据时会被调用。这个回调函数也有可能在错误出现时被调用。但不保证被调用。你需要注册"error"事件处理器在可写上用来监测错误。

write()方法有一个非常重要的返回值。当你在流上调用write()时,它会接收并缓冲你传递的数据块。如果内部缓冲不满,它会返回true,如果内部缓存满,会返回false。这个返回值只是建议,你可以无视它。可写流会按需充它们的内部空间,如果你持续调用write()函数。但是你要记得,使用流API的原因是它避免了一次性的消耗大量内存。

write()方法的返回值为false意味着一种形式的反压力。你向流中写入数据的速度大于处理的速度。合适的处理反压力的方式是停止调用write()直到流出现"drain"事件。标志着又有可用的缓冲。下例是当可写时写更多数据的例子。

function write(stream,chunk,callback){
      let hasMoreRoom=stream.write(chunk);
      
  if(hasMoreRoom){
      setImmediate(callback);
  }else{
      stream.once("drain",callback);
  }
}

实际上,有时可以一次调用write()很多次,有时你必须等待事件发生。这是使用pipe()方法的一个原因,你使用pipe(),Node会自动处理反压力的情况。

如果你使用await async在你的程序中,并且对待可读流是异步迭代的。你可以直接实现基于Promise版本的write()功能函数,可以适当的处理反压力。在我们看过的async版本的grep()函数中,我们并没有处理反压力。下面的异步的copy()函数表示了他们怎样正确实现的。注意这个函数简单的从源到目的拷贝数据,调用copy函数和调用source.pipe()非常相似。

function write(stream,chunk){
    let hasMoreRoom=stream.write(chunk);

    if(hasMoreRoom){                      //If buffer is not full,return an already resolved Promise object.
        return Promise.resolve(null);
  }else{
        return new Promise(resolve=>{      //Otherwise return a Promise taht resolves on the drain event
            stream.once("drain",resolve);
        }
    }
}



//Copy data from the source stream to the destination stream ,respecting backpressure from the destination stream.
//This is much like calling source.pipe(destination);

async function copy(source,destination){
          //set an error handler on the destination stream in case standard output closed unexpectely
          destination.on("error",err=>process.exit());

        for await(let chunk of source){
              await write(destination,chunk);
        }
 }

copy(process.stdin,process.stdout);

在我们总结写流的讨论之前,再次提醒失败的相应反压力会导致你的程序比正常情况下耗费更多的内存。当可写流的内部缓冲区溢出或者变得更大时。如果你想写一个网络服务器,这会导致隐藏的安全问题。假设你写了一个HTTP服务器通过网络发送文件,你没有使用pipe()且你没有处理write()方法的反压力。攻击者可以写一个HTTP客户端,请求许多大文件,比如图像。但却从不从相应体内读数据。因为客户端没有通过网络读数据,服务器也没有相应反压力。服务器的缓冲区会溢出,。当有足够的并发连接从攻击者那建立,会导致延迟响应或者服务器崩溃。

16.5.4 读流和事件

Node的可读流有两种模式,每种模式有自己的读API。如果你不能使用pipe或者异步迭代器。你就需要使用两种基于事件的API去处理流。重要的是你必须使用一种,而不是混用两种。

Flowing模式
在flowing模式中,当可读数据到达时,它会立即发送data事件。为了在这种模式下从流中读取数据,简单的注册一个data事件的处理器。然后流就会推送给你数据块一旦他们可用。注意在flowing模式下不用调用read()方法。你只需要处理data事件。注意,新创建的流并不是以flowing模式开始的,注册data事件处理器切换流到flowing模式。流不会发送data事件直到你注册data事件处理器。

如果你使用flowing模式从可读流中读取数据,处理它然后写入可写流。那么你就需要处理可写流的反压力。如果write()方法返回false,意味着写缓存已满,你可以在读取流上调用pause(),临时阻止data事件。然后,如果你获得一个写入流的drain事件,你可以调用可读流的resume()来重新开启data事件的流入。

当流到达结尾时,flowing模式的流会发送end事件。这个事件标志着没有更多的data事件被发送。当错误发生时,会有error事件产生。

在流的本章开始时,我们展示了无流的copyFile()函数,并承诺稍后会给一个更好的版本。之后的代码展示了如何实现基于流的copyFile()函数,使用flowing模式的API,并处理反压力。如果使用Pipe来实现此功能更为简单,但我们想要用多个事件处理器来协调数据的流动。

const fs=require("fs");

function copyFile(sourceFilename,destinatinFilename,callback){
    let input=fs.createReadStream(sourceFilename);
    let output=fs.createWriteStream(destinationFilename);

  input.on("data",(chunk)=>
    let hasRoom=output.write(chunk);
    if(!hasRoom){
        input.parse();
    });

    input.on("end",()=>
      output.end();
    );

    input.on("error",err=>
        callback(err);
        process.exit();
    );

    output.on("drain",()=>
        input.resume();
    );
    out.put.on("error",err=>{
      callback(err);
       process.exit();
    }

  output.on("finish",()=>{
    callback(null);
  }
  );

  let from=process.argv[2],to=process.argv[3];
  console.log(`Copying file ${from} to ${to}...`);
    copyFile(from,to,err=>{
        if(err){
          console.error(err);
        }else{
            console.log("done");
        }
      });
}

Paused Mode
另一种可读流的模式是paused mode,这是流默认开始的方式。如果你从没有注册data事件处理器,或从没有调用pipe()方法。那么可读流就保持paused mode。在paused mode中,流不会在data事件中推送数据给你。取而代之的是,你明确使用read()方法从流中获取数据。这不是一个阻塞调用,如果流中无可读数据,它会返回Null.由于没有同步API等待数据,paused mode 的api也是基于事件的。可读流在paused mode会发送readable事件,当数据在流中可读时。作为回应,你的代码应该调用read()方法去读数据。你必须使用循环来读,直到read()方法返回null为止。彻底读空流的缓冲区是必须的,这样未来才会触发新的readable事件。如果你在流中有数据时停止读取,你就不会获得readable事件,那么你的程序会挂起。

流在paused mode中也会发送end ,error事件,和flowing mode类似。如果你想写一个程序,从可读流中读取数据,在把它写入可写流,paused mode不是一个好选择。为了能够适当的处理反压力,你只想在输入流可读且输出流没有堵塞时读。在paused mode中,这意味读和写直到read()返回null或write()返回false。然后开始读或写,在readable,drain事件发生时。这样实现不优雅,你会发现flowing mode或pipe更容易实现这个功能。

下面的代码展示了如何计算给丁文件内容的SHA256哈希值。它使用了paused mode的可读流读取文件的内容。然后计算hash

const fs=require("fs");
const crypto=require("crypto");

function sha256(filename,callback){
  let input = fs.createReadStream("filename");
  let hasher=crypto.createHash("sha256");

  input.on("readable",()=>{
        let chunk;
        while(chunk=input.read()){
            hasher.update(chunk);
      }
  });

  input.on("end",()=>{
      let hash=hasher.digit("hex");
      callback(null,hash);
  });

    input.on("error",callback);
}


sha256(process.argv[2],(err,hash)=>{
    if(err){
      console.error(err.toString());
    }else{
      console.log(hash);
  }
  });

16.6 进程,CPU,操作系统细节。

全局Process对象有一系列有用的属性和函数和当前Node进程的状态有关。查询Node文档获取细节。这里有一些属性和函数你需要了解。
process.argv//命令行数组
process.arch//CPU架构 比如x64
process.cwd()//当前工作目录
process.chdir()//设置当前工作目录
process.cpuUsage()//cpu使用率
process.env//环境变量对象
process.execPath//node处理的绝对文件路径
process.exit()//结束程序
process.exitCode//退出程序时的整数代码值
process.getuid()//返回当前用户的Unix用户id
process.hrtime.bigint()//高解析度毫秒时间戳
process.kill()//发送信号给另一个进程
process.memoryUsage()//返回内存使用细节对象。
process.nextTick()//和setImmediate()类似,很快的调用一个函数
process.pid//当前进程的进程id
process.ppid//父进程id
process.platform//操作系统平台"linux" "darwin" "win32"等
process.resourceUseage()//返回资源使用细节对象
process.setuid()//设置当前用户id或名
process.title//ps列表中的进程名
process.umask()//设置或返回对新文件的默认许可
process.uptime()//返回进程的Uptime
process.version//Node的版本字符串
process.versions//Node依赖的库的版本字符串

os模块,需要显示用require包含。提供相似的低等级细节,关于Node运行的电脑和操作系统。
const os=require("os");
os.arch()//CPU架构 x64 或arm
os.constants//有用的常数,比如os.constants.signals.SIGINT
os.cpus()//关于CPU的核心数据,包含使用时间
os.endianness()//CPU本地的字节顺序,BE或LE
os.EOL//CPU的本地换行符"\n" 或"\r\n"
os.freemem()//可用RAM的字节数
os.getPriority()//返回进度优先级
os.homedir()//当前用户的home目录
os.hostname()//电脑的主机名
os.loadavg()//1,5,15分load平均值
os.newworkInterface()/可使用网络,链接的细节信息
os.platform()//操作系统平台名"linux" "win32"等
os.release()//操作系统版本号
os.setPriority()//设置进程调度优先级
os.tmpdir()//默认临时目录
os.totalmem()//总RAM字节数
os.type()//OS类型
os.userInfo()//uid home shell of current user

posted @ 2025-04-04 14:00  zhongta  阅读(22)  评论(0)    收藏  举报