深入浅出nodejs重点内容

2018-06-15  本文已影响0人  yozosann

2. NODE模块端实现

2.2 node模块的实现

引入模块:

2.2.1 优先从缓存加载

所有模块二次加载都会采用缓存优先加载方式。

2.2.2 路径分析文件定位

2.2.3 模块编译:

js模块的编译

头尾包装

(function(exports, require, module, __filename, __dirname)
{\n ---- \n});

之后的代码会通过vm原生模块runInThisContext()方法执行,返回一个具体的function对象。最后将当前模块的各个参数传给这个function执行。

可以给exports赋值,但是由于它是形参并不能改变作用域外的值。

c/c++ 模块的编译

其实不需要编译,模块的exports对象和node模块产生联系返回给调用者。

json模块

直接变成对象赋值给模块对象的exports,以供外部调用。

2.3 核心模块

c/c++编写的模块存储在node项目src下,js文件在lib下。

2.3.1 js核心模块编译过程

  1. 转存为c/c++代码
  2. 编译js核心模块:和文件模块区别: 从内存中加载,以及缓存执行结果的位置。

2.3.2 c/c++核心模块的编译过程

c++主内,js主外,开发速度和性能的平衡点。

2.4 c/c++ 扩展模块

2.5 模块调用栈

文件模块(js -> c/c++)
|
核心模块(js -> c/c++)

2.6 包与npm

2.6.1 包结构:

2.6.2 描述文件和npm:

存放在根目录下。npm行为与包描述文件息息相关。

CommonJs中的package.json:

npm实现规范中的包:

2.6.3 NPM包常用功能

  1. 查看帮助:
    • -v 版本号
    • npm help + command 查看命令帮助
  2. 安装依赖包
path.resolve(process.execPath, '..', '..', 'lib', 'node_modules');

推算出来。

npm install underscore --registry=http://registry.url

npm config set registry http://registry.url
  1. NPM勾子命令:
"scripts": {
    "preinstall":
    "install":
    "uninstall"
    "test":
}
  1. 包发布:
  1. 包分析:npm ls 包依赖树

Kwalitee(quality):

2.7 前后端公用模块

2.7.1 模块的侧重点

异步IO

I/O调用交给操作系统,继续其他调用,等io结束执行回调。

操作系统内核对于io只有两种方式阻塞和非阻塞。在调用阻塞io时应用程序需要等待io完成返回结果。非阻塞,调用后立即返回但是不返回结果。需要文件描述符再次读取。非阻塞io返回之后cpu时间段可以用来处理其他事物。

非阻塞io:因为仅仅返回了调用状态所以并不是业务希望获取到的结果,需要程序不断重复调用io确认是否完成,这样的过程叫做轮询。

轮询演进:

cpu没有得到有效利用,不够好。

3.2.2 理想的非阻塞异步io

发出请求后 cpu继续自己的事情,然后等待返回数据执行回调。linux:aio:缺点 无法利用系统缓存

3.2.3 现实的异步io

js执行在单线程里罢了,再node中无论nix或者windows平台里,内部完成io的还是线程池。

3.3 node的异步io

3.3.1 时间循环

进程启动时,node便会启动一个类似于while true的循环,每执行一次循环体的过程我们称为Tick。每个tick的过程就是查看是否有事件等待处理,如果有,就取出事件及相关的回调函数,如果存在关联的回调函数,就执行他们然后进入下一个循环,如果不再有事件处理,3就退出进程。

3.3.2 观察者

每个事件循环都有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。这如同点单小妹,厨师就是事件执行者询问小妹是否有餐需要去做。
浏览器采用了类似的机制。

3.3.3 请求对象

js发起调用到内核执行完io操作过渡过程中,存在一种中间产物 它叫做请求对象。例如 open打开一个文件。

调用node核心模块 - 调用c++内建模块 - 内建模块通过libuv进行系统调用。 libuv判断平台分别执行不同的执行方法。实际上调用了uv_fs_open()。 在调用该函数过程中,我们创建了一个FSReqWrap请求对象。从js层传入的参数和当前方法都被封装在这个请求对象中,回调被绑定在oncomplete_sym上。然后将这个对象推到线程池中等待。

然后js返回,都不会影响到js线程的后续执行。

3.3.4 执行回调

等线程池中io调用完毕之后,会将获取到的结果存储在req-result上,然后调用某函数通知IOCP。我们可以通过get函数提取。这个时候就用到时间循环的tick,每次循环都会通过get提取结果,如果存在就会当作请求处理。


事件循环 异步IO过程

事件循环 观察者 请求对象 io线程池四者共同构成了node异步io的主要模型。

3.4 非IO的异步APi

setTimeout setInterval setImmediate process.nextTick()

3.4.1 定时器

setTimeout 和 setInterval 与浏览器一致,他们不需要io线程池的参与。每次调用都会插入到定时器观察者内部的一个红黑树里,每次tick执行会检查是否超过定时时间,如果超过就形成一个事件。setInterval是重复性检测。
定时器时间不一定准,尽管一次tick很快,但是如果一个1ms定时器 上次tick执行了5ms 那就超时了4ms。


定时器

3.4.2 process.nextTick()

在未了解process.nextTick之前,很多人也许会为了立即异步执行一个任务,用setTimeout设置0ms。但是实际上这个步骤需要动用红黑树,创建定时器对象和迭代等操作,浪费性能,时间上使用nextTick()这样的方法更为轻量。它简单的将回调放入队列中 下次tick执行。

3.4.3 setImmediate

process.nextTick采用的idle观察者,而setImmediate采用check观察者,idle观察者优先级高于check观察者。
process.nextTick会在每轮循环中将数组中的回调全部执行完,而setImmediate的结果保存在链表中,在每轮循环执行链表中的一个一个回调。

3.5 事件驱动与高性能服务器

网络io也采用事件驱动,无须为每一个请求创建对应的线程。因为线程少,上线文切换代价很低,所以即使有大量链接也不受线程切换上线文开销的影响。
传统:同步,每进程每请求,每线程每请求。

4. 异步编程

4.2 异步编程的优缺点

4.2.1 优点

高效率解决io密集型的

4.2.2 难点

  1. 异常处理:传统try catch没有作用,一般异常作为回调函数第一个参数,如果为空则表示没有异常。
  2. 函数嵌套过深
    回调执行 回调执行 回调执行。。。。
  3. 阻塞代码 while 时间判断是错误的,用setTimeout效果更好。
  4. 多线程编程 web workers
  5. 异步转同步:async

