当前位置: 首页 > news >正文

Node.js 之 HTTP实现详细分析

分针网每日分享:Node.js 之 HTTP实现详细分析

 

Node.js的强项是处理网络请求,那我们就来分析一个HTTP请求在Node.js中是怎么被处理的,以及JavaScript在这个过程中引入的开销到底有多大。
 
Node.js采用的网络请求处理模型是IO多路复用。它与传统的主从多线程并发模型是有区别的:只使用有限的线程数(1个),所以占用系统资源很少;操作系统级的异步IO支持,可以减少用户态/内核态切换,并且本身性能更高(因为直接与网卡驱动交互);JavaScript天生具有保护程序执行现场的能力(闭包),传统模型要么依赖应用程序自己保存现场,或者依赖线程切换时自动完成。当然,并不能说IO多路复用就是最好的并发模型,关键还是看应用场景。
 
我们来看“hello world”版Node.js网络服务器:
 
require('http').createServer((req, res) => {
res .end('hello world');
}).listen(3333);
 
代码思路分析
 
createServer([requestListener])
 
createServer创建了http.Server对象,它继承自net.Server。事实上,HTTP协议确实是基于TCP协议实现的。createServer的可选参数requestListener用于监听request事件;另外,它也监听connection事件,只不过回调函数是http.Server自己实现的。然后调用listen让http.Server对象在端口3333上监听连接请求并最终创建TCP对象,由tcp_wrap.h实现。最后会调用TCP对象的listen方法,这才真正在指定端口开始提供服务。我们来看看涉及到的所有JavaScript对象:
 
 
 
涉及到的C++类大多只是对libuv做了一层包装并公布给JavaScript,所以不在这里特别列出。我们有必要提一下http-parser,它是用来解析http请求/响应消息的,本身十分高效:没有任何系统调用,没有内存分配操作,纯C实现。
 
connection事件
 
当服务器接受了一个连接请求后,会触发connection事件。我们可以在这个结点获取到套接字文件描述符,之后就可以在这个文件描述符上做流式读或写,也就是所谓的全双工模式。上文提到net.Server的listen方法会创建TCP对象,并且提供TCP对象的onconnection事件回调方法;这里可以利用字段net.Server.maxConnections做过载保护,后面会讲到。并且会把clientHandle(本次连接的套接字文件描述符)封装成net.Socket对象,作为connection事件的参数。我们来看看调用过程:
 
tcp_wrap.cc
 
void TCPWrap::Listen(const FunctionCallbackInfo<Value>& args) {
int err = uv_listen(reinterpret_cast<uv_stream_t*>(&wrap->handle_),
backlog ,
OnConnection );
args .GetReturnValue().Set(err);
}
 
OnConnection 在connection_wrap.cc中定义
 
// ...省略不重要的代码
uv_stream_t * client_handle =
reinterpret_cast <uv_stream_t*>(&wrap->handle_);
// uv_accept can fail if the new connection has already been closed, in
// which case an EAGAIN (resource temporarily unavailable) will be
// returned.
if (uv_accept(handle, client_handle))
return;
 
// Successful accept. Call the onconnection callback in JavaScript land.
argv [1] = client_obj;
// ...省略不重要的代码
wrap_data ->MakeCallback(env->onconnection_string(), arraysize(argv), argv);
 
上文提到的clientHandle实际上是uv_accept的第二个参数,指服务当前连接的套接字文件描述符。net.Server的字段 _handle 会在JavaScript侧存储该字段。最后我们上一张流程图:
 
 
 
request事件
 
connection事件的回调函数connectionListener(lib/_http_server.js)中,首先获取http-parser对象,设置parser.onIncoming回调(马上会用到)。当连接套接字有数据到达时,调用http-parser.execute方法。http-parser在解析过程中会触发如下回调函数:
 
on_message_begin:在开始解析HTTP消息之前,可以设置http-parser的初始状态(注意http-parse有可能是复用的而不是重每次新创建)
 
on_url:解析请求的url,对响应消息不起作用
 
on_status, 解析状态码,只对http响应消息起作用
 
on_head_field, 头字段名称
 
on_head_value:头字段对应值
 
on_headers_complete:当所有头解析完成时
 
on_body:解析http消息中包含的payload
 
on_message_complete:解析工作结束
 
Node.js中Parser类是对http-parser的包装,它会注册上面所有的回调函数。同时,暴露给JavaScript5个事件:
kOnHeaders,kOnHeadersComplete,kOnBody,kOnMessageComplete,kOnExecute。在lib/_http_common.js中监听了这些事件。其中,当需要强制把头字段回传到JavaScript时会触发kOnHeaders;例如,头字段个数超过32,或者解析结束时仍然有头字段没有回传给JavaScript。当调用完http_parser_execute后触发kOnExecute。kOnHeadersComplete事件触发时,会调用parser的onIncoming回调函数。仅仅HTTP头解析完成之后,就会触发request事件。执行流程如下:
 
 
 
总结
 
说了那么多,其实仍然离不开最基础的套接字编程步骤,对于服务器端依次是:create、bind,listen、accept和close。客户端会经历create、bind、connect和close。想了解更多套接字编程的同学可以参考《UNIX网络编程》。
 
HTTP场景分析
 
