nodejs解析http协议源码解析

2023-05-16

上篇文章讲到nodejs创建一个服务并且建立tcp连接的过程。接下来分析一下,在建立tcp连接后,nodejs是如何解析http协议的。我们首先看一下nodejs在建立tcp连接时执行net.js层的回调时做了什么操作。下面是核心代码。

// clientHandle代表一个和客户端建立tcp连接的实体
function onconnection(err, clientHandle) {
  var handle = this;
  var self = handle.owner;

  debug('onconnection');

  if (err) {
    self.emit('error', errnoException(err, 'accept'));
    return;
  }
  // 建立过多,关掉
  if (self.maxConnections && self._connections >= self.maxConnections) {
    clientHandle.close();
    return;
  }

  var socket = new Socket({
    handle: clientHandle,
    allowHalfOpen: self.allowHalfOpen,
    pauseOnCreate: self.pauseOnConnect
  });
  socket.readable = socket.writable = true;
}
...

建立tcp连接后nodejs新建了一个Socket实体。我们看一下new Socket的核心逻辑。

 stream.Duplex.call(this, options);
 this._handle = options.handle; 
 initSocketHandle(this);
 // 触发底层注册一些函数
 this.read(0);
function initSocketHandle(self) {
	if (self._handle) {
	    self._handle.owner = self;
	    // 这个函数在底层有数据时会回调
	    self._handle.onread = onread;
	    self[async_id_symbol] = getNewAsyncId(self._handle);
	}
}

另一个重点是read(0)这个函数的逻辑。

Socket.prototype.read = function(n) {
  if (n === 0)
    return stream.Readable.prototype.read.call(this, n);

  this.read = stream.Readable.prototype.read;
  this._consuming = true;
  return this.read(n);
};

read函数最终会调用_read函数读数据。我们看一下ReadableStream里的read。

在read里会执行_read
this._read(state.highWaterMark);

而_read是由Socket函数实现的。因为Socket继承了ReadableStream。_read执行了一个很重要的操作。

this._handle.readStart();

_handle代表的是一个TCP对象,即tcp_wrapper.cc里创建的。所以我们去看tcp_wrapper的代码。但是没找到该函数。原来该函数在tcp_wrapper的子类stream_wrap里实现的。

int LibuvStreamWrap::ReadStart() {
  return uv_read_start(stream(), [](uv_handle_t* handle,
                                    size_t suggested_size,
                                    uv_buf_t* buf) {
    static_cast<LibuvStreamWrap*>(handle->data)->OnUvAlloc(suggested_size, buf);
  }, [](uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf) {
    static_cast<LibuvStreamWrap*>(stream->data)->OnUvRead(nread, buf);
  });
}

其实就是调用了libuv的uv_read_start函数。该函数在stream.c里。我们继续往下看。

  stream->read_cb = read_cb;
  stream->alloc_cb = alloc_cb;
  // 注册读事件
  uv__io_start(stream->loop, &stream->io_watcher, POLLIN);

主要是注册了事件和回调函数,该函数会在数据到来时被执行。到此,就告一段落了。现在就要等数据的到来。上篇文章我们分析过,数据到来时执行的函数是uv__stream_io。

 if (events & (POLLIN | POLLERR | POLLHUP))
    uv__read(stream)

有读事件到来的时候,uv__stream_io会调uv_read函数。

	buf = uv_buf_init(NULL, 0);
    stream->alloc_cb((uv_handle_t*)stream, 64 * 1024, &buf);
   if (buf.base == NULL || buf.len == 0) {
     /* User indicates it can't or won't handle the read. */
     stream->read_cb(stream, UV_ENOBUFS, &buf);
     return;
   }

这两个函数就是刚才注册的。我们再次回到nodejs的c++代码。看一下这两个函数做了什么。

void LibuvStreamWrap::OnUvRead(...) {
	EmitRead(nread, *buf);
}

EmitRead在stream_base-inl.h里定义,他又是一个子类。

inline void StreamResource::EmitRead(ssize_t nread, const uv_buf_t& buf) {
  if (nread > 0)
    bytes_read_ += static_cast<uint64_t>(nread);
  listener_->OnStreamRead(nread, buf);
}

在stream_base.c定义

