티스토리 뷰

Java

Java - HTTP 서버 만들기 2

gyeonghyun 2024. 11. 25. 20:25

 

 

HTTP 요청 메시지와 응답 메시지를 객체로 만들어 보자

 

HTTP 요청 메시지

public class HttpRequest {

    private String method;
    private String path;
    private final Map<String, String> queryParameters = new HashMap<>();
    private final Map<String, String> headers = new HashMap<>();

    public HttpRequest(BufferedReader reader) throws IOException {
        parseRequestLine(reader);
        parseHeaders(reader);
        // 메시지 바디는 이후에 처리
    }

    //GET /search?q=hello HTTP/1.1
    //HOST: localhost:12345
    private void parseRequestLine(BufferedReader reader) throws IOException {
        String requestLine = reader.readLine();
        if (requestLine == null) {
            throw new IOException("EOF : No request line received");
        }

        String[] parts = requestLine.split(" ");
        if (parts.length != 3 ) {
            throw new IOException("Invalid request line : " + requestLine);
        }

        method = parts[0];
        String[] pathParts = parts[1].split("\\?");
        path = pathParts[0];

        if (pathParts.length >1) {
            parseQueryParameters(pathParts[1]);
        }

    }

    private void parseQueryParameters(String queryString) {
        for (String param : queryString.split("&")) {
            String[] keyValue = param.split("=");
            String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8);
            String value = keyValue.length > 1 ? URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8) : "";
            queryParameters.put(key, value);
        }
    }

    private void parseHeaders(BufferedReader reader) throws IOException {
        String line;
        while (!(line = reader.readLine()).isEmpty()) {
            String[] headerParts = line.split(":");
            headers.put(headerParts[0].trim(), headerParts[1].trim());
        }
    }

    public String getMethod() {
        return method;
    }

    public String getPath() {
        return path;
    }

    public String getParameter(String name) {
        return queryParameters.get(name);
    }

    public String getHeader(String name) {
        return headers.get(name);
    }

    @Override
    public String toString() {
        return "HttpRequest{" +
                "method='" + method + '\'' +
                ", path='" + path + '\'' +
                ", queryParameters=" + queryParameters +
                ", headers=" + headers +
                '}';
    }
}

 

 

HTTP 요청 메시지는 다음과 같다.

 

 

시작 라인을 통해 method, path, queryParameters를 구할 수 있다.

  • method : GET
  • path : /search
  • queryParameters : [q=hello]

query, header의 경우 key=value 형식이기 때문에 Map을 사용하면 이후에 편리하게 데이터를 조회할 수 있다.

 

만약 다음과 같은 내용이 있다면 queryParameters의 Map에 저장되는 내용은 다음과 같다.

  • /search?q=hello&type=text
  • queryParameters : [q=hello, type=text]

퍼센트 디코딩도 URLDecoder.decode()를 사용해서 처리한 다음에 Map에 보관한다. 따라서 HttpRequest 객체를 사용하는 쪽에서는 퍼센트 디코딩을 고민하지 않아도 된다.

 

 

