Back to the Basics

[Web Server][Node.js]- Node.js - Anatomy of an HTTP Transaction 문서 파해치기 본문

Programming Languages/JavaScript & Node.js

[Web Server][Node.js]- Node.js - Anatomy of an HTTP Transaction 문서 파해치기

9Jaeng 2021. 11. 4. 18:41
728x90

Anatomy of an HTTP Transaction

이 글은 Anatomy of an HTTP Transaction을 읽고 정리한 글입니다. 개인적인 정리이니 의역 다분함을 명심해 주세요.

이 내용을 알기 위해서는 EventEmitter와 Streams에 대해 어느 정도 친숙해야 한다고 한다. (그것을 가정하고 작성된 문서이다)

Create the Server

node web server는 createServer를 사용하여 web server object를 생성하면서 시작하다. server object는 EventEmitter이다.

  • http.createServer([options][, requestListener])
    • Syntax
    cosnt http=require('http') http.createServer(function(request,response){     ..... }
    http.createServer는 server instance를 만들어서 반환하는 메서드이다.
    • optionsoptions의 인자로는 http.IncomingMessage , http.ServerResponse, insercureHTTPParser, masHTTPHeaderSize가 들어갈 수 있다.
    • requestListenerrequestListener는 function으로 server가 request를 받으면 자동으로 실행된다. 두 번째 인자로 들어가는 매개변수 response는 ServerResponse 객체이며, 사용자에게 응답 스트림을 다시 handling 하는 메서드가 있는 객체이다. (writavle stream) Method는 아래와 같다.(W3 school을 참고하였다)
    • ServerResponse Methods and Properties
      addTrailers() Adds HTTP trailing headers
      end() Signals that the the server should consider that the response is complete
      finished Returns true if the response is complete, otherwise false
      getHeader() Returns the value of the specified header
      headersSent Returns true if headers were sent, otherwise false
      removeHeader() Removes the specified header
      sendDate Set to false if the Date header should not be sent in the response. Default true
      setHeader() Sets the specified header
      setTimeout Sets the timeout value of the socket to the specified number of milliseconds
      statusCode Sets the status code that will be sent to the client
      statusMessage Sets the status message that will be sent to the client
      write() Sends text, or a text stream, to the client
      writeContinue() Sends a HTTP Continue message to the client
      writeHead() Sends status and response headers to the client
-   첫 번째 인자로 들어가는 매개변수 request는 Request Object이며 IncomingMessage 객체이다.

createServer()를 사용하여 server 객체를 만들고 listener는 아래와 같이 나중에 추가할 수도 있다.

const server=http.createServer(); server.on('request',(request,response)=>{ 

// request 요청이 왔을 때 실행 할 code }

여기서, listener는 'request'라고 명명된 named event가 발생되면 (미리 명명되어 등록되어있는 event를 말한다) 실행이 되는 callback 함수이다.

HTTP 요청이 들어오면 ("request"로 명명된 event가 발생하면) node는 request handler 함수를 호출한다.

실제로 요청을 처리하기 위해서는 listen method가 server 객체에서 호출되어야 한다. server.listen() method에 넘겨줘야 할 첫 번째 인자는 port number이고, 두 번째 인자는 function(서버 오픈 시 실행할 코드)

  • listen() 함수는 listen() 함수는 연결 대기 상태로, 원하는 포트에 서버를 오픈하는 문법이라고 한다.
  • 요청 대기란 클라이언트로부터 연결 요청을 받아들일 수 있는 상태를 말한다

Method, URL and Headers

request르 처리할 때, 첫 번째로 확인해야 할 것은 methodURL을 먼저 알아야만 적절한 action을 취할 수 있다. Node.js는 request 객체에 유용한 속성들을 만들어 놓았기 때문에 , 클라이언트가 요청한 request의 method와 url, header 등을 쉽게 알 수 있다.

const {method,url,header}=request;

아래의 표는 Sprint에서 사용한 properties를 나열하였으며, MDN문서를 참조하였다.

Request Object Properties

Properties Description
Request.body A ReadableStream of the body contents.
Request.headers Contains the associated Headers object of the request.
Request.method Contains the request's method (GET, POST, etc.)
Request.URL Contains the URL of the request.

모든 headers는 lower case로 표현된다. 만약 반복되는 header가 있다면, 값들은 덮어씌워지거나 comma-separated string으로 표현된다.

Request Body

POST 또는 PUT 요청과 함께 전달되는 body data는 'Stream의 'data'와 'end' event를 통해 data를 바로 가져올 수 있다. 'data' event는 Buffer 객체인 chunk를 발생시킨다.

  • Buffer란 Node.js에서 제공하는 Binary의 데이터를 담을 수 있는 Object이다. Buffer는 toSTring()을 사용하여 stgin으로 변환할 수 있다.

request body는 대부분이 문자일 것이므로 'data' event를 통한 chunk를 배열에 담은 후 'end' event의 handler에서 Concat을 사용하여 하나의 buffer Object로 만들고, String으로 바꾸는 방법을 사용한다.

  • concat-streamconcat-stream은 한 stram의 모든 데이터를 하나의 buffer로 모을 때 사용한다. Writable stream으로 모든 stream의 모든 data를 연결하고 결과와 함께 callback을 호출한다.
.on("data", (chunk) => body.push(chunk)) // chunk를 배열에 담는다.
.on("end", () => {         body = Buffer.concat(body).toString(); // 16진수를 string으로 변경                
  • 'end' EventNode.js Module 문서에 따르면, 'end' Evnet는 더 이상 소비할 데이터가 없으며 stream을 다음 mode로 넘어감을 의미한다. stream.read()를 반복적으로 사용하면 데이터가 다 소비된다.

A Quick Thing About Errors

request 객체는 ReadableStream이면서 EventEmitter이므로 error 발생 시 객체같이 작동을 한다. stream에 'error' event를 발생시키면서 request stream에 error를 발생시킨다.

만약 error를 handling 하기 위한 listener가 없다면, Node.js program을 종료시키기 때문에 'error' event listener를 request stream에 추가해야 한다. (HTTP error responase를 보내는 것이 가장 좋다)

What We'be Got so Far

지금까지 다룬 것은 createServer를 통해 server를 만들고, request 객체에서 필요한 method, headers, url을 얻은 다음 'data'와 'end' event를 통해 request로 받은 body를 받고 string의 형태로 받있다. 이를 코드로 정리하면 아래와 같다.

const http=require('http')

// 서버 객체를 생성한다
const server=http.createServer((request,response)=>{
    // request 객체를 통해 받은 properties
    {method, url, headers}=request;

    // request body를 받을 배열
    let body=[];

    // 'error' event 발생 시 호출된 handler
    request.on('error',(err)=>{
    console.error(err);
    })
    // 'data'event를 통해 전달되는 Buffer 형태의 chunk를 body에 push한다.
    .on('data',(chunk)=>{
    body.push(chunk)
})
// 더이상 data 처리?가 다 끝냈으면 'end'event handler를 통해string으로 바꾼다. 
    .on('end',()=>{
    body=Buffer.concat(body).toString();

HTTP Status Code

status code를 따로 설정하지 않는다면, HTTP respons의 statue code는 200이다. 다른 status code를 설정하고 싶다면, response.statuCode를 사용하여 status code를 setting 할 수 있다.

Setting Response Headers

setHeader를 사용하여 Headers를 설정할 수 있다. header를 설정할 때에는 대, 소문자는 상관이 없으며 반복적으로 header를 설정할 경우 마지막에 설정한 header가 세팅된다.

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

Explicitly Sending Header Data

writeHead method를 사용하여 header를 response stream에 명시적으로 작성할 수 있다. 이 메서드는 starue code와 headers를 stream에 write 한다.

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


response.writeHead(200, preflightCorsHeader)

const preflightCorsHeader = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Accept",
  "Access-Control-Max-Age": 10 // preflight request에 대한 응답을 캐시할 수 있는 시간(초)
};

Sending Response Body

response Object는 WritableStream 이므로, Client에세 resonse body를 보내는 것은 stream method를 사용하면 된다. stream의 end() 함수에도 bit data를 적을 수 있으므로 write 메서드를 사용하지 않고, end에 넣어서 작성항 수 있다.

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

// 또는

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

Put All Togeter

소개했던 내용들을 참고하여 mini server를 작성할 수 있다.

이 mini server는 브라우저에서 보내는 문자열을 뒤집어서 응답으로 보내준다.

코드는 아래와 같다. 추가적으로, Cross-site에서 보낼 경우 proflight에 대한 응답과 POST인 경우에 대한 응답, 그리고 POST 중 url에 따라 다른 응답을 보내는 server이다.

const http = require("http");

const server = http.createServer((request, response) => {
  let body = []; // body를 배열로 받는 이유/는 Buffer로 한 개씩 데이터가 들어오기 때문
  const { method, url, headers } = request; // request 객체 안에 있는 정보들

  // 만약 method가 options이면 preflight 응답을 보낸다
  if (method === "OPTIONS") {
    response.writeHead(200, preflightCorsHeader); 
    response.end();
  }

  if (method === "POST") {
    request 
      .on("error", (err) => console.error(err))
      .on("data", (chunk) => body.push(chunk))
      .on("end", () => {
        body = Buffer.concat(body).toString(); 
        response.writeHead(201, preflightCorsHeader);
        if (url === "/reverse") 
                    response.end(body.split("").reverse().join(""));
      });
  }
});

server.listen(5000, () => console.log(`id: locallhost:5000`));


// Cross-Origin Proflight header
const preflightCorsHeader = {
  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Accept",
  "Access-Control-Max-Age": 10, // preflight request에 대한 응답을 캐시할 수 있는 시간(초)
};

pipe method에 대한 정리도 해보자

pipe

readable.pipe는 data를 한 스트림에서 다른 스트림으로 직접 연결하는 메서드이다.

  • Syntax

readable.pipe(destination [, options])

-   destination : <stream.Writable>
-   options : <Object>
-   returns : <stream.Writable> :

readable.pipe() method는 Writable stream을 readabal stream에 붙여서, 자동으로 flowing mode로 전환하여 모든 데이터를 Writable stream에 push 한다. Writable stream이 더 빠른 readable stream에 압도되지 않도록 자동으로 관리된다.
readable.pipo() 메서드는 pipe로 연결된 stream의 chain을 만들 수 있는 대상 stream의 참조를 반환한다.

const fs=require('fs');
const r=fs.createReadStream('file.txt');
const z=zlib.createGzip();
const w=fs.createWritetStream('file.txt.gz');
r.pipe(x).pipe(w);

Readable stream이 'end'를 내보내면, stream.end()가 기본값으로 대상 Writable stream에 호출되고 대상 Writable stream은 더 이상 writable이 아니게 된다. 만약 기본 설정을 바꾸고 싶다면, pipe method의 end opion을 false로 설정하면 된다.

reader.pipe(writer,{end:false});
reader.on('end',()=>{
writer.end('Goodby\n');
});

한 가지 중요한 점은, Readable stream이 error를 발생시키는 경우, Writabel destination은 자동으로 닫히지 않는다. memory leaks를 방지하기 위해 각 stream을 수동적으로 닫아야 한다.




Error documentation

Error documentation.

항상 명심해야 할 것은, 오류는 언제든 발생할 수 있으므로 이에 대한 적절한 처리가 필요하다. 실제 애플리케이션에는 오류를 검사하여 올바른 상태 코드와 message를 파악하고자 하다. 오류 설명서를 참조해야 한다.. Error처리를 할 때 위의 문서를 참고해 보자..!

728x90
Comments