OnStreamRead() {
	 stream->CallJSOnreadMethod(nread, obj);
}
CallJSOnreadMethod() {
	wrap->MakeCallback(env->onread_string(), arraysize(argv), argv);
}

在env.h里我们知道onread_string就是onread,所以这里就是执行js层的onread函数。该函数就是在new Socket的时候注册的。我们回到js的代码。

	function onread() {
		var ret = self.push(buffer);
	}

push函数是在readableStream里定义的。他经过一系列处理触发ondata事件。

function addChunk(...) {
	...
	stream.emit('data', chunk);
	...
}

那是谁监听了ondata事件呢,我们首先看一下nodejs在建立一个连接到再_http_server.js层做了什么处理。

function Server(requestListener) {
  if (!(this instanceof Server)) return new Server(requestListener);
  net.Server.call(this, { allowHalfOpen: true });
  // 收到http请求时执行的回调
  if (requestListener) {
    this.on('request', requestListener);
  }
  this.httpAllowHalfOpen = false;
  // 建立tcp连接的回调
  this.on('connection', connectionListener);

  this.timeout = 2 * 60 * 1000;
  this.keepAliveTimeout = 5000;
  this._pendingResponseData = 0;
  this.maxHeadersCount = null;
}

connectionListener代码如下。

function connectionListener(socket) {
  defaultTriggerAsyncIdScope(
    getOrSetAsyncId(socket), connectionListenerInternal, this, socket
  );
}

function connectionListenerInternal(server, socket) {
   httpSocketSetup(socket);
	if (socket.server === null)
    socket.server = server;
	if (server.timeout && typeof socket.setTimeout === 'function')
    socket.setTimeout(server.timeout);
  
  socket.on('timeout', socketOnTimeout);
  var parser = parsers.alloc();
  parser.reinitialize(HTTPParser.REQUEST);
  parser.socket = socket;
  socket.parser = parser;
  parser.incoming = null;

  // Propagate headers limit from server instance to parser
  if (typeof server.maxHeadersCount === 'number') {
    parser.maxHeaderPairs = server.maxHeadersCount << 1;
  } else {
    // Set default value because parser may be reused from FreeList
    parser.maxHeaderPairs = 2000;
  }

  var state = {
    onData: null,
    onEnd: null,
    onClose: null,
    onDrain: null,
    outgoing: [],
    incoming: [],
    outgoingData: 0,
    keepAliveTimeoutSet: false
  };
  // 收到tcp连接中的数据时回调
  state.onData = socketOnData.bind(undefined, server, socket, parser, state);
  state.onEnd = socketOnEnd.bind(undefined, server, socket, parser, state);
  state.onClose = socketOnClose.bind(undefined, socket, state);
  state.onDrain = socketOnDrain.bind(undefined, socket, state);
  socket.on('data', state.onData);
  socket.on('error', socketOnError);
  socket.on('end', state.onEnd);
  socket.on('close', state.onClose);
  socket.on('drain', state.onDrain);
  parser.onIncoming = parserOnIncoming.bind(undefined, server, socket, state);

  // We are consuming socket, so it won't get any actual data
  socket.on('resume', onSocketResume);
  socket.on('pause', onSocketPause);

  // Override on to unconsume on `data`, `readable` listeners
  socket.on = socketOnWrap;

  // We only consume the socket if it has never been consumed before.
  if (socket._handle) {
    var external = socket._handle._externalStream;
    if (!socket._handle._consumed && external) {
      parser._consumed = true;
      socket._handle._consumed = true;
      parser.consume(external);
    }
  }
  parser[kOnExecute] =
    onParserExecute.bind(undefined, server, socket, parser, state);

  socket._paused = false;
}

主要是注册了一系列的回调函数,这些函数在收到数据或者解析数据时会被执行。所以收到数据后执行的函数是socketOnData。该函数就是把数据传进http解析器然后进行解析。

	function socketOnData(server, socket, parser, state, d) {
	 	...
	    var ret = parser.execute(d);
	    onParserExecuteCommon(server, socket, parser, state, ret, d);
	}

我们先看一下parser是个什么。parser是在_http_server.js的onconnection回调里,parsers.alloc()分配的。而parsers又是个啥呢?他在_http_common.js里定义。

