TCP/IP 소켓으로 서버 구현하기

jewoo·2021년 11월 26일
4
post-thumbnail

서론

학습 목표 📖

  • 각종 서버 프레임워크의 내부 동작을 알게된다.

본론

학습 계기

TCP/IP 소켓으로 웹통신 구현 수업에 참여하게 되었다.

사전 지식 학습

서버를 구현하기 전에 알아둬야 할 개념들을 먼저 습득한 후 단계별로 구현하도록 하자.

인터넷과 웹의 차이

둘이 같은거 아닌가요..?

인터넷과 웹이 같다고 생각하는 경우가 있는데 둘은 엄연히 다르다.

"야, 인터넷 안되는데?"
"야, 웹사이트 안되는데?"

위의 문장을 들었을때 어떠한 대답이 올 수 있을까?

A : "야, 나 인터넷 안되는데?
B : "니 와이파이 연결 되있어? 연결 끊긴거 아니야?"

위의 문장을 보아 인터넷은 네트워크를 의미함을 알 수 있다.

A : "야, 웹사이트 안되는데?"
B : "어, 그러네 아까 됬는데 서버 다운됬나보다"

위의 문장을 보아 웹사이트는 네트워크에서 돌아가는 한 서비스를 의미함을 알 수 있다.

즉 인터넷은 서로 연결된 네트워크를 의미하고 웹은 그 연결된 네트워크에서 제공되는 한 서비스를 말한다.
웹에는 다양한 웹 사이트들이 존재하며 웹 사이트는 여러 웹 페이지들로 구성되어 있다.
그리고 그 웹 페이지를 보기위해 우리는 웹 브라우저를 사용하는 것이다.

이제 각각 이름이 왜 웹 "브라우저", "사이트", "페이지" 인지 알 수 있다.

서버란 무엇인가?

아르바이트 서빙 할때 쓰는 단어 아닌가요?

맞다.
아르바이트 서빙 할때 쓰는 그 단어이다.

Server = Serve + er
Serve : 제공하다
Server : 제공자

식당에서 손님이 "저기요! 돈고츠 라멘 하나 주세요!" 라고 말하면 "네 알겠습니다. 돈고츠요~" 라고 갖다주는게 서버이다.

웹에서도 마찬가지이다.
프로그램이나 하드웨어의 일부로서 다른 디바이스에게 기능을 제공하는 ...
거창하게 이런거 다 필요없고 그냥 요청이 오면 응답 해주는 것 이라고 생각하면 된다.
웹에서 클라이언트와 서버는 파일을 요청하고 응답하게 된다.

소켓이란 무엇인가?

소켓이 콘센트라구요?


TCP/IP 소켓을 이용하여 서버를 구현한다고 했는데 그럼 소켓이 무엇인지 알아보자.
소켓을 검색했을때 나오는 이미지는 콘센트가 나온다.

먼저 과거에는 어떻게 통신을 했을까?
단순하게 컴퓨터가 2대일 경우에 통신을 하기 위해서는 구리선만 있으면 된다.
구리선으로 내 컴퓨터에 하나 꽂고 상대 컴퓨터에 하나 꽂고 전기적 신호를 주고 받으면 그게 바로 통신이다.

이렇게 전기적 신호를 주고 받을때 들어가고 나가는 곳이 바로 소켓이다.

자 그럼 다시 정의로 돌아가서
"네트워크 소켓은 네트워크에서 데이터를 보내고 받는 엔드포인트로서, 소프트웨어 구조로 하나의 네트워크 노드를 ...."
대체 왜 이러는 걸까? 당연히 못알아 듣는다.

역시나 거창하게 이런거 다 필요없고 그냥 출입구 라고 생각하면 된다.
우리가 콘센트를 꽂고 전자제품을 사용하듯이 우리는 이 소켓이라는 접점, 출입구를 만들고 데이터를 주고 받게 된다.

이래도 이해가 안되면 그냥 롯데월드를 생각해보자.
혜성특급 타려고 기다렸다가 입장하고 놀이기구 타고 반대쪽으로 나오지 않는가?
우리는 데이터이고 놀이기구 타고 이동하는게 스트림이고 입장하고 나오는 곳이 각각 소켓이다.

URL은 어떻게 구성되어 있는가?

