Node.js 中的 HTTP 模块

Node.js 中的 HTTP 模块

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
2
3
4
5
6
7
8
9
10
11
12
13
14
var http = require('http')

// 创建 server 对象,并添加 request 事件监听器
var server = http.createServer(function (req, res) {
res.writeHeader(200, { 'Content-Type': 'text/plain' })
res.end('baidu.com')
})

// 创建 server 对象,通过 server 对象的 request 事件添加事件事件监听器
var server = new http.Server()
server.on('request', function (req, res) {
res.writeHeader(200, { 'Content-Type': 'text/plain' })
res.end('baidu.com')
})

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
2
3
4
5
6
7
let server = http.createServer((req, res) => {
let { pathname, query } = url.parse(req.url, true)
console.log(pathname) // index.html
console.log(query) // { name: 123 }
console.log(req.url) // /index.html?name=123
console.log(req.headers) // 获取请求头
})

response 代表着响应信息

1
2
3
4
5
6
7
8
9
let server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'text/html;charset=utf8')
// 一旦调用会立刻向客户端发送
res.writeHead(200, {
'Content-Type': 'text/html;charset=utf8'
})
res.statusCode = 400
res.end()
})

客户端

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{ 
domain: null,
_events: { free: [Function] },
_maxListeners: undefined,
defaultPort: 80,
protocol: 'http:',
options: { path: null },
requests: {},
sockets: {},
freeSockets: {},
keepAliveMsecs: 1000,
keepAlive: false,
maxSockets: Infinity,
maxFreeSockets: 256
}

GET 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const http = require('http')
const options = {
host: 'localhost',
port: 8080,
method: 'get',
path: '/post'
}

let req = http.request(options)

// 当服务器把请求体发回来的时候,或者说客户端接受到响应的时候
req.on('response', (res) => {
let result = []
res.on('data', (data) => {
result.push(data)
})
res.on('end', () => {
let str = Buffer.concat(result)
console.log(str.toString())
})
})

// 只有调用 end() 才会真正向服务器发请求
req.end()

对应服务端代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const express = require('express')
const app = express()
const bodyParser = require('body-parser')

// 处理 JSON 的请求体
app.use(bodyParser.json())

// GET 请求的内容是存储在 req.body 当中
app.get('/post', (req, res, next) => {
// console.log(req.body)
res.send('123')
})

app.listen(8080)

POST 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
let http = require('http')
let options = {
host: 'localhost',
port: 8080,
method: 'POST',
path: '/post',
headers: {
'Content-Type': 'application/json'
}
}

// 需要注意,此时请求并没发出
let req = http.request(options)

// 当服务器把请求体发回来的时候,或者说客户端接受到响应的时候
req.on('response', (res) => {
let result = []
res.on('data', (data) => {
result.push(data)
})
res.on('end', (data) => {
let str = Buffer.concat(result)
console.log(str.toString())
})
})

// 向请求体写数据
req.write('{"name": "zhangsan"}')

// 是结束写入请求体,只有调用 end() 才会真正向服务器发请求
req.end()

对应服务端代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
const express = require('express')
const app = express()
const bodyParser = require('body-parser')

// 针对 POST 请求,因为内容是一个 chunk 数据流累计的结果,所以采用 bodyParser 来进行处理
app.use(bodyParser.json())

app.post('/post', (req, res, next) => {
// console.log(req.body)
res.send('123')
})

app.listen(8080)

请求与响应过程

先来回顾一下之前的示例,创建一个基本的服务器

1
2
3
4
5
const http = require('http')

http.createServer((req, res) => {
res.end('hello world')
}).listen(8080)

使用起来就是这么简单,因为 Node.js 已经把具体实现细节给封装起来了,我们只需要调用 HTTP 模块提供的方法即可,那么,一个请求是如何处理,然后响应的呢?我们先来简单的梳理一下

1
2
3
4
5
6
7
             _______