var parsers = new FreeList('parsers', 1000, function() {
  var parser = new HTTPParser(HTTPParser.REQUEST);

  parser._headers = [];
  parser._url = '';
  parser._consumed = false;

  parser.socket = null;
  parser.incoming = null;
  parser.outgoing = null;

  // Only called in the slow case where slow means
  // that the request headers were either fragmented
  // across multiple TCP packets or too large to be
  // processed in a single run. This method is also
  // called to process trailing HTTP headers.
  parser[kOnHeaders] = parserOnHeaders;
  parser[kOnHeadersComplete] = parserOnHeadersComplete;
  parser[kOnBody] = parserOnBody;
  parser[kOnMessageComplete] = parserOnMessageComplete;
  parser[kOnExecute] = null;

  return parser;
});

class FreeList {
  constructor(name, max, ctor) {
    this.name = name;
    this.ctor = ctor;
    this.max = max;
    this.list = [];
  }

  alloc() {
    return this.list.length ?
      this.list.pop() :
      this.ctor.apply(this, arguments);
  }

  free(obj) {
    if (this.list.length < this.max) {
      this.list.push(obj);
      return true;
    }
    return false;
  }
}

他其实是管理http解析器的。重点是HTTPParser,他定义在node_http_parser.cc是对http解析器的封装。真正的解析器在http_parser.c。回到刚才的地方。nodejs收到数据后执行 parser.execute(d);execute函数对应的是node_http_parser里的Execute。该函数进行了重载。入口是下面这个函数。

static void Execute(const FunctionCallbackInfo<Value>& args) {
	Local<Value> ret = parser->Execute(buffer_data, buffer_len);
}


Local<Value> Execute(char* data, size_t len) {
      http_parser_execute(&parser_, &settings, data, len);
 }

http_parser_execute函数定义在http_parser.c,该函数就是进行真正的http协议解析。它里面会有一些钩子函数。在解析的某个阶段会执行。例如解析完头部。

if (settings->on_headers_complete) {
      switch (settings->on_headers_complete(parser)) {
       	...
 	  }
}

具体的定义在node_http_parser.cc

const struct http_parser_settings Parser::settings = {
  Proxy<Call, &Parser::on_message_begin>::Raw,
  Proxy<DataCall, &Parser::on_url>::Raw,
  Proxy<DataCall, &Parser::on_status>::Raw,
  Proxy<DataCall, &Parser::on_header_field>::Raw,
  Proxy<DataCall, &Parser::on_header_value>::Raw,
  Proxy<Call, &Parser::on_headers_complete>::Raw,
  Proxy<DataCall, &Parser::on_body>::Raw,
  Proxy<Call, &Parser::on_message_complete>::Raw,
  nullptr,  // on_chunk_header
  nullptr   // on_chunk_complete
};

这里我们以on_header_complete钩子来分析。

const uint32_t kOnHeadersComplete = 1
int on_headers_complete() {
	Local<Value> cb = obj->Get(kOnHeadersComplete);	
	 MakeCallback(cb.As<Function>(), arraysize(argv), argv);
}

最后会执行kOnHeadersComplete这个函数。我们看到这个kOnHeadersComplete 等于1,其实这个是在js层复赋值的。在_http_common.js中的开头。

const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0;

然后在新建一个http解析器的函数注册了该函数。

parser[kOnHeadersComplete] = parserOnHeadersComplete;

所以当解析头部结束就会执行parserOnHeadersComplete。

function parserOnHeadersComplete(...) {
	parser.incoming = new IncomingMessage(parser.socket);
	...
	return parser.onIncoming(parser.incoming, shouldKeepAlive);
}

新建了一个IncomingMessage对象,然后执行_http_server.js注册的回调onIncoming 。该回调函数也是再建立tcp连接时注册的。

function parserOnIncoming() {
	var res = new ServerResponse(req);
	...
	server.emit('request', req, res);
}

生成一个ServerResponse对象,然后触发request事件。该函数是在我们执行http.createServer时传进行的函数。

function Server(requestListener) {
  ...
  // 收到http请求时执行的回调
  if (requestListener) {
    this.on('request', requestListener);
  }
}