[프로토콜] :// {[아이디]:[패스워드]@} [서버주소] {:포트번호} [리소스의 경로] {?쿼리스트링} {#해시}

우리는 소켓으로 웹통신이 어떻게 되는지를 알기 전에 먼저 URL이 어떻게 구성되어 있는지를 알 필요가 있다.

URL : Unified Resource Location 의 줄임말로 아래와 같이 구성되어 있다.

[프로토콜] :// {[아이디]:[패스워드]@} [서버주소] {:포트번호} [리소스의 경로] {?쿼리스트링} {#해시}

[]는 필수이며 {}는 생략이 가능하다.

포트번호 같은 경우에 기본 포트 번호가 아닌 경우 명시해줘야 하는데 기본포트 번호는 다음과 같이 이루어져 있다.
http : 80
https : 443
ftp : 21
ssh/sftp : 22
telent : 23

통신 원리

자 그럼 기본적인 용어들과 개념을 학습했으니 통신 원리에 대해서 알아보자.

어떠한 프로그램이든지 무조건 데이터 통신을 하려면 소켓이라는 것이 필요하다.
입출구가 있어야 데이터들이 나가고 들어올것 아닌가?

그럼 그 소켓은 어떻게 생성이 되냐면 TCP/UDP를 통해 연결을 했을때 소켓이 생성이 된다.
그 소켓에서 데이터를 주고 받으면 buffer 객체로 데이터가 스트림 된다.

생성된 소켓에서 웹을 통해 데이터를 주고 받을때 사용하는 프로토콜이 바로 HTTP 이다.
여기서 중요한 점은 데이터 통신을 한다고 무조건 웹을 사용하는 것이 아니라는 것이다.
구리선으로 그냥 컴퓨터 2대 연결하고 주고받으면 웹 사용안해도 통신할 수 있을거 아닌가?
이 말은 즉슨 HTTP는 인터넷에서 웹 서비스를 이용할때에 사용되는 프로토콜임을 기억하면 된다.

만약 웹브라우저 없이 그냥 프로그램끼리 데이터를 주고받는 채팅 프로그램을 구현한다면?
HTTP 프로토콜은 사용되지 않는다.

위 그림은 TCP/IP 계층도인데 아래 레이어들을 거쳐서 상위 레이어를 사용하는 거라고 이해하면 된다.
그림에는 없지만 맨 상위에 HTTP 프로토콜이 위치하는데 즉 TCP/UDP 기반위에서 사용되는 프로토콜임을 알 수 있다.

서버 구현

Request Message

드디어 서버 구현 단계에 도달했다.
라이브 스트리밍과 같이 신뢰성보다 패킷전달이 우선시 되는 경우가 아니면 기본적으로 TCP를 통해 소켓을 만들고 통신하게된다.

const net = require('net');

const server = net.createServer(socket=>{
    socket.on("data",buffer=>{
        console.log(buffer.toString());
    })
});

server.listen(4000);

NodeJS를 실행한 후 브라우저를 열고 127.0.0.1:4000 을 입력하면

위와 같이 나오는데 위 메세지가 브라우저가 서버에게 보내는 request 메세지이다.

첫번째줄
GET / HTTP/1.1
띄어 쓰기로 구분 되어 있으며 의미하는 바는 다음과 같다.
HTTP Method, Resource Location, HTTP Version
GET 메소드 요청, 리소스 위치는 root, HTTP Version은 1.1을 사용한다.

두번째줄 ~ 마지막
Host: 127.0.0.1:4000
Key, Value로 이루어져 있는 이 부분을 우리는 Request Header라고 부른다.
Request Header에는 여러 항목들이 있지만 HTTP 명세서에 따르면 Host 부분이 필수로 들어가야 한다고 나와있다.
나는 내 컴퓨터에서 서버를 돌리고 있기 때문에 내 IP주소인 127.0.0.1과 포트번호 4000이 나오는걸 확인할 수 있다.

Response Message

서버는 Request가 왔으니 그에 맞는 Response를 해주어야 한다.
그럼 우리는 어떤 메세지를 보내줘야 할까?
확인하기 위해 우리가 "naver.com"을 접속하고 Request Message를 날렸을때 네이버에서 어떻게 Response Message를 날리는지 확인해보자.

