Node.js 中的 HTTP 模块

Node.js 中的 HTTP 模块

HTTP 模块是 Node.js 中非常重要的一个核心模块,通过 HTTP 模块,可以使用其 http.createServer() 方法创建一个 HTTP 服务器,也可以使用其 http.request() 方法创建一个 HTTP 客户端,Node.jsHTTP 协议及相关 API 的封装比较底层,其仅能处理流和消息,对于消息的处理,也仅解析成『报文头』和『报文体』,但是不解析实际的报文头和报文体内容,这样不仅解决了 HTTP 原本比较难用的特性,也可以支持更多的 HTTP 应用

本文内容主要分为两部分『客户端』与『服务端』,我们下面就一个一个来进行了解

服务端

实现 HTTP 服务端功能,要通过 http.createServer() 方法创建一个服务端对象 http.Server,这个方法接收一个可选传入参数 requestListener,该参数是一个函数,传入后将做为 http.Serverrequest 事件监听,不传入时,则需要通过在 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,当客户端请求到来的时候触发该事件,提供两个参数 requestresponse,分别是 http.ServerRequesthttp.ServerResponse,表示请求和响应的信息
  • connection,当 TCP 建立连接的时候触发该事件,提供了一个参数 socket,为 net.socket 的实例(底层协议对象)
  • close,当服务器关闭的时候会被触发

除此之外还有 checkContinueupgradeclientError 等事件,一般比较常见的还是 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,包含 responsesocketupgradecontinue 等事件

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 对象来处理请求
  • 每次收到请求,都先解析生成 reqhttp.IncomingMessage)和 reshttp.ServerResponse),然后交由用户函数处理
  • 用户函数调用 res.end() 来结束处理,响应请求

我们先来看看 http.IncomingMessagehttp.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 实例,该实例监听了 requestconnection 两个事件

  • request 事件绑定 requestListener() 函数,reqres 准备好时触发
  • 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 对象,填充相关的属性,比如 urlhttpVersionmethodheaders
  • 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 对象,并填充相关的属性,比如 urlhttpVersionmethodheaders
  • 在配置完 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

×