경일게임아카데미 멀티 디바이스 메타버스 플랫폼 개발자 양성과정 20220919 ~ 0920 개인 공부 - 보충 2022/04/04~2022/12/14

Jinho Lee·2022년 9월 20일
0

2022.09.19 ~ 20 개인 보충학습 - 22.09.15 경일 메타버스 24주차 3일 수업내용. Node.js - http 모듈 ( 쿠키, 세션, 클러스터 )

http 모듈로 서버 만들기

  • 자료 : 교과서 “Node.js 교과서” ch. 4 http 모듈로 서버 만들기 p. 169

  • 실제 서버 동작에 필요한 쿠키와 세션 처리, 요청 주소별 라우팅 방법

쿠키, 세션

  • p. 190 ~ 198
  • 클라이언트가 누구인지 기억하기 위해 서버에서 보내는 ‘키-값’ 쌍

  • 유효 기간이 있다

  • 서버는 클라이언트에 요청자를 추정할 만한 정보쿠키로 만들어 전송
    ⇒ 클라이언트로부터 쿠키를 받아 요청자를 파악

  • 실제 코드

    • 쿠키는 요청의 헤더(Cookie)에 담긴다.
      ↔ 브라우저(클라이언트)는 응답의 헤더(Set-Cookie)에 따라 쿠키를 저장한다.

      • 쿠키는 요청과 응답의 헤더를 통해 오간다.
    • 쿠키 간에는 세미콜론(;)으로 구분

  • 예시 코드 1

// 4_3_cookie.js
const http = require('http');

http.createServer((req, res) => {
  console.log(req.url, req.headers.cookie);
  res.writeHead(200, { 'Set-Cookie': 'mycookie=test' });
  res.end('Hello Cookie');
})
  .listen(8083, () => {
  console.log('8083번 포트에서 서버 대기 중입니다!')
});
// console
> node 4_3_cookie
8083번 포트에서 서버 대기 중입니다!
/ undefined
/favicon.ico mycookie=test
  • 파비콘 (favicon) : 웹 사이트 탭에 보이는 이미지
  • 브라우저는 파비콘이 무엇인지 HTML에서 유추할 수 없으면 서버에 파비콘 정보에 대한 요청을 보낸다. ⇒ 위 콘솔 결과의 두 번째 요청 “/favicon.ico mycookie=test
  • 예시 코드 2

    • 사용자를 식별하는 방법
// 4_3_cookie2.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8"/>
    <title>쿠키&세션 이해하기</title>
  </head>
  <body>
    <form action="/login">
      <input id="name" name="name" placeholder="이름을 입력하세요"/>
      <button id="login">로그인</button>
    </form>
  </body>
</html>
const http = require('http');
const fs = require('fs').promises;
const url = require('url');
const qs = require('querystring');

const parseCookies = (cookie = '') =>
cookie
.split(';')
.map(v => v.split('='))
.reduce((acc, [k, v]) => {
  acc[k.trim()] = decodeURIComponent(v);
  return acc;
}, {});