const net = require('net');

const socket = net.createConnection(80,"naver.com",()=>{
    socket.write(`
        GET / HTTP/1.1
        Host: naver.com
    `);

    socket.on("data",buffer=>{
        console.log(buffer.toString());
    });
})

첫번째 줄과 Request 필수 헤더인 Host만 넣고 테스트 해보자.
NodeJS를 실행하면 아래와 같은 메세지가 뜬다.

Response Message를 해석해보자.

첫번째 줄
HTTP/1.1 302 Moved Temporarily
HTTP Version, Status Code, Status Message
HTTP 1.1 Version 사용, 302 코드, 일시적으로 이동 했다는 메세지 이다.

Status Code는 서버에서 요청에 맞는 응답코드를 의미하는데 각 응답코드가 뜻하는 것은 아래와 같다.

2xx : 정상처리 되었음
3xx : 리소스가 다른 주소로 이동하였음
4xx : 리소스가 존재하지 않거나 처리할 수 없음
5xx : 웹서버 내부에서 오류가 발생하였음

두번째 줄 ~ 일곱번째 줄
Key, Value로 이루어져 있으며 이 부분이 Response Header이다.
Response Header에서는 Content-typeContent-length가 필수 요소로 명세되어 있다.

아홉번째 줄 ~ 마지막
html 태그들을 볼 수 있으며 이 부분이 서버에서 보내는 Content이다.

Server

Response Message 까지 확인했으니 처음 작업한 부분을 업그레이드해서 naver처럼 응답하는 서버를 만들어보자.

const net = require('net');
const fs = require('fs');

const server = net.createServer(socket=>{
    socket.on("data",buffer=>{
        const requestMessage = buffer.toString();
        const [first] = requestMessage.split('\r\n');
        let [method, resource, version] = first.split(' ');

        if(resource[resource.length - 1] === "/"){
            resource += "index.html";
        }

        if(fs.existsSync("./source"+resource)){
            const content = fs.readFileSync("./source"+resource);
            socket.write(Buffer.from("HTTP/1.1 200 OK\r\n"));
            socket.write(Buffer.from("Content-Type: text/html\r\n"));
            socket.write(Buffer.from(`Content-Length: ${content.length}\r\n`));
            socket.write(Buffer.from("\r\n"));
            socket.write(content);
            socket.write(Buffer.from("\r\n"));
            return;
        }

        const content = `<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Document</title></head><body>
                            <h1>NOT FOUND</h1></body></html>`;

        socket.write(Buffer.from("HTTP/1.1 404 Not Found\r\n"));
        socket.write(Buffer.from("Content-Type: text/html\r\n"));
        socket.write(Buffer.from(`Content-Length: ${content.length}\r\n`));
        socket.write(Buffer.from("\r\n"));
        socket.write(content);
        socket.write(Buffer.from("\r\n"));
    })

});

server.listen(4000);

시나리오는 다음과 같다.

  1. TCP 3-way handshake 후 소켓 생성
  2. Client가 브라우저를 통해 Request Message를 보냄
  3. Server 쪽에서 소켓을 통해 받은 Request Message(버퍼객체)를 확인 후 해석함
  4. Request Message에 따라 file system 모듈로 파일을 읽음 (폴더로 요청시에 기본값은 "index.html"로 보내게 설정)
  5. 파일이 존재하지 않을 경우에는 404 페이지를 보냄

비록 예제는 기본적으로 GET요청 임을 전제로 구현하였지만 GET, POST, PUT, DELETE에 따라 분기 처리 또한 필요하며 수많은 Header들을 조작하는 것도 가능하다.

결론

위 예제를 통해 Express나 Spring Boot 같은 프레임워크가 내부적으로 어떻게 동작하는지를 알 수 있다.

우리는 단순히 app.get(), app.post()를 사용했는데 내부적으로는 Request Message를 해석하고 그에 맞는 Response Message를 생성해서 보내주는 것이었다.

시간과 레벨이 되면 express 소스를 열어서 해석해보는 것도 굉장히 도움이 된다하니 참고하자.

profile
Software Engineer

1개의 댓글

comment-user-thumbnail
2022년 4월 27일

너무 유익하게 잘봤습니다 ㅠ 한번 더 볼게요

답글 달기