深入理解nodejs的HTTP處理流程

flydean發表於2021-02-03

簡介

我們已經知道如何使用nodejs搭建一個HTTP服務,今天我們會詳細的介紹nodejs中的HTTP處理流程,從而對nodejs的HTTP進行深入的理解。

使用nodejs建立HTTP服務

使用nodejs建立HTTP服務很簡單,nodejs提供了專門的HTTP模組,我們可以使用其中的createServer方法來輕鬆建立HTTP服務:

const http = require('http');

const server = http.createServer((request, response) => {
  // magic happens here!
});

首先createServer方法傳入的是一個callback函式,這個callback函式將會在每次服務端接收到客戶端的請求時呼叫。所以這個callback函式,也叫做 request handler.

再看看createServer的返回值,createServer返回的是一個EventEmitter物件。

之前我們也介紹過了EventEmitter,它可以傳送和接收事件,所以我們可以使用on來監聽客戶端的事件。

上面的程式碼相當於:

const server = http.createServer();
server.on('request', (request, response) => {
  // the same kind of magic happens here!
});

當傳送request事件的時候,就會觸發後面的handler method,並傳入request和response引數。我們可以在這個handler中編寫業務邏輯。

當然,為了讓http server正常執行,我們還需要加上listen方法,來繫結ip和埠,以最終啟動服務。

const hostname = '127.0.0.1'
const port = 3000

server.listen(port, hostname, () => {
  console.log(`please visit http://${hostname}:${port}/`)
})

解構request

上面的request引數實際上是一個http.IncomingMessage物件,我們看下這個物件的定義:

    class IncomingMessage extends stream.Readable {
        constructor(socket: Socket);

        aborted: boolean;
        httpVersion: string;
        httpVersionMajor: number;
        httpVersionMinor: number;
        complete: boolean;
        /**
         * @deprecate Use `socket` instead.
         */
        connection: Socket;
        socket: Socket;
        headers: IncomingHttpHeaders;
        rawHeaders: string[];
        trailers: NodeJS.Dict<string>;
        rawTrailers: string[];
        setTimeout(msecs: number, callback?: () => void): this;
        /**
         * Only valid for request obtained from http.Server.
         */
        method?: string;
        /**
         * Only valid for request obtained from http.Server.
         */
        url?: string;
        /**
         * Only valid for response obtained from http.ClientRequest.
         */
        statusCode?: number;
        /**
         * Only valid for response obtained from http.ClientRequest.
         */
        statusMessage?: string;
        destroy(error?: Error): void;
    }

通常我們需要用到request中的method,url和headers屬性。

怎麼從request中拿到這些屬性呢?對的,我們可以使用ES6中解構賦值:

const { method, url } = request;

const { headers } = request;
const userAgent = headers['user-agent'];

其中request的headers是一個IncomingHttpHeaders,它繼承自NodeJS.Dict。

處理Request Body

從原始碼可以看出request是一個Stream物件,對於stream物件來說,我們如果想要獲取其請求body的話,就不像獲取靜態的method和url那麼簡單了。

我們通過監聽Request的data和end事件來處理body。

let body = [];
request.on('data', (chunk) => {
  body.push(chunk);
}).on('end', () => {
  body = Buffer.concat(body).toString();
  // at this point, `body` has the entire request body stored in it as a string
});

因為每次data事件,接收到的chunk實際上是一個Buffer物件。我們將這些buffer物件儲存起來,最後使用Buffer.concat來對其進行合併,最終得到最後的結果。

直接使用nodejs來處理body看起來有點複雜,幸運的是大部分的nodejs web框架,比如koa和express都簡化了body的處理。

處理異常

異常處理是通過監聽request的error事件來實現的。

如果你在程式中並沒有捕獲error的處理事件,那麼error將會丟擲並終止你的nodejs程式,所以我們一定要捕獲這個error事件。

request.on('error', (err) => {
  // This prints the error message and stack trace to `stderr`.
  console.error(err.stack);
});

解構response

response是一個http.ServerResponse類:

    class ServerResponse extends OutgoingMessage {
        statusCode: number;
        statusMessage: string;

        constructor(req: IncomingMessage);

        assignSocket(socket: Socket): void;
        detachSocket(socket: Socket): void;
        // https://github.com/nodejs/node/blob/master/test/parallel/test-http-write-callbacks.js#L53
        // no args in writeContinue callback
        writeContinue(callback?: () => void): void;
        writeHead(statusCode: number, reasonPhrase?: string, headers?: OutgoingHttpHeaders): this;
        writeHead(statusCode: number, headers?: OutgoingHttpHeaders): this;
        writeProcessing(): void;
    }

對於response來說,我們主要關注的是statusCode:

response.statusCode = 404; 

Response Headers:

response提供了setHeader方法來設定相應的header值。

response.setHeader('Content-Type', 'application/json');
response.setHeader('X-Powered-By', 'bacon');

還有一個更加直接的同時寫入head和status code:

response.writeHead(200, {
  'Content-Type': 'application/json',
  'X-Powered-By': 'bacon'
});

最後,我們需要寫入response body,因為response是一個WritableStream,所以我們可以多次寫入,最後以end方法結束:

response.write('<html>');
response.write('<body>');
response.write('<h1>Hello, World!</h1>');
response.write('</body>');
response.write('</html>');
response.end();

或者我們可以用一個end來替換:

response.end('<html><body><h1>Hello, World!</h1></body></html>');

綜上,我們的程式碼是這樣的:

const http = require('http');

http.createServer((request, response) => {
  const { headers, method, url } = request;
  let body = [];
  request.on('error', (err) => {
    console.error(err);
  }).on('data', (chunk) => {
    body.push(chunk);
  }).on('end', () => {
    body = Buffer.concat(body).toString();
    // BEGINNING OF NEW STUFF

    response.on('error', (err) => {
      console.error(err);
    });

    response.statusCode = 200;
    response.setHeader('Content-Type', 'application/json');
    // Note: the 2 lines above could be replaced with this next one:
    // response.writeHead(200, {'Content-Type': 'application/json'})

    const responseBody = { headers, method, url, body };

    response.write(JSON.stringify(responseBody));
    response.end();
    // Note: the 2 lines above could be replaced with this next one:
    // response.end(JSON.stringify(responseBody))

    // END OF NEW STUFF
  });
}).listen(8080);

本文作者:flydean程式那些事

本文連結:http://www.flydean.com/nodejs-http-in-depth/

本文來源:flydean的部落格

歡迎關注我的公眾號:「程式那些事」最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

相關文章