Nodejs 系列六:HTTP 模块

前言

本文是笔者所总结的有关 Nodejs 基础系列之一,

本文为作者原创作品,转载请注明出处;

纲目

https://nodejs.org/dist/latest-v8.x/docs/api/http.html 中有关 HTTP 的介绍非常多,也比较的零散,前后的逻辑性非常的分散,笔者不打算按照官网上的顺序照本宣科;本文着重对 Nodejs 的 HTTP 模块的脉络进行梳理,通过下面的这样一个组件图,梳理出对应的纲目;

组件图

nodejs http module component diagram

如图,囊括了 Node.js 中核心的模块以及相应笔者认为比较重要的事件;

  1. http.Agent 对象
    该对象用作客户端连接,作为 http.request(options[, callback]) 方法的 options 参数的一部分,用来向 Server 发起请求;其主要作用就是在客户端的连接中,针对不同的 Server 请求保持同一个 Socket 连接既 Connection,以达到 Connection 复用的目的;不过能否重用 Connection 还要受到 Server 的限制,具体详情参考 http.Agent 小节;
  2. http.ClientRequest
    http.request(options[, callback]) 方法调用以后返回的对象,它表示的是一个 in-progress 的请求,也就是说还没有发送之前的 Request 对象,直到我们对其调用 end() 方法使其发送出去;要注意的是,http.ClientRequest 对象所对应的回调事件是 'response' 事件及其其它事件;详情参考 http.ClientRequest 小节;
  3. http.request(options[, callback]) 客户端发起连接的方法其 options 里面包含诸多的参数,里面就包含 http.Agent;该方法调用返回一个 in-progress 的 http.ClientRequest 请求对象;详情参考 http.request() 小节;
  4. http.Server
    该对象由 http.createServer([requestListener]) 方法返回,http.Server 继承自 net.server,详情参考 http.Server 小节;
  5. http.ServerResponse
    就是 http.Server 的 'request' 事件回调方法的第二个参数;;
  6. http.createServer([requestListener]) 返回一个 http.Server 对象;详情参考 http.createServer 小节;
  7. http.IncomingMessage
    Stream 对象,http.ClientRequest 的 'response' 事件的回调参数 response 以及 http.Server 的 'request' 事件的回调参数 request 都是 http.IncomingMessage 类型的对象,归纳起来,既是 Client 请求所获得的 response 数据以及 Server 获得的 request 都是通过 http.IncomingMessage 来处理的;这也是 Node.js 异步处理网络请求的核心机制;注意,http.IncomingMessage 继承自 stream.Readable 对象,是一个实现了 EventEmitter 接口的对象;

下面笔者将针对各个核心的组件和要素逐个进行分析,

http.Agent

该对象用在 Client 端连接,作为 http.request(options[, callback]) 方法的 options 参数的一部分,用来向 Server 发起请求;Agent 实例的主要作用就是针对同一个 Server 请求保持同一个 Socket 连接既 Connection,以达到 Connection 复用的目的,当然不同的 Server 将会维护多个不同的 Connection,形成一个 Connection Pool;这样,客户端针对同一个 Server 发起的不同 Request,都会复用同一个 Connection,因为 Client 使用同一个端口,所以 Server 必然同样会使用同一个 Server Connection 来进行处理,最终达到 Client 和 Server 各自都重用各自的 Connection 来进行连接,这样就极大的减少了资源的浪费,提升性能;备注,客户端通过设置 http 连接属性 keepAlive = true 来达到这个目的;

不过能否重用 Connection 还要受到 Server 的限制,有些时候,Server 要求,不同的 request 必须使用不同的 Connection,这个时候,即便是在 Client 发情请求的时候通过 http.Agent 设置了 Connection 复用,但是在实际连接的过程中,针对每个不同的 Request 仍然需要开启不同的 Connection ( 不同的 Client Connenction 和不同的 Server Connection );

当客户端退出的时候,或者不再对 Servers 发起请求的时候,最好是通过 http.Agent.destroy() 方法来销毁 Agent 实例,这样便可以将 keep-alive 的 Connection 关闭掉已释放资源;

如果我们要将某个 socket 连接移除出其对应的 Agent 实例,两种做法,

  • emit ‘close’ 事件

    1
    2
    3
    4
    5
    http.get(options, (res) => {
    // Do stuff
    }).on('socket', (socket) => {
    socket.emit('close');
    });

    该事件将会关闭该 socket,自然该 socket 也就会移除出 Agent;

  • emit ‘agentRemove’ 事件

    1
    2
    3
    4
    5
    http.get(options, (res) => {
    // Do stuff
    }).on('socket', (socket) => {
    socket.emit('agentRemove');
    });

    该事件并不是启动关闭该 socket,相反,它试图长期的保持该 socket 连接的存在,上面这个事件的意义就在于,让该 socket 脱离该 agent 独立存在;

