HTTP 模块是 Node.js 中非常重要的一个核心模块,通过 HTTP 模块,可以使用其 http.createServer() 方法创建一个 HTTP 服务器,也可以使用其 http.request() 方法创建一个 HTTP 客户端,Node.js 对 HTTP 协议及相关 API 的封装比较底层,其仅能处理流和消息,对于消息的处理,也仅解析成『报文头』和『报文体』,但是不解析实际的报文头和报文体内容,这样不仅解决了 HTTP 原本比较难用的特性,也可以支持更多的 HTTP 应用
本文内容主要分为两部分『客户端』与『服务端』,我们下面就一个一个来进行了解
服务端
实现 HTTP 服务端功能,要通过 http.createServer() 方法创建一个服务端对象 http.Server,这个方法接收一个可选传入参数 requestListener,该参数是一个函数,传入后将做为 http.Server 的 request 事件监听,不传入时,则需要通过在 http.Server 对象的 request 事件中单独添加,下面是两种创建 http.Server 对象及添加 request 事件监听器的示例
1 | var http = require('http') |
http.server
http.server 是一个基于事件的 HTTP 服务器,所有的请求都被封装到独立的事件当中,我们只需要对事件编写相应的函数就可以实现 HTTP 服务器的所有功能,它继承自 EventEmitter,提供了以下的事件
request,当客户端请求到来的时候触发该事件,提供两个参数request和response,分别是http.ServerRequest和http.ServerResponse,表示请求和响应的信息connection,当TCP建立连接的时候触发该事件,提供了一个参数socket,为net.socket的实例(底层协议对象)close,当服务器关闭的时候会被触发
除此之外还有 checkContinue、upgrade、clientError 等事件,一般比较常见的还是 request 事件,所以官方也提供了一个更为简便的创建方式 http.createServer([requestListener]),就如上面示例当中的一样
request && response
request 代表着请求信息,比如我们请求的 url 地址为 http://localhost:8080/index.html?name=123,则服务器接收到的信息如下
1 | let server = http.createServer((req, res) => { |
response 代表着响应信息
1 | let server = http.createServer((req, res) => { |
客户端
HTTP 模块不仅可以做为 HTTP 服务器使用,也适用于客户端,HTTP 模块提供了创建 HTTP 客户端对象的方法,使用客户端对象可以创建对 HTTP 服务的访问,http.request() 方法用于创建 HTTP 请求,该方法会返回一个 http.ClientRequest 对象, 是 http.createClient() 方法的替代方法
请求创建后并不会立即发送请求,我们还可以继续访问和设置请求头,比如使用 setHeader(name, value)、getHeader(name) 和 removeHeader(name) 等 API 进行修改,实际的请求头会与第一个数据块一起发送或当调用 request.end() 时发送
http.ClientRequest
http.ClientRequest 对象由 http.request() 创建并返回,它是一个正在处理的 HTTP 请求,其头部已经在队列中,Header 将会随着第一个数据块发送,或在连接关闭时发送
http.ClientRequest 实现了 Writable Stream 接口,其对于向服务器发送数据,本质上是对这个可写流的操作,它还是一个 EventEmitter,包含 response、socket、upgrade、continue 等事件
http.Agent
http.Agent 是会把套接字做成资源池,用于 HTTP 客户端请求,当需要自定义一些自定义的代理参数(如主机的套接字并发数、套接字发送 TCP KeepAlive 包的频率等)时可以设置此对象,该对象由构造函数 new Agent([options]) 创建返回
更多详细内容可以参考官方文档 new Agent([options])
http.globalAgent
Agent 的全局实例,是 HTTP 客户端的默认请求代理对象,其结构类似如下
1 | { |
GET 请求
1 | const http = require('http') |
对应服务端代码如下
1 | const express = require('express') |
POST 请求
1 | let http = require('http') |
对应服务端代码如下
1 | const express = require('express') |
请求与响应过程
先来回顾一下之前的示例,创建一个基本的服务器
1 | const http = require('http') |
使用起来就是这么简单,因为 Node.js 已经把具体实现细节给封装起来了,我们只需要调用 HTTP 模块提供的方法即可,那么,一个请求是如何处理,然后响应的呢?我们先来简单的梳理一下
1 | _______ |
- 先调用
http.createServer()生成一个http.Server对象来处理请求 - 每次收到请求,都先解析生成
req(http.IncomingMessage)和res(http.ServerResponse),然后交由用户函数处理 - 用户函数调用
res.end()来结束处理,响应请求
我们先来看看 http.IncomingMessage 和 http.ServerResponse
IncomingMessage
在 Node.js 服务器接收到请求时,会利用 http-parser 对象来解析请求报文,为了便于开发者使用,Node.js 会基于解析后的请求报文创建 IncomingMessage 对象,IncomingMessage 构造函数(代码片段)如下
1 | function IncomingMessage(socket) { |
HTTP 协议是基于请求和响应,请求对象我们已经介绍了,那么接下来就是响应对象,在 Node.js 中,响应对象是 ServerResponse 类的实例
ServerResponse
1 | function ServerResponse(req) { |
通过以上代码,我们可以发现 ServerResponse 继承于 OutgoingMessage,在 OutgoingMessage 对象中会包含用于生成响应报文的相关信息,下面就让我们正式开始探寻 http.createServer() 方法的内部原理
http.createServer
http.createServer 的实现如下
1 | // lib/http.js |
http.createServer() 函数返回一个 http.Server 实例,该实例监听了 request 和 connection 两个事件
request事件绑定requestListener()函数,req和res准备好时触发connection事件绑定connectionListener()函数,连接时触发
用户函数是 requestListener(),也就是说,在触发 request 事件后,就会调用我们设置的 requestListener 函数,如下
1 | (req, res) => { |
connectionListenerInternal
connection 事件,顾名思义用来跟踪网络连接,因此我们需要知道 request 事件何时触发
1 | function connectionListener(socket) { |
在 connectionListenerInternal 函数内部可以发现有一个 parser 对象,parser 对象是由一个叫做 FreeList 的数据结构实现,其主要目的是复用 parser,通过调用 parsers.alloc() 和 parsers.free(parser) 来获取释放 parser,下面就先来看看 FreeList 这个对象
FreeList
在 Node.js 中为了避免频繁创建和销毁对象,有一个通用的 FreeList 机制,在 HTTP 模块中,就利用到了 FreeList 机制,即用来动态管理 http-parser 对象
1 | var parsers = new FreeList('parsers', 1000, function () { |
具体实现如下
1 | class FreeList { |
在处理 HTTP 请求的场景下,当新的请求到来时,我们通过调用 parsers.alloc() 方法来获取 http-parser 对象,从而解析 HTTP 请求,当完成 HTTP 解析任务后,我们可以通过调用 parsers.free() 方法来归还 http-parser 对象
parserOnIncoming
既然,HTTP 报文是由 parser 来解析的,那么就让我们来看看 parser 是如何创建的吧
1 | var parsers = new FreeList('parsers', 1000, function () { |
在上面以 parser 开头的这些对象,都是定义在 _http_common.js 文件中的函数对象,让我们来简单的梳理一下
parserOnHeaders,当请求头跨多个TCP数据包或者过大无法再一个运行周期内处理完才会调用该方法kOnHeadersComplete,请求头解析完成后,会调用该方法,方法内部会创建IncomingMessage对象,填充相关的属性,比如url、httpVersion、method和headers等parserOnBody,不断解析已接收的请求体数据
这里需要注意的是,请求报文的解析工作是由 C++ 来完成,内部通过 binding 来实现,具体可以参考 deps/http_parser 目录
1 | const { methods, HTTPParser } = process.binding('http_parser') |
在 connectionListenerInternal 函数中,在最后一行设置了 parser 对象的 onIncoming 属性为绑定后的 parserOnIncoming 函数
1 | function parserOnIncoming(server, socket, state, req, keepAlive) { |
通过观察上面的代码,我们终于发现了 request 事件的踪迹,在 parserOnIncoming 函数内,我们会基于 req 请求对象创建 ServerResponse 响应对象,在创建响应对象后,会判断请求头是否包含 expect 字段,然后针对不同的条件做出不同的处理,对于之前最早的示例来说,程序会直接走 else 分支,即触发 request 事件,并传递当前的请求对象和响应对象
最后我们来回顾一下整个流程
- 调用
http.createServer()方法创建server对象,该对象创建完后,我们调用listen()方法执行监听操作 - 当
server接收到客户端的连接请求,在成功创建socket对象后,会触发connection事件 - 当
connection事件触发后,会执行对应的connectionListener回调函数,在函数内部会利用http-parser对象,对请求报文进行解析 - 在完成请求头的解析后,会创建
IncomingMessage对象,并填充相关的属性,比如url、httpVersion、method和headers等 - 在配置完
IncomingMessage对象后,会调用parserOnIncoming函数,在该函数内会构建ServerResponse响应对象,如果请求头不包含expect字段,则server就会触发request事件,并传递当前的请求对象和响应对象 request事件触发后,就会执行我们设定的requestListener函数