Native HTTP server

Those of you who have studied Nodejs will be familiar with the following code:

const http = require('http');
let server = http.createServer((req, res) = > {
  / /... The callback function that prints Hello World
  res.end('hello world! ')
})
server.listen(3000)
Copy the code

With just a few lines of code, you’ve built a simple server that handles HTTP requests in the form of callback functions. A more explicit equivalent of the above code is as follows:

let server = new http.Server();
server.on("request".function(req, res){
  / /... The callback function that prints Hello World
  res.end('hello world! ')}); server.listen(3000);
Copy the code

An instance of HttpServer is created to listen for request events on port 3000. HttpServer inherits net.Server. It parses the connected socket object using http_Parser. After parsing HTTP headers, the Request event is raised and the body data continues to be stored in the stream until the data event is received.

Req is an instance of HTTP. IncomingMessage (which also implements the Readable Stream interface), as documented

Res is an instance of HTTP. ServerResponse (which also implements the Writable Stream interface), as described in the documentation

Koa writes to the HTTP server

A Koa application is an object containing a set of middleware functions that are organized and executed in a stack-like fashion.

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);
Copy the code

The way Koa writes to the HTTP server is very different from the way we write directly through the Node HTTP module. Node’s HTTP server is created using methods such as http.createserver. How does Koa encapsulate a KOA-style server from a native method? To understand this principle is to understand the concept of Koa framework design.

Koa source code parsing

The best way to understand this is to look directly at Koa’s source code. The Koa code is very compact, about 1700 lines, and not too difficult to read. Let’s take the above demo as an example to conduct an analysis. I divide koA execution into two stages. The first stage is the initialization stage, where the main work is to initialize the used middleware (async/await form) and listen at the specified port.

Initialization phase

The two main functions used in the first phase are app.use and app.listen. These two functions exist in application.js. The main function of app.use is to push middleware into a list called middleware.

use(fn) {
    ...
    this.middleware.push(fn);
    return this;
  }
Copy the code

The main purpose of Listen is to create an HTTP server and listen on the specified port as we did in part 1. The listener for the request event is this.callback(), which returns (req, res) => {}.

listen(... args) {const server = http.createServer(this.callback());
    returnserver.listen(... args); }Copy the code

Parse the callback function as follows:

/** * Return a request handler callback * for node's native http server. * * @return {Function} * @api public */

  callback() {
    const fn = compose(this.middleware); // Combine the middleware functions into a function fn
    // ...
    const handleRequest = (req, res) = > {
      const ctx = this.createContext(req, res);  // Create a context CTX using req and res
      return this.handleRequest(ctx, fn); 
    };

    return handleRequest;
  }
Copy the code

Now that the first phase is complete, through source code analysis, we can see that it actually performs roughly the same things we did in part 1 using the Node HTTP module. For example, the compose function is composed. The async/await function returns the form Promise, how can it be guaranteed to be executed sequentially? My initial idea was to put the next middleware in the then method of the previous middleware execution result, which goes something like this:

compose(middleware) {
        return (a)= > {
          let composePromise = Promise.resolve();   
          middleware.forEach(task= > { composePromise = composePromise.then((a)= >{return task&&task()}) }) 
          returncomposePromise; }}Copy the code

F1 ().then(f2).then(f3).. Koa uses another approach for koa-compose:

function compose (middleware) {
  // ...
  return function (context, next) {
    // last called middleware #
    let index = - 1
    return dispatch(0)
    
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if(! fn)return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)}}))catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
Copy the code

It starts from the first middleware, meets next, interrupts the code execution of this middleware, jumps to the code of the corresponding next middleware execution period… Until the last middleware, and then reverse back to the penultimate middleware next part of the code execution, after completion will continue to back… Until the next part of the code execution of the first middleware, next, is complete, the middleware is completely executed. So we can realize what we call the onion ring model.

Request processing phase

When a request comes in, it goes into the callback function of the Request event, encapsulated in handleRequest in Koa:

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    // KoA's default error handler, which handles the end of an exception caused by an error
    const onerror = err= > ctx.onerror(err);
    The function responds when HTTP code is empty, HTTP method returns head, and body returns a stream or json
    const handleResponse = (a)= > respond(ctx);
    // a third-party function that listens for the end of an HTTP response event and executes a callback
    // If there is an error in response, the logic in ctx.onerror is executed to set the response type, status code and error message, etc
    onFinished(res, onerror);
    
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
Copy the code

When the request comes in, it executes the compose function encapsulated in the first phase, then enters the handleResponse for some finishing touches. At this point, the entire request processing phase is complete.

conclusion

Koa is a very streamlined Web framework, and the source code itself does not contain any middleware, allowing us to combine some middleware to use as needed. It implements the Onion pattern with async/await.