最后在我们的回调里就拿到了这两个对象。但是这时候只是解析完了头部,request对象里还拿不到body的数据。我们需要自己获取。

	var str = "";    
    req.on('data', (data) => {
        str += data;   
    });    
    req.on('end',() => {})
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

nodejs解析http协议源码解析 的相关文章

  • 微信客服对接-唯一客服系统文档中心

    微信客服官方网址为 xff1a https kf weixin qq com xff0c 可以在微信内 外各个场景中接入微信客服 xff0c 提供一致的咨询体验 xff0c 企业可通过API接口回复消息 xff0c 做好客户服务 微信客服或
  • 访客接入-唯一客服系统文档中心

    网站可以通过多种方式接入客服系统 xff0c 直接跳转链接 xff0c 或者在页面右下角弹窗 访客链接 可以在自己的网站接入 xff0c 访客独立链接 xff0c 入口形式可以完全自己写 xff0c 只是跳转链接 例如下面的访客链接 htt
  • 知识库AI机器人客服(基于ChatGPT3.5)对接-唯一客服系统文档中心

    此功能是利用chatgpt训练企业知识开发个性化客服系统 xff0c 可以上传自有数据 xff0c 基于向量数据库与OpenAI Embedding xff0c 以及OpenAI chat completions接口 xff0c 实现的基于
  • AX7A200教程(6): 串口接收图片数据,通过hdmi接口输出显示

    本章节主要使用uart接收图片数据 xff0c 然后通过ddr3缓存 xff0c 最后通过hdmi接口显示输出 xff0c 功能框图如下图所示 uart接收的图片数据位1024 768 3分辨率大小的数据 xff0c 一共2359296个字
  • ROS 程序初读一(gps_driver)

    先来看到 launch 文件夹 xff0c 有三个 launch 文件 xff0c 也不知道从哪看起 xff0c 就从第一个看看先 第一个文件为 gps data get launch xff0c 内容如下 lt launch gt lt
  • 什么是SLO?

    Short term Liquidity Operation即短期流动性调节工具 SLO以7天期以内短期回购为主 xff0c 遇节假日可适当延长操作期限 xff0c 采用市场化利率招标方式开展操作 SLO原则上在公开市场常规操作的间歇期使用
  • 21.6.7爬虫日志

    一 爬虫的目的 采集数据 xff0c 为软件服务 xff01 xff01 那么数据从那儿来 xff1f 都是从生活中来的 xff01 xff01 1 手工采集 可以采集数据 xff0c 但是操作效率低下 2 内部数据 公司内部数据 xff0
  • 什么是自贸区?什么是离岸债券?

    1 自由贸易区 中国自由贸易区是指在国境内关外设立的 xff0c 以优惠税收和海关特殊监管政策 为主要手段 xff0c 以贸易自由化便利化为主要目的的多功能经济性特区 自由贸易区有两个本质上存在差异很大的概念 xff1a 一个是FTA xf
  • 什么是SLO?

    SLO xff1a 短期流动性调节工具 xff08 Short termLiquidityOperations xff09 每周二 周四 xff0c 央行一般都会进行公开市场操作 xff0c 目前最主要的是回购操作 回购操作又分成两种 xf
  • strcpy、strncpy与memcpy的区别与使用方法

    strcpy strncpy 与memcpy 的区别与使用方法 strcpy strncpy memcpy这三个C语言函数我们在主机代码编写中会很频繁的使用到 xff0c 但是三个函数的区别 使用时该注意什么还是有必要说下的 本文参考 C
  • C语言:存取结构体成员的点运算符(.)和箭头运算符(->)的区别

    一直以为这两个是没有什么区别的 xff0c 可以相互替换 xff0c 今天又翻了一下 C语言核心技术 xff0c 明白了其中的奥妙 相同点 xff1a 两个都是二元操作符 xff0c 其右操作符是成员的名称 不同点 xff1a 点操作符左边
  • DB2 命令行中如何执行sql脚本

    原文链接 xff1a http space itpub net 8231934 viewspace 584635 db2 61 gt connect to dbName user xxx using password db2 61 gt s
  • 性能测试知多少 --并发用户数与TPS之间的关系

    1 背景 在做性能测试的时候 xff0c 很多人都用并发用户数来衡量系统的性能 xff0c 觉得系统能支撑的并发用户数越多 xff0c 系统的性能就越好 xff1b 对TPS不是非常理解 xff0c 也根本不知道它们之间的关系 xff0c
  • ubuntu编译 opencv undefined referece to `cv::imread()`

    Ubuntu下编译一个C 43 43 文件 xff0c C 43 43 源程序中使用了opencv xff0c opencv的安装没有问题 xff0c 但是在编译的过程中出现如下错误 xff1a undefined reference to
  • 基于深度学习的3D pose estimation总结(包括几篇2D pose estimation)

    一 任务描述 给定一幅图或者是一段视频 xff0c 人体姿态估计就是恢复出其中的人体关节点位置的过程 二 挑战和难点 1 人体肢体运动较为灵活 xff1b 2 视角的变化 xff1b 3 附着物的变化 xff08 比如遮挡 xff0c 衣物
  • STM32的空闲中断

    最近发现了STM32的USART的空闲中断非常的舒爽 xff0c 但是在前期配置的时候会出现一些小问题导致没有办法进入终中断或者是一直空闲中断 xff0c 现将它记下来 xff0c 给各位和自己留一个参考 xff1a 1 不进入中断 我是这
  • Github上最受欢迎的7个开源AI机器学习框架

    在过去的几年中 xff0c 人工智能正在占领技术的许多领域 来自不同背景的开发人员最终意识到了AI为他们带来的机遇 xff0c 而不管他们的需求如何 在今天的文章中 xff0c 我们列出了7种最佳的开源AI 机器学习系统和框架 1 Tens
  • ubuntu更换shell

    ubuntu更换shell zsh配置 span class token comment 安装zsh span span class token function sudo span span class token function ap
  • 网络调试助手NetAssist的使用

    一 使用场景 xff1a 项目定制需求 xff1a 前端的车载终端把gps 报警信息 报警图片 其他检测数据发往约定的第三方服务器 xff0c 车载终端通过公网 xff08 SIM拨号或者有线网 xff09 以udp或者tcp连接服务器 x
  • vlc代码分析(4)——mpgv的demux

    Mpgv c 是对mpeg vedio的解码部分 xff0c 从demux开始 xff0c 到sample到输出 其中 xff0c 核心部分是函数ParseMPEGBlock 两种数据格式 xff1a video format 是video

