Java ServerSocket 활용 WAS 서버 만들기 Version1

jkky98·2025년 2월 28일

Java

목록 보기
51/51

구현 코드

public class HttpServerV1 {
    
    private final int port;

    public HttpServerV1(int port) {
        this.port = port;
    }
    
    public void start() throws IOException {
        ServerSocket serverSocket = new ServerSocket(port); // localhost를 주지 않는다면 기본적으로 0.0.0.0으로 바인딩 된다.
        log("서버 시작 port: " + port);
        
        while (true) {
            Socket socket = serverSocket.accept();
            process(socket);
        }
    }

    private void process(Socket socket) throws IOException {
        try (
                socket;
                BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), UTF_8));
                PrintWriter writer = new PrintWriter(socket.getOutputStream(), false, UTF_8)) {

            String requestString = requestToString(reader);
            if (requestString.contains("/favicon.ico")) {
                log("favicon 요청");
                return;
            }

            log("HTTP 요청 정보 출력");
            System.out.println(requestString);

            log("HTTP 응답 생성중...");
            responseToClient(writer);
        }
    }

    private void responseToClient(PrintWriter writer) {
        // 웹 브라우저에 전달하는 내용

        String body = "<h1>Hello World</h1>";
        int length = body.getBytes(UTF_8).length;

        StringBuilder sb = new StringBuilder();
        sb.append("HTTP/1.1 200 OK\r\n");
        sb.append("Content-Type: text/html\r\n");
        sb.append("Content-Length: ").append(length).append("\r\n");
        sb.append("\r\n");
        sb.append(body);

        log("HTTP 응답 정보 출력");
        sleep();
        writer.println(sb);
        writer.flush();
    }

    private static void sleep() {
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private static String requestToString(BufferedReader reader) throws IOException {
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            if (line.isEmpty()) {
                break;
            }
            sb.append(line).append("\n");
        }

        return sb.toString();
    }
}

분석

	public HttpServerV1(int port) {
        this.port = port;
    }
    
    public void start() throws IOException {
        ServerSocket serverSocket = new ServerSocket(port); // localhost를 주지 않는다면 기본적으로 0.0.0.0으로 바인딩 된다.
        log("서버 시작 port: " + port);
        
        while (true) {
            Socket socket = serverSocket.accept();
            process(socket);
        }
    }

서버 객체는 port 인자를 받아 생성되며 start() 메서드로 하여금 시작된다.

시작시 요청에 대한 서버 소켓을 만들어줄 ServerSocket객체를 생성한다.

이에 port만 생성자 인자로 부여하게 된다면 기본적으로 모든 네트워크 인터페이스 0.0.0.0에 바인딩된다.

.accept()로 하여금 요청을 기다리고 요청이 도착하면 서버 소켓이 ServerSocket으로 부터 생성된다.
그리고 process로직을 이어나간다.

이 코드를 보면 바로 예측되는 문제점이 나타난다.

ServerSocket이 제공한 Socket의 process작업이 끝날 때 까지 다음 요청은 대기하게 된다.

서버는 현재 한 번에 하나의 요청만 처리할 수 있다.(후에 이를 개선해보자.)

private void process(Socket socket) throws IOException {
        try (
                socket;
                BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), UTF_8));
                PrintWriter writer = new PrintWriter(socket.getOutputStream(), false, UTF_8)) {

            String requestString = requestToString(reader);
            if (requestString.contains("/favicon.ico")) {
                log("favicon 요청");
                return;
            }

            log("HTTP 요청 정보 출력");
            System.out.println(requestString);

            log("HTTP 응답 생성중...");
            responseToClient(writer);
        }
    }

streamReader로는 BufferedReader, streamWriter로는 PrintWriter를 선택했다 그 이유는 아래와 같다.

  • BufferedReader 사용 이유
    BufferedReader는 InputStreamReader와 함께 사용되어 네트워크 소켓의 입력 스트림에서 텍스트 데이터를 효율적으로 읽기 위한 것이다.

    • 버퍼링: 데이터를 버퍼에 모아서 읽으므로, 한 문자씩 읽는 것보다 성능이 우수하다.
    • 라인 단위 읽기: readLine() 메서드를 사용해 한 줄씩 읽을 수 있어, HTTP 요청 메시지와 같이 라인 단위로 구성된 데이터를 다루기에 적합하다.
  • PrintWriter 사용 이유
    PrintWriter는 소켓의 출력 스트림에 텍스트 데이터를 출력할 때 편리하게 사용할 수 있는 클래스이다.

    • 편리한 출력 메서드: print(), println(), printf() 등의 메서드를 제공하여 텍스트 출력을 쉽게 할 수 있다.
    • 문자 인코딩 지원: 생성자에서 UTF-8 인코딩을 지정하여 올바른 문자 인코딩으로 데이터를 전송할 수 있다.
  • PrintWriter의 autoFlush 인자 false 사용 이유
    PrintWriter의 생성자에서 두 번째 인자로 전달하는 autoFlush 옵션은, true로 설정하면 println() 호출 시 자동으로 flush()가 호출된다.

    • false 설정: 이 경우는 자동 flush가 발생하지 않으므로, 개발자가 직접 flush()를 호출하여 출력 버퍼를 제어할 수 있게 된다.
    • 제어의 유연성: 자동 flush 대신 명시적으로 flush()를 호출하면, 출력 성능이나 전송 타이밍을 보다 세밀하게 관리할 수 있다.

요청을 requestToString으로 하여금 String으로 파싱한다.

private static String requestToString(BufferedReader reader) throws IOException {
        StringBuilder sb = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            if (line.isEmpty()) {
                break;
            }
            sb.append(line).append("\n");
        }

        return sb.toString();
    }

HTTP 스펙은 \r\n으로 하여금 줄바꿈을 나타내기에 이를 한줄한줄 읽기 위해 위와 같은 코드를 구성했다.

줄바꿈에 대해 IntelliJ console에 예쁘게 나타내기 위해 \n을 다시 주어 스트링 빌더 객체를 완성해서 리턴한다.

private void responseToClient(PrintWriter writer) {
        // 웹 브라우저에 전달하는 내용

        String body = "<h1>Hello World</h1>";
        int length = body.getBytes(UTF_8).length;

        StringBuilder sb = new StringBuilder();
        sb.append("HTTP/1.1 200 OK\r\n");
        sb.append("Content-Type: text/html\r\n");
        sb.append("Content-Length: ").append(length).append("\r\n");
        sb.append("\r\n");
        sb.append(body);

        log("HTTP 응답 정보 출력");
        sleep();
        writer.println(sb);
        writer.flush();
    }

버전1 구현에서 가장 중요한 부분이다. 기존에 스프링부트를 사용하며 Tomcat이 해주던 일을 직접느껴볼 수 있다.

HTTP 스펙에 맞게 Response Line을 구성하고 Response Headers를 구현하고 한줄 공백을 주고 Body를 작성해야 한다.

이 스펙에 맞게 브라우저는 응답라인과 응답헤더를 읽고 헤더에 나타난 Content-Length만큼의 body를 읽어낸다.

profile
자바집사의 거북이 수련법

0개의 댓글