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
函数