4.3 异步编程解决方案

4.3.1 发布订阅者模式

  1. 继承events模块 util.inherits(Stream, EventEmitter);
  2. once()解决雪崩:没有缓存时,大批量数据同时访问数据库。
var proxy = new events.EventEmitter();
var status = "ready";
var select = function(cb) {
  proxy.once("selected", callback);
  if(status === "ready") {
    status = "pending";
    db.select("SQL", function(r){
      proxy.emit("selected", r);
      status = "ready";
    })
  } 
}
  1. 异步之间的写作方案
// 解决多异步提供条件:
// 原本:
fs.readdir(xxx, function (err, files) {
  files.forEach(function (item) {
    fs.readfile(item, function (err, res) {
      // 一系列操作.....
    })
  })
});

// 现在我们以ui渲染为例,我们需要拿到模板,数据,本地化资源才能进行渲染:
// 我们首先引入哨兵函数:
// 我们每拿到一样东西计数加一,等拿到所有东西执行回调,该函数为哨兵函数工厂可以自己定义回调和需要几样结果
var after = function (times, callback) {
  var count = 0, results = {};
  return function (key, value) {
    results[key] = value;
    count++;
    if (count === times) {
      callback(results);
    }
  }
}

// 初始化哨兵函数
var done = after(times, render);
var emitter = new event.Emitter();
emitter.on("done", done);

fs.readFile(template_path, "utf-8", function (err, template) {
  emitter.emit("done", "template", template);
})

db.query(sql, function (err, data) {
  emitter.emit("done", "data", data);
})

l10n.get(template_path, function (err, resources) {
  emitter.emit("done", "resources", resources);
})
// 如此就解决了回调地狱的问题。

// 看似完美,但是也有不足在于每次我们都需要提取结果进行emit,每次都需要准备done函数,如果这些都抽象化?
// 采用EventProxy模块
var proxy = new EventProxy();

proxy.all("template", "data", "resources", function (template, data, resources) {
  // TODO
})

fs.readFile(template_path, "utf-8", function (err, template) {
  proxy.emit("done", "template", template);
})

db.query(sql, function (err, data) {
  proxy.emit("done", "data", data);
})

l10n.get(template_path, function (err, resources) {
  proxy.emit("done", "resources", resources);
})

// 用all订阅多个事件,当每个事件都触发all回调才触发
// 除了all 还有 tail区别在于 all只会执行一次, tail在于如果执行一次后,某一次事件再次触发将会采用最新的数据继续执行回调。
// 订阅事件列表和参数列表一致。
// 除此之外还提供after函数

proxy.after("data", 10, function (datas) {
  // TODO datas是十次数据的数组
});
// 执行十次data事件后执行回调
  1. EventProxy的异常处理
// 曾经的处理方式
var proxy = new EventProxy();

proxy.all("template", "data", "resources", function (template, data, resources) {
  // TODO
})

proxy.bind('err', function(err) {
  // TODO
})

fs.readFile(template_path, "utf-8", function (err, template) {
  if(err) {
    proxy.emit('err', err);
  }
  proxy.emit("done", "template", template);
})

db.query(sql, function (err, data) {
  if(err) {
    proxy.emit('err', err);
  }
  proxy.emit("done", "data", data);
})

l10n.get(template_path, function (err, resources) {
  if(err) {
    proxy.emit('err', err);
  }
  proxy.emit("done", "resources", resources);
})

// 因为异常处理的原因代码量一下子就对了起来,而在ep实践过程中改善了这个问题
// 将emit done 和 err都绑定到了一个done方法上
proxy.fail(callback);

fs.readFile(template_path, "utf-8", proxy.done('template'));
db.query(sql, proxy.done('data'));
l10n.get(template_path, proxy.done('resources'));
// 我们只需要关注业务逻辑,不需要关注错误处理

proxy.fail(callback) 
// 等价于
proxy.bind('err', function(err){
  // 解绑所有函数
  proxy.unbind();
  callback(err);
})

proxy.done('resources');
// 等价于
let anonymous = function (err, resources) {
  if(err) {
    proxy.emit('err', err);
  }
  proxy.emit("done", "resources", resources);
}

// 如果只有一个回调函数传给ep 那么无须考虑异常,done会为你自己处理,我们还可以自定义一些数据处理而非默认的
// 默认的
let anonymous = function (err, resources) {
  if(err) {
    proxy.emit('err', err);
  }
  proxy.emit("done", "resources", resources);
}
// 数据处理
proxy.done('tpl', function(content) {
  content.replace('s', 'S');
  return content;
});

// 这里会帮你自己做异常处理,除此之外回调的数据是处理过的数据。

4.3.2 Promise/Deferred模式

  1. Promises/A
// Promise实现
var Promise = function () {
  EventEmitter.call(this);
}
util.inherits(Promise, EventEmitter);

Promise.prototype.then = function (fullfilledHandler, errorHandler, progressHandler) {
  if (typeof fulfilledHandler === 'function') {
    this.once('success', fullfilledHandler);
  }

  if (typeof errorHandler === 'function') {
    this.once('erroe', errorHandler);
  }

  if (typeof progressHandler === 'function') {
    this.once('progress', progressHandler);
  }

  return this;
}
// then只是将方法存放了起来,还需要一个地方触发执行这些回调
var Deferred = function () {
  this.state = 'unfulfilled';
  this.promise = new Promise();
}

Deferred.prototype.resolve = function (obj) {
  this.state = 'fulfilled';
  this.promise.emit('success', obj);
}

Deferred.prototype.reject = function (obj) {
  this.state = 'failed';
  this.promise.emit('error', obj);
}

Deferred.prototype.progress = function (obj) {
  this.promise.emit('progress', obj);
}
// 最终实现api
var promisify = function (res) {
  var deferred = new Deferred();
  var result = '';
  res.on('data', function (chunk) {
    result += chunk;
    deferred.progress(chunk);
  });
  res.on('end', function () {
    deferred.resolve(result);
  });
  res.on('error', function (err) {
    deferred.reject(err);
  });

  return deferred.promise;
}
// 这样我们的promise就封装好了
// 原来的调用
res.setEncoding('utf8');
res.on('data', function (c) {
  console.log('BODY: ', c);
})
res.on('end', function (c) {
  // DONE
})
res.on('error', function (c) {
  // ERROR
})

// 现在变为
promisify(res).then(function () {
  // DONE
}, function () {
  // ERROR
}, function (chunk) {
  // progress
  console.log('BODY: ', chunk)
})