| | <== res
request ==> | ? |
|_______| ==> req
/\
||
http.createServer()
  • 先调用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function IncomingMessage(socket) {
Stream.Readable.call(this)

this.socket = socket
this.connection = socket

this.httpVersion = null
this.complete = false
this.headers = {} // 解析后的请求头
this.rawHeaders = [] // 原始的头部信息

// request (server) only
this.url = '' // 请求 url 地址
this.method = null // 请求地址
}

util.inherits(IncomingMessage, Stream.Readable)

HTTP 协议是基于请求和响应,请求对象我们已经介绍了,那么接下来就是响应对象,在 Node.js 中,响应对象是 ServerResponse 类的实例

ServerResponse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function ServerResponse(req) {
OutgoingMessage.call(this)

if (req.method === 'HEAD') this._hasBody = false

this.sendDate = true
this._sent100 = false
this._expect_continue = false

if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) {
this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te)
this.shouldKeepAlive = false
}
}

util.inherits(ServerResponse, OutgoingMessage)

通过以上代码,我们可以发现 ServerResponse 继承于 OutgoingMessage,在 OutgoingMessage 对象中会包含用于生成响应报文的相关信息,下面就让我们正式开始探寻 http.createServer() 方法的内部原理

http.createServer

http.createServer 的实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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 事件后,就会调用我们设置的 requestListener 函数,如下

1
2
3
(req, res) => {
res.end('hello world')
}

connectionListenerInternal

connection 事件,顾名思义用来跟踪网络连接,因此我们需要知道 request 事件何时触发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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)

// 获取 parser 对象(见下方)
var parser = parsers.alloc()
parser.reinitialize(HTTPParser.REQUEST)
parser.socket = socket
socket.parser = parser
parser.incoming = null

var state = {
outgoing: [],
incoming: [],
//...
}
parser.onIncoming = parserOnIncoming.bind(undefined, server, socket, state)
}

在 connectionListenerInternal 函数内部可以发现有一个 parser 对象,parser 对象是由一个叫做 FreeList 的数据结构实现,其主要目的是复用 parser,通过调用 parsers.alloc() 和 parsers.free(parser) 来获取释放 parser,下面就先来看看 FreeList 这个对象

FreeList

在 Node.js 中为了避免频繁创建和销毁对象,有一个通用的 FreeList 机制,在 HTTP 模块中,就利用到了 FreeList 机制,即用来动态管理 http-parser 对象

1
2
3
4
var parsers = new FreeList('parsers', 1000, function () {
var parser = new HTTPParser(HTTPParser.REQUEST)
//...
}

具体实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 请求的场景下,当新的请求到来时,我们通过调用 parsers.alloc() 方法来获取 http-parser 对象,从而解析 HTTP 请求,当完成 HTTP 解析任务后,我们可以通过调用 parsers.free() 方法来归还 http-parser 对象

parserOnIncoming

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

return parser
})

在上面以 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function parserOnIncoming(server, socket, state, req, keepAlive) {

// 缓冲 IncomingMessage 实例
state.incoming.push(req)

var res = new server[kServerResponse](req)

if (socket._httpMessage) {
// 缓冲 ServerResponse 实例
state.outgoing.push(res)
} else {
res.assignSocket(socket)
}

// 判断请求头是否包含 expect 字段且 http 协议的版本为 1.1
if (req.headers.expect !== undefined &&
(req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) {
// continueExpression: /(?:^|\W)100-continue(?:$|\W)/i
// Expect: 100-continue
if (continueExpression.test(req.headers.expect)) {
res._expect_continue = true

if (server.listenerCount('checkContinue') > 0) {
server.emit('checkContinue', req, res)
} else {
res.writeContinue()
server.emit('request', req, res)
}
} else if (server.listenerCount('checkExpectation') > 0) {
server.emit('checkExpectation', req, res)
} else {
// http 协议中的 417 Expectation Failed 状态码表示客户端错误
// 意味着服务器无法满足 Expect 请求消息头中的期望条件
res.writeHead(417)
res.end()
}
} else {
server.emit('request', req, res)
}
return 0
}

通过观察上面的代码,我们终于发现了 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 函数

参考

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×