随机推荐

  • 关于使用CMT2300A FIFO缓存区间设置为64Byte的问题

    首先请看 xff0c CMT2300A 是什么产品 xff0c 或者说是 模组吗 xff1f 请看介绍 xff1a https blog csdn net sishuihuahua article details 105095994 以及R
  • 不合理超时设置带来的“坑”

    在后台服务的实现中 xff0c 我们通过基于TCP UDP协议封装起来的RPC机制实现了跟不同系统的通信 xff0c 进而协同各个系统完成一系列业务流程和功能的执行 xff0c 在这个过程中 xff0c 每个参与合作的子系统都有自己的能力描
  • 以太坊的三种同步模式

    全同步 xff1a 获取区块链所有区块的header和body xff0c 并校验其中的每一笔交易 xff0c 需要下载所有的区块数据信息 xff0c 同步速度最慢 xff0c 但是数据最全 xff1b 快速同步 xff1a 获取区块链所有
  • 基于订单号可重入的交易系统接口设计探讨

    基于订单号可重入的交易系统接口设计探讨 在交易系统的设计和实现中 xff0c 接口的可重入性设计是及其关键的 xff0c 可重入性也叫接口操作的冥等性保障 xff0c 那么什么叫冥等性呢 xff1f 在交易系统中 xff0c 为什么需要这个
  • 什么是手机的IMEI?

    一 xff0c IMEI是什么 xff1f IMEI xff08 International Mobile Equipment Identity xff09 是移动设备国际身份码的缩写 是由15位数字组成的 电子串号 xff0c 该码是全球
  • PBOC2.0安全系列之—脱机认证之静态数据认证(SDA)

    PBOC2 0安全系列之 脱机认证之静态数据认证 SDA 一 xff0c 什么是PBOC2 0 2005年3月13日 xff0c 人民银行发布第55号文 xff0c 正式颁发了 中国金融集成电路 xff08 IC xff09 卡规范 简称P
  • 第三方支付架构设计之—帐户体系

    第三方支付架构设计之 帐户体系 一 xff0c 什么是第三方支付 xff1f 什么是第三方支付 xff1f 相信很多人对这个名字很熟悉 xff0c 不管是从各种媒体等都经常听到 xff0c 可以说是耳熟能熟 但 xff0c 如果非得给这个名
  • ELF文件格式详解

    ARM的可执行文件的格式是ELF格式文件 xff0c 下文对ELF格式做个详细的介绍 序言 1 OBJECT文件 导言 ELF头 ELF Header Sections String表 String Table Symbol表 Symbol
  • ROS小车(SLAM+物体追踪)

    属于交通运输工程设计的论文 xff0c 包含SLAM与物体追踪这两个方向 ROS智能车设计 一 系统描述 1 车轮子 单轮平衡式结构 xff0c 能量利用率高 xff0c 但转弯的时候需要倾角高速运动 xff0c 很难控制 差速转向平衡两轮
  • 配置apache服务器的用户认证

    经常上网的读者会遇到这种情况 xff1a 访问一些网站的某些资源时 xff0c 浏览器弹出一个对话框 xff0c 要求输入用户名和密码来获取对资源的访问 这就是用户认证的一种技术 用户认证是保护网络系统资源的第一道防线 xff0c 它控制着
  • 强大的grep用法详解:grep与正则表达式

    from http hi baidu com nearlove blog item 11db98b6b5b8aff831add1e5 html 首先要记住的是 正则表达式与通配符不一样 它们表示的含义并不相同 正则表达式只是一种表示法 只要
  • Linux中通过/proc/stat等文件计算Cpu使用率

    from xff1a http www blogjava net fjzag articles 317773 html proc文件系统 proc文件系统是一个伪文件系统 xff0c 它只存在内存当中 xff0c 而不占用外存空间 它以文件
  • 关于mysql的错误 - no query specified

    Mysql error no query specified mysql下抛出错误 xff1a error no query specified 出现此错误是sql不合法原因 xff1a 如 xff1a select from abc G
  • 详解coredump

    一 xff0c 什么是coredump 我们经常听到大家说到程序core掉了 xff0c 需要定位解决 xff0c 这里说的大部分是指对应程序由于各种异常或者bug导致在运行过程中异常退出或者中止 xff0c 并且在满足一定条件下 xff0
  • freertos 源码详解四 堆栈初始化

    在prvInitialiseNewTask函数中 xff0c 有一个步骤7 xff1a pxNewTCB gt pxTopOfStack 61 pxPortInitialiseStack pxTopOfStack pxTaskCode pv
  • STL - vector

    C 43 43 STL中的verctor好比是C语言中的数组 xff0c 但是vector又具有数组没有的一些高级功能 与数组相比 xff0c vector就是一个可以不用再初始化就必须制定大小的边长数组 xff0c 当然了 xff0c 它
  • 计算机专业术语,收藏用

    显示内存 与主板上的内存功能一样 xff0c 显存也是用于存放数据的 xff0c 只不过它存放的是显示芯片处理后的数据 显存越大 xff0c 显示卡支持的最大分辨率越大 xff0c 3D应用时的贴图精度就越高 xff0c 带3D加速功能的显
  • 7_资源文件添加

    资源文件添加 在QMainWindow中我们已经设立了一些部件 xff0c 现在我们来设置图标 通常有相对路径读取和资源文件读取两种方法 QAction actionNew 61 new QAction 基本格式 ui gt actionN
  • 智能除草机

    专利设计 xff1a 智能除草机 1 除草机背景 业界主要采用滚刀式草坪修剪车来进行运动场的除草 除草不仅浪费人力 xff0c 而且也受到天气状况的限制 传统割草机产生的环境污染也较大 随着社会经济的不断发展 xff0c 人力成本也不断攀升
  • nodejs解析http协议源码解析

    上篇文章讲到nodejs创建一个服务并且建立tcp连接的过程 接下来分析一下 xff0c 在建立tcp连接后 xff0c nodejs是如何解析http协议的 我们首先看一下nodejs在建立tcp连接时执行net js层的回调时做了什么操