// Q模块的实现
defer.prototype.makeNodeResolver = function() {
  var that = this;
  return function(error , value) {
    if(error) {
      self.reject(error);
    } else if(arguments.length > 2) {
      self.resolve(array_slice(arguments, 1));
    } else { 
      self.resolve(value);
    }
  };
};

var readFile = function(file, encoding) {
  var deferred = Q.defer();
  fs.readFile(file, encoding, deferred.makeNodeResolver());
  return deferred.promise;
}
  1. Promise中的多异步协作
// 类似于EventProxy
Deferred.prototype.all = function(promises) {
  var count = promises.length;
  var that = this;
  var results = [];
  promises.forEach(function(promise, i) {
    promise.then(function(data) {
      count--;
      results[i] = data;
      if(count === 0) {
        that.resolve(results);
      }
    }, function(err) {
      that.reject(err);
    });
  });
  return this.promise;
}

// 应用场景
var promise1 = readFile('foo.txt', "utf-8");
var promise2 = readFile('bar.txt', "utf-8");
deferred.all([promise1, promise2]).then(function(results) {
  // TODO
}, function(error) {
  // TODO
});
  1. Promise的进阶知识
// 回调地狱:每一异步依赖于上一次异步结果
obj.api1(function (value1) {
  obj.api2(value1, function (value2) {
    obj.api3(value2, function (value3) {
      obj.api4(value3, function (value4) {
        cb(value4);
      })
    })
  })
})

// 采用event.emitter
var emitter = new event.emitter();
emitter.on("step1", function () {
  obj.api1(function (value1) {
    emitter.emit("step2", value1);
  })
})
emitter.on("step2", function () {
  obj.api1(function (value2) {
    emitter.emit("step3", value2);
  })
})
emitter.on("step3", function () {
  obj.api1(function (value3) {
    emitter.emit("step4", value3);
  })
})
emitter.on("step4", function () {
  obj.api1(function (value4) {
    cb(value4);
  })
})
emitter.emit("step1");

// 这确实揭开了回调,但是明显更复杂,代码量更多了。
// 理想情况
Promise().then(obj.api1).then(obj.api2).then(obj.api3).then(obj.api4).then(function(value4) {
  // Do something with value4
}, function(error) {
  // 1-4 's error
}).done();

// 改造一下Deferred
var Deffered = function() {
  this.promise = new Promise();
}

// 完成态
Deffered.prototype.resolve = function(obj) {
  var promise = this.promise;
  var handler;
  while((handler = promise.queue.shift())) {
    if(handler && handler.fulfilled) {
      var ret = handler.fulfilled(obj);
      if(ret && ret.isPromise) {
        ret.queue = promise.queue;
        this.promise = ret;
        return;
      }
    }
  }
}

// 失败态
Deffered.prototype.reject = function(obj) {
  var promise = this.promise;
  var handler;
  while((handler = promise.queue.shift())) {
    if(handler && handler.error) {
      var ret = handler.error(obj);
      if(ret && ret.isPromise) {
        ret.queue = promise.queue;
        this.promise = ret;
        return;
      }
    }
  }
}

// 生成回调函数
Deferred.prototype.callback =  function() {
  var that = this;
  return function(err, file) {
    if(err) {
      return that.reject(err)
    }
    that.resolve(file);
  }
}

var Promise = function() {
  this.queue = [];
  this.isPromise = true;
}

Promise.prototype.then = function (fullfilledHandler, errorHandler) {
  var handler = {};
  if (typeof fulfilledHandler === 'function') {
    handler.fulfilled = fulfilledHandler;
  }

  if (typeof errorHandler === 'function') {
    handler.error = errorHandler;
  }

  this.queue.push(handler);
  return this;
}

// 实际运用
var readFile1 = function(file, encoding) {
  var deferred = new Deffered();
  fs.readFile(file, encoding, defferred.callback());
  return deferred.promise;
}

var readFile2= function(file, encoding) {
  var deferred = new Deffered();
  fs.readFile(file, encoding, defferred.callback());
  return deferred.promise;
}

readFile1('1.txt', 'utf8').then(function(file1) {
  return readFile2(file1.trim(), 'utf8');
}).then(function(file2){
  console.log(file2);
});

主要有两个主要步骤:

  1. 将所有回调都存到队列中
  2. 一旦监测到返回了新的Promise对象停止执行,然后将当前Deferred对象的promise引用改为新的Promise对象,并将队列中余下的回调转交给他。
var smooth = function(method) {
  return function() {
    var deferred = new Deffered();
    var args = Array.prototype.slice.call(arguments, 0);
    args.push(deferred.callback());
    method.apply(null, args);
    return deferred.promise;
  }
}

var readFile = smooth(fs.readFile);

4.3.3 流程控制库
尾触发:常见关键词next,执行该函数后执行下一个流程

function xxx (req,res,next) {
  // TODO
  next();
}

app.use(xxx) 
// => 
app.use = function(route, fn) {
  this.stack.push({route: route, fandle: fn});
  return this;
}

function next() {
  // some code
  layer = stack[index++];
  layer.handle(req, res, next);
}

所有嫌异步编程开发复杂开发者都可以参考该流程的处理,对于业务逻辑,逐步处理均有效。

// 异步串行 (前者执行完执行后者再执行cb,没有依赖)
async.series([
  function(cb) {
    fs.readFile('file1.txt', 'utf-8',cb);
  }, 
  function(cb){
    fs.readFile('file2.txt', 'utf-8',cb);
  }
], function(err, results) {
  // results -> [file1.txt, file2.txt]
});

// 异步并行 (两个同时执行完再执行cb,最终cd依赖两个值)
async.parallel([
  function(cb) {
    fs.readFile('file1.txt', 'utf-8',cb);
  }, 
  function(cb){
    fs.readFile('file2.txt', 'utf-8',cb);
  }
], function(err, results) {
  // results -> [file1.txt, file2.txt]
});

// 后者依赖前者数据
async.waterfall([
  function(cb) {
    fs.readFile('file1.txt', 'utf-8',cb);
  }, 
  function(arg1, cb){
    fs.readFile('arg1', 'utf-8',cb);
  }
], function(err, result) {
  // result -> file2.txt
});

// 声明依赖自动处理
var deps = {};
async.auto(deps);
  1. step (自行了解)
  2. wind

4.4异步并发控制

同步的每个io都是阻塞的,虽然慢但是总是一个接着一个调用,不会初夏耗用文件描述符太多的情况,而异步不同,对于异步io虽然并发容易实现,但是由于太容易实现,依然需要控制,尽管是压榨底层系统的性能,但是给予一定的过载保护,以防止过犹不及。

