본문 바로가기

java

[Java] http 요청과 응답 구현하기 - 1(socket으로 http 요청하기)

네트워크 - 요청과 응답

네트워크를 이용하면 멀리 떨어진 대상과도 자료를 주고 받을 수 있다. 가령 이 블로그 글의 정보를 얻기 위해 사용자는 개인 PC 서버에서 웹브라우저를 통해 tistory 서버 어딘가에 저장된 글이라는 데이터를 받고 있다.

네트워크는 OSI 7계층이나 TCP/IP 4계층 등으로 분류되는 개념으로 동작한다. 전기 신호를 직접 전달하는 하드웨어가 위치한 계층에서부터 실제 데이터를 주고 받는 주체인 'application'까지 계층적으로 각 역할을 수행한다. 요청을 주고 받는 과정을 TCP/IP 4계층의 용어로 설명하면, 'Network Access Layer'의 하드웨어를 이용해 'Internet Layer'에서 통상 IP로 목적지 네트워크 주소를 판별한다. 이후 'Transport Layer'를 통해 실제 데이터를 주고 받는 port를 선별하면, 실행 중인 process(application)에 데이터를 요청한다. 이후 역순으로 요청한 데이터를 응답한다.

 

HTTP 프로토콜을 Socket으로 요청할 수 있을까?

데이터는 이미지, 텍스트, 파일 등 종류가 다양할 수 있다. 현재 위치한 블로그 글이라는 자원은 (사실 훨씬 많지만) html 형식의 파일이다. 이때 사용자는 웹브라우저를 매개로 'http' 라는 프로토콜을 통해 어떤 자원을 어떻게 요청하고, 어떤 자원을 어떻게 응답할 지를 주고 받는다.

어떤 데이터든 양측의 서버가 특정 데이터를 주고 받는다는 개념으로 볼 때에 어떤 방식으로 주고 받던지 상관이 없을 수 있다. "앞으로 '1'이라는 숫자를 요청할 때는 항상 '100'으로 응답해야 해" 라는 약속이 있었다면, 그 약속된 방식으로 데이터를 주고 받으면 될 일이다. 이러한 약속의 집합을 'Protocol'이라 하고, 현재 블로그 글은 웹브라우저를 통해 'http'라는 약속(프로토콜)에 따라 데이터를 주고 받고 있는 것이다.

양 서버 내부적으로 실행중인 process 간 TCP/IP 기반으로 데이터를 주고 받는다고 하면 그 연결고리가 되는 것이 'Socket' 인터페이스다. Socket을 통해 데이터를 주고 받는다고 하면, 위에서 말한 것처럼 어떤 프로토콜을 통해 데이터를 '설명'할 것인가는 정하기 나름이다. 이번 글에서는 Java의 Socket 클래스를 이용해 웹브라우저나 http 프로토콜을 지원하는 라이브러리를 사용하지 않고 간단한 http 통신을 주고 받는 내용을 살펴보고자 한다.

 

Socket을 이용해 HTTP 요청하기

SpringBoot 기반으로 간단히 서버가 잘 동작하는지 확인하는 health check controller를 하나 만든다.

@RestController
public class HealthCheckController {

    @GetMapping("/healthCheck")
    public String healthCheck() {
        return "ok";
    }
}

 

HealthCheck 서버에 요청을 보낼 SocketClient 클래스를 정의한다. 연결 대상 host와 port를 전달하는 Socket 인스턴스와 데이터를 주고 받을 I/O stream을 정의한다. request는 http message 규칙에 맞게 작성한다. http message는 크게 시작줄, 헤더, 본문으로 나뉜다. 시작줄과 헤더는 개행으로, 헤더는 종료 시점에 개행을 한 번 더 넣는다. 본문은 포함될 수도 안 될 수도 있다. 자세한 내용은 다음 글을 확인한다.

public class SocketClient {

    private Socket client;
    private PrintWriter out;
    private BufferedReader in;

    public void startConnection(String host, int port) throws IOException {
        this.client = new Socket(host, port);
        out = new PrintWriter(client.getOutputStream());
        in = new BufferedReader(new InputStreamReader(client.getInputStream()));
    }

    public void request() throws IOException {
        out.println("GET /healthCheck HTTP/1.1");
        out.println("Host: 127.0.0.1");  //HTTP 1.1 버전에서 Host header는 필수값
        out.println("Connection: close");  //단순 health check request이므로 즉시 연결 종료
        out.println();
        out.flush();

        String result;
        while((result = in.readLine()) != null){
            System.out.println(result);
        }
    }

    public void stopConnection() throws IOException {
        in.close();
        out.close();
        client.close();
    }
}

 

이제 SocketClient 인스턴스를 생성해서, 연결을 수립하고 요청을 보낸 뒤 연결을 종료한다. 정상적으로 200 ok 응답을 받는 것을 확인할 수 있다.

@Test
void http_request_with_socket() throws IOException {
    ClientSocket client = new ClientSocket();
    client.startConnection("127.0.0.1", 8083);
    client.request();
    client.stopConnection();
}

 

마치며 - http 요청은 처리되었다. 응답은 어떻게 이루어지는가?

client 입장에서 Socket을 활용해 http 요청을 보내는 코드를 살펴보았다. 사실 위 코드는 client가 OutputStream에 단순히 문자열을 전달한 것에 지나지 않는다. 이 문자열을 http message 규칙에 맞게 구성한 것 뿐이다. 하지만 apache tomcat 기반의 Spring 서버에서는 해당 문자열을 적절히 해석하여 요청을 처리해 응답하였다. 그렇다면 apache tomcat 기반의 Spring 서버에서는 해당 문자열을 어떻게 해석하여 응답하였는지 다음 글을 통해서 알아보고자 한다.

 

[참고자료]

그림으로 배우는 네트워크 원리 - Gene

HTTP 완벽 가이드 - 데이빗 고울리 외