A Simple HTTP Server With Java ServerSocket

1. 개요

HTTP 서버는 일반적으로 요청하는 클라이언트에게 자원을 제공합니다. Java에는 운영 준비 상태의 웹 서버들이 있습니다.

하지만, ServerSocket 클래스를 사용하여 HTTP 서버가 어떻게 작동하는지를 배우는 것이 가능합니다. 이 클래스는 IP 주소와 포트 번호를 가진 TCP 연결을 수신하는 서버를 생성할 수 있게 해줍니다.

이 튜토리얼에서는 ServerSocket 클래스를 사용하여 간단한 서버를 만드는 방법을 배웁니다. 또한, 간단한 HTTP 서버로 GET 요청을 수행할 것입니다. 이 서버는 교육 목적으로 만들어졌으며 생산 환경에는 적합하지 않습니다.

2. ServerSocket을 이용한 웹 서버의 기본

서버는 먼저 클라이언트 애플리케이션으로부터 연결을 기다립니다. 클라이언트 애플리케이션은 브라우저, 다른 프로그램, API 도구 등일 수 있습니다. 연결이 성공적으로 이루어지면 서버는 클라이언트에게 자원을 제공하며 응답합니다.

ServerSocket 클래스는 지정된 포트에서 서버를 생성할 수 있는 메소드를 제공합니다. 이 클래스는 accept() 메소드를 사용하여 정의된 포트에서 들어오는 연결을 수신합니다.

*accept()* 메소드는 연결이 설정될 때까지 차단되며, Socket 인스턴스를 반환합니다. 이 Socket 인스턴스는 서버와 클라이언트 간의 통신을 위한 입력 및 출력 스트림에 접근할 수 있게 해줍니다.

3. ServerSocket 인스턴스 생성하기

먼저, 지정된 포트로 ServerSocket 객체를 생성해 봅시다:

int port = 8080;
ServerSocket serverSocket = new ServerSocket(port);

다음으로, accept() 메소드를 사용하여 들어오는 연결을 수락합니다:

while (true) {
    Socket clientSocket = serverSocket.accept();
    //  ...
}

위의 코드에서 우리는 while 루프를 사용하여 연결을 기다립니다. 그런 다음, ServerSocket 객체에서 accept() 메소드를 호출하여 연결을 수신하고 수락합니다.

연결이 설정되면 메소드는 서버와 클라이언트 간의 통신을 위해 Socket 객체를 반환합니다.

4. 입력 및 출력 처리하기

일반적으로 서버는 클라이언트로부터 입력을 받고 적절한 응답을 전송합니다. 우리는 Socket 클래스의 getInputStream()getOutputStream() 메소드를 사용하여 클라이언트에 데이터를 읽고 쓰기 위한 스트림을 제공합니다.

예제를 확장하여 스트림을 읽고 쓰는 방법을 살펴보겠습니다:

while (true) {
    // ...
    BufferedReader in = new BufferedReader(
        new InputStreamReader(clientSocket.getInputStream())
    );
    BufferedWriter out = new BufferedWriter(
        new OutputStreamWriter(clientSocket.getOutputStream())
    ); 
    // ...
}

위의 코드에서 우리는 clientSocket 객체에서 getInputStream() 메소드를 사용하여 클라이언트와 서버 간의 활성 연결에 연결된 입력 스트림을 가져옵니다. 스트림은 텍스트 데이터를 보다 효율적으로 읽기 위해 BufferedReader로 래핑됩니다.

유사하게, getOutputStream()BufferedWriter로 래핑되어 서버가 클라이언트에 응답을 편리하게 전송할 수 있습니다.

우리의 경우, 입력에는 http://localhost:8080 URL로의 HTTP 요청이 포함됩니다.

다음으로, BufferedWriter 객체에서 write() 메소드를 호출하여 서버 응답을 작성합니다. 전형적인 HTTP 응답은 헤더와 본문으로 구성됩니다.

먼저 응답 본문을 작성합니다:

String body = """
    <html>
        <head>
            <title>Baeldung Home</title>
        </head>
        <body>
            <h1>Baeldung Home Page</h1>
            <p>Java Tutorials</p>
            <ul>
                <li>
                    <a href="/get-started-with-java-series"> Java </a>
                </li>
                <li>
                    <a href="/spring-boot"> Spring </a>
                </li>
                <li>
                    <a href="/learn-jpa-hibernate"> Hibernate </a>
                </li>
            </ul>
         </body>
     </html>
""";

위의 코드에서 우리는 응답 본문으로 간단한 HTML 페이지를 생성합니다. 다음으로, 콘텐츠 길이를 계산하여 헤더에 추가합니다:

int length = body.length();

다음으로, 출력 스트림에 HTTP 헤더와 본문을 작성합니다:

while (true) {
    // ...
    String clientInputLine;
    while ((clientInputLine = in.readLine()) != null) {
        if (clientInputLine.isEmpty()) {
            break;
        }

        out.write("HTTP/1.0 200 OK\r\n");
        out.write("Date: " + now + "\r\n");
        out.write("Server: Custom Server\r\n");
        out.write("Content-Type: text/html\r\n");
        out.write("Content-Length: " + length + "\r\n");
        out.write("\r\n");
        out.write(body);
    }
}

위의 코드에서는 write() 메소드를 사용하여 HTTP 헤더와 본문을 정의합니다. 특히, 헤더와 본문은 \r\n (빈 줄)로 구분하여 헤더의 끝을 나타냅니다.

5. 다중 스레드 서버

우리의 간단한 서버는 단일 스레드에서 요청을 처리하므로 성능에 영향을 미칩니다. 서버는 동시에 여러 요청을 처리할 수 있어야 합니다.

초기 예제를 수정하여 각 요청을 별도의 스레드에서 처리하도록 해보겠습니다. 먼저, SimpleHttpServerMultiThreaded라는 클래스를 생성합니다:

class SimpleHttpServerMultiThreaded {

    private final int port;
    private static final int THREAD_POOL_SIZE = 10;

    public SimpleHttpServerMultiThreaded(int port) {
        this.port = port;
    } 
}

위의 클래스에서 우리는 포트 번호와 스레드 풀 크기를 나타내기 위해 두 개의 필드를 정의합니다. 포트 번호는 서버 객체가 생성될 때 생성자에 의해 전달됩니다.

다음으로, 클라이언트 통신을 처리하는 메소드를 정의하겠습니다:

void handleClient(Socket clientSocket) {
    try (BufferedReader in = new BufferedReader(
            new InputStreamReader(clientSocket.getInputStream()));
        BufferedWriter out = new BufferedWriter(
            new OutputStreamWriter(clientSocket.getOutputStream()))) {

        String clientInputLine;
        while ((clientInputLine = in.readLine()) != null) {
            if (clientInputLine.isEmpty()) {
                break;
            }
        }
        LocalDateTime now = LocalDateTime.now();

        out.write("HTTP/1.0 200 OK\r\n");
        out.write("Date: " + now + "\r\n");
        out.write("Server: Custom Server\r\n");
        out.write("Content-Type: text/html\r\n");
        out.write("Content-Length: " + length + "\r\n");
        out.write("\r\n");
        out.write(body);

    } catch (IOException e) {
        // ...
    } finally {
        try {
            clientSocket.close();
        } catch (IOException e) {
            // ...
        }
    }
}

위의 메소드는 클라이언트와의 입력 및 출력 통신을 처리하는 방법을 보여줍니다. bodylength는 이전 섹션의 예제와 동일합니다.

다음으로, 각 연결을 별도의 스레드에서 설정하기 위한 start() 메서드를 생성하겠습니다:

void start() throws IOException {
    try (ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
        ServerSocket serverSocket = new ServerSocket(port)) {

        while (true) {
            Socket clientSocket = serverSocket.accept();
            threadPool.execute(() -> handleClient(clientSocket));
        }
    }
}

위의 코드에서 우리는 ExecutorService를 인스턴스화하여 스레드 풀을 생성합니다. 다음으로, threadPool 객체에서 execute() 메소드를 호출하여 각 클라이언트 연결에 대한 작업을 제출합니다.

스레드 풀의 스레드에 클라이언트 연결을 할당함으로써 서버는 여러 요청을 동시에 처리할 수 있으며, 성능이 크게 향상됩니다.

더욱이, 클라이언트가 연결할 때마다 accept() 메소드는 새로운 Socket 인스턴스를 생성합니다. 이 Socket은 클라이언트 연결에 특정하며, 서버와 클라이언트 간의 전용 통신 채널을 제공합니다.

6. 서버 테스트하기

이제 main 메소드에서 서버를 인스턴스화하여 실행해 보겠습니다:

static void main(String[] args) throws IOException {
    int port = 8080;
    SimpleHttpServerMultiThreaded server = new SimpleHttpServerMultiThreaded(port);
    server.start();
}

그 다음 http://localhost:8080를 브라우저에서 열어 서버를 테스트합니다.

7. 결론

이번 글에서는 ServerSocket 클래스를 사용하여 간단한 서버를 만드는 방법을 배웠습니다. 또한, 이 클래스를 사용하여 단일 스레드 및 다중 스레드 서버를 생성하는 예제를 보았습니다.

항상 그렇듯이, 예제의 전체 소스 코드는 GitHub에서 확인할 수 있습니다.

원본 출처

You may also like...

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다