for (var i=0; i<100; i++){
  async();
}

4.4.1 bagpipe的解决方案

var Bagpipe = require('bagpipe');

var bagpipe = new Bagpipe(10);

for (var i = 0; i < 100; i++) {
  bagpipe.push(async, function () {
    // xxxx
  });
}

bagpipe.on('full', function (length) {
  console.log('XXXXX');
});

4.4.2 async的解决方案

5.内存控制

5.1 V8垃圾回收机制和内存限制

5.1.2 Node与V8

V8的内存管理机制导致,node使用js操作内存(64位约为1.4GB,32位约为0.7GB)

5.1.3 V8的对象分配

为什么是1.5g源于垃圾回收机制,以1.5g的垃圾回收为例,v8做一次小的垃圾回收需要50ms而做一次非增量的垃圾回收需要1000ms以上,前端和服务器端这些事件都无法响应,应用的性能和响应能力都会降低,所以在当时的考虑下直接限制了内存。限制并非不能打开:

node --max-old-space-size=1700 
node --max-new-space-size=1024

上述参数在v8初始化时生效,一旦生效无法动态改变。

5.1.4 V8的垃圾回收机制

  1. V8主要的垃圾回收算法:
    按对象的存活时间将内存的垃圾回收进行不同的分代,然后对不同分代进行不同高效算法。

典型的牺牲空间换取时间的算法,但是对于新生代非常合适,因为新生代的生命周期一般都比较短。

三种算法比较

5.1.5 查看垃圾回收日志

5.2 高效使用内存

5.2.1 作用域

定义一个函数,每次调用的时候会创建一个作用域,然后作用域中生命的局部变量都会分配在该作用域上,随着作用域的销毁而销毁。

var foo = function() {
  local = {};
}

local很小会分配在from空间中,下一次垃圾回收时被释放。

  1. 标识符查找:当前作用域查找local找不到就继续向上作用域查找。
  2. 作用域:b调用a函数, c调用b函数就会产生一条作用域链,当使用一个变量找不到时就会一层一层向上查找,最后找不到抛错。(不清楚去看js高级程序设计)
  3. 变量主动释放:定义在global上的变量,知道进程退出才会被销毁,因此导致引用的对象常驻在老生代中。可以使用delete来删除引用,或者重新赋值,让旧的对象脱离引用关系,在接下来的老生代清楚和整理的过程中,会被释放。delete会影响v8自动优化,所以使用赋值方式最后。

5.2.2 闭包:可以导致内存不被释放。(不清楚去看js高级程序设计)

5.3 内存指标

  1. 查看进程内存占用
    process.memoryUsage() 可以看到内存使用情况,
{rss:13853672, heapTotal:6131200, heapUsed: 2757120}

rss是进程常驻内存,进程的内存一共分为几部分,一部分是rss其余在swap和filesystem中。

  1. 查看系统内存占用
    os.totalmem() os.freemem()

5.3.3 堆外内存

用buffer申请对象的时候内存分配并非通过V8分配详见第6章。

5.4 内存泄漏

一个字节的泄漏导致堆积,导致花费更长时间进行对象扫描,应用响应缓慢直到崩溃。

5.4.1 谨慎将内存当作缓存

var cache = {};
var get = function(key) {
  if(cache[key]) {
    return cache[key];
  } else {
    // get from ...
  }
}

var set = function(key, value) {
  cache[key] = value;
}

cache会无限堆积造成泄漏。

  1. 缓存限制策略
  1. 缓存的解决方案
    局限:不同进程中无法共享
    方案:使用node之外的进程进行缓存 不影响node性能:redis。

5.4.2 关注队列状态

5.5 内存泄漏排查

5.6 大内存应用:stream

6.理解Buffer

6.1 Buffer结构

Buffer是一个Array的对象,主要用于操作字节。

6.1.1 结构模块

Buffer是一个典型的js和c++结合的模块,性能部分由c++实现,非性能部分由js实现。

6.1.2 Buffer对象

Buffer类似于数组,元素为16进制的两位数,0到255的数值。
Buffer受Array类型的影响很大,可以访问length属性得到长度,也可以通过下标访问元素。

var buf = new Buffer(100);
console.log(buf.length); // 100
console.log(buf[10]);

通过下标访问刚初始化的Buffer元素得到的是0~255间的一个随机值。
同样我们可以通过下标给Buffer元素进行赋值。

buf[20] = -100; // 156
buf[21] = 300; // 44
buf[22] = 3.1415 // 3

如果我们给buffer元素赋不在0-255范围的值:

6.1.3 Buffer 内存分配

slab分配机制:slab(一块申请好的固定大小内存区域):

  1. 分配小Buffer对象:小于8kb。
    使用一个局部变量pool作为中间处理,处于分配状态的slab单元指向他:
var pool;
function allocPool() {
  pool = new SlowBuffer(Buffer.poolSize);
  pool.used = 0;
}

目前为一个新构造的slab单元,出去empty状态。
当我们new Buffer(1024); 这次构造将会去检查pool对象,如果pool没有创建。将会创建一个新的slab单元指向他。

if(!pool || pool.length - pool.used < this.length) allocPool();

同时当前Buffer对象的parent属性指向该slab,并记录下是从这个slab的哪个位置开始使用,slab对象自身也记录使用了多少字节:

this.parent = pool;
this.offset = pool.used;
pool.used += this.length;
if(pool.used & 7) pool.used = (pool.used + 8) & ~7;

这个时候slab的状态为partial。
再次创建Buffer的时候,会判断这个slab剩余空间是否足够。如果足够使用剩余,否则更新slab分配状态。不够将会创造一个新的slab,原slab剩余空间将会造成浪费,比如 1 和 8192字节。第二次分配时不够使用所以创建新的slab,第一个slab将会被1字节的buffer对象独占。 由于一个slab会被多个Buffer使用,所以只有这些小的buffer对象在作用域被释放被回收这个slab的8kb空间才会被回收。

  1. 分配大的Buffer对象:
this.parent = new SlowBuffer(this.length);
this.offset = 0;

直接分配一个SlowBuffer对象作为slab单元,这个单元将会被这个大的Buffer对象独占。该类在C++中定义,引用Buffer模块可以访问到它但是不建议直接操作。都能够被v8标记回收,但是由于SlowBuffer是C++中定义的,所以内存不在V8的堆中。

  1. 小结:小的Buffer进行,先分配再使用,使得js到操作系统间不必有过多Buffer操作,对于大的Buffer而言,使用C++层面提供的内存。

