node http transport resolution

Posted by calmchess on Mon, 07 Mar 2022 20:08:51 +0100

One time HTTP transmission resolution

The purpose of this guide will give you a clear and complete understanding of HTTP transmission processing. Regardless of the specific programming language and development environment, we assume that you already know how HTTP works in general. We also assume that you are familiar with node JS EventEmitters And Streams . Of course, if you really don't understand them, we strongly recommend that you read them quickly and completely.

Create a background service

Any web service application always needs to create a service object first. This is in node JS createServer Method.

const http = require('http');

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

Whenever an HTTP request arrives at the server, createServer The function passed in is automatically executed. So this function is also called request processing function. In fact, by createServer Constructor returned Server Object is a EventEmitter , we only simplify the process of creating {server} and adding listening events to it.

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

When an HTTP arrives at the server, node calls the request handler and generates some readily available objects to handle the transmission. These objects are , request , and , response. We'll talk about it soon.

In fact, in order to process the request, listen The server {method needs to be called explicitly on the object. In most cases, you just need to pass the port number as a parameter into the listen method as the listening port. Of course, there are also some other options, which can be referred to for details API reference documents.

Method, access address and request header

When processing a request, the first thing you need to do is look at the method and its access address to determine what reasonable behavior you should take. Node.js makes it relatively easy for us to handle by attaching these behavior attributes to the request object.

const { method, url } = request;

Note: the request # object is IncomingMessage An example of.

The "method" here is always an ordinary HTTP method action behavior (verb), and the "url" refers to the complete access address without server protocol and port number. A typical access address usually means including the third slash and everything after it.

Request headers are not hard to get. They are also in the request object and are called "headers".

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

It is very important that all request headers are lowercase, regardless of how they are actually transmitted. Therefore, in any case, parsing the request header is simplified.

If some request headers are duplicated, their values are either overwritten or divided by English semicolons. Which way depends on the specific header. In some cases, this may cause problems, so we can also use it directly rawHeaders.

Request body

When you receive a POST or PUT request, the request body is very important to your application. Compared with the access request header, getting the request body is a bit cumbersome. The {request} of the incoming request object is actually implemented ReadableStream Interface, this information flow can be monitored or connected with other flows. We can get the data by listening to 'data' and 'end' events.

Each time the captured data block is triggered in the 'data' event Buffer . If you know it is a string object, the best solution is to collect this data into an array, then splice it in the 'end' event and convert it into a string.

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
});

Note: this seems a little boring, and it is true in most cases. But fortunately, because npm There are too many such as concat-stream And body A class library shields part of the detailed logic and does these things for you. Of course, it's important for you to know what these libraries do before you use them. That's why you need to read this article!

I brought some information about the error in one stroke

Because , request , is a ReadableStream Object, it is also EventEmitter Object. So when a mistake occurs, the behavior is very similar. When an error occurs on the request stream, it will automatically fire its own 'error' event. If you don't listen to this event, this error will be thrown, which will cause your program to crash. You should add an 'error' event to listen to your request object anyway, even if you just make a log or handle it in your own unique way (of course, the best way is to return some error information, which is later).

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

Of course, there are other ways to Processing error , such as other abstract concepts and tools. But you always have to realize that mistakes do happen, so you should deal with them.

We've talked so much

Until now, we have talked about how to create an object, if you get the method, request address, request header and request body from the request. When we put them together, it looks like this:

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();
    // At this point, we have the headers, method, url and body, and can now
    // do whatever we need to in order to respond to this request.
  });
}).listen(8080); // Activates this server, listening on port 8080.

If we run this sample code, we can only {receive requests but not} responses. In fact, if you run this example in the browser, your request will only time out because the server doesn't return anything to the client at all.

After talking for so long, we haven't talked about the response object yet. It's a ServerResponse Instance, and ServerRespose is WritableStream . It contains many methods that can be used to return data to the client. We will now touch on this topic.

HTTP status code

If you don't want to set it, the default status code returned to the client is always 200. Of course, not every HTTP return code must be 200. In some cases, you must want to return a different status code, so you should set the "statusCode" attribute.

response.statusCode = 404; // Tell the client that the resource wasn't found.

We also have other shortcuts to do this, which we will see soon.

Set response header

