웹 프로그래밍

웹 서버 기초 (#1. Echo Server)

2025-12-08

모든 서버 개발자의 고향: Echo Server

  • HTTP 프로토콜은 TCP 프로토콜 위에 위치(TCP 자체가 어떻게 상세하게 동작하는지는 우리의 관심사가 아님)
  • 우리가 TCP에 대해 알아야 할 것은, 이것이 원시 바이트(raw bytes)를 전송하기 위한 양방향 채널이며 HTTP나 SSH와 같은 다른 애플리케이션 프로토콜을 위한 전송 수단(carrier)이라는 점
  • 따라서 우리는 HTTP에 대해서만 고민하도록 하겠음

1. 요청-응답

  • TCP 연결의 각 방향은 독립적으로 작동할 수 있지만, 많은 프로토콜은 요청-응답(request-response) 모델을 따름
  • 클라이언트는 요청을 보내고, 서버는 응답을 보내며, 그 후 클라이언트는 동일한 연결을 사용하여 추가적인 요청과 응답을 주고받을 수 있음
  • HTTP 요청 또는 응답은 헤더(header)와 그 뒤에 오는 선택적인 페이로드(payload)로 구성
 client        server
| req1 |  ==>
         <==  | res1 |
| req2 |  ==>
         <==  | res2 |
         ...

2. 요청-응답(HTTP) 예제

netcat 명령어는 WSL, macOS 또는 Linux에서 사용할 수 있음

  • nc(netcat) 명령어는 목적지 호스트와 포트로 TCP 연결을 생성한 후, 해당 연결을 표준 입력(stdin)표준 출력(stdout)에 연결
$ nc example.com 80

GET / HTTP/1.0
Host: example.com
 

(마지막에 비어 있는 한 줄을 위해 ’엔터’를 눌러야 합니다)

  • 다음과 같은 응답을 받게 됨
HTTP/1.0 200 OK
Content-Type: text/html
ETag: "84238dfc8092e5d9c0dac8ef93371a07:1736799080.121134"
Last-Modified: Mon, 13 Jan 2025 20:11:20 GMT
Cache-Control: max-age=86000
Date: Fri, 12 Sep 2025 06:16:49 GMT
Content-Length: 1256
Connection: close
X-N: S

<!doctype html>
...

3. TCP 서버 코딩하기

  • 프로토콜 계층
    • 네트워크 프로토콜은 여러 계층으로 나뉘며, 상위 계층은 하위 계층에 의존하고 각 계층은 서로 다른 기능을 제공
 top
  /\    | App |     message or whatever
  ||    | TCP |     byte stream
  ||    | IP  |     packets
  ||    | ... |
bottom

바이트 스트림은 단순히 순서가 있는 바이트의 연속

  • 패킷 기반 통신 방식은 애플리케이션이 해결해야 할 많은 문제점을 안고 있어 쉽지 않음
    • 메시지 데이터가 단일 패킷의 용량을 초과하면 어떻게 할 것인가?
    • 패킷이 손실되면 어떻게 할 것인가?
    • 패킷 순서가 뒤바뀌면 어떻게 할 것인가?
  • 문제를 단순화하기 위해, IP 패킷 위에 다음 계층인 TCP가 추가
    • 패킷 대신 바이트 스트림
    • 신뢰성 있고 순서가 보장되는 전달

4. 소켓 기본 연산 (Socket Primitives)

  • 소켓 API는 언어와 라이브러리에 따라 다른 형태로 제공
  • 기본을 모른 채 API 문서에 뛰어들면 혼란스러워지기 쉬움
  1. 리스닝 소켓(Listening sockets): 주소에서 리스닝하여 얻음 \(\rightarrow\) TCP 서버는 특정 주소(IP + 포트)에서 리스닝(listen)하며 해당 주소로부터 클라이언트 연결을 수락(accept)
  2. 연결 소켓(Connection sockets): 리스닝 소켓으로부터 클라이언트 연결을 수락하여 얻음 \(\rightarrow\) 리스닝 주소 역시 소켓 핸들(socket handle)로 표현되며, 새로운 클라이언트 연결을 수락하면, 그 TCP 연결의 소켓 핸들을 얻게 됨
  3. 전송(send)수신(receive)읽기(read)쓰기(write)라고도 불림 - 쓰기 측면에서는, 상대방에게 더 이상 보낼 데이터가 없음을 알리는 방법이 있음, 소켓을 닫으면(closing) 연결이 종료되고 TCP FIN이 전송

5. 소켓 기본 연산 목록

  • 리스닝 소켓

    • bind & listen
    • accept
    • close
  • 연결 소켓

    • read
    • write
    • close

6. Node.js의 소켓 API

  • 소켓 API를 소개하기 위해 작은 실습을 진행
  • 클라이언트로부터 데이터를 읽어 동일한 데이터를 다시 써주는 TCP 서버입
  • 이를 “에코 서버(echo server)”라고 함

0단계: 템플릿 복사

1단계: 리스닝 소켓 생성하기

import * as net from "net";

let server = net.createServer();
server.listen({ host: '127.0.0.1', port: 12345 }, () => {
  console.log('Server is listening on port 12345');
});

2단계: 새 연결 수락하기

  • server.on('connection', newConn)newConn 콜백 함수를 등록
  • 런타임은 자동으로 accept 연산을 수행하고 새 연결을 인자로 하여 콜백을 호출
function newConn(socket: net.Socket): void {
  console.log('new connection', socket.remoteAddress, socket.remotePort);
  // ...
}

...
server.on('connection', newConn);
...

3단계: 오류 처리하기

  • 'connection' 인자는 ’이벤트(event)’라고 불리며, 콜백을 등록할 수 있는 대상
  • 리스닝 소켓에는 다른 이벤트들도 있음(예를 들어, 오류가 발생했을 때 호출되는 'error' 이벤트가 있음)
server.on('error', (err: Error) => { throw err; });

4단계: 읽기와 쓰기

  • 연결로부터 수신된 데이터 또한 콜백을 통해 전달
  • 소켓에서 읽기와 관련된 이벤트는 'data' 이벤트와 'end' 이벤트
  • 'data' 이벤트는 상대방으로부터 데이터가 도착할 때마다 호출되며, 'end' 이벤트는 상대방이 전송을 종료했을 때 호출
  • socket.write() 메서드는 상대방에게 데이터를 다시 보냄
    socket.on('end', () => {
        console.log('EOF.');
    });
    socket.on('data', (data: Buffer) => {
        console.log('data:', data);
        socket.write(data);
    });

5단계: 연결 닫기

  • socket.end() 메서드는 전송을 종료하고 소켓을 닫음
  • 여기서는 데이터에 문자 ’q’가 포함되어 있을 때 socket.end()를 호출하여 이 시나리오를 쉽게 테스트할 수 있도록 함
    socket.on('data', (data: Buffer) => {
        ...
        // actively closed the connection if the data contains 'q'
        if (data.includes('q')) {
            console.log('closing.');
            socket.end();   // this will send FIN and close the connection.
        }
    });          

6단계: 테스트하기

$ nc 127.0.0.1 12345
hello
hello
world
world
q

7. 서버 코드(echo-server.ts)

import * as net from "net";

function newConn(socket: net.Socket): void {
    console.log('new connection', socket.remoteAddress, socket.remotePort);
    
    socket.on('end', () => {
        console.log('EOF.');
    });
    
    socket.on('data', (data: Buffer) => {
        console.log('data:', data);
        socket.write(data);
        if (data.includes('q')) {
            console.log('closing.');
            socket.end();
        }
    });
}

let server = net.createServer();
server.on('error', (err: Error) => { throw err; });
server.on('connection', newConn);
server.listen({host: '127.0.0.1', port: 12345});        

8. 클라이언트 코드(echo-client.ts)

import * as net from 'net';

const HOST = '127.0.0.1';
const PORT = 12345;

const client = new net.Socket();

client.connect(PORT, HOST, () => {
  console.log(`서버(${HOST}:${PORT})에 연결되었습니다.`);
  const message = 'Hello, Echo Server!';
  console.log('보냄:', message);
  client.write(message);
});

client.on('data', (data) => {
  console.log('받음:', data.toString());
  client.destroy();
});

client.on('close', () => {
  console.log('연결이 종료되었습니다.');
});

client.on('error', (err) => {
  console.error('에러 발생:', err.message);
});

업데이트 이력

버전 변경 이력

버전 날짜 변경 내용
v.20250922 2025-09-22 Echo Server 코드 추가

참고문헌