6.2 Buffer的转换

目前支持的类型:

6.2.1 字符串转Buffer

new Buffer(str, [encoding]),不传默认utf-8;
一个Buffer可以存储不同编码类型的字符串转码值buf.write(string, [offset],[length],[encoding])
由于可以不断写入内容到Buffer对象中,并且可以制定每次写入的编码,所以Buffer可以存在多种编码转化后的结果。

6.2.2 Buffer转字符串

buf.toString([encoding], [strat], [end]) 如果类型不同就需要在局部指定不同编码。

6.2.3 Buffer不支持的编码类型:

Buffer.isEncoding(encoding) 判断是否支持某种编码。
不支持的编码:

var iconv = require('iconv-lite');
var str = iconv.decode(buf, 'win1252');
var buf = iconv.encode('sssss', 'win1252'); 

6.3 Buffer的拼接

var fs = require('fs');

var rs = fs.createReadStream('test.md');
var data = '';

rs.on('data', function (chunk) {
  data += chunk;
});

rs.on('end', function () {
  console.log(data);
})

我们在国外网站上经常看见类似这样的示例,新人通常带着字符串的思想来看待,所以不会觉得有任何异常,但事实上读取的chunk是buffer类型。
在执行:data += chunk; 实际上是执行:data = data + chunk.toString(); 而我们知道当buf.toString不带参数默认是 utf-8进行编码,这对于英文(单字节)来说没有任何问题,但是对于汉字这样的多字节编码就会有问题,只是chunk的长度越大出现错误的概率越小,但实际上这样但处理还是有点问题的,比如:

var fs = require('fs');

var rs = fs.createReadStream('test.md', {highWaterMark: 11});
var data = '';

rs.on('data', function (chunk) {
  data += chunk;
});

rs.on('end', function () {
  console.log(data);
})

每次Buffer只读11个字节。我们知道一个汉字由3个字节构成。如果test.md的内容为:
床前明月光,疑似地上霜;举头望明月,低头思故乡。
那么读取的内容为:
床前明��光,疑���地上霜;举头��明月,���头思故乡。
原因在于一次性读11个字节,那么能够成功解码3个汉字,剩下还有两个字节无法被解析就变成了�。
所以在处理多字节编码的时候,需要细心处理。

6.3.2 setEncoding() 与 string_decoder()

在createReadStream的rs对象中还有一个setEncoding() 方法。

var fs = require('fs');

var rs = fs.createReadStream('test.md', {highWaterMark: 11});
rs.setEncoding('utf8');
var data = '';

rs.on('data', function (chunk) {
  data += chunk;
});

rs.on('end', function () {
  console.log(data);
})

这代表data事件中传递的不再是一个Buffer对象,而是编码后的字符串。为此再次执行得到结果:床前明月光,疑似地上霜;举头望明月,低头思故乡。
这样输出就不再受Buffer大小的影响了。
不管设置否编码触发data事件的次数依然相同,这意味着设置编码并没有改变按段读取的基本方式,但是为什么乱码问题被解决了,在于内置的decoder对象。

var StringDecoder = require('string_decoder').StringDecoder;
var decoder = new StringDecoder('utf8');

当设置了编码之后,decoder知道utf-8编码下是以3个字节的方式存储的,所以第一次读取了11个字节他会先解决3(11/3 = 3.xxx) *3个字节,还剩2个字节它会自动保存下实例内部,等到下一次11个字节进入执行时,再将剩余2个字节拼接到11个字节上继续采用同样的解析方式,所以乱码问题就得到了解决。
但是它目前只能处理UTF-8、BASE64、UCS-2/UTF-16LE三种编码。虽然能解决大部分问题,但不能从根本解决该问题。

6.3.3 正确拼接Buffer

var fs = require('fs');
var iconv = require('iconv-lite');
var chunks = [];
var size = 0;

var rs = fs.createReadStream('test.md', {highWaterMark: 11});

rs.on('data', function (chunk) {
  chunks.push(chunk);
  size += chunk.length;
});

rs.on('end', function () {
  var buf = Buffer.concat(chunks, size);
  var str = iconv.decode(buf, 'utf8');
  console.log(str);
})

正确的拼接方法是用一个数组存储收到的所有Buffer碎片,然后调用Buffer.concat方法生成一个合并的Buffer对象,读取所有Buffer后一次性使用iconv-lite解析。
Buffer.concat的实现,Array的每一个元素可能由n个字节组成所以在拼接成Buffer需要记录字节长度。

Buffer.concat = function(list, length) {
  if(!Array.isArray(list)) {
    throw new Error('Usage: xxxx');
  }

  if(list.length === 0) {
    return new Buffer(0);
  } else if(list.length === 1) {
    return list[0];
  }

  if(typeof length !== 'number') {
    length = 0;
    for(var i=0; i<list.length; i++) {
      var buf = list[i];
      length += buf.length;
    }
  }

  var buffer = new Buffer(length);
  var pos = 0;
  for(var i=0; i<list.length; i++) {
    var buf = list[i];
    buf.copy(buffer, pos);
    pos += buf.length;
  }

  return buffer;
}

6.4 Buffer与性能

var http = require('http');
var haloworld = '';

// haloworld = new Buffer(haloworld);

for (var i = 0; i < 1024 * 10; i++) {
  haloworld += 'a';
}

http.createServer(function (req, res) {
  res.writeHead(200);
  res.end(haloworld);
}).listen(8001);

我们使用字符串进行网络传输:发起100个并发客户端:
ab -c 100 -t 50 http://127.0.0.1:8001/
得到结果:

Transfer rate:          4671.68 [Kbytes/sec] received

然后我们取消注释,使用Buffer进行网络传输得到结果:

Transfer rate:          4903.68 [Kbytes/sec] received

(原作中是 -c 200 -t 100,性能几乎提高一倍,我的mac做不到,但也能看到有性能提升。)
换成Buffer之后,性能提高了许多,原因在于预先转换静态内容为Buffer,可以有效减少CPU重复使用,节省服务器资源,通过预先换为Buffer的方式,使性能得到提升。因为文件自身使二进制数据。

{highWaterMark: 64 * 1024}