The response header passes through a setHeader The properties of are easy to set.

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

When setting response headers, their names are case sensitive. If you repeatedly set the response header, the last set value is the value obtained by the system.

Send header display data

The method of setting the response header and status code we discussed earlier is based on the way you use "implicit setting", which means that you rely on node to send the request header before sending the message body.

If you like, you can rewrite the response header for the return stream. To do this, you can use writeHead Method rewrites the status code and response header to the message flow.

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

Once the response header is set (either implicitly or explicitly), you are ready to send the return data.

Send return body

Since the response object is a WritableStream , writing the return body to the client is just a matter of ordinary streaming methods.

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

The end method on the message flow can also bring in some optional data as the last data to be sent on the flow, so we can simply simplify the above code in the following form:

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

Note: it is important that you set the status and response headers only before you start writing data to the return body. Because the response header information always arrives before the message body.

Another thing I've brought one by one about mistakes

response backflow will also trigger the 'error' event. To some extent, you have to deal with it yourself. All the previous methods for handling errors in the # request # message flow are also applicable here.

Put everything you've learned together

Now that we've learned how to handle HTTP return information, let's put these pieces together. Based on the previous example code, we will make a server that can return all the information received from the user to the user. We will pass JSON Stringify formats the message data.

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);

Sample code for server response

Let's simplify the previous code and make a simple server that can respond. It can also return any information received to the client. All we have to do is take the request data out of the request stream and write it back to the return stream as it is. It's as simple as we did before.

const http = require('http');

http.createServer((request, response) => {
  let body = [];
  request.on('data', (chunk) => {
    body.push(chunk);
  }).on('end', () => {
    body = Buffer.concat(body).toString();
    response.end(body);
  });
}).listen(8080);

Now let's adjust. We only respond to the following conditions:

  • The request method is POST.
  • The access path is / echo.

In any other case, 404 is returned.

const http = require('http');

http.createServer((request, response) => {
  if (request.method === 'POST' && request.url === '/echo') {
    let body = [];
    request.on('data', (chunk) => {
      body.push(chunk);
    }).on('end', () => {
      body = Buffer.concat(body).toString();
      response.end(body);
    });
  } else {
    response.statusCode = 404;
    response.end();
  }
}).listen(8080);

Note: in order to check the request path, we designed a routing format. Other forms of routing , switch, simple can be checked in the form of , switch, complex such as express Framework, if you are looking for a route and don't need to do anything else, it's easy to use router.

That is great! Now let's simplify it further. Remember, request # is a ReadableStream Object, the response object is a WritableStream . That means we can use pipe Flow directly from one stream to another. That's exactly what we need:

const http = require('http');

http.createServer((request, response) => {
  if (request.method === 'POST' && request.url === '/echo') {
    request.pipe(response);
  } else {
    response.statusCode = 404;
    response.end();
  }
}).listen(8080);

this is it!

We haven't finished yet. As mentioned many times before, errors can happen at any time, so we need to deal with them.

To handle the error on the request stream, we record the error in the # stderr # object, and then send back a 400 code to represent the # error request. In real life, we want to check and analyze errors and understand their correct status codes and specific error information. For details, please refer to Error - Document Information

For returns, we log the error to stderr.

const http = require('http');

http.createServer((request, response) => {
  request.on('error', (err) => {
    console.error(err);
    response.statusCode = 400;
    response.end();
  });
  response.on('error', (err) => {
    console.error(err);
  });
  if (request.method === 'POST' && request.url === '/echo') {
    request.pipe(response);
  } else {
    response.statusCode = 404;
    response.end();
  }
}).listen(8080);

Now that we have covered most of the basic knowledge of HTTP requests, you should have:

  • Instantiate an HTTP service, which has a function to process requests and listen to a specific port.
  • Get the request header, access path, method and message body from # request #.
  • Let the routing decision depend on the access path or in other data of the request object.
  • Send the response header, HTTP status code and message body through the response object.
  • Connect the # request # object with the # response # object to transmit data.
  • Handle errors in the # request # and # response # streams.

From these basic knowledge, about node Some practical cases of HTTP service of JS have been gradually built, and the API document also provides a lot of other instructions, so please read them in detail EventEmittersStreams And HTTP.

Topics: node.js