HTTP 요청 보내기 - CURL 구현하기

김태훈·2023년 6월 28일
0

사전 지식

try-with-resource가 무엇인지 알아야 합니다.

들어가기에 앞서

앞에서 Http Request Message까지 만드는 건 성공했습니다. 그런데 해당 메세지를 서버에 보내려면 어떻게 해야 할까요?
정답은 Socket을 사용하는 것입니다. 엥, 우리는 HTTP 메세지를 보낼 건데 소켓이 왜 나오는 걸까 의문이 생깁니다.
사실 Http는 Socket 기술 위에서 구현되어 있는데, 이 글을 통해 어떻게 Http를 사용한 통신이 이뤄지는지 알아보겠습니다.

구현

사전 작업

지난번 작성한 코드를 약간 변경하겠습니다. 지난 글의 마지막을 보시면 코드가 올라와 있습니다.
Main, Curl 클래스를 아래와 같이 변경하고 HttpRequestFactory 객체를 추가해주세요.

package org.kimtaehoondev;

import org.kimtaehoondev.factory.HttpRequestFactory;

public class Main {
    public static void main(String[] args) {
        Curl curl = new Curl(new HttpRequestFactory());
        curl.run(args);
    }
}
package org.kimtaehoondev;

import java.net.URL;
import java.util.Arrays;
import java.util.List;
import org.kimtaehoondev.domain.HttpRequest;
import org.kimtaehoondev.factory.HttpRequestFactory;
import org.kimtaehoondev.utils.UrlParser;

public class Curl {
    private final HttpRequestFactory httpRequestFactory;
    public Curl(HttpRequestFactory httpRequestFactory) {
        this.httpRequestFactory = httpRequestFactory;
    }

    public void run(String[] args) {
        // 입력값을 통해 Http 요청을 만든다
        URL url = UrlParser.parse(args[args.length - 1]);
        String[] argsExceptUrl = Arrays.copyOfRange(args, 0, args.length - 1);
        HttpRequest request = httpRequestFactory.make(url, argsExceptUrl);

        // 특정 서버로 해당 요청을 보낸다
        // 해당 서버에서 받은 응답을 Http로 만든다
    }

}
package org.kimtaehoondev.factory;

import java.net.URL;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.kimtaehoondev.domain.HttpRequest;
import org.kimtaehoondev.utils.ArgsParser;

public class HttpRequestFactory {
    public HttpRequest make(URL url, String[] args) {
        HttpRequest httpRequest = new HttpRequest(url);
        CommandLine commandLine = ArgsParser.makeCmdUsingArgs(args);
        for (Option option : commandLine.getOptions()) {
            httpRequest.setValueUsingParams(option);
        }
        return httpRequest;
    }
}

CURL 객체는 HttpRequest를 만드는 것에 대한 책임이 없습니다. 지금부터 CURL 객체에 로직들이 추가될 텐데 코드가 스파게티가 되는걸 막기 위해 위와 같이 코드를 분리했습니다.

구현

package org.kimtaehoondev;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
import org.kimtaehoondev.domain.HttpRequest;
import org.kimtaehoondev.factory.HttpRequestFactory;
import org.kimtaehoondev.utils.UrlParser;

public class Curl {
    private final HttpRequestFactory httpRequestFactory;
    public Curl(HttpRequestFactory httpRequestFactory) {
        this.httpRequestFactory = httpRequestFactory;
    }

    public void run(String[] args) {
        URL url = UrlParser.parse(args[args.length - 1]);
        String[] argsExceptUrl = Arrays.copyOfRange(args, 0, args.length - 1);
        HttpRequest request = httpRequestFactory.make(url, argsExceptUrl);
			
        // 여기 아래로 추가되었습니다.
        try (Socket socket = new Socket(url.getHost(), url.getPort())) {
            BufferedReader readerFromServer =
                new BufferedReader(new InputStreamReader(socket.getInputStream()));
            BufferedWriter writerToServer =
                new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));

            // 특정 서버로 해당 요청을 보낸다
            sendRequestToServer(request, writerToServer);


            // 해당 서버에서 받은 응답을 Http로 만든다
        } catch (UnknownHostException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

    }

    private void sendRequestToServer(HttpRequest request, BufferedWriter writerToServer)
        throws IOException {
        List<String> lines = request.serialize();
        for (String line : lines) {
            writerToServer.write(line + HttpRequest.CRLF);
        }
        writerToServer.flush();
    }

}

로직은 다음과 같이 진행됩니다.

1. URL의 host, port 정보를 사용해 소켓을 생성한다.(커넥션 연결됨)
2. 만들어둔 HttpRequest 객체의 serialize를 사용해 HttpRequestMessage를 만든다.
3. 연결된 서버에 HttpRequestMessage를 한줄씩 전달한다.

Socket은 AutoCloseable 인터페이스를 구현한 객체이기 때문에 try-with-resources를 사용할 수 있습니다. 따라서 해당 블록이 끝나면 Socket을 닫습니다. 다른 말로 서버와의 연결을 끊습니다.

결과 확인

Http 요청이 제대로 갔는지 확인하기 위해서 Wireshark를 통해 확인해보겠습니다.

프로그램을 실행하면 HttpRequestMessage가 네트워크를 타고 서버(다음 예시에서는 localhost:8080)로 전달된 걸 확인할 수 있습니다.

정리

이번 예시를 통해 Http 요청은 다음과 같은 흐름으로 동작한다는 걸 알 수 있었습니다.

1. 특정 서버와 연결된 Socket을 생성한다
2. 만들어진 HttpMessage를 전달한다.
3. 응답을 받아온다.(아직 미구현)
4. Socket을 닫아 연결을 종료한다.

사실 해당 글을 쓰기 전, 저는 HTTP가 소켓 위에서 동작한다는 사실을 알지 못했습니다. 자료를 탐색하다가 해당 내용을 봤을 때 그저 의문이었습니다.
HTTP는 비연결성이라는 특징을 가진 걸로 아는데, 소켓은 연결을 지속하기 위해 사용하는 기술이 아닌가? 라는 생각이 가득했는데, 이번 어플리케이션을 구현하면서 해당 의문을 해결할 수 있었습니다.

To Be...

생각보다 쉽죠? 사실 Http가 소켓 위에서 동작한다는 것만 알면 쉽게 구현할 수 있는 부분이었습니다. 다음 글에서는 응답을 받아오는 부분을 구현해보겠습니다.

profile
작은 지식 모아모아

0개의 댓글