完成一次读取时,从这个Buffer中通过slice()方法读取部分数据作为一个小的Buffer对象,然后通过data事件传递给调用方,如果Buffer用完,则重新分配,如果有剩余继续使用。
如果highWaterMark设置过小,可能分配过多或者导致系统调用次数过多。理想状态下,每次读取的长度就是用户指定的highWaterMark,但是有可能读到了结尾或者本身就没有highWaterMark这么大。pool是常驻内存,当pool小于128字节,才会重新分配一个新的Buffer对象,这与Buffer的内存分配类似。
由于fs.createReadStream内部采用fs.read实现,将会引起对磁盘的系统调用,如果highWaterMark过小,调用次数越多,系统调用次数越多,性能越差。

7.网络编程

7.1 TCP:

7.2 创建TCP服务器端:

server:

var net = require('net');

var server = net.createServer(function(socket) {
  // 新的链接
  socket.on('data', function(data) {
    socket.write('你好');
  });

  socket.on('end', function(){
    socket.write('断开链接');
  });

  socket.write('欢迎光临');
});

server.listen(8124, function() {
  console.log('server bound');
});

client:

var net = require('net');
var client = net.connect({port: 8124}, function() {
  console.log('connect OK!');
  client.write('world!\r\n');
});

client.on('data', function(data) {
  console.log(data.toString());
  client.end();
});

client.on('end', function() {
  console.log('client disconnected!');
});

客户端得到console:

connect OK!
欢迎光临
你好
client disconnected!

7.1.3 TCP服务的事件:

1.服务器事件:

  1. 链接事件:

7.2 构建UDP

客户端想要和另一个tcp服务通信,需要穿件一个套接字来完成连接。在udp中,一个套接词可以与多个udp服务通信,提高简单面向事务不可靠信息传输服务,在网络差存在严重丢包,但是由于它无须链接,资源消耗低,处理迅速灵活,所有常常应用在偶尔丢一两个包也不会产生影响的场景,如视频音频。

7.2.1 创建UDP套接字:

udp-server:

var dgram = require('dgram');
var server = dgram.createSocket("udp4");

server.on('message', function(msg, rinfo) {
  console.log('server got:' + msg + " from " + rinfo.address + ":" + rinfo.port);
});

server.on('listening', function() {
  var address = server.address();
  console.log('server address ' + address.address + ' port ' + address.port)
});

server.bind(41234);

udp-client:

var dgram = require('dgram');

var message = new Buffer('射弩前地');
var client = dgram.createSocket('udp4');
client.send(message, 0, message.length, 41234, '0.0.0.0', function(err, bytes){
  client.close();
});

服务端打印的信息:

server address 0.0.0.0 port 41234
server got:射弩前地 from 127.0.0.1:58172

send方法可以发送信息到网络中,虽然参数列表相对复杂,但是更为灵活,可以随意发送数据到网络中的服务器,而tcp需要套接词构建。

7.2.4 UDP套接词事件

7.3 构建HTTP服务

tcp于udp都属于网络传输层协议,如果构建高效网络应用,应该从传输层入手。但是对于经典场景我们无须从传输层入手构造应用,对于普通应用直接使用http或者smtp等经典应用层协议绰绰有余。

7.3.1 HTTP

var http = require('http');

http.createServer(function(req, res) {
  res.writeHead(200,{'Content-type': 'text/plain'});
  res.end('Hello World\n');
}).listen(1337);

console.log('Start 1337 ...');

调用curl -v http://127.0.0.1:1337 查看所有报文信息:

// 第一部分:三次握手
* Rebuilt URL to: http://127.0.0.1:1337/
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 1337 (#0)

// 第二部分:发送请求
> GET / HTTP/1.1
> Host: 127.0.0.1:1337
> User-Agent: curl/7.51.0
> Accept: */*
>

// 第三部分:处理后返回请求
< HTTP/1.1 200 OK
< Content-type: text/plain
< Date: Wed, 30 May 2018 09:32:37 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked
<
Hello World

// 最后部分:结束对话
* Curl_http_done: called premature == 0
* Connection #0 to host 127.0.0.1 left intact

7.3.2 http模块:

异步事件循环支持高并发。
TCP服务以connection为单位进行服务,HTTP服务以request为单位进行服务,对connection进行了封装。
http将套接字的读写抽象为ServerRequest 和 ServerResponse对象。

  1. HTTP请求:
> GET / HTTP/1.1
> Host: 127.0.0.1:1337
> User-Agent: curl/7.51.0
> Accept: */*
>
  1. HTTP响应
    res.setHeader()
    res.writeHead()
    两个步骤,set可以多次,但是只有调用write之后才会写入连接中,http模块会自动帮你设置一些头信息。实体部分则是调用res.write()和res.end()实现,区别在于end会先调用write发送数据,然后发送信号告诉这次响应结束。一旦发送数据setHeader和writeHead不再生效。
    务必在结束时候调用end,也可以延迟end实现客户端与服务器之间长链接。

  2. HTTP服务的事件:

7.3.3 HTTP客户端:

http.request 用于构造HTTP客户端,与curl命令相同。

var http = require('http');

var options = {
  hostname: '127.0.0.1',
  port: 1337,
  path: '/',
  method: 'GET'
}

var req = http.request(options, function (res) {
  console.log(res.statusCode);
  console.log(JSON.stringify(res.headers));
  res.setEncoding('utf8');
  res.on('data', function (chunk) {
    console.log(chunk);
  })
});
  1. HTTP响应
    解析完响应头触发response事件,同时传递一个响应对象以操作ClientResponse。后续响应报文体以只读流方式提供。
  2. HTTP代理
    为了重用TCP连接,http模块包含一个代理对象http.globalAgent。他对每个服务器端创建的连接进行管理,默认情况下,通过ClientRequest对象对同一个服务端发起HTTP最多可以创建5个连接。
    如果发送10次,得到处理的只有5个,后续请求需要等到某个请求结束才会真正发出,一旦请求量过大会影响性能。如果改变可以在options传递agent选项,默认为5。也可以agent为false;是请求不受并发限制。
var agent = new http.Agent({
  maxSockets: 10
});
var options = {
  agent: agent
}

agent对象的sockets和requests两个值表示使用中连接数,和处于等待状态的请求数。

  1. HTTP客户端事件:

7.4 构建WebSocket服务:

7.4.1

建立连接时通过HTTP发起请求报文:

GET /chat HTTP/1.1
Host: xxx.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: xxxx
Sec-WebSocket-Protocal:chat,superchat //子协议 
Sec-WebSocket-Version: 13 // 版本号

与普通的HTTP请求协议区别在于:
Upgrade 和 Connection,上述字段表示将请求服务器升级协议为WebSocket。其中Sec-WebSocket-Key用于安全校验,随机生成的base64编码字符串服务端收到后与字符串xxx相连,形成后的字符串通过sha1安全散列算法计算出结果后,进行Base64编码返回给客户端。
服务端处理请求完成后响应

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: xxxx
Sec-WebSocket-Protocal:chat,superchat

上面报文告之客户端正在更换协议,更新为WebSocket协议,并在当前套接字上应用新的协议,客户端校验Sec-WebSocket-Accept值如果成功,将开始数据传输。

7.4.2 WebSocket数据传输

协议升级过程示意图

握手完毕触发socket.onopen事件。
服务端没有onopen事件可言。
客户端send()数据,服务器onmessage()数据反之亦然。 为了安全考虑客户端需要对发送的数据帧进行掩码处理,服务器如果收到无掩码帧将关闭连接,而服务器端无须掩码,同样如果客户端收到带掩码的数据帧连接也会关闭。


数据帧定义
返回数据帧定义

7.5 网络服务与安全

  1. 密钥
    TLS/SSL是一个公钥私钥的结构,它是一个非对称的结构。客户端使用服务端公钥加密消息发送,服务端收到后私钥解密,然后使用客户端公钥加密,客户端接收到私钥解密。
    问题在于在通信前客户端和服务器需要交换公钥,然后这一步很容易受到中间人攻击(在客户端和服务器之间扮演中间人,获取双方的公钥伪造双方的交流,所以我们需要认证这个公钥是来自服务器而不是中间人)。
    不过如果公钥和密钥如果都可以安全交换,那么数据为什么不行?然后我们采取的方法就是密钥交换采取第三方认证(数字签名),然后数据再按照之前描述的方式交换。
    生成私钥:
// 服务器私钥
openssl genrsa -out server.key 1024

// 客户端私钥
openssl genrsa -out client.key 1024

上述命令生成了两个1024位长的RSA私钥文件,我们继续生成公钥:

// 服务器公钥
openssl rsa -in server.key -pubout -out server.pem

// 客户端公钥
openssl rsa -in client.key -pubout -out client.pem
  1. 数字签名
    我们上述的第三方就是CA(Certificate Authority,数字证书认证中心)。CA作用是颁发证书,这个证书中具有CA通过自己公钥私钥实现的签名。
    为了得到签名证书,服务器需要通过自己的私钥生成CRS(Certificate Signing Request,证书签名请求),然后ca会为服务器颁发属于该服务器的签名证书,通过CA机构就能验证证书是否合法。
    但是证书颁发是一个繁琐的过程,很多中小企业采用自签名证书来构建安全网络。

以下是生成私钥生成CSR通过私钥自签名生成证书的过程:

openssl genrsa -out ca.key 1024
openssl req -new -key ca.key -out ca.csr
openssl x509 -req -in ca.csr -signkey ca.key -out ca.crt
生成自签名证书

意思步骤完成了扮演CA所需要的文件。接下来回到服务器向CA申请签名:

openssl req -new -key server.key -out server.csr
openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in server.csr -out server.crt

生成好证书后,客户端发起安全连接前回去获取服务器端的证书,并通过CA验证证书真伪:


客户端通过CA验证服务器证书真伪

客户端需要验证CA证书真伪,如果知名CA他们证书被预装在浏览器中。

7.5.2 TLS服务

  1. 服务器
var tls = require('tls');
var fs = require('fs');

var options = {
  key: fs.readFileSync('./server.key'),
  cert: fs.readFileSync('./server.crt'),
  requestCert: true,
  ca: [ fs.readFileSync('./ca.crt')]
};

var server = tls.createServer(options, function(stream) {
  console.log('server connected', stream.authorized ? 'authorized' : 'unauthorized');
  stream.write('welcome!\n');
  stream.setEncoding('utf8');
  stream.pipe(stream);
});

server.listen(8000, function() {
  console.log('bound!');
})

openssl s_client -connect 127.0.0.1:8000可以测试证书是否正常。

  1. 客户端
    生成必要信息:
openssl req -new -key client.key -out client.csr
openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -in client.csr -out client.crt

创建客户端:

var tls = require('tls');
var fs = require('fs');

var options = {
  key: fs.readFileSync('./client.key'),
  cert: fs.readFileSync('./client.crt'),
  ca: [fs.readFileSync('./ca.crt')]
};

var stream = tls.connect(8000, options, function () {
  console.log('client connected', stream.authorized ? 'ok' : 'no');
  process.stdin.pipe(stream);
});

stream.setEncoding('utf8');
stream.on('data', function (data) {
  console.log(data);
})

stream.on('end', function() {
  console.log(11);
});

与普通tcp创建连接相比只是多了一个传入证书的过程,其他没有啥差别。

7.5.3 HTTPS服务
HTTPS就是工作在TLS/SSL上的HTTP。

var https = require('https');
var fs = require('fs');

var options = {
  key: fs.readFileSync('./server.key'),
  cert: fs.readFileSync('./server.crt')
};

https.createServer(options, function (req, res) {
  res.writeHead(200, { 'Content-type': 'text/plain' });
  res.end('Hello World\n');
}).listen(1337);

console.log('Start 1337 ...');

curl -k https://localhost:1337/ 忽略证书验证得到结果。
curl --cacert keys/ca.crt https://localhost:1337/ 安全获得到结果。

var https = require('https');
var fs = require('fs');

var options = {
  hostname: '127.0.0.1',
  port: 1337,
  path: '/',
  method: 'GET',
  key: fs.readFileSync('./client.key'),
  cert: fs.readFileSync('./client.crt'),
  ca: [fs.readFileSync('./ca.crt')],
}

options.agent = new https.Agent(options);

var req = https.request(options, function (res) {
  res.setEncoding('utf8');
  res.on('data', function (chunk) {
    console.log(chunk);
  })
});

req.end();

req.on('error', function (e) {
  console.log(e);
})

8.构建Web应用

8.1 基础功能

http.createServer(function (req, res) {
  res.writeHead(200, { 'Content-type': 'text/plain' });
  res.end('Hello World\n');
}).listen(1337);

8.1.1 请求方法

根据req.method做不同处理 请求方法的处理

8.1.2 请求路径

根据req.url 获取到路径,进行路径解析url.parse
一般有采用 /controller/action/a/b/c处理方法

8.1.3 查询字符串

var query = querystring.parse(url.parse(req.url).query);
// foo=bar&foo=baz foo将会是个数组
{
  foo: ['bar', 'baz'];
}

8.1.4 Cookie

服务器写入cookie:Set-Cookie: name=value; Path=/; Expires=xxx; Domian=.domain.com;

8.1.5 Session

Cookie弊端:cookie可以被前端串改,而且数据量不能太大,并且是明文传输,对于敏感数据gg。
Session数据只存在于客户端,客户端无法修改,且解决数据敏感问题。
但是如何将每个客户和服务器中的数据一一对应?

var sessions = {};
var key = 'session_id';
var EXPIPERS = 20 * 60 * 1000;

var generate = function () {
  var session = {};
  session.id = (new Date()).getTime() + Math.random();
  session.cookie = {
    expire: (new Date()).getTime() + EXPIPERS
  };
  sessions[session.id] = session;
  return session;
};

var app = function (req, res) {
  var id = req.cookies[key];
  if (!id) {
    req.session = generate();
  } else {
    var session = sessions[id];
    if (session) {
      if (session.cookie.expire > (new Date()).getTime()) {
        session.cookie.expire = (new Date()).getTime() + EXPIPERS;
        req.session = session;
      } else {
        delete sessions[id];
        req.session = generate()
      }
    } else {
      req.session = generate();
    }
  }
  handle(req, res);
}

// 响应给客户端
res.writeHead = function() {
  var cookies = res.getHeader('Set-Cookie');
  // 写入cookies
  var session = serilaize(key, req.session.id);
  cookies = Array.isArray(cookies) ? cookies.concat(sessions) : [cookies,session];
  res.setHeader('Set-Cookie', cookies);
  return writeHead.apply(this, arguments);
}
  1. Session与内存:
    我们如果按照以上的那些写法,session是写在内存中的,但是我们第五章讲过,如果session太多会引起垃圾回收的频繁扫描。并且我们可能为了利用多核cpu启动多个进程,内存无法共享。
    我们选择采用高速缓存,例如Redis。利用第三方缓存的问题在于引起网络访问,数据要比本地磁盘访问要慢。但是我们仍然采用高速缓存的原因在于:
  1. Session与安全:
    口令可以被伪造,所以有安全隐患。
    一个做法将口令通过私钥加密:
// 将值通过私钥签名,由.分割原值和签名
var sign = function(val, secret) {
  return (val + '.' + crypto).creatHmac('sha256', secret).update(val).digest('base64').replace(/\=+$/, '');
}

// 响应时
var val = sign(req.sessionID, secret);
res.setHeader('Set-Cookie', cookie.serialize(key, val));

// 取出口令检查签名
var unsign = function(val, secret) {
  var str = val.slice(0, val.lastIndexOf('.'));
  return sign(str, secret) == val ? str : false; 
}

这样以来只有sessionId是无法得到信息的。但是如果攻击者能拿到私钥就无法防御,一种解决方案是除了验证这些信息意外还会验证用户的独一无二信息 比如手机号?ua或者ip。

8.1.6 缓存

如果一个站点不怎么更新,每次用户打开页面都要去请求相同的东西,会造成不必要的浪费,因此节约不必要的传输,对用户和服务器提供者来说都有好处。

var handle = function(res, req) {
  fs.stat(filename, function(err, stat){
    var lastModified = stat.mtime.toUTCString();
    if(lastModified == req.headers['if-modified-since']) {
      res.writeHead('304', "Not Modified");
      res.end();
    } else {
      fs.readFile(filename, function(err, file) {
        var lastModified = stat.mtime.toUTCString();
        res.setHeader("Last-Modified", lastModified);
        res.writeHead(200, "OK");
        res.end(file);
      })
    }
  })
}

这里有些缺陷:

// If-None-Match/ETag
var getHash = function(str) {
  var shasum = crypto.createHash('sha1');
  return shasum.update(err).digest('base64');
}

var handle = function(res, req) {
  fs.readFile(filename, function(err, file){
    var hash = getHash(file);
    var noneMatch = req.headers['if-none-match'];

    if(hash === noneMatch) {
      res.writeHead('304', "Not Modified");
      res.end();
    } else {
      fs.readFile(filename, function(err, file) {
        res.setHeader("ETag", hash);
        res.writeHead(200, "OK");
        res.end(file);
      })
    }
  })
}

浏览器收到了ETag 之后,会放在请求头If-None-Match中。
尽管条件请求可以在文件内容没有修改的情况下节省带宽,但是它依然会发起一个HTTP请求,最好的方式是请求都不用发,这个时候就可以设置Expire和Cache-Control头。 浏览器根据该值进行缓存。
HTTP 1.0 使用Expire是一个GMT格式的时间字符串,浏览器接收到这个过期值之后,只要本地还存在这个缓存文件,在到期时间之前都不会再发起请求。 不过缺陷在于服务器和客户端的时间可能不一致,可能提前或者过期删除,HTTP1.1引入了Cache-Control设置了max-age值。优点在于更精确,解决一致性问题,并且可以精确控制,如果max-age和Expire同时存在,将进行Expire覆盖。

8.1.7 Basic认证

如果一个页面需要Basic认证,它会检查请求报文头中的Authorization字段的内容,由认证方式加上加密值组成:

Authorization Basic dxNlcdnuewew

如果用户首次访问该页面,URL地址中没有携带认证内容,那么浏览器会响应一个401未授权的状态码。

响应头中WWW-Authorization字段告诉浏览器采用什么样认证和加密,然后浏览器会弹出一个对话框进行交互式提交认证信息。
Basic有太多缺点,base64传输几乎等同于明文,一般在HTTPS情况下才使用,不过Basic认证支持范围十分广泛,几乎所有浏览器都支持。

8.2 数据上传

我们常用的GET请求可以让服务器进行大多数业务逻辑操作了,但是单纯的头部报文无法携带大量的数据,在业务中往往需要接收一些数据,比如表单提交,文件提交,JSON上传等。
http模块只对HTTP报文头部进行了解析,然后触发request事件,如果请求还有内容部分需要用户自行接收和解析,通过Transfer-Encoding和Content-Length即可判断。

var hasBody = function (req) {
  return 'transfer-encodeing' in req.headers || 'content-length' in req.headers;
}

function app(req, res) {
  if(hasBody(req)) {
    var buffers = [];
    req.on('data', function(chunk) {
      buffers.push(chunk);
    });

    req.on('end', function() {
      req.rawBody = Buffer.concat(buffers).toString();
      handle(req, res);
    })
  } else {
    handle(req, res);
  }
 }

获取没有乱码的字符串数据挂载在req.rawBody处。

8.2.1 表单数据

上一篇 下一篇

猜你喜欢

热点阅读