上面提到的Node.js版hello world只涵盖了HTTP处理最基本的情况,但是也足以说明Node.js处理得非常简洁。现在,我们来分析一些典型的HTTP场景。
 
1. keep-alive
 
对于前端应用,HTTP请求瞬间数量比较多,但每个请求传输的数据一般不大;这时,用同一个TCP连接处理同一个用户发出的HTTP请求可以显著提高性能。但是keep-alive也不是万能的,如果用户每次只发起一个请求,它反而会因为延长连接的生存时间,浪费服务器资源。
 
针对同一个连接,Node.js会维持一个incoming队列和一个outgoing队列。应用程序通过监听request事件,可以访问ServerResponse和IncomingMessage对象,当请求处理完成之后(调用response.end()),ServerResponse会响应finish事件。如果它是本次连接上最后一个response对象,则准备关闭连接;否则,继续触发request事件。每个连接最长超时时间默认为2分钟,可以通过http.Server.setTimeout调整。
现在把我们的Node.js版hello world修改一下
 
var delay = [2000, 30, 500];
var i = 0;
require('http').createServer((req, res) => {
// 为了让请求模拟更真实,会调整每个请求的响应时间
setTimeout(() => {
res .end('hello world');
}, delay[i]);
i = (i+1)%(delay.length);
}).listen(3333, () => {
// listen的回调函数
console .log('listen at 3333');
});
 
客户端代码如下:
 
var http = require('http');
 
// 设置HTTP agent开启keep-alive模式
// 套接字的打开时间维持1分钟
var agent = new http.Agent({
keepAlive : true,
keepAliveMsecs : 60000
});
 
// 每次请求结束之后,都会再发起一次请求
// doReq每调用一次只会触发2次请求
function doReq(again, iter) {
let request = http.request({
hostname : '192.168.1.10',
port : 3333,
agent :agent
}, (res) => {
console .log(`${new Date().valueOf()} ${iter} ${again} Headers: ${JSON.stringify(res.headers)}`);
console .log(request.socket.localPort);
// 设置解析响应的编码格式
res .setEncoding('utf8');
// 接收响应
res .on('data', (chunk) => {
console .log(`${new Date().valueOf()} ${iter} ${again} Body: ${chunk}`);
});
if (again) doReq(false, iter);
});
// 发起请求
request .end();
}
 
for (let i = 0; i < 3; i++) {
doReq(true, i);
}
 
套接字复用的时序如下:
 
 
 
本文转自: http://www.f-z.cn/id/287

 

转载于:https://www.cnblogs.com/fenzhen/p/7169097.html

相关文章:

  • 断言
  • html相关
  • 4、使用WebAPITestClient
  • 王自如与老罗的辩论赛谁赢了?!
  • 01背包模板
  • jQuery的masonry插件实现瀑布流布局
  • Mongo基本使用:
  • “可信网站”真的可信吗?
  • 安装wdcp linux一键安装包云系统客户端教程
  • HDU 5547 Sudoku(DFS)
  • FreeRTOS 消息队列
  • lintcode-109-数字三角形
  • 关于python ide
  • git学习
  • SVM
  • 【挥舞JS】JS实现继承,封装一个extends方法
  • 【面试系列】之二:关于js原型
  • 2017前端实习生面试总结
  • Angular6错误 Service: No provider for Renderer2
  • CSS实用技巧干货
  • es6
  • FastReport在线报表设计器工作原理
  • Java 23种设计模式 之单例模式 7种实现方式
  • javascript 总结(常用工具类的封装)
  • Java应用性能调优
  • niucms就是以城市为分割单位,在上面 小区/乡村/同城论坛+58+团购
  • Spring Cloud Feign的两种使用姿势
  • 创建一种深思熟虑的文化
  • 大主子表关联的性能优化方法
  • 翻译 | 老司机带你秒懂内存管理 - 第一部(共三部)
  • 配置 PM2 实现代码自动发布
  • 深入浅出Node.js
  • 消息队列系列二(IOT中消息队列的应用)
  • 在Unity中实现一个简单的消息管理器
  • AI又要和人类“对打”,Deepmind宣布《星战Ⅱ》即将开始 ...
  • 格斗健身潮牌24KiCK获近千万Pre-A轮融资,用户留存高达9个月 ...
  • 选择阿里云数据库HBase版十大理由
  • ​ssh免密码登录设置及问题总结
  • ​无人机石油管道巡检方案新亮点:灵活准确又高效
  • #QT(一种朴素的计算器实现方法)
  • #常见电池型号介绍 常见电池尺寸是多少【详解】
  • $.ajax,axios,fetch三种ajax请求的区别
  • (附程序)AD采集中的10种经典软件滤波程序优缺点分析
  • (附源码)ssm航空客运订票系统 毕业设计 141612
  • (附源码)计算机毕业设计SSM疫情社区管理系统
  • (一)Java算法:二分查找
  • (转)http-server应用
  • (转载)PyTorch代码规范最佳实践和样式指南
  • (最完美)小米手机6X的Usb调试模式在哪里打开的流程
  • ../depcomp: line 571: exec: g++: not found
  • .NET CORE 第一节 创建基本的 asp.net core
  • /boot 内存空间不够
  • :not(:first-child)和:not(:last-child)的用法
  • @Autowired自动装配
  • @NestedConfigurationProperty 注解用法