Go언어 HTTP 클라이언트 작성
2022. 10. 09.
Go의 HTTP 클라이언트
HTTP는 클라이언트-서버 기반의 세션을 갖지 않는 프로토콜이며 애플리케이션 계층의 프로토콜
하위 계층의 전송 프로토콜로는 TCP를 사용한다. *2021년 7월 HTTP/3가 공개되며 TCP만이 아닌 UDP를 사용하는 HTTP가 등장했다.
통합 리소스 식별자 (URL)
클라이언트가 웹 서버를 찾고 요청된 리소르를 식별하는데 사용되는 일종의 주소
스키마(scheme) | 권한 정보(authority) | 경로(path) | 쿼리 파라미터(query arameter) | 쿼리 파라미터 (query parameter) | 정보 조각 (fragment) |
---|---|---|---|---|---|
scheme:// | user:password@ | host:port/path | ?key1=value1 | &key2=value2 | #table_of_contents |
위에 표처럼 구성되어 있으며 주로 인터넷 상 URL은 최소한 스키마와 호스트 네임만을 포함한다.
스키마는 브라우저에게 HTTPS를 사용한다고 알렸고, images.google.com/ 의 경로로 기본리소스를 요청하였다.
클라이언트 리소스 요청
HTTP Request은 클라이언트가 서버에게 특정한 리소르를 응답하도록 요청하는 메시지이다.
HTTP은 4가지로 구성되는데 메서드, 대상 리소스, 헤더, 보디로 구성된다.
메서드는 서버에게 대상 리소스로 무엇을 할 것인지에 대한 의도를 나타나고, 요청 헤더에는 전송 요청 시 보내는 데이터에 대한 메타데이터가 포함된다.
만약 PUST 메소드로 보디에 이미지를 담아 전송하려는 경우 요청 헤더에 Content-Length 부분에 이미지의 바이트수가 기록되게 된다.
그리고 요청 보디에는 네트워크로 전송하기 적합한 형태로 인코딩된 이미지를 전송하게 된다.
잠시 netcat 명령어로 구글의 robots.txt 파일 요청을 보내보겠다.
응답은 이러하다
맨 위부터 상태라인, 일련의 헤더, 중간의 보디와 구분하는 공백 라인, 응답 보디의 robots.txt 파일이 전송된다.
Go의 net/http 패키지를 이용하면 HTTP 메서드와 URL만 가지고 HTTP 요청을 만들 수 있다.
요청 메서드의 종류
GET | 서버 리소스를 요청한다. |
---|---|
HEAD | 요청한 리소스가 생각한 것보다 큰 경우를 대비해 리소스의 정보를 담은 헤더를 우선 요청한다. |
POST | 서버에 리소스를 추가하려고 할떄 사용된다. |
PUT | 이미 서버에 존재하는 리소스를 업데이터하거나 교체할때 사용한다. |
PATCH | 이미 서버에 존재하는 리소스의 일부분을 수정하는 경우 사용한다. |
DELETE | 서버에 존재하는 리소스를 제거하기 위해 사용한다. |
OPTIONS | 서버의 특정 리소스에 대해 존재하는 메서드를 알아내기 위해 사용한다. |
CONNECT | 웹 서버에 HTTP 터널링을 요청하거나 대상 목적지와 TCP 세션을 수립하고 클라이언트와 목적지 간 데이터 프락싱을 할 수 있게 해준다. |
TRACE | 웹 서버에게 요청을 처리하지 말고 에코잉하도록 한다 |
위에 메서드는 모든 서버에서 정확하게 구현하라는 의무는 없어 올바르게 구현되지 않은 웹 서버도 존재한다. 그러니 사용하기 전에 검증을 하는 것이 좋다.
서버 응답
자 외우기 귀찮다 그냥 200, 404, 403 정도만 알아두자…
Hypertext Transfer Protocol (HTTP) Status Code Registry
Go에서 웹 리소스 가져오기
Go언어에서는 브라우저 같이 화면에 HTML 페이지를 렌더링 하지는 않는다.
이제 요청을 만들고 클라이언트 측에서 발생하는 사소한 실수들을 알아보자
Go의 기본 HTTP 클라이언트 이용하기
net/http 패키지는 일회성으로 HTTP 요청을 할 수 있는 기본 클라이언트가 있다.
예를 들어 http.Head 함수를 이용하여 주어진 URL로 Head 요청을 보낼 수 있다.
다음 코드는 Head 요청을 통해 시간을 불러와 컴퓨터 시간과 비교하는 코드이다.
대략 2초 정도 차이나는것을 확인할 수 있다.
위에 코드중 3가지 부분에 집중해보자.
첫번째로 Http.Get 함수를 이용한 기본 리소스 요청 부분, 이때 Go의 HTTP 클라이언트는 자동으로 URL 스키마에 지정된 https 프로토콜로 변경한다.
두번째로 응답 보디을 닫는 부분, 잠시 뒤 응답 보디를 읽지는 않지만 반드시 닫아야 하는 이유를 알아보자
마지막으론 응답을 받은 후 서버가 응답을 생성한 시간에 대한 정보인 Date 헤더를 받아오는 부분, 이 정보를 이용해 현재 컴퓨터의 시간과 얼마나 차이가 나는지 비교해 볼 수 있다.
응답 보디 닫기
HTTP/1.1은 클라이언트가 서버와의 TCP 연결을 유지하여 여러 개의 HTTP 요청을 유지할 수 있는 기능인 keepalive가 존재한다. 그럼에도 클라이언트는 이전 응답에 읽지 않은 바이트가 존재할 경우 TCP 세션을 재사용할 수 없다고 하는데 Go의 HTTP 클라이언트는 응답 보디를 닫을 때 자동으로 모든 바이트를 소비하여 재사용 할 수 있게 만들어 준다.
따라서 응답 보디를 닫는 것은 TCP 세션 재사용하기 위해 중요하다.
그러나 암목적으로 응답 보디를 소비하는 것은 좋지 못하다.
이때 2가지 방법을 선택할 수 있는데
-
head 메소드를 이용해 필요한 데이터인지 확인하고 요청한다.
-
io.Copy 함수와 ioutil.Discard 함수를 활용한 명시적 소비
다음과 같이 Body의 모든 바이트를 읽어서 ioutil.Discard에 전부 쓰는 형태로 응답을 소비한다.
또한 다음 코드에서 _ (언더스코어)를 이용해 반환값을 무시했다는 것을 알린다.
타인아웃과 취소 구현
위에 코드는 아무런 문제가 없어 보일 수도 있다.
하지만 심각한 문제가 있으니 타임아웃 시간이 설정되있지 않다는 것이다.
이는 즉 실서비스를 해당 코드로 운영하게 된다면 특정 endpoint에 요청이 쌓여 서비스가 오작동하는 경우가 발생할 수 있다는 뜻이다.
다음은 net/http/httptest 패키지에 있는 함수들을 이용해 구현한 루프가 발생하는 서버에 요청을 보낸 경우이다.
httptest.NewServer 함수를 이용해 서버를 생성하는데 HandlerFunc으로 blockIndefinitely 이란 함수를 할당했다.
위에 보이다시피 blockIndefinitely은 사용자정의 함수이고 아무런 핸들링을 하지 않은 것을 볼 수 있다.
다음 서버의 URL로 Get 헬퍼 함수로 요청을 보내지만 타임아웃이 존재하지 않기에 테스트시간이 종료될때까지 갇히게 된다.
테스트 최대 시간 30초로 설정, 오류와 함께 30초에 종료된걸 볼 수 있다. 책에서는 이걸 Go테스트 러너가 타임아웃되어 테스트를 중단하고 스택 트레이스를 출력했다 고 표현했다.
이제 데드라인 콘텍스트를 사용해 연결에 타임아웃을 추가해보자, 또한 타임아웃 후에 연결을 cancel 함수로 취소하는 것 또한 구현해보도록 하자.
위에 코드에 서버로부터 5초간 응답이 없을때 요청을 타임아웃 시키는 기능을 추가했다.
실행 결과는 다음과 같다.
5초 안에 끝났으며 자동으로 cancel 처리해 오류도 출력되지 않음
또는 다음 코드처럼
영속적 TCP 연결 비활성화
작성중…