서버와 클라이언트의 관계
- 클라이언트가 서버로 요청(request)을 보냄
- 서버는 요청을 처리
- 처리 후 클라이언트를 응답(response)을 보냄
http 요청에 응답하는 노드 서버
- createServer로 요청 이벤트에 대기
- req 객체는 요청에 관한 정보가, res 객체는 응답에 관한 정보가 담겨 있음
포트는 서버 내에서 프로세스를 구분하는 번호
//server1.js
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type':'text/html; charset=utf-8'});
res.write('<h1>Hello Node!</h1>');
res.write('<p>Hello server</p>');
res.end('<p>Hello ZeroCho<p>');
}).listen(8080);
server.on('listening',()=>{
console.log('8080번 포트에서 서버 대기 중입니다.');
});
server.on('error', (error)=> {
console.error(error);
})
localhost:8080 으로 접속하면 아래와 같은 결과를 얻을 수 있다.
Hello Node!
Hello server
Hello ZeroCho
코드를 수정하였으면 한 번 껐다가 켜 주어야 수정이 된다.
포트 다르게 하면 동시에 서버를 추가로 기동할 수 있다.
아래와 같이 html 파일을 읽어서 서버를 기동할 수도 있다.
//server2.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Node.js 웹서버</title>
</head>
<body>
<h1>Noe.js 웹 서버</h1>
<p>만들 준비 되셨나요?</p>
</body>
</html>
//server2.js
const http = require('http');
const fs = require('fs').promises;
const server = http.createServer(async(req, res) => {
try{
res.writeHead(200, { 'Content-Type':'text/html; charset=utf-8'});
const data = await fs.readFile('./server2.html')
res.end(data);
}catch(error){
console.error(error);
res.writeHead(200, {'Content-Type':'text/plain; charset=utf-8'});
res.end(error.message);
}
}).listen(8080);
server.on('listening',()=>{
console.log('8080번 포트에서 서버 대기 중입니다.');
});
server.on('error', (error)=> {
console.error(error);
})
localhost:8080 으로 접속하면 아래와 같은 결과를 얻을 수 있다.
Noe.js 웹 서버
만들 준비 되셨나요?
REST API(Representational State Transfer)
- 서버의 자원을 정의하고 자원에 대한 주소를 지정하는 방법
- /user이면 사용자 정보에 관한 정보를 요청하는 것
- /post 면 게시글에 관련된 자원을 요청하는 것
HTTP 요청 메서드
- GET : 서버 자원을 가져오려고 할 때 사용
- POST : 서버에 자원을 새로 등록하고자 할 때 사용(또는 뭘 써야할 지 애매할 때)
- PUT : 서버의 자원을 요청에 들어있는 자원으로 치환하고자 할 때 사용
- PATCH : 서버 자원의 일부만 수정하고자 할 때 사용
- DELETE : 서버의 자원을 삭제하고자 할 때 사용
서버 주소 뒤에 아무 것도 입력하지 않으면 GET 요청을 보내겠다는 뜻이다.
HTTP 상태 코드 에서 201은 생성 되었다는 의미가 있다.
GET, POST, PUT, DELETE 요청 보내기
//restServer.js
const http = require('http');
const fs = require('fs').promises;
const path = require('path');
const users = {}; // 데이터 저장용
http.createServer(async (req, res) => {
try {
if (req.method === 'GET') {
if (req.url === '/') {
const data = await fs.readFile(path.join(__dirname, 'restFront.html'));
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(data);
} else if (req.url === '/about') {
const data = await fs.readFile(path.join(__dirname, 'about.html'));
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(data);
} else if (req.url === '/users') {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
return res.end(JSON.stringify(users));
}
// /도 /about도 /users도 아니면
try {
const data = await fs.readFile(path.join(__dirname, req.url));
return res.end(data);
} catch (err) {
// 주소에 해당하는 라우트를 못 찾았다는 404 Not Found error 발생
}
} else if (req.method === 'POST') {
if (req.url === '/user') {
let body = '';
// 요청의 body를 stream 형식으로 받음
req.on('data', (data) => {
body += data;
});
// 요청의 body를 다 받은 후 실행됨
return req.on('end', () => {
console.log('POST 본문(Body):', body);
const { name } = JSON.parse(body);
const id = Date.now();
users[id] = name;
res.writeHead(201, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('등록 성공');
});
}
} else if (req.method === 'PUT') {
if (req.url.startsWith('/user/')) {
const key = req.url.split('/')[2];
let body = '';
req.on('data', (data) => {
body += data;
});
return req.on('end', () => {
console.log('PUT 본문(Body):', body);
users[key] = JSON.parse(body).name;
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
return res.end(JSON.stringify(users));
});
}
} else if (req.method === 'DELETE') {
if (req.url.startsWith('/user/')) {
const key = req.url.split('/')[2];
delete users[key];
res.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
return res.end(JSON.stringify(users));
}
}
res.writeHead(404);
return res.end('NOT FOUND');
} catch (err) {
console.error(err);
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end(err.message);
}
})
.listen(8082, () => {
console.log('8082번 포트에서 서버 대기 중입니다');
});
<쿠키의 필요성>
요청에는 한 가지 단점이 있음
- 누가 요청을 보냈는지 모름(IP 주소와 브라우저 정보 정도만 앎)
- 로그인을 구현하면 됨
- 쿠키와 세션이 필요
쿠키 : 키=값의 쌍
- name=zerocho
- 매 요청마다 서버에 동봉해서 보냄
- 서버는 쿠키를 읽어 누구인지 파악
쿠키 넣는 것을 직접 구현
- writeHead: 요청 헤더에 입력하는 메서드
- set-Cookie : 브라우저에게 쿠키를 설정하라고 명령
처음에 접속할 때 set-Cookie 를 통해서 쿠키를 생성한 후에
새로고침을 하게 되면 쿠키를 갖고 접속하게 된다.
개발자도구 Network 탭에서 Request Headers 에서 Cookie 에 입력되어서 브라우저에서 보내준다.
(처음에는 없었음)
//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번 포트에서 서버 대기 중입니다.')
})
//실행 결과 값
D:\code\node\4.3>node cookie.js
8083번 포트에서 서버 대기 중입니다.
/ undefined <-- 처음은 쿠키가 없음
/favicon.ico mycookie=test
/ mycookie=test <-- 새로고침 하면 쿠키가 입력되어서 오게 된다.
/favicon.ico mycookie=test
http 요청과 응답은 헤더와 본문을 가짐
- 헤더는 요청 또는 응답에 대한 정보를 가짐
- 본문은 주고받는 실제 데이터
- 쿠키는 부가적인 정보이므로 헤더에 저장
< 세션 사용하기 >
쿠키의 정보는 노출되고 수정되는 위험이 있음
- 중요한 정보는 서버에서 관리되고 클라이언트에는 세션 키만 제공
- 서버에 세션 객체(session)생성 후, uniqueInt(키)를 만들어 속성명으로 사용
- 속성 값에 정보 저장하고 uniqueInt를 클라이언트에 보냄
https
웹 서버에 SSL 암호화를 추가하는 모듈
- 오고 가는 데이터를 암호화해서 중간에 다른 사람이 요청을 가로채더라도 내용을 확인할 수 없음
- 요즘에는 https 적용이 필수(개인 정보가 있는 곳은 특히)
//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번 포트에서 서버 대기 중입니다!');
});
인증기관에서 인증서를 받아야 한다.
https 는 port를 443으로 생략이 가능하다.
http2
SSL 암호화와 더불어 최신 HTTP 프로토콜인 http/2를 사용하는 모듈
- 요청 및 응답 방식이 기존 http/1.1 보다 개선됨
- 웹의 속도도 개선됨
//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
실무에서는 http2 적용하면서 cluster 도 같이 적용하는 것이 좋다.
기본적으로 싱글 스레드인 노드가 CPU 코어를 모두 사용할 수 있게 해주는 모듈
- 포트를 공유하는 노드 프로세스를 여러 개 둘 수 있음
- 요청이 많이 들어왔을 때 병렬로 실행된 서버의 개수만큼 요청이 분산됨
- 서버에 무리가 덜 감
- 코어가 8개인 서버가 있을 때 : 보통은 코어 하나만 활용
- cluster로 코어 하나당 노드 프로세스 하나를 배정 가능
- 성능이 8배가 되는 것은 아니지만 개선됨
- 단점 : 컴퓨터 자원(메모리, 세션 등) 공유 못함
- Redis 등 별도 서버로 해결
//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);
// cluster.fork(); 주석 처리 하고 코어개수만큼 접속하면 코어개수 넘어갔을 때 서버가 연결이 안됨
// 주석 풀면 코어개수 이상만큼 접속해도 계속 프로세스 띄우기 때문에 서버 연결이 가능함
});
} 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}번 워커 실행`);
}
//실행결과
D:\code\node\4.5>node cluster.js
마스터 프로세스 아이디: 19868
19928번 워커 실행
19960번 워커 실행
19988번 워커 실행
19968번 워커 실행
20020번 워커 실행
20036번 워커 실행
20068번 워커 실행
20168번 워커 실행
20128번 워커 실행
20220번 워커 실행
20088번 워커 실행
20292번 워커 실행
// 코어개수만큼 접속하면 코어개수 넘어갔을 때 서버가 연결이 안됨
20088번 워커가 종료되었습니다.
code 1 signal null
20292번 워커가 종료되었습니다.
code 1 signal null
20220번 워커가 종료되었습니다.
code 1 signal null
20168번 워커가 종료되었습니다.
code 1 signal null
20128번 워커가 종료되었습니다.
code 1 signal null
20068번 워커가 종료되었습니다.
code 1 signal null
20036번 워커가 종료되었습니다.
code 1 signal null
20020번 워커가 종료되었습니다.
code 1 signal null
19928번 워커가 종료되었습니다.
code 1 signal null
19968번 워커가 종료되었습니다.
code 1 signal null
19960번 워커가 종료되었습니다.
code 1 signal null
19988번 워커가 종료되었습니다.
code 1 signal null