Java HTTP Server 를 이용해서 훨씬 더 쉽게 HTTP Server 를 만들어보자

Jeong·2023년 9월 26일
0

HTTP

목록 보기
4/6
post-thumbnail

키워드

  • Java HTTP Server
  • Java NIO
  • Java Lambda expression(람다식)
    • Java Functional interface(함수형 인터페이스)

최종 목표

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

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

현재 목표

Java HTTP Server 를 이용해서 훨씬 더 쉽게 HTTP Server 를 만들어보자.

이전에는?

지금까지 우리는 저수준으로 했다. 자바에는 고수준의 API 가 준비되어 있다. 이걸 사용해보자.

Java HTTP Server 는 내부적으로 NIO 를 쓴다. (Non-Blocking IO)
우리가 그냥 하는 것보다 훨씬 더 효율적으로 되는 걸 기대할 수 있다.

1️⃣ 서버 객체 준비

전체 코드

package com.ahastudio.http.server;

import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.net.InetSocketAddress;

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

    private void run() throws IOException {
    	// 서버 객체 준비
        InetSocketAddress address = new InetSocketAddress(8080);
        HttpServer httpServer = HttpServer.create(address, 0);

        httpServer.start();
    }
}

결과

curl localhost:8080 을 해보자. 404 에러는 나지만 성공적으로 페이지가 나온다.

2️⃣ URL (정확히는 Path) 에 핸들러 지정

이번에는 주소에 따라 처리하도록 해보자.

HTTPHandler 인터페이스가 가지고 있는 메소드가 하나라서 다음과 같이 람다를 사용할 수 있다.

httpServer.createContext("/", (exchange) -> {
	// TODO  
});

전체 코드

package com.ahastudio.http.server;

import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.net.InetSocketAddress;

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

    private void run() throws IOException {
        InetSocketAddress address = new InetSocketAddress(8080);
        HttpServer httpServer = HttpServer.create(address, 0);
		
        // URL (정확히는 Path) 에 핸들러 지정
        httpServer.createContext("/", (exchange) -> {
            
        });

        httpServer.start();
    }
}

3️⃣ Listen

앞 코드에서도 계속 써주긴 했다.

httpServer.start();

👉👉 Request

위에서 Listen 까지 코드를 작성하고 실행해보면 물려있는 걸 볼 수 있다. 아무것도 처리를 안해줘서 묶여있는 것이다.

우리는 무엇을 알아야 할까? HTTP 요청 메시지을 알고 싶다.

  • Start line
    • HTTP MEthod
    • path
  • Headers
  • 빈 줄
  • Body

얘네 전부 exchange 에서 얻을 수 있다.

전체 코드

http localhost:8080 name=tester 을 해보자. 성공적으로 데이터가 나온다.

package com.ahastudio.http.server;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.List;

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

    private void run() throws IOException {
        InetSocketAddress address = new InetSocketAddress(8080);
        HttpServer httpServer = HttpServer.create(address, 0);

        httpServer.createContext("/", (exchange) -> {
            // 1. Request
            String method = exchange.getRequestMethod();
            System.out.println("Method: " + method); // `HTTP MEthod` 출력 결과: GET

            URI uri = exchange.getRequestURI();
            String path = uri.getPath();
            System.out.println("Path: " + path); // `path` 출력 결과: /

            Headers headers = exchange.getRequestHeaders();
            for (String key : headers.keySet()) {
                List<String> values = headers.get(key);
                System.out.println(key + ": " + values);
                // `Headers` 출력 결과 ⬇️
                // Accept: [*/*]
                // Host: [localhost:8080]
                // User-agent: [curl/8.1.2]
            }

            InputStream inputStream = exchange.getRequestBody();
            byte[] bytes = inputStream.readAllBytes();
            String body = new String(bytes);

            System.out.println(body);
            // 요청하면서 넣은 데이터가 없기 때문에 아무것도 안 나온다

            // httpie 를 이용하면 쉽게 Body에 JSON 데이터를 넣을 수 있다.
            // http localhost:8080 name=tester
 
 		 	// `Body` 출력 결과 ⬇️
 			// {"name": "tester"}		
        });

        httpServer.start();
    }
}

결과

curl localhost:8080 을 해보자. 성공적으로 결과가 나온다.


👈👈 Response

Response 를 안 주니까 여전히 물려있다.

Response 를 할 때 HTTP Status CodeContent-length 는 항상 줘야한다. 이때 Content-length 바이트로 잡아야 한다. 그래서 중간에 바이트로 한 번 변환 후에 길이로 넘겨준다.

`byte[] bytes = content.getBytes();`

그리고 재밌는건 브라우저에서 / 아래에 어떤 경로를 추가해도 같은 곳으로 요청을 한다.
그래서 만약 따로 해주고 싶다면 다음 코드로 주소 계속 추가해주면 다른 것도 처리할 수 있다.

 httpServer.createContext("path_name", (exchange) -> { // TODO }

전체 코드

중복되는 코드가 많아서 Refactor -> Extract Method 로 함수 추출을 해서 작성했다.

package com.ahastudio.http.server;

import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.List;

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

    private void run() throws IOException {
        InetSocketAddress address = new InetSocketAddress(8080);
        HttpServer httpServer = HttpServer.create(address, 0);
		
        // Path : "/"
        httpServer.createContext("/", (exchange) -> {
            // 1. Request
            displayRequest(exchange);

            // 2. Response
            String content = "Hello, world\n";
            sendContent(exchange, content);
        });
		
        // Path : "/hi"
        httpServer.createContext("/hi", (exchange) -> {
            // 1. Request
            displayRequest(exchange);
            
            // 2. Response
            String content = "Hi, world\n";
            sendContent(exchange, content);
        });

        httpServer.start();
    }

	// Request 에서 쓰는 함수 (이름 짓기가 애매해서 displayRequest로 했다. request 를 화면에서 보는 거니까?) 
    private void displayRequest(HttpExchange exchange) throws IOException {
        String method = exchange.getRequestMethod();
        System.out.println("Method: " + method);

        URI uri = exchange.getRequestURI();
        String path = uri.getPath();
        System.out.println("Path: " + path);

        Headers headers = exchange.getRequestHeaders();
        for (String key : headers.keySet()) {
            List<String> values = headers.get(key);
            System.out.println(key + ": " + values);
        }

        InputStream inputStream = exchange.getRequestBody();
        String body = new String(inputStream.readAllBytes());

        System.out.println(body);
    }
	
    // Response 에서 쓰는 함수
    private void sendContent(HttpExchange exchange, String content) throws IOException {
        byte[] bytes = content.getBytes();

        exchange.sendResponseHeaders(200, bytes.length);

        OutputStream outputStream = exchange.getResponseBody();
        outputStream.write(bytes);
        outputStream.flush();
    }
}

결과

아하! 포인트

자바에 기본으로 있는 HTTP Server 를 써봤다.
Socket 으로 했을 때보다 훨씬 쉽게 경로 여러 개에 대해 처리할 수 있었다.
exchange 를 이용해서 getRequestMethod 함수를 이용하는 부분도 이전과 달리
파싱으로 하지 않고 바로 얻어서 편리했다. 또한 Header 에 대한 부분도 key, value 로 얻을 수 있었다.

다음에는

다음에는 Spring 을 이용해서 이것보다 훨씬 쉽게 처리해보자.

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

0개의 댓글