HTTP Server 를 가볍게 구현해보자

Jeong·2023년 9월 17일
0

HTTP

목록 보기
3/6
post-thumbnail

키워드

  • Java ServerSocket
  • Blocking vs Non-Blocking

최종 목표

HTTP 에 대해서 간단히 알아보고 직접 구현해보자.

웹의 핵심 요소인 HTTP는 웹 개발자에게 있어서 필수적으로 알아야 하는 지식이다. 그러나 상당수의 개발자들이 HTTP가 어떠한 원리로 동작하는지 제대로 알지 못하고 있다. 이번 과정을 통해 HTTP 통신을 처리하는 웹 서버가 의외로 간단한 원리로 구현되어 있다는 걸 알게 될 것이다. 직접 간단한 웹 서버를 구현해보면서, HTTP를 제대로 이해하고 동시에 Spring Web MVC가 어떤 편의를 제공하는지 절실하게 느껴보자.

현재 목표

HTTP Server 를 가볍게 구현해보자.

HTTP 서버를 간단하게 만들어보자

세팅은 HTTP Client 에서 했던 것과 동일하게 해준다.

  1. gradle init
  2. AppTest.java 내용을 날린다.
    3.설정 > tools 에서 on Save 맨 위에 2개를 체크 표시로 바꿔준다.
  3. 구성 편집 에서 app:run 을 Run Server 로 잡는다.

이는 프로젝트마다 초기화 되기 때문에 매번 해줘야 한다.


HTTP 서버는 1, 2, 4, 5 단계를 처리하면 된다.

1단계: 서버는 접속 요청을 받기 위한 소켓을 연다. → Listen
3단계: 서버는 접속 요청을 받아서 클라이언트와 통신할 소켓을 따로 만든다. → Accept
4단계: 소켓을 통해 서로 데이터를 주고 받는다. → Send & Receive ⇒ 반복!
5단계: 통신을 마치면 소켓을 닫는다. → Close ⇒ 상대방은 Receive로 인지할 수 있다.

1️⃣ Listen

이는 다른 곳에 접속하는 게 아니기 때문에 포트 번호만 정하면 된다. ('이 포트 번호로 요청을 줘') 만약 80 포트를 사용 중이라면 8080 등 다른 포트 번호를 쓰면 된다.

int port = 8080;

그리고 Java에서는 Socket 을 여는데 ServerSocket이라는 별도의 클래스를 사용한다.

ServerSocket 은 그냥 Socket 과 다르게 Send, Recive 를 안 할 것이기 때문에 별로의 Server Socket 을 만들어준 것이다. 그래서 Socket을 상속한 게 아니라, 완전히 구별된다는 점을 주의해야 한다.
(내부를 보면 Socket 을 상속받지 않고 바로 java.io.Closeable 을 상속받는다.)

backlog 는 요청이 여러 번 들어올 때 얼마나 대기를 세울 지이다.

한 줄로 Listen 처리가 된다.

ServerSocket listener = new ServerSocket(port, backlog); // backlog 의 기본은 50이다.

ServerSocket listener = new ServerSocket(port, 0); 

서버는 클라이언트와 다르게 Listen 되고 있는 상태에서 Accept 를 여러 번 할 수 있다.

Accpt 는 클라이언트의 접속을 기다린다. 클라이언트가 접속하면 통신용 소켓을 따로 만들어서 돌려준다.

Socket socket = listener.accept();

연결을 하면 한 번 받아주고 바로 끊기게 된다. 그리고 기다리게 된다.

I/O에서 이렇게 기다리는 걸 Blocking이라고 한다. 파일 읽기, 쓰기 등도 모두 Blocking 동작이지만, TCP 통신에서는 네트워크 상태 같은 요인에 의해 크게 지연될 수 있고, accept처럼 상대방의 요청이 없으면 영원히 기다리는 (Blocking) 일이 벌어질 수 있다. 그래서 멀티스레드나 비동기, 이벤트 기반 처리 등이 필요하다. (이에 대해 지금은 다루지 않겠다.)

2️⃣ Request

서버 입장에서 Request 는 요청을 하는 게 아니라, 요청을 받는 것이다. 따라서 먼저 읽어야 한다.

코드는 클라이언트에서 다룬 것과 100% 동일하다.

InputStream inputStream = socket.getInputStream();

Reader reader = new InputStreamReader(inputStream);

CharBuffer charBuffer = CharBuffer.allocate(1_000_000);

reader.read(charBuffer);

charBuffer.flip();

String text = charBuffer.toString();

System.out.println(text);

3️⃣ Response

클라이언트의 요청과 마찬가지로, 응답 메시지를 만들어서 전송하면 된다.

HTTP/1.1 200 OK
(빈 줄)
Hello, world!

Java 코드

String message = """
	HTTP/1.1 200 OK
	
	Hello, world!
	""";

또는

String message = "" +
	"HTTP/1.1 200 OK\n" +
	"\n" +
	"Hello, world!\n";

⚠️ 마지막 줄에 Newline(\n)을 넣는 걸 잊지 말자.


제대로 하려면 Content-Type과 Content-Length를 더해주는 게 좋다.

String body = "Hello, world!";
byte[] bytes = body.getBytes();
String message = "" +
	"HTTP/1.1 200 OK\n" +
	"Content-Type: text/html; charset=UTF-8\n" +
	"Content-Length: " + bytes.length + "\n" +
	"\n" +
	body;

⚠️ Content-Length로 정확한 크기를 알 수 있기 때문에 마지막 줄에 Newline(\n)을 넣지 않아도 된다.

전송 코드는 클라이언트와 100% 동일하다.

4️⃣ Close

마찬가지로 클라이언트와 100% 동일하다.

전체 코드

package server;

import java.io.*;
import java.net.*;
import java.nio.*;

public class App {
    public static void main(String[] args) throws IOException {
        App app = new App();
        app.run();
    }

    private void run() throws IOException {
        // 1. Listen
        int port = 8080;

        ServerSocket listener = new ServerSocket(port, 0);

        System.out.println("Listen!");

        while (true) {
            
            // 2. Accept
            Socket socket = listener.accept();
            System.out.println("Accept!");

            // 3. Request
            InputStream inputStream = socket.getInputStream();
            Reader reader = new InputStreamReader(inputStream);

            CharBuffer charBuffer = CharBuffer.allocate(1_000_000);

            reader.read(charBuffer);

            charBuffer.flip();

            String text = charBuffer.toString();

            System.out.println(text);

            // 4. Response
            String body = "Hello, world!";
            byte[] bytes = body.getBytes();
            String message = "" +
                    "HTTP/1.1 200 OK\n" +
                    "Content-Type: text/html; charset=UTF-8\n" +
                    "Content-Length: " + bytes.length + "\n" +
                    "\n" +
                    body;

            Writer writer = new OutputStreamWriter(socket.getOutputStream());
            writer.write(message);
            writer.flush();

            // 5. Close
            socket.close();
        }
    }
}

아하! 포인트

HTTP 서버가 1, 2, 4, 5 단계를 어떻게 처리하는지 좀 감을 잡은 것 같다.

다음에는?

Blocking 같은 부분도 편하게 처리할 수 있는 준비된 것들에 대해 알아보자.

profile
성장중입니다 🔥 / 나무위키처럼 끊임없이 글이 수정됩니다!

0개의 댓글