Node.js源码解析-HTTP请求响应过程

Node.js源码解析-HTTP请求响应过程

欢迎来我的博客阅读:《Node.js源码解析-HTTP请求响应过程》

在 Node.js 中,起一个 HTTP Server 非常简单,只需要如下代码即可:

const http = require('http')

http.createServer((req, res) => {
  res.end('Hello World\n')
}).listen(3000)
$ curl localhost:3000
Hello World

对,就这么简单。因为 Node.js 已经把具体实现细节给封装起来了,我们只需要调用 http 模块提供的方法即可

那么,一个请求是如何处理,然后响应的呢?让我们来看看源码

Base

首先,让我们理一理思路

             _______
            |       | <== res
request ==> |   ?   | 
            |_______| ==> req
               /\
               ||
        http.createServer()
  • 先调用 http.createServer() 生成一个 http.Server 对象 ( 黑盒 ) 来处理请求
  • 每次收到请求,都先解析生成 req ( http.IncomingMessage ) 和 res ( http.ServerResponse ),然后交由用户函数处理
  • 用户函数调用 res.end() 来结束处理,响应请求

综上,我们的切入点有:

http.createServer

让我们先来看看 http.createServer()

// lib/http.js

function createServer(requestListener) {
  return new Server(requestListener);
}

// lib/_http_server.js

function Server(requestListener) {
  if (!(this instanceof Server)) return new Server(requestListener);
  net.Server.call(this, { allowHalfOpen: true });

  if (requestListener) {
    this.on('request', requestListener);
  }

  this.on('connection', connectionListener);

  // ...
}

http.createServer() 函数返回一个 http.Server 实例

该实例监听了 request 和 connection 两个事件

  • request 事件:绑定 requestListener() 函数,req 和 res 准备好时触发
  • connection 事件:绑定 connectionListener() 函数,连接时触发

用户函数是 requestListener(),因此,我们需要知道 request 事件何时触发

// lib/_http_server.js

function connectionListener(socket) {
  // ...

  // 从 parsers 中取一个 parser
  var parser = parsers.alloc();
  parser.reinitialize(HTTPParser.REQUEST);
  parser.socket = socket;
  socket.parser = parser;

  // ...

  state.onData = socketOnData.bind(undefined, this, socket, parser, state);

  // ...

  socket.on('data', state.onData);

  // ...
}

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

当连接建立时,触发 connnection 事件,执行 connectionListener()

这里的 socket 正是连接的 socket 对象,给 socket 绑定 data 事件用来处理数据,处理数据用到的 parser 是从 parsers 中取出来的

data 事件触发时,执行 socketOnData(),最后调用 parser.execute() 来解析 HTTP 报文

值得一提的是 parsers 由一个叫做 FreeList ( wiki ) 的数据结构实现,其主要目的是复用 parser

// lib/internal/freelist.js

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;
  }
}

module.exports = FreeList;

通过调用 parsers.alloc()parsers.free(parser) 来获取释放 parser ( http 模块中 max 为 1000 )

解析生成 req 和 res

既然,HTTP 报文是由 parser 来解析的,那么,就让我们来看看 parser 是如何创建的吧

// lib/_http_common.js

const binding = process.binding('http_parser');
const HTTPParser = binding.HTTPParser;

// ...

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;

  parser[kOnHeaders] = parserOnHeaders;
  parser[kOnHeadersComplete] = parserOnHeadersComplete;
  parser[kOnBody] = parserOnBody;
  parser[kOnMessageComplete] = parserOnMessageComplete;
  parser[kOnExecute] = null;

  return parser;
});

function parserOnHeadersComplete(versionMajor, versionMinor, headers, method, url, statusCode, statusMessage, upgrade, shouldKeepAlive) {
  // ...
    
  if (!upgrade) {
    skipBody = parser.onIncoming(parser.incoming, shouldKeepAlive);
  }

  // ...
}

// lib/_http_server.js

function connectionListener(socket) {
  // ...
  parser.onIncoming = parserOnIncoming.bind(undefined, this, socket, state);
  // ...
}

parser 是由 http_parser 这个库实现。不难看出,这里的 parser 也是基于事件的

在解析过程中,所经历的事件:

  • kOnHeaders:不断解析获取的请求头
  • kOnHeadersComplete:请求头解析完毕
  • kOnBody:不断解析获取的请求体
  • kOnMessageComplete:请求体解析完毕
  • kOnExecute:一次解析完毕 ( 无法一次性接收 HTTP 报文的情况 )

当请求头解析完毕时,对于非 upgrade 请求,可以直接执行 parser.onIncoming(),进行响应

// lib/_http_server.js

// 生成 response,并触发 request 事件
function parserOnIncoming(server, socket, state, req, keepAlive) {
  state.incoming.push(req);

  // ...

  var res = new ServerResponse(req);
  
  // ...

  if (socket._httpMessage) {
    state.outgoing.push(res);
  } else {
    res.assignSocket(socket);
  }

  // ...

  res.on('finish', resOnFinish.bind(undefined, req, res, socket, state, server));

  // ...

    server.emit('request', req, res);
  
  // ...
}

function resOnFinish(req, res, socket, state, server) {
  // ...

  state.incoming.shift();

  // ...

  res.detachSocket(socket);

  // ...
    var m = state.outgoing.shift();
    if (m) {
      m.assignSocket(socket);
    }
  }
}

