본문 바로가기

Node.js

Node.js(5) - TCP, HTTP(Request Message / Response Message), express

728x90
반응형

1) Node.js

   1-1) TCP

   1-2) HTTP

   1-3) request message / response message

   1-4) express

 

 

 

 

 

1) Node.js

1-1) TCP

OSI 7계층
LAN 선이 꽂혔을 때 데이터가 변하는 과정을 OSI 7계층으로 표현한 것이다!

5계층 ~ 7계층까지가 사람이 개발하는 영역(User 영역)이다!!
3계층(IP)과 4계층(TCP)은 Kernel 영역이다!

TCP는 순서를 지키고 연결이라는 작업이 있다!
UDP는 연결 작업이 없기에 순서를 지키지 않고 데이터를 보낸다!
데이터를 보낼 시 쪼개서 보내기 때문에 순서를 잘 지켜서 보내야 한다!

Node.js와 브라우저의 차이
브라우저에서 실행하는 코드는 내 컴퓨터를 조작할 수 없지만 Node.js에서 실행하는 코드는 내 컴퓨터를 조작할 수 있다!

  • 프로그램 : 코드
  • 프로세스 : 실행 단계(코드를 실행시키는 것)

실행이 되서 돌아가고 있으면 프로세스, 실행시키지 않은 상태에서는 프로그램이다!
여기서 프로세스가 생성됐다(실행이 되서 돌아가고 있다는 것)를 다른 말로

"인스턴스를 생성했다(코드를 실행해서 어떠한 결과물로 메모리에 적재하는 행위)"고 표현한다!!

 

인스턴스 : 하나의 프로그램이 실행되었을 때의 프로세스

"파일 생성이 가능하다"는 의미는 "socket을 만들 수 있다(네트워크가 가능하다는 의미)"는 뜻이다!

 

  • Shell : 우리가 조작할 수 있는 영역
  • Kernel : 우리가 조작할 수 없는 영역(OS 깊숙히 박혀 있다고 보면 됨!!)

즉, TCP(프로토콜이기에 통신할 시 기본적으로 규격이 정해져 있다!)는 Kernel 영역이기에 우리가 조작할 수 없다!

따라서 TCP 코드는 우리가 직접 개발하는 영역이 아니기에 그저 가져다 쓴다고 보면 된다!

Node.js는 파일을 조작해서 socket을 만들 수 있으며,

이 socket을 가지고 TCP한테 데이터를 줄 수 있는, 즉 명령어를 내릴 수 있는 내장 모듈이 'Net'이다!!

이 Net에는 2가지 기능이 있는데 바로 "socket을 만드는 기능", "TCP에게 데이터를 보내는 기능"이다!!

Net을 쓰면 통신이 가능하다!

 


네트워크
직역하면 관계 혹은 연결이라는 의미로 네트워크는 컴퓨터가 최소 2대 필요하다!
우리 입장에선 컴퓨터가 한 대이기에 프로세스가 2개 필요하다!

TCP 코드의 특징은 "연결"이라는 개념이 있는데 이때 연결은 물리적인 연결이 아니라

논리적인 연결(TCP는 서로 데이터만 주고 받는데 이를 연결이라고 한다!!)이다!
TCP의 논리적인 연결 방식이 "3-way Handshaking"이다!

 


TCP 연결 순서

  1. 통화를 하기 위해서는 먼저 핸드폰을 개통해야 하듯이 통신을 하기 위해서는 우선 서버를 만들어야 한다!
  2. 서버는 항상 대기 상태(계속 프로세스가 돌고 있는 상태)여야 한다!
  3. 이후 Client로부터 "3-way Handshaking"이 일어난다!

+) 추가
web server : 웹에서 요청했을 때 응답을 주는 서버
브라우저 : TCP 프로세스(Client)가 구현된 것(브라우저도 Client Server에 해당됨!!)

 

server.js

const net = require("net");
const PORT = process.env.SERVER_PORT || 3000;
const HOST = process.env.SERVER_HOST || "127.0.0.1";

const server = net.createServer((client) => {
  client.setEncoding("utf8");

  client.on("data", (chunk) => {
    console.log(chunk);

    if (chunk === "index.html 줄래?") {
      client.write("<h1>index.html이야!!</h1>");
    }
  });
});

server.on("connection", () => {
  console.log("connected to client");
});

server.listen(PORT, HOST, () => {
  console.log("server start");
});

 

client.js

const net = require("net");

const config = { port: 3000, host: "127.0.0.1" };
const socket = net.connect(config);