HTTP 명세에서 헤더가 끝나는 부분을 빈 라인으로 구분한다.

  • while (!(line = reader.readLine()).isEmpty()

이렇게 하면 시작 라인의 다양한 정보와 헤더를 객체로 구조화 할 수 있다.

 

 

HTTP 응답 메시지

public class HttpResponse {
    private final PrintWriter writer;
    private int statusCode = 200;
    private final StringBuilder bodyBuilder = new StringBuilder();
    private String contentType = "text/html; charset=UTF-8";

    public HttpResponse(PrintWriter writer) {
        this.writer = writer;
    }
    public void setStatusCode(int statusCode) {
        this.statusCode = statusCode;
    }
    public void setContentType(String contentType) {
        this.contentType = contentType;
    }

    public void writeBody(String body) {
        bodyBuilder.append(body);
    }

    public void flush() {
        int contentLength = bodyBuilder.toString().getBytes(StandardCharsets.UTF_8).length;
        writer.println("HTTP/1.1 " + statusCode + " " + "OK");
        writer.println("Content-Type: " + contentType);
        writer.println("Content-Length: " + contentLength);
        writer.println();
        writer.println(bodyBuilder);
        writer.flush();
    }

    private String getReasonPhrase(int statusCode) {
        switch (statusCode) {
            case 200:
                return "OK";
            case 404:
                return "Not Found";
            case 500:
                return "Internal Server Error";
            default:
                return "Unknown status";
        }
    }
}

 

 

HTTP 응답 메시지는 다음과 같다.

 

 

시작 라인

  • HTTP 버전 : HTTP/1.1
  • 응답 코드 : 200
  • 응답 코드의 간단한 설명 : OK

응답 헤더

  • Content-Type : HTTP 메시지 바디에 들어있는 내용의 종류
  • Content-Length : HTTP 메시지 바디의 길이
public class HttpRequestHandlerV4 implements Runnable{
    private final Socket socket;
    public HttpRequestHandlerV4(Socket socket) {
        this.socket = socket;
    }
    
    @Override
    public void run() {
        try {
            process();
         } catch (Exception e) {
            log(e);
        }

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

            HttpRequest request = new HttpRequest(reader);
            HttpResponse response = new HttpResponse(writer);

            if (request.getPath().equals("/favicon.ico")) {
                log("favicon 요청");
                return;
            }

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

            log("HTTP 응답 생성중 ...");
            if (request.getPath().equals("/site1")) {
                site1(response);
            } else if (request.getPath().equals("/site2")) {
                site2(response);
            } else if (request.getPath().equals("/search")) {
                search(request,response);
            } else if (request.getPath().equals("/")) {
                home(response);
            } else {
                notFound(response);
            }
            response.flush();
            log("HTTP 응답 전달 완료");

        } catch (Exception e) {
            log(e);
        }
    }

    private void notFound(HttpResponse response) {
        response.setStatusCode(404);
        response.writeBody("<h1>404 페이지를 찾을 수 없습니다. </h1>");

    }

    private void home(HttpResponse response) {
        // 원칙적으로 Content-Length를 계산해서 전달해야 하지만, 단순하게 하기위해 제외
        response.writeBody("<h1>home</h1>");
        response.writeBody("<ul>");
        response.writeBody("<li><a href='/site1'>site1</a></li>");
        response.writeBody("<li><a href='/site2'>site2</a></li>");
        response.writeBody("<li><a href='/search?q=hello'>검색</a></li>");
        response.writeBody("</ul>");
    }

    // "/search?q = hello
    private void search(HttpRequest request, HttpResponse response) {

        String query = request.getParameter("q");
        response.writeBody("<h1>Search</h1>");
        response.writeBody("<ul>");
        response.writeBody("<li>query: " + query + "</li>");
        response.writeBody("</ul>");

    }

    private void site2(HttpResponse response) {
        response.writeBody("<h1>Site2</h1>");
    }

    private void site1(HttpResponse response) {
        response.writeBody("<h1>Site1</h1>");
    }

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

        String body = "<h1>Hello World </h1>";
        int length = body.getBytes(StandardCharsets.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 응답 정보 출력");
        System.out.println(sb);
        writer.println(sb);
        writer.flush();
    }

    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();
    }

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

 

 

  • 클라이언트의 요청이 오면 요청 정보를 기반으로 HttpRequest 객체를 와 HttpResponse 객체를 만든다.
  • HttpRequest를 통해서 필요한 정보를 편리하게 찾을 수 있다.
  • /search의 경우 퍼센트 디코딩을 고민하지 않아도 된다. 이미 HttpRequest에서 다 처리해두었다.
  • 응답의 경우 HttpResponse를 사용하고, HTTP 메시지 바디에 출력할 부분만 적어주면 된다. 나머지는 HttpResponse 객체가 대신 처리해준다.
  • response.flush()는 꼭 호출해주어야 한다. 그래야 실제 응답이 클라이언트에 전달된다.
public class HttpServerV4 {

    private final ExecutorService es = Executors.newFixedThreadPool(10);
    private final int port;

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


    public void start() throws IOException {
        ServerSocket serverSocket = new ServerSocket(port);
        log("서버 시작 port: " + port);

        while (true) {
            Socket socket = serverSocket.accept();
            es.submit(new HttpRequestHandlerV4(socket));

        }
    }
}

 


public class ServerMainV4 {
    private static final int PORT = 12345;

    public static void main(String[] args) throws IOException {
        HttpServerV4 server = new HttpServerV4(PORT);
        server.start();

    }
}

 

 

HttpRequest, HttpResponse 객체가 HTTP 요청과 응답을 구조화한 덕분에 많은 중복을 제거하고, 또 코드도 매우 효과적으로 리팩토링 할 수 있었다.

 

지금 까지 학습한 내용을 생각해보면 전체적인 코드가 크게 2가지로 분류 되는 것을 확인할 수 있다.

 

  • HTTP 서버와 관련된 부분 ( HttpServer, HttpRequestHandler, HttpRequest, HttpResponse)
  • 서비스 개발을 위한 로직 ( home(), site1(), site2(), search(), notFound() 

 

만약 웹을 통한 회원 관리 프로그램 같은 서비스를 새로 만들어야 한다면, 기존 코드에서 HTTP 서버와 관련된 부분은 거의 재사용하고, 서비스 개발을 위한 로직만 추가하면 된다.

 

 

 

 

'Java' 카테고리의 다른 글

Java - 애노테이션  (0) 2024.11.26
Java - 리플렉션  (0) 2024.11.25
Java - URL 인코딩  (0) 2024.11.25
Java - HTTP 서버 만들기 1  (1) 2024.11.18
Java - 클라이언트,서버 프로그램  (0) 2024.11.14
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함