앞서 브라우저에서 URL을 입력시 화면에 보이는 과정 중
서버로 부터 리소스를 받으면 받은 리소스가 렌더링되어 화면에 보여짐을 학습했다.
이번에는 서버로 리소스를 요청하고 응답 받는 과정이 어떻게 일어나는지 학습하고 실습해보자.
네트워크는 둘 이상의 컴퓨터와 이들을 연결하는 링크의 조합이다.
Node + Link + Node 형태로 표현된다.
DNS는 Domain을 IP 주로소 변환하는 작업을 수행한다.
전화번호부처럼 Domain의 IP를 가지고 있는게 DNS Server다.
통신사, 구글 등 기업이 운영하고 있으며, 로컬(사용 컴퓨터) 에도 존재한다.
클라이언트를 통해 url(도메인)을 입력하여 요청하면 DNS Server에서 해당 IP를 반환해준다.
반환된 IP는 다시 사용 될 수 있도록 로컬 DNS Server 캐시에 저장된다.
호스트 이름으로 IP를 look up 할수 있다.
const dns = require("node:dns");
dns.lookup("example.org", (err, address, family) => {
console.log("address: %j family: IPv%s", address, family);
});
// address: "93.184.216.34" family: IPv4
인터넷은 한 가지의 프로토콜만 쓰지 않는다. 네트워크(TCP/IP 모델)은 4가지의 계층으로 이루어진다.
각 계층마다 사용 하는 프로토콜이 존재하며, 여기서 Web에 사용되는 프로토콜이 HTTP다.
서버에 요청을 보내기 전에 나의(클라이언트) 존재를 알려주는 과정에 TCP 프로토콜이 사용된다.
서버와 처음 인사할 때 3-way Handshake 기법으로 예의바르게 서버 요청을 준비한다.
서버는 특정 포트가 바인딩된 소켓을 가지고 있다.
해상 서버는 클라이언트의 연결 요청을 소켓을 통해 리스닝 하면서 기다린다.
클라이언트는 서버의 호스트 이름과 포트 번호로 연결을 시도한다.
Socket이란 네트워크 환경에 연결할 수 있게 만들어진 연결부로 TCP에서 동작하는 소켓을 TCP Socket라 한다.
TCP 소켓을 추상화한 클래스이며 EventEmitter 이기도 하다.
사용자가 생성하여 반환되는 net.createConnection()로 서버와 연결하는데 사용할 수 있다.
또한, 서버 입장에서 net.Server에서 발생한 이벤트 리스너로 클라이언트와 상호작용할 수 있다.
new net.Socket()는 Node.js의 net 모듈을 사용하여 TCP 소켓을 생성하는 방법 중 하나다. net.Socket()은 서버와 클라이언트 모두에서 사용할 수 있는 TCP 소켓 인터페이스를 제공한다.
이를 이용하여 소켓을 만들고 socket.connect() 메소드를 사용하여 서버와 연결하거나, socket.write() 메소드를 사용하여 소켓에 데이터를 쓰고, socket.on("data") 이벤트 핸들러를 사용하여 소켓으로부터 데이터를 읽을 수 있다. 또한, socket.on("error") 이벤트 핸들러를 사용하여 소켓 에러를 처리할 수도 있다.
const net = require("net");
const socket = new net.Socket();
socket.connect({ port: 80, host: "example.com" }, () => {
console.log("Connected to server!");
socket.write("Hello, server!");
});
socket.on("data", (data) => {
console.log(`Received data: ${data}`);
});
socket.on("error", (err) => {
console.error(`Socket error: ${err}`);
});
socket.on("close", () => {
console.log("Socket closed");
});
TCP Socket in NodeJS
net Socket 모듈
User Datagram Protocol의 축약어로 컴퓨터가 다른 컴퓨터와 데이터 통신을 하기 위한 규약(프로토콜)의 일종이다. UDP는 세계 통신표준으로 개발된 OSI 모형에서 4번째 계층인 전송 계층(Transport Layer)에서 사용하는 규약이다.
동일 계층에서 사용하는 또 다른 프로토콜로 TCP가 존재한다.
간단하게 TCP의 모든 신뢰성 기능이 없다고 보면 된다.
상대와 접속했고, 전송속도를 48 kbps로 설정했으면 48 kbps로 데이터를 전송하기만 한다.
받는 쪽에서 데이터를 제대로 받고 있는지조차도 신경 안 쓴다.
UDP 헤더에는 목적지주소, 데이터순서, checksum과 실데이터만 포함되고,
확인응답 같은 것이 없기 때문에 TCP보다 용량이 가볍고 송신속도가 빠르다.
하지만 확인응답을 하지 못하기때문에 신뢰도가 TCP보다 떨어지게 된다.
따라서 UDP는 비연결형이라 부르며 TCP는 연결형이라 구분한다.
TCP와 UDP는 모두 인터넷 프로토콜(IP) 스택에서 사용되는 전송 프로토콜이다. 하지만 TCP와 UDP는 목적과 특성이 다르다.
TCP (Transmission Control Protocol)는 연결 지향적인 프로토콜로서 데이터를 전송하기 전에 먼저 연결을 설정하고, 데이터를 보내고 나서는 연결을 종료하는 방식으로 동작한다. 이러한 접근 방식은 신뢰성과 안정성을 보장 한다는 특징이 있다. TCP는 데이터를 분할하여 전송하고, 이를 받는 측에서는 이를 재조립한다. 또한, 데이터 전송 도중에 오류가 발생하면 TCP는 다시 보내는 것으로 재전송을 시도 한다. 이러한 기능은 인터넷 브라우징, 이메일, 파일 전송 등과 같이 데이터 무결성이 중요한 애플리케이션에서 사용된다.
반면, UDP (User Datagram Protocol)는 연결이 없는 프로토콜이다. 이것은 데이터를 보내기만 하고, 수신측에서는 어떠한 응답도 하지 않는다. UDP는 TCP와 달리 데이터를 분할하지 않으며, 데이터의 무결성을 검사하지 않는다. 이러한 기능은 비디오 스트리밍, DNS 조회, 온라인 게임 등과 같이 속도와 실시간 성능이 중요한 애플리케이션에서 사용된다.
따라서, TCP는 안정적인 데이터 전송을 위해 사용되며, UDP는 빠른 속도와 실시간 성능이 필요한 애플리케이션에서 사용된다.
네트워크 모델에는 TCP/IP 모델과 OSI 7계층 모델이 있다.
OSI 7계층 모델은 ISO(국제표준화기구)에서 개발한 모델로 네트워크 프로토콜이 통신하는 구조를 7개의 계층으로 분리하여 각 계층간 상호 작동하는 방식을 정해 놓은 것이다.
계층 | 이름 | 단위(PDU) | 예시 | 프로토콜(Protocols) |
---|---|---|---|---|
7 | 응용 계층 (Application Layer) | Data | 텔넷(Telnet), 구글 크롬, 이메일, 데이터베이스 관리 | HTTP, SMTP, SSH, FTP, Telnet, DNS, modbus, SIP, AFP, APPC, MAP |
6 | 표현 계층 (Presentation Layer) | Data | 인코딩, 디코딩, 암호화, 복호화 | ASCII, MPEG, JPEG, MIDI, EBCDIC, XDR, AFP, PAP |
5 | 세션 계층 (Session Layer) | Data | NetBIOS, SAP, SDP, PIPO, SSL, TLS, NWLink, ASP, ADSP, ZIP, DLC | |
4 | 전송 계층 (Transport Layer) | TCP-Segment, UDP-datagram | 특정 방화벽 및 프록시 서버 | TCP, UDP, SPX, SCTP, NetBEUI, RTP, ATP, NBP, AEP, OSPF |
3 | 네트워크 계층 (Network Layer) | Packet | 라우터 | IP, IPX, IPsec, ICMP, ARP, NetBEUI, RIP, BGP, DDP, PLP |
2 | 데이터링크 계층 (DataLink Layer) | Frame | MAC 주소, 브리지 및 스위치 | Ethernet, Token Ring, AppleTalk, PPP, ATM, MAC, HDLC, FDDI, LLC, ALOHA |
1 | 물리 계층 (Physical Layer) | Bit | 전압, 허브, 네트워크 어댑터, 중계기 및 케이블 사양, 신호 변경(디지털,아날로그) | 10BASE-T, 100BASE-TX, ISDN, wired, wireless, RS-232, DSL, Twinax |
컴퓨터 간 데이터를 주고받을 때 HTTP,FTP 등의 프로토콜을 사용한다.
이 프로토콜들은 '응용 계층'에 해당되며, 브라우저에서 사용되는 프로토콜이다.
위 TCP/IP 모델에서 처럼 응용 계층에서 전송 -> 인터넷 -> 인터페이스 계층으로 전달되면서
정보가 추가되고, 상대 컴퓨터에게 보내는 방식이다.
Hyper Text(HTML)을 전송하기 위한 프로토콜
HTTP 요청은 클라이언트가 서버로 보내는 이전 데이터 패키지다.
이 외에도 다른 헤더들이 존재한다. 헤더들은 요청의 내용과 목적에 따라 변경된다.
또한, 헤더의 일부는 선택적이기도 하며, 일부 서버는 이러한 선택적 헤더를 무시할 수도 있다.
HTTPS 란 HyperText Transfer Protocol Security로 사용하는 HTTP 프로토콜의 보안 버전이다.
하나의 TCP Connection에서 여러 요청을 처리할 수 있는 stream을 지원하게 되었다.
TCP 방식의 오버헤드 때문에 자주 연결하는 HTTP 요청에는 비효율 적이라고 하여
UDP 기반 QUIC 프로토콜과 HTTP3.0이 논의되고 있다.
앞서 HTTP를 통해 서버에 리소스를 요청하면 서버로 부터 응답을 받는다.
응답 받은 리소스를 어떻게 해석하는지는 이전 글로 확인 할 수 있다.
HTTP Response는 요청에 대한 서버의 답변이다.
HTTP Header는 아래와 같이 구분된다.
HTTP에서의 Content-Length는 HTTP 요청 또는 응답에서 전송될 콘텐츠의 크기를 바이트 단위로 나타내는 헤더다. 이 헤더는 메시지 본문의 크기를 알려주기 때문에 수신자는 메시지 본문의 크기를 미리 알 수 있으므로 메시지를 끊임없이 읽어오는 대신에, 내용을 빠르게 처리할 수 있다.
Content-Length 헤더는 요청 메시지와 응답 메시지 모두에 사용될 수 있으며, 값은 0보다 큰 정수여야 한다. 예를 들어, Content-Length: 1234 헤더는 해당 요청 또는 응답 메시지의 본문이 1234바이트라는 것을 의미한다.
Content-Length 헤더는 HTTP/1.0과 HTTP/1.1 모두에서 지원되며, 이 헤더가 포함되어 있지 않으면, 수신자는 서버로부터 전체 메시지를 받을 때까지 대기해야 하므로, Content-Length 헤더는 HTTP의 성능을 향상시키는 중요한 역할을 한다.
HTTP의 상태 코드는 HTTP 응답 메시지에 포함되는 3자리 숫자로, 클라이언트가 요청한 작업의 처리 결과를 나타내는 중요한 정보를 제공한다.
HTTP 응답 메시지에서는 "HTTP/1.x" 형태의 상태 라인으로 시작하며, 이 상태 라인은 상태 코드, 간단한 상태 메시지와 함께 구성되는데, 상태 코드의 첫 번째 숫자가 응답 클래스를, 뒤의 두 숫자가 응답의 상세한 상태를 나타낸다.
1xx (Informational) - 정보 전달
2xx (Successful) - 성공적으로 완료
3xx (Redirection) - 추가 조치 필요
4xx (Client Error) - 클라이언트 오류
5xx (Server Error) - 서버 오류
대표적인 상태 코드로는 200 OK (성공적인 요청), 404 Not Found (요청한 자원이 존재하지 않음), 500 Internal Server Error (서버에서 오류가 발생) 등이 있다.
HTTP 상태 코드는 클라이언트와 서버 간의 통신을 보다 쉽게 이해할 수 있도록 돕는 중요한 통신 수단이다.
캐시란 일반적으로 시스템이나 응용 프로그램에서 자주 사용되는 데이터를 저장하는 임시 저장소다.
캐시를 사용하면 데이터 접근 속도를 높이고 시스템 성능을 향상시킬 수 있다.
캐시 정책은 캐시된 데이터를 언제 유지하고, 언제 삭제하고, 어떤 데이터를 캐시할 것인지를 결정하는 규칙들의 모음이다.
캐시 정책은 캐시에서 데이터를 관리하는 방법에 따라 다양하게 구성된다.
여러 가지 캐시 정책 중에서 대표적인 것은 다음과 같다.
이외에도 여러 가지 다른 캐시 정책이 있으며,
어떤 정책이 가장 적합한지는 캐시 사용 목적과 데이터 특성에 따라 달라진다.
컴퓨터 네트워킹에서, 레이어의 커뮤니케이션 프로토콜의 최대 전송 단위란
해당 레이어가 전송할 수 있는 최대 프로토콜 데이터 단위의 크기이다.
MTU 지표는 일반적으로 커뮤니케이션 인터페이스와 관련되어 나타난다.
클라이언트가 서버에 요청하고 응답 받는 흐름을 예제로 살펴보자.
본 페이지를 따라서 연습한 내용입니다.
클라이언트가 컨택하면 현재 Datetime을 콜백해주는 "서버"를 만들어보자.
import net from "net";
const server = net.createServer((socket) => {
socket.end(`${new Date()}\n`);
});
server.listen(59090);
클라이언트를 만들어보자.
클라이언트가 서버에 접속하면 datetime를 얻고 종료한다.
서버는 연결을 자동으로 닫으므로 클라이언트가 명시적으로 닫을 필요가 없다.
import net from "net";
const client = new net.Socket();
client.connect({ port: 59090, host: process.argv[2] ?? "localhost" });
client.on("data", (data) => {
console.log(data.toString("utf-8"));
});
import net from "net";
const server = net.createServer((socket) => {
console.log(
"Connection from",
socket.remoteAddress,
"port",
socket.remotePort
);
socket.on("data", (buffer) => {
console.log(
"Request from",
socket.remoteAddress,
"port",
socket.remotePort
);
socket.write(`${buffer.toString("utf-8").toUpperCase()}\n`);
});
socket.on("end", () => {
console.log("Closed", socket.remoteAddress, "port", socket.remotePort);
});
});
server.maxConnections = 20;
server.listen(59898);
대문자화 서버용 클라이언트
서버로 보내면 대문자로 답이 온다.
import net from "net";
import readline from "readline";
const client = new net.Socket();
client.connect(59898, process.argv[2] ?? "localhost", () => {
console.log("Connected to server");
});
client.on("data", (data) => {
console.log(data.toString("utf-8"));
});
const reader = readline.createInterface({ input: process.stdin });
reader.on("line", (line) => {
client.write(`${line}\n`);
});
reader.on("close", () => {
client.end();
});
한 라인만 보내는 클라이언트를 원하는 경우
클라이언트는 수신한 데이터를 처리 후 소켓을 파괴해야 한다.
import net from "net";
const client = new net.Socket();
client.connect({ port: 59898 }, process.argv[2] ?? "localhost", () => {
client.write(`${process.argv[3]}\r\n`);
});
client.on("data", (data) => {
console.log(`Server says: ${data.toString("utf-8")}`);
client.destroy();
});
특정 url를 입력받으면 DNS Server를 통해 IP를 확인하고,
TCP socket을 통해 이 IP와 소통하고 HTTP request를 해보자.
HTTP requests with TCP Sockets in NodeJs
JavaScript Socket Programming Examples
// socket을 위한 모듈
const net = require("net");
const socket = new net.Socket();
// DNS Server를 위한 모듈
const dns = require("dns");
// URL 모듈
const URL = require("url");
// Readline 모듈
const readline = require("readline");
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
// Readline으로 input값 받기
// Test URL 1) http://example.com
// Test URL 2) http://www.disney.co.kr
const main = () => {
rl.setPrompt(`\x1b[32mURL > \x1b[0m`);
rl.prompt();
rl.on("line", (line) => {
const http = new HttpRequest(line);
http.init();
}).on("close", () => {
rl.close();
process.exit();
});
};
// HTTP request 클래스
class HttpRequest {
constructor(url) {
this.url = URL.parse(url);
this.host = this.url.hostname;
// HTTP 디폴트 PORT = 80, HTTPS 디폴트 PORT = 443
this.port = this.url.port || this.url.protocol === "https:" ? 443 : 80;
this.path = this.url.path;
this.ip;
}
// DNS로 IP 가져오기
getIP() {
return new Promise((resolve, reject) => {
dns.lookup(this.host, (err, address, family) => {
if (err) reject(err);
else resolve(address);
});
});
}
// request 요청
makeRequest() {
// socket으로 URL 연결
socket.connect(this.port, this.host);
// 연결 후 이벤트
socket.on("connect", () => {
console.log(`\n\x1b[32mConnected to\x1b[0m ${this.host}`);
console.log(`\x1b[32mTCP Connetction :\x1b[0m ${this.ip} ${this.port}\n`);
// Request Message (request line + header 등)
const rawHttpRequest =
`GET ${this.path}home/index.jsp HTTP/1.1\r\n` +
`Accept: text/html\r\n` +
`Host: ${this.host}\r\n` +
`User-Agent: node.js/v18.12.0\r\n\r\n`;
console.log(`\x1b[32mHTTP Request >\x1b[0m\n${rawHttpRequest}`);
// 소켓에 Request Message 전송
socket.write(rawHttpRequest);
});
let response = "";
// response 이벤트
socket.on("data", (data) => {
// response 문자열 저장
response += data.toString();
console.log(`\x1b[32mHTTP Response >\x1b[0m\n${response}`);
// 소켓 파괴
socket.destroy();
});
// 에러 발생시 이벤트
socket.on("error", (error) => {
console.error(`Socket error: ${error}`);
});
}
async init() {
this.ip = await this.getIP();
this.makeRequest();
}
}
main();