socket.on("connect", () => {
  console.log("connected to server");

  socket.write("index.html 줄래?");
});

socket.on("data", (chunk) => {
  console.log(chunk.toString());
  socket.write("user.html 줄래?");
});

 

 

 

1-2) HTTP

HTTP 프로토콜 이해
HTTP(Hyper Text Transfer Protocol)
HTTP는 Server와 Client가 데이터를 주고 받기 위한 프로토콜(규칙)

HTTP 특징
- HTTP는 연결 상태를 유지하지 않는 비 연결성(Stateless)이다!

  • 비 연결성(Stateless) : 연결 후에 TCP 연결을 끊는다는 의미
  • 여기서 TCP의 연결의 경우, 논리적 연결에 해당하기에 논리적 연결만 끊는다!!

- HTTP는 기본골격 요청이 있으며, 무조건 응답을 준다!!

 

 

HTTP 동작 방식

브라우저에서 URI 입력 시 동작 형태

 

1-3) request message / response message

HTTP 요청 메시지

GET /user?name=sangbeom HTTP/1.1
Host: 127.0.0.1:3000
User-agent: ...
Content-type:application/x-www-form-...

name=sangbeom


일단 요청, 응답 메시지는 크게 2가지 형태로 읽어야 함!!
여기서 2가지 영역은 'Header', 'body' 영역으로 구분해서 읽는 것을 의미함!!
"request header", "request body"를 합친 것이 "request message"이다!

Start line

GET /user?name=sangbeom HTTP/1.1
[요청 메서드] [요청 URI] [HTTP 프로토콜 버전]

 

요청 메서드(Request Method)

  • GET : 데이터 요청의 의미로 사용함!
  • POST : 데이터 입력의 의미로 사용함!
  • PUT : 데이터 수정의 의미로 사용함!
  • DELETE : 데이터를 삭제하기 위해서 사용함!(여기서 실제 데이터 삭제가 이루어지는 것이 아니라 단지 삭제해달라는 요청이다!!)
  • OPTIONS : 웹 서버가 지원하는 메서드의 종류 요청

요청 URI

  • host를 제외한 나머지 URI를 적음

HTTP 프로토콜 버전

  • HTTP 버전이 1.0, 1.1, 2.0이 있는데 우리는 1.1만 다룰 예정

Request header
Content-Type의 의미를 정확히 알아야 함!!

Request body
요청 데이터를 넣는 공간

 

 

(참고) AJAX, Fetch, Axios

  • AJAX : 자바스크립트로 요청을 보내는 것(비동기 통신이라는 의미)
  • Fetch : AJAX를 대체하기 위해 등장함
  • Axios : 외부 라이브러리를 만듦

 

 

HTTP 응답 메시지

HTTP/1.1 200 OK
Date : ...
Content-Length : 6821
Content-Type : text/html

<html>
... 내용
</html>

 

Response header

HTTP/1.1 200 OK
Date : ...
Content-Length : 6821
Content-Type : text/html


Start line
[HTTP 프로토콜 버전] [상태 코드] [상태 메시지]

200 <-- 상태 코드(Status Code)
1XX:
2XX: 성공
3XX:
4XX: 페이지가 없음
5XX: 서버 터짐

OK: 상태 메시지(Status Message)

 

 

Response body

<html>
... 내용
</html>

 

 

 

1-4) express
"express"는 외부 모듈이기에 다운로드 받으려면 "npm"을 사용해야 함!!

$ npm init -y <-- package.json 파일 생성
$ npm install express

 

server.js

 
const express = require('express');
const app = express(); // --> 마치 "createServer()"와 같기에 app 변수는 server 변수와 같다고 볼 수 있음!!

app.get('/', (req, res) => {
	res.send('hello world!');
});

app.listen(3000, () => {
	console.log('server start');
});

 

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <h1>Logo!!</h1>
    <a href="http://127.0.0.1:3000/list">게시판 리스트 가기</a>
  </body>
</html>

 

list.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    리스트 페이지 입니다.
    <a href="http://127.0.0.1:3000/write">글쓰기 입니다.</a>
  </body>
</html>

 

write.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <form method="post" action="http://127.0.0.1:3000/write">
      <input type="text" name="userid" />
      <button type="submit">전송</button>
    </form>
  </body>
</html>

 

server.js

const express = require("express");
const path = require("path"); // path는 Node.js의 내장모듈이다!
const app = express();

// GET /
// GET /list
// GET /write
// POST /write

// GET / HTTP/1.1
app.get("/", (req, res) => {
  const body = path.join(__dirname, "views", "index.html"); // "__dirname"을 통해 index.html 파일의 경로를 절대경로로 완성함!!
  console.log(body);
  res.sendFile(body);
});