http.createServer(async (req, res) => {
  const cookies = parseCookies(req.headers.cookie);

  // 주소가 /login으로 시작하는 경우
  if (req.url.startsWith('/login')) {
    const { query } = url.parse(req.url);
    const { name } = qs.parse(query);
    const expires = new Date();
    // 쿠키 유효 시간을 현재 시간 + 5분으로 설정
    expires.setMinutes(expires.getMinutes() + 5);
    res.writeHead(302, {
      Location: '/',
      'Set-Cookie': `name=${encodeURIComponent(name)}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
    });
    res.end();

    // name이라는 쿠키가 있는 경우
  } else if (cookies.name) {
    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(`${cookies.name}님 안녕하세요`);
  } else {
    try {
      const data = await fs.readFile('./cookie2.html');
      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
      res.end(data);
    } catch (err) {
      res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
      res.end(err.message);
    }
  }
})
  .listen(8084, () => {
  console.log('8084번 포트에서 서버 대기 중입니다!');
});
  • 코드 설명

    1. 쿠키는 문자열 ⇒ parseCookies 함수로 객체 형식으로 변환

    2. 주소가 /login으로 시작할 경우

      • url 모듈로 주소, querystring 모듈로 주소에 딸려오는 query 분석

      • 302 응답 코드와 리다이렉트 주소, 쿠키를 헤더에 삽입

        • Set-Cookie로 쿠키 설정 - 쿠키 옵션

          1. 만료 시간(Expires, 유효 기간)

          2. HttpOnly

          3. Path

        • 헤더에는 한글을 설정할 수 없다
          name 변수를 encodeURIComponent 메소드로 인코딩

        • Set-Cookie 값은 제한된 ASCII 코드만 가능 → 줄바꿈 제외

    3. 그 외의 경우 else

      • 쿠키 유무 확인

        • 있다면 인사말

        • 없다면 로그인 페이지로 ⇒ cookie2.html 전송

  • 결과 - 새로고침을 해도 (요청이 다시 이루어져도) 로그인 유지 (요청자 파악)

  • 쿠키 구성 - 파트 2 부연 설명

    • 쿠키명=쿠키값

      • 기본적인 쿠키의 값
    • Expires=날짜

      • 만료 기한

      • 이 기한이 지나면 쿠키 제거

      • 기본값 → 클라이언트가 종료될 때까지

    • Max-age=초

      • 만료 기한

      • 날짜 대신 초를 입력 가능, 해당 초가 지나면 쿠키 제거

      • Expires보다 우선됨

    • Domain=도메인명

      • 쿠키가 전송될 도메인 특정

      • 기본값 → 현재 도메인

    • Path=URL

      • 쿠키가 전송될 URL 특정

      • 기본값 → ‘/

        • 이 경우 모든 URL에서 쿠키 전송 가능
    • Secure

      • HTTPS일 경우에만 쿠키 전송
    • HttpOnly

      • 설정 시 자바스크립트에서 쿠키 접근 불가

      • 쿠키 조작을 방지

세션 (Session)

  • 서버사용자 정보 저장

  • 클라이언트와 세션 아이디로만 소통

  • 세션 쿠키 : 세션을 위해 사용하는 쿠키

  • 예시 코드 3

    • 위 ‘예시 코드 2’는 쿠키가 노출되어 있다

    • 서버가 사용자 정보를 관리 → 세션

const http = require('http');
const fs = require('fs').promises;
const url = require('url');
const qs = require('querystring');

const parseCookies = (cookie = '') =>
cookie
.split(';')
.map(v => v.split('='))
.reduce((acc, [k, v]) => {
  acc[k.trim()] = decodeURIComponent(v);
  return acc;
}, {});

const session = {};

http.createServer(async (req, res) => {
  const cookies = parseCookies(req.headers.cookie);

  // 주소가 /login으로 시작하는 경우
  if (req.url.startsWith('/login')) {
    const { query } = url.parse(req.url);
    const { name } = qs.parse(query);
    const expires = new Date();
    // 쿠키 유효 시간을 현재 시간 + 5분으로 설정
    expires.setMinutes(expires.getMinutes() + 5);

    // 저번 예시와 달라진 부분 - 이름 -> uniqueInt, session 작성
    const uniqueInt = Date.now();
    session[uniqueInt] = {
      name,
      expires,
    };

    res.writeHead(302, {
      Location: '/',
      'Set-Cookie': `session=${uniqueInt}; Expires=${expires.toGMTString()}; HttpOnly; Path=/`,
    });
    res.end();

    // 세션 쿠기가 존재하고, 만료 기간이 지나지 않았다면
  } else if (cookies.session && session[cookies.session].expires > new Date()) {
    res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    res.end(`${cookies.name}님 안녕하세요`);
  } else {
    try {
      const data = await fs.readFile('./4_3_cookie2.html');
      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
      res.end(data);
    } catch (err) {
      res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
      res.end(err.message);
    }
  }
})
  .listen(8085, () => {
  console.log('8085번 포트에서 서버 대기 중입니다!');
});
  • ‘예시 코드 2’에서 변경점

    • 쿠키에 문자열(name) 대신 숫자 값(uniqueInt)를 담아 전송

    • 사용자 이름과 만료 시간은 uniqueInt 속성명 아래에 있는 session 객체에 저장

    • cookie.session이 존재 && 만료 기한을 넘기지 않음
      session 변수에서 사용자 정보를 가져와 사용

  • 주의 : 위 예시 코드들은 보안상 매우 취약 → 절대 실제 서비스에 사용하지 말 것!

https, http2

  • p. 198 ~ 201

https

  • 웹 서버에 SSL 암호화 추가

    • GET / POST 요청에서 오가는 데이터를 암호화 → 중간에 요청을 가로채도 내용 확인 불가

    • 브라우저 주소창에 자물쇠 표시

    • 인증해줄 기관이 필요 → 인증서 파일 (pem, crt, key 확장자)

  • 예시 코드 - 4.1 챕터 http 서버 코드

// 4_1_server1.js
const http = require('http');

http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
  res.write('<h1>Hello Node!</h1>');
  res.end('<p>Hello Server!</p>');
})
  .listen(8080, () => { // 서버 연결
  console.log('8080번 포트에서 서버 대기 중입니다!')
})
  • 예시 코드 - https 서버 코드
// 4_4_server1-3.js
// 인증서 필요
const https = require('https');
const fs = require('fs');

https.createServer({
  // 인증서 경로
  cert: fs.readFileSync('도메인 인증서 경로'),
  key: fs.readFileSync('도메인 비밀키 경로'),
  ca: [
    fs.readFileSync('상위 인증서 경로'),
    fs.readFileSync('상위 인증서 경로'),
  ],
}, (req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
  res.write('<h1>Hello Node!</h1>');
  res.end('<p>Hello Server!</p>');
})
  .listen(443, () => { // 서버 연결
  console.log('443번 포트에서 서버 대기 중입니다!')
})

http2

  • SSL 암호화와 더불어 최신 HTTP 프로토콜인 http/2 사용 가능

    • http/2 : 기존 http/1.1보다 개선된 요청 및 응답 방식 ⇒ 효율적 요청, 웹 속도 개선
  • 예시 코드 - http2 서버 코드

// 4_4_server1-4.js
// 인증서 필요
const http2 = require('http2');
const fs = require('fs');

http2.createSecureServer({
  // 인증서 경로
  cert: fs.readFileSync('도메인 인증서 경로'),
  key: fs.readFileSync('도메인 비밀키 경로'),
  ca: [
    fs.readFileSync('상위 인증서 경로'),
    fs.readFileSync('상위 인증서 경로'),
  ],
}, (req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
  res.write('<h1>Hello Node!</h1>');
  res.end('<p>Hello Server!</p>');
})
  .listen(443, () => { // 서버 연결
  console.log('443번 포트에서 서버 대기 중입니다!')
});

cluster

  • p. 201 ~ 205

  • 싱글 프로세스로 동작하는 노드가 CPU 코어를 모두 사용 가능토록 하는 모듈

    • 노드는 보통 CPU 코어 하나만 활용 ⇒ 코어 하나당 노드 프로세스 하나 할당

    • 장점

      • 병렬로 실행된 서버의 개수만큼 요청 분산 가능 → 서버의 부담 경감

      • 예상치 못한 에러로 인한 서버 종료 방지 - 근본적인 해결은 아님

    • 단점 : 복수의 프로세스 → 메모리 공유 불가

      • 레디스 등 서버를 도입해 해결 가능
  • 예시 코드

// 4_5_cluster.js
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if(cluster.isMaster) {
  console.log(`마스터 프로세스 아이디: ${process.pid}`);
  // CPU 개수만큼 워커를 생산
  for (let i = 0; i < numCPUs; i += 1) {
    cluster.fork();
  }
  // 워커가 종료되었을 때
  cluster.on('exit', (worker, code, signal) => {
    console.log(`${worker.process.pid}번 워커가 종료되었습니다.`);
    console.log('code', code, 'signal', signal);
  });
} else {
  // 워커들이 포트에서 대기
  http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
    res.write('<h1>Hello Node!</h1>');
    res.end('<p>Hello Cluster!</p>');
    setTimeout(() => { // 워커가 존재하는지 확인하기 위해 1초마다 강제 종료
      process.exit(1);
    }, 1000);
  }).listen(8086);

  console.log(`${process.pid}번 워커 실행`);
}
// console
> node 4_5_cluster
마스터 프로세스 아이디: 15728
5396번 워커 실행
15688번 워커 실행
6732번 워커 실행
11944번 워커 실행
1928번 워커 실행
14572번 워커 실행
15768번 워커 실행
3628번 워커 실행
696번 워커 실행
1716번 워커 실행
15576번 워커 실행
14268번 워커 실행
14268번 워커가 종료되었습니다.
code 1 signal null
15576번 워커가 종료되었습니다.
code 1 signal null
1716번 워커가 종료되었습니다.
code 1 signal null
696번 워커가 종료되었습니다.
code 1 signal null
3628번 워커가 종료되었습니다.
code 1 signal null
15768번 워커가 종료되었습니다.
code 1 signal null
14572번 워커가 종료되었습니다.
code 1 signal null
1928번 워커가 종료되었습니다.
code 1 signal null
11944번 워커가 종료되었습니다.
code 1 signal null
6732번 워커가 종료되었습니다.
code 1 signal null
15688번 워커가 종료되었습니다.
code 1 signal null
5396번 워커가 종료되었습니다.
code 1 signal null
  • worker_threads 예제(ch. 3.5.7)와 모양이 비슷

  • 마스터 프로세스와 워커 프로세스

    • 마스터 프로세스는 CPU 개수만큼 워커 프로세스를 생성
      → 8086 포트 대기
      → 요청을 워커 프로세스에 분배

    • 워커 프로세스는 실질적 작업을 수행

0개의 댓글