如果我们不期望 Client 在连接的过程中复用 Connection,那么可以通过设置 { agent : false } 来达到此目的,

1
2
3
4
5
6
7
8
http.get({
hostname: 'localhost',
port: 80,
path: '/',
agent: false // create a new agent just for this one request
}, (res) => {
// Do stuff with response
});
http.request(options[, callback]) 方法同理;最后,来看一下如何通过构建一个 Agent 实例来通过 http.request() 方法发起请求,
1
2
3
4
const http = require('http');
const keepAliveAgent = new http.Agent({ keepAlive: true });
options.agent = keepAliveAgent;
http.request(options, onResponseCallback);

初始化一个 Agent 的时候支持使用如下的参数,
nodejs how new agent and options.png

http.ClientRequest

http.ClientRequest 对象由方法 http.request(options[, callback]) 调用后返回,它表示的是一个 in-progress 的请求,也就是说还没有发送之前的 Request 对象,直到我们对其调用 end() 方法使其发送出去;要注意的是,http.ClientRequest 对象所对应的回调事件有 'response' 事件,该事件是在 http.request() 方法中默认添加的;

从下面的代码片段中,我们来看一看如何控制 ClientRequest 对象,

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
const postData = querystring.stringify({
'msg': 'Hello World!'
});

const options = {
hostname: 'www.google.com',
port: 80,
path: '/upload',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData)
}
};

const req = http.request(options, (res) => {
...
});

req.on('error', (e) => {
console.error(`problem with request: ${e.message}`);
});

// write data to request body
req.write(postData);
req.end();

完整的例子参考例子

  1. 给 ClientRequet 注册事件,上述代码 20-22 行
  2. 代码 25 行,将参数写入 request body,注意通过 querystring 转换以后参数为 msg=Hello%20World!
  3. 代码 26 行,发送请求;

http.request()

http.request(options[, callback])

客户端发起连接的方法其 options 里面包含诸多的参数,里面就包含 http.Agent;该方法调用返回一个 in-progress 的 http.ClientRequest 请求对象;

  • 初始化参数一览
    nodejs http request method parameters.png
  • 处理 Response,

    To get the response, add a listener for ‘response’ to the request object. ‘response’ will be emitted from the request object when the response headers have been received. The ‘response’ event is executed with one argument which is an instance of http.IncomingMessage.

    During the ‘response’ event, one can add listeners to the response object; particularly to listen for the ‘data’ event.

    上面的意思大致是,当通过 http.request 方法执行请求的时候,‘response’ 事件在 request 请求接收到 Server Response 的那一刹那,便会自动的被触发,返回一个 http.IncomingMessage 类型的 response,由于 http.IncomingMessage 是一个 EventEmitter 对象,因此,我们可以在该对象上继续添加事件我们感兴趣的事件,来看下面这段代码片段,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    require('http')

    ...

    const req = http.request(options, (res) => {
    console.log(`STATUS: ${res.statusCode}`);
    console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
    res.setEncoding('utf8');
    res.on('data', (chunk) => {
    console.log(`BODY: ${chunk}`);
    });
    res.on('end', () => {
    console.log('No more data in response.');
    });
    });

    通过 http.request 方法发起了请求,调用该方法的同时,会初始化一个 http.ClientRequest 对象,并且在源码 _http_client.js 中的第 164 行会注册一个只会被调用一次的 ‘response’ 事件,
    debug http client request response event register.png
    由此可知,当我们在调用 http.request() 方法向 Server 发起请求的同时,我们在 http.ClientRequest 对象上注册了只会被调用一次的 ‘response’ 事件,并且该事件将会在 Server 反馈的那一刹那被触发,并且将回调上面例子中的 (res) => {...} 函数,将 response 对象作为参数传入,而由前面的分析可知,response 是一个 EventEmitter 对象,所以,我们可以继续对它绑定事件,以便获取我们需要的信息,比如,通过绑定约定的 ‘data’ 事件来获取 response body,

    1
    2
    3
    res.on('data', (chunk) => {
    console.log(`BODY: ${chunk}`);
    });

    又比如绑定约定的 ‘end’ 事件来表示 response 结束,

    1
    2
    3
    res.on('end', () => {
    console.log('No more data in response.');
    });

http.Server

该对象由 http.createServer([requestListener]) 方法返回,http.Server 继承自 net.server,下面我们大致来看一下通过 net.createServer 创建 net.Server 的过程,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const net = require('net');

const server = net.createServer((c) => {
// 'connection' listener
console.log('client connected');
c.on('end', () => {
console.log('client disconnected');
});
c.write('hello\r\n');
c.pipe(c);
});