app.get("/list", (req, res) => {
  const body = path.join(__dirname, "views", "list.html");
  console.log(body);
  res.sendFile(body);
});

app.get("/write", (req, res) => {
  const body = path.join(__dirname, "views", "write.html");
  console.log(body);
  res.sendFile(body);
});

app.post("/write", (req, res) => {
  const body = path.join(__dirname, "views", "index.html");
  console.log(body);
  res.sendFile(body);
});

app.listen(3000, () => {
  console.log("server start");
});

 

"express"는 웹 서버를 만드는 마이크로 프레임워크이다!

"express" 안에 TCP 통신이 내장되어 있기에 이를 통해 TCP 통신을 할 수 있다!

또한 "express"는 HTTP Request Message와 HTTP Response Message를 만들어 준다!

 

 

server.js

const net = require("net");
const res = require("./lib/res");
const PORT = process.env.SERVER_PORT || 3000;
const HOST = process.env.SERVER_HOST || "127.0.0.1";

const server = net.createServer((client) => {
  client.setEncoding("utf8");

  client.on("data", (chunk) => {
    console.log(chunk);
    const responseMessage = res("hello world!");
    client.write(responseMessage);
  });
});

server.on("connection", () => {
  console.log("connected to client");
});

server.listen(PORT, HOST, () => {
  console.log("server start");
});

 

res.js

const message = (content) => {
  const body = Buffer.from(content);

  return `HTTP/1.1 200 OK
Connection:Close
Content-Type:text/html; charset=UTF-8
Content-Length:${body.length}

${body.toString()}`;
};

module.exports = message;

 

 

server.js

const net = require("net");
const resFn = require("./lib/res");
const PORT = process.env.SERVER_PORT || 3000;
const HOST = process.env.SERVER_HOST || "127.0.0.1";

const server = net.createServer((client) => {
  client.setEncoding("utf8");
  const res = resFn(client);
  client.on("data", (chunk) => {
    console.log(chunk);
    res.send("hello world!!!!!");
  });
});

server.on("connection", () => {
  console.log("connected to client");
});

server.listen(PORT, HOST, () => {
  console.log("server start");
});

 

res.js

const message = (content) => {
  const body = Buffer.from(content);

  return `HTTP/1.1 200 OK
Connection:Close
Content-Type:text/html; charset=UTF-8
Content-Length:${body.length}

${body.toString()}`;
};

// 의존성 주입
module.exports = (client) => {
  return {
    send: (body) => {
      const response = message(body);
      client.write(response);
    },
    sendFile: (body) => {},
  };
};

 

req.js(Start line 분류 작업)

const msg = `GET /user?name=sangbeom&age=27 HTTP/1.1
Host: localhost:3000
Connection: keep-alive
sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
sec-ch-ua-platform: "macOS"
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: http://localhost:3000/efef
Accept-Encoding: gzip, deflate, br
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7

name=sangbeom`;

const getQuery = (queryString) => {
  // console.log(queryString);
  if (queryString === undefined) return null;
  // @TODO: "&" 기준으로 배열로 나누기  // name=sangbeom&age=27 --> [name=sangbeom, age=27] --> [[name, sangbeom], [age, 27]]
  // {[name, sangbeom], [age, 27]} --> { name: 'sangbeom', age: '27' }
  return queryString
    .split("&")
    .map((v) => v.split("="))
    .reduce((acc, value) => {
      const [key, val] = value;
      // const key = value[0]
      // const val = value[1]
      acc[key] = val;
      return acc;
    }, {});
};

const parser = (message) => {
  console.log(message); // data type: string --> array \n
  console.log("=====");
  // @TODO : Start line
  // @TODO : header, body 분리
  // @TODO : header 객체 만들기

  const header = message.split("\n");
  const [method, url, version] = header.shift().split(" "); // data type : string --> array [ 'GET', '/user?name=sangbeom&age=27', 'HTTP/1.1' ]
  // 바로 위의 구조분해 할당 부분과 동일한 코드가 바로 아래 코드들이다!(더 간단히 표현하기 위해 구조분해 할당 구문을 사용함!)
  // const startLine = header.shift().split(" ");
  // const method = startLine[0];
  // const url = startLine[1];
  // const version = startLine[2];
  // console.log(method, url, version);

  const [path, queryString] = url.split("?"); // ['/user', 'name=sangbeom&age=27']
  // const uri = url.split('?')
  // const path = uri[0]
  // const queryString = uri[1]
  // console.log(path, queryString);

  const query = getQuery(queryString); // { name: 'sangbeom', age: '27' }
  console.log(query);
  return null;
};