可以看出,在源码中,对每一个 socket,维护了 state.incomingstate.outgoing 两个队列,分别用于存储 req 和 res

当 finish 事件触发时,将 req 和 res 从队列中移除

执行 parserOnIncoming() 时,最后会触发 request 事件,来调用用户函数

调用用户函数

就拿最开始的例子来说:

const http = require('http')

http.createServer((req, res) => {
  res.end('Hello World\n')
}).listen(3000)

通过调用 res.end('Hello World\n') 来告诉 res,处理完成,可以响应并结束请求

res.end

ServerResponse 继承自 OutgoingMessage,res.end() 正是 OutgoingMessage.prototype.end()

// lib/_http_outgoing.js

OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
  // ...
  if (chunk) {
    // ...
    write_(this, chunk, encoding, null, true);
  } else if (!this._header) {
    this._contentLength = 0;
    // 生成响应头
    this._implicitHeader();
  }

  // ...
  // 触发 finish 事件
  var finish = onFinish.bind(undefined, this);

  var ret;
  if (this._hasBody && this.chunkedEncoding) {
    // trailer 头相关
    ret = this._send('0\r\n' + this._trailer + '\r\n', 'latin1', finish);
  } else {
    // 保证一定会触发 finish 事件
    ret = this._send('', 'latin1', finish);
  }

  this.finished = true;

  // ...
};

function write_(msg, chunk, encoding, callback, fromEnd) {
  // ...

  if (!msg._header) {
    msg._implicitHeader();
  }

  // ...

  var len, ret;
  if (msg.chunkedEncoding) { // 响应大文件时,分块传输
    if (typeof chunk === 'string')
      len = Buffer.byteLength(chunk, encoding);
    else
      len = chunk.length;

    msg._send(len.toString(16), 'latin1', null);
    msg._send(crlf_buf, null, null);
    msg._send(chunk, encoding, null);
    ret = msg._send(crlf_buf, null, callback);
  } else {
    ret = msg._send(chunk, encoding, callback);
  }

  // ...
}

执行函数时,如果给了 chunk,就先 write_(chunk),写入 chunk。再 _send('', 'latin1', finish) 绑定 finish 函数。待写入完成后,触发 finish 事件,结束响应

在写入 chunk 之前,必须确保 headers 已经生成,如果没有则调用 _implicitHeader() 隐式生成 headers

// lib/_http_server.js

ServerResponse.prototype._implicitHeader = function _implicitHeader() {
  this.writeHead(this.statusCode);
};

ServerResponse.prototype.writeHead = writeHead;
function writeHead(statusCode, reason, obj) {
  // ...
  // 将 obj 和 this[outHeadersKey] 中的所有 header 放到 headers 中
  // ...

  var statusLine = 'HTTP/1.1 ' + statusCode + ' ' + this.statusMessage + CRLF;

  this._storeHeader(statusLine, headers);
}

// lib/_http_outgoing.js

OutgoingMessage.prototype._storeHeader = _storeHeader;
function _storeHeader(firstLine, headers) {
  // > GET /index.html HTTP/1.1\r\n
  // < HTTP/1.1 200 OK\r\n

  // ...
  // 校验 header 并生成 HTTP 报文头
  // ...

  this._header = state.header + CRLF;
  this._headerSent = false;

  // ...
}

headers 生成后,保存在 this._header 中,此时,响应报文头部已经完成,只需要在响应体之前写入 socket 即可

write_() 函数,内部也是调用 _send() 函数写入数据

// lib/_http_outgoing.js

OutgoingMessage.prototype._send = function _send(data, encoding, callback) {
  if (!this._headerSent) { // 将 headers 添加到 data 前面
    if (typeof data === 'string' &&
        (encoding === 'utf8' || encoding === 'latin1' || !encoding)) {
      data = this._header + data;
    } else {
      var header = this._header;
      if (this.output.length === 0) {
        this.output = [header];
        this.outputEncodings = ['latin1'];
        this.outputCallbacks = [null];
      } else {
        this.output.unshift(header);
        this.outputEncodings.unshift('latin1');
        this.outputCallbacks.unshift(null);
      }
      this.outputSize += header.length;
      this._onPendingData(header.length);
    }
    this._headerSent = true;
  }
  return this._writeRaw(data, encoding, callback);
};

OutgoingMessage.prototype._writeRaw = _writeRaw;
function _writeRaw(data, encoding, callback) {
  const conn = this.connection;

  // ...

  if (conn && conn._httpMessage === this && conn.writable && !conn.destroyed) {
    if (this.output.length) { // output 中有缓存,先写入缓存
      this._flushOutput(conn);
    } else if (!data.length) { // 没有则异步执行回调
      if (typeof callback === 'function') {
        nextTick(this.socket[async_id_symbol], callback);
      }
      return true;
    }
    // 直接写入 socket
    return conn.write(data, encoding, callback);
  }
  // 加入 output 缓存
  this.output.push(data);
  this.outputEncodings.push(encoding);
  this.outputCallbacks.push(callback);
  this.outputSize += data.length;
  this._onPendingData(data.length);
  return false;
}

对于 res.end() 来说,直接在 nextTick() 中触发 finish 事件,结束响应

End

到这里,我们已经走完了一个请求从接收到响应的过程

除此之外,在 http 模块中,还考虑了许多的实现细节,非常值得一看

参考:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容