server.on('error', (err) => {
throw err;
});

server.listen(8124, () => {
console.log('server bound');
});
  1. 代码第 3 - 11 行,创建了一个 net.Server 对象,其中,通过回调函数捕获客户端的连接 connection,这里用的参数 _c_ 表示,通过对 _c_ 绑定 ‘end’ 事件来捕获是否连接完成,可以直接通过 _c_.write 方法向客户端发送信息,
  2. 代码第 17 - 18 行,表示该 Server 将会在哪个端口上进行监听;

有关 net.Server 的更多信息参考 https://nodejs.org/dist/latest-v8.x/docs/api/net.html#net_net_createserver_options_connectionlistener

http.Server 继承自 net.Server,表示 Node.js 中的一个 Server Socket,就类似于 java 中的 ServerSocket 对象;

http.ServerResponse

http.ServerResponse 就是 http.Server 的 'request' 事件回调方法的第二个参数;用来表示 Server 的 Response;

http.CreateServer()

http.createServer([requestListener]) 返回一个 http.Server 对象;来看一个例子

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
const http = require('http');

// Create an HTTP server
const srv = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('okay');
});
srv.on('upgrade', (req, socket, head) => {
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
'\r\n');

socket.pipe(socket); // echo back
});

// now that server is running
srv.listen(1337, '127.0.0.1', () => {

// make a request
const options = {
port: 1337,
hostname: '127.0.0.1',
headers: {
'Connection': 'Upgrade',
'Upgrade': 'websocket'
}
};

const req = http.request(options);
req.end();

req.on('upgrade', (res, socket, upgradeHead) => {
console.log('got upgraded!');
socket.end();
process.exit(0);
});
});

可见,代码第 4 行通过 http.createServer 创建了一个 server 实例,在回调方法中通过 res 既是 http.ServerResponse 向客户端进行反馈;并设置 server 实力在 1337 端口上进行监听;这个例子好玩的地方是,该 server 既是服务端又是自己的客户端,嗲吗 21 - 37 行可以看到,当 server 一旦开始 listening 也就是 ‘listening’ 事件被触发以后,其实也就是当 Server 准备开始监听了,会触发这里的回调方法,这里好玩的地方就是,它会对自己发起一个 ‘Upgrade’ 请求,也就是自己同时是自己的 Client;这里的回调方法的回调过程参考,

This function is asynchronous. When the server starts listening, the ‘listening’ event will be emitted. The last parameter callback will be added as a listener for the ‘listening’ event.

有关 server.listen() 的相关详情参考 https://nodejs.org/dist/latest-v8.x/docs/api/net.html#net_server_listen

常用的 util 模块

querystring module

用来解析和生成 URL 中的 parameters 的,

  1. 生成,

    1
    2
    3
    4
    5
    6
    7
    const querystring = require('querystring')

    const postData = querystring.stringify({
    'msg': 'Hello World!'
    });

    console.log(postData)

    输出,

    1
    msg=Hello%20World!
  2. 解析 URL 中的 parameters

url module

假设我们有如下的一个 request url,

1
'/status?name=ryan'

通过 url 模块便可以将上述的 url 分解成链接的各个部分,通过一个 json 数据返回,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ node
> require('url').parse('/status?name=ryan')
Url {
protocol: null,
slashes: null,
auth: null,
host: null,
port: null,
hostname: null,
hash: null,
search: '?name=ryan',
query: 'name=ryan',
pathname: '/status',
path: '/status?name=ryan',
href: '/status?name=ryan' }

但是,query 中的参数并没有被解析出来,两种方式可以对其进行解析

  1. 使用 querystring 模块
  2. 传递参数 true 到上面的方法调用中,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    $ node
    > require('url').parse('/status?name=ryan', true)
    Url {
    protocol: null,
    slashes: null,
    auth: null,
    host: null,
    port: null,
    hostname: null,
    hash: null,
    search: '?name=ryan',
    query: { name: 'ryan' },
    pathname: '/status',
    path: '/status?name=ryan',
    href: '/status?name=ryan' }

    可以看到,url 中的参数 query 属性以解析后的 json 格式返回了;

附录

例子

调试过程中所使用到的例子,

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
const http = require('http')
const querystring = require('querystring')

const postData = querystring.stringify({
'msg': 'Hello World!'
});

const options = {
hostname: 'www.google.com',
port: 80,
path: '/upload',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(postData)
}
};

const req = http.request(options, (res) => {
console.log(`STATUS: ${res.statusCode}`);
console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
res.setEncoding('utf8');
res.on('data', (chunk) => {
console.log(`BODY: ${chunk}`);
});
res.on('end', () => {
console.log('No more data in response.');
});
});

References

https://nodejs.org/dist/latest-v8.x/docs/api/http.html