const rst = parser(msg);
console.log(rst);

 

req.js(header, body 분리 / header 객체 만들기)

const msg = `GET /user?name=sangbeom&age=27 HTTP/1.1
Host: localhost:3000
Connection: keep-alive
sec-ch-ua: "Not?A_Brand";v="8", "Chromium";v="108", "Google Chrome";v="108"
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
sec-ch-ua-platform: "macOS"
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: no-cors
Sec-Fetch-Dest: image
Referer: http://localhost:3000/efef
Accept-Encoding: gzip, deflate, br
Accept-Language: ko-KR,ko;q=0.9,en-US;q=0.8,en;q=0.7

{
    name:'sangbeom',
    age:'27',
}`;

const getQuery = (queryString) => {
  // console.log(queryString);
  if (queryString === undefined) return null;
  // @TODO: "&" 기준으로 배열로 나누기  // name=sangbeom&age=27 --> [name=sangbeom, age=27] --> [[name, sangbeom], [age, 27]]
  // {[name, sangbeom], [age, 27]} --> { name: 'sangbeom', age: '27' }
  return queryString
    .split("&")
    .map((v) => v.split("="))
    .reduce((acc, value) => {
      const [key, val] = value;
      // const key = value[0]
      // const val = value[1]
      acc[key] = val;
      return acc;
    }, {});
};

// @TODO : header 객체 만들기
const getHeader = (arr) => {
  // console.log(arr);
  return arr
    .map((v) => v.split(":"))
    .reduce((acc, value) => {
      let [key, val, port] = value;
      if (port !== undefined) val += `:${port}`;
      acc[key] = val;
      return acc;
    }, {});
};

const getBody = (line) => {
  // console.log(line);

  // for (let i = 0; i < line.length; i++) {
  //   if (line[i] === "") console.log(i);
  // }

  let flag = false;
  let body = "";
  for (const key in line) {
    if (flag) {
      body = line
        .splice(key)
        .map((v) => v.trim())
        .join("");
    }
    if (line[key] === "") flag = true;
  }
  line.pop();
  //   console.log("body: ", body);
  //   console.log("header: ", line);
  const headers = getHeader(line);
  return [line, body];
};

const parser = (message) => {
  // console.log(message); // data type: string --> array \n
  // console.log("=====");
  // @TODO : Start line

  const header = message.split("\r\n");
  const [method, url, version] = header.shift().split(" "); // data type : string --> array [ 'GET', '/user?name=sangbeom&age=27', 'HTTP/1.1' ]
  // 바로 위의 구조분해 할당 부분과 동일한 코드가 바로 아래 코드들이다!(더 간단히 표현하기 위해 구조분해 할당 구문을 사용함!)
  // const startLine = header.shift().split(" ");
  // const method = startLine[0];
  // const url = startLine[1];
  // const version = startLine[2];
  // console.log(method, url, version);

  const [path, queryString] = url.split("?"); // ['/user', 'name=sangbeom&age=27']
  // const uri = url.split('?')
  // const path = uri[0]
  // const queryString = uri[1]
  // console.log(path, queryString);

  const query = getQuery(queryString); // { name: 'sangbeom', age: '27' }
  // console.log(query);

  // @TODO : header, body 분리
  const [headers, body] = getBody(header);
  // const a = getBody(header); // []
  // const headers = a[0]
  // const body = a[1]

  // 객체 리터럴
  return {
    method,
    path,
    version,
    url: headers.Host + url,
    query,
    headers,
    body,
  };
};

module.exports = parser;

 

server.js

const net = require("net");
const resFn = require("./lib/res");
const reqFn = require("./lib/req");
const PORT = process.env.SERVER_PORT || 3000;
const HOST = process.env.SERVER_HOST || "127.0.0.1";

const server = net.createServer((client) => {
  client.setEncoding("utf8");
  const res = resFn(client);
  client.on("data", (chunk) => {
    const req = reqFn(chunk);
    console.log(req); // request message

    if (req.method === "GET" && req.path === "/") {
      res.send("hello index.html");
    } else if (req.method === "GET" && req.path === "/list") {
      res.send("hello list.html");
    } else if (req.method === "GET" && req.path === "/write") {
      res.send("hello write.html");
    } else {
      res.send("Not found");
    }
  });
});

server.on("connection", () => {
  console.log("connected to client");
});

server.listen(PORT, HOST, () => {
  console.log("server start");
});