WWW(World wide web)은 수억명의 유저가 접근하는 (그리고 역시 수억의 컴퓨터가 연결하는) 가장 큰 분산 시스템다. 그리고 그 중 가장 성공한 서비스는 HTTP 기반의 웹 서비스일 것이다. 웹 브라우저라고 부르는 웹 클라이언트 프로그램을 이용해서 서핑이라는 행위를 하는 정보의 바다 말이다. 현재 HTTP(Hyper-Text Transport Protocol)의 최신 버전은 1.1로 거의 모든 서버와 클라이언트들이 사용하는 버전이다.
이번 장에서는 HTTP를 개략적으로 살펴보고, HTTP를 지원하는 Go의 프로그래밍 도구들에 대해서 살펴 볼 것이다.
HTTP 살펴보기
HTTP에 대한 자세한 내용은 HTTP문서를 참고하자. 여기에서는 요약하는 정도로 넘어간다.
URL과 리소스
인터넷에 유저가 접근하는 목적은 "자원을 찾아서 사용하기" 위함이다. 자원(resource)에 접근하기 위해서는 2가지 정보가 필요하다.
인터넷 상에서 컴퓨터의 위치 : IP 주소로 인터넷상에서 컴퓨터의 위치를 찾을 수 있다.
컴퓨터내에서 자원의 위치 : 컴퓨터는 특정한 위치에 자원을 (보통 파일의 형태로)저장하고 있다. URL을 이용해서 컴퓨터안에서 자원의 위치를 기술할 수 있다.
여기에서 자원이란 HTML 문서, 이미지, 영상파일과 데이터베이스로 부터 동적으로 만들어진 객체 등 서버에서 접근할 수 있는 모든 것을 의미한다.
유저 에이전트는 인터넷 주소와 URL을 이용해서 서버를 찾아서 자원을 요청하면, 서버는 그 자원을 반환한다. 클라이언트는 자원을 다운로드하거나 화면에 표현하는 등의 일을 한다. 예를들어 HTML 파일이라면 화면에 표현하고 음악파일이라면 다운로드해서 로컬 디스크에 저장할 것이다.
HTTP의 특성
HTTP는 stateless, connectionlesss, reliable한 프로토콜이다.
Stateless 하다는 것은 상태를 저장하지 않는다는 의미로, 각각의 요청을 서로 독립적으로 처리한다. 예를 들어 클라이언트가 서버에 바로 앞전 요청으로 로그인을 성공했다고 하더라도, 서버는 이번 요청이 로그인 성공한 건지 아닌지 알 방법이 없다.
하지만 상업적인 용도로 쓰기 위해서는 상태저장이 반드시 필요하기 때문에, 쿠키(cookie)를 이용해서 상태정보를 저장한다. (여기에서는 쿠키를 자세히 다루지 않을 것이다.)
Connectionless하다는 것은 서버와 클라이언트가 서로 연결을 맺지 않는 다는 의미다. 클라이언트가 요청을 하고, 서버가 요청에 대해서 응답을 끝내면, 서버와 클라이언트간 연결이 끊어진다. 다음 요청을 위해서는 새로운 연결을 맺어야 한다. 요청이 매우 많은 서버의 경우 빠른 시간에 응답을 끝내서, TCP 연결을 빠르게 해제할 필요가 있다. 클라이언트와 서버와의 연결을 유지하는데에도 자원이 소모되기 때문이다.
HTTP 버전
HTTP는 3개의 버전이 있다.
Version 0.9 : 완전히 사용하지 않는다.
Version 1.0 : 거의 사용하지 않는다.
Version 1.1 : 현재 사용 버전
HTTP 1.0의 구성
HTTP0.9는 사용하지 않으니 건너 뛴다. 1.0을 건너뛰지 않는 이유는 HTTP 1.0이 HTTP의 기본적인 골격을 만든 버전이기 때문이다. HTTP1.1은 1.0을 기반으로 몇 가지 기능이 추가된 버전이다. 실제 HTTP 1.0이 거의 사용하지 않는 버전임에도 불구하고 이런 역사적/기술적인 이유로, 대부분의 웹 서버와 클라이언트가 1.0을 지원한다.
HTTP 요청 포맷은 다음과 같다.
요청은 simple-request와 full-request가 있는데, 지금에 와서는 simple-request는 생각할 필요가 없다. 모든 요청은 full-request라고 생각하자.
Full-Request의 첫째 줄에는 Request-Line이 온다. 유저의 요청방법과 요청한 자원의 위치가 들어가는 영역이다. Request-Line의 형식은 다음과 같다.
HTTP/1.1 200 OK
Date: Fri, 29 Aug 2003 00:59:56 GMT
Server: Apache/2.0.40 (Unix)
Accept-Ranges: bytes
Content-Length: 1595
Connection: close
Content-Type: text/html; charset=ISO-8859-1
HTTP 1.1의 구성
HTTP1.1은 HTTP1.0을 기본으로 하고 있으며, HTTP 1.0의 많은 문제점들을 수정했다. 기능상의 많은 개선이 있지만 덕분에 복잡해 졌다.
Request-Line은 아래와 같다. 버전이 1.1인 걸 제외하면 1.0과 차이가 없다.
GET http://www.w3.org/index.html HTTP/1.1
HTTP1.0에 비해서 달라진 점들은 다음과 같다.
TRACE, CONNECT와 같은 메서드가 추가됐다.
Virtual hosts를 위한 hostname identification
content negotiation : 자원은 언어나 미디어 타입등에 대한 표현 정보를 가질 수 있다. 예를들어 클라이언트는 가능하면 불어로 응답을 주되, 불어가 안된다면 영어로 응답을 달라고 서버와 협상할 수 있다.
Psersistent connections을 지원한다. : 하나의 연결에서 하나 이상의 요청을 보낼 수 있다. TCP 오버헤드를 줄일 수 있다.
Chunked transfers : 응답 데이터의 크기를 알 수 없는 경우에는 content-length대신, chunked transfer를 이용할 수 있다.
proxy support
byte ranges : 리소스의 특정 범위의 데이터를 요청할 수 있다. 어디에 써먹을 수 있을지는 잘 모르겠다.
Simple user-agents
유저 에이전트는 아래와 같은 응답 데이터를 받을 것이다.
type Response struct {
Status string // 200 OK 같은 응답 코드
StatusCode int // 200 응답 코드
Proto string // 응답 프로토콜 : HTTP/1.0
ProtoMajor int // 프로토콜의 메이저 버전
ProtoMinor int // 프로토콜의 마이너 버전
RequestMethod string //"HEAD", "CONNECT", "GET"등의 요청 메서드
Header map[string]string
Body io.ReadCloser
ContentLength int64
TransferEncoding []string
Close bool
Trailer map[string]string
}
HTTP1.1에서 지원하는 메서드들 중 "HEAD"가 있다. 이 메서드로 요청을 받은 서버는 HTTP 서버의 정보를 반환한다. Go 언어에서는 Head 함수를 이용해서 "HEAD"요청을 보낼 수 있다. HEAD 요청을 보내는 간단한 유저 에이전트 프로그램을 이용해서, 서버 응답 메시지를 분석해 보려 한다.
Go는 HTTP 유저 에이전트를 위한 저수준(lower-level) 인터페이스를 제공한다. 저수준 인터페이스를 사용하면, 정밀한 요청제어가 가능하지만, 제대로된 요청을 만들기 위해서는 더 많은 지식을 가지고 있어야 한다.
유저 요청을 제어하기 위해서는 Request type을 사용한다. 아래는 Go 문서에서 제공하는 Request type이다. 원문은 여기에서 확인. 영문주석은 한글로 번역했다.
type Request struct {
Method string // GET, POST, PUT 등의 HTTP 메서드
RawURL string // 요청을 위한 raw URL 정보
URL *URL // 파싱된 URL
Proto string // 프로토콜과 버전 "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
// 헤더정보는 필드이름과 값으로 구성된 맵으로 저장한다.
// 만약 헤더 정보가 아래와 같다면
//
// accept-encoding: gzip, deflate
// Accept-Language: en-us
// Connection: keep-alive
//
// 아래와 같은 맵으로 설정할 수 있다.
//
// Header = map[string]string{
// "Accept-Encoding": "gzip, deflate",
// "Accept-Language": "en-us",
// "Connection": "keep-alive",
// }
// HTTP의 헤더이름은 대소문자를 구분하지 않는다.
// 요청 파서는 "콜론"을 구분자로 필드이름을 분리해 낸다.
// 그후 필드의 첫글자와 하이픈(-)다음에 오는 첫글자를 대문자로 변환하고
// 나머지는 소문자로 변환한다.
Header map[string]string
// 메시지 바디(body)
Body io.ReadCloser
// ContentLength는 컨텐츠의 길이를 저장한다.
// 길이를 알 수 없을 때는 -1을 설정한다.
// 값이 0보다 클 경우, Body에서 길이만큼을 읽는다.
ContentLength int64
// TransferEncoding lists the transfer encodings from outermost to innermost.
// An empty list denotes the "identity" encoding.
TransferEncoding []string
// 요청에 대한 응답후 연결을 끊을 것인지
Close bool
// 요청 호스트의 이름을 설정한다.
// 설정 값은 "Host:" 헤더 값을 설정하는데 사용한다.
// 웹 서버가 하나 이상의 도메인으로 서비스 할때,
// 도메인을 특정하기 위해서 사용한다.
Host string
// 어느 사이트를 통해서 방문했는지를 남기기 위해서 사용한다.
// referer는 referrer의 오타인데, RFC 1945에 실수로 referer로 쓴 이후로
// 그냥 referer로 쓰게 됐다.
Referer string
// 요청을 보내는 유저 에이전트 이름을 적는다.
// Mozill/5.0, Chrome/37.0.2049.0 등으로 사용한다.
// 서버측에서는 브라우저 접속 통계를 만들거나
// 브라우저별로 처리를 분기 하기 위한 목적으로 사용할 수 있다.
UserAgent string
// 파싱된 From을 보내기 위해서 사용한다.
Form map[string][]string
// Trailer maps trailer keys to values. Like for Header, if the
// response has multiple trailer lines with the same key, they will be
// concatenated, delimited by commas.
Trailer map[string]string
}
요청 정보는 다양한 방식으로 설정할 수 있다. 그냥 간단히 요청만 하고 싶다면, 굳이 필드들을 일일이 설정할 필요는 없다. 많은 경우 아래와 같은 간단한 요청으로도 충분하다.
응답 메시지는 Content-Type를 포함할 수 있는데, text/HTML, application/JSON과 같은 컨텐츠의 타입을 명시한다. 클라이언트는 Content-Type를 보고, 컨텐츠 처리 방법을 결정할 수 있다. 컨텐츠가 문자셋(charset) 상태를 가질 경우, text/html; charset=UTF-8등 문자셋 정보도 보낼 수 있다. 만약 charset이 설정되어있지 않으면 ISO8859-1인 것으로 가정한다.
Client 객체
HTTP 기반의 유저 에이전트를 만드는 가장 간단한 방법은 Client 객체를 만드는 거다. 이 객체는 여러개의 요청을 관리할 수 있으며, 서버와의 TCP 연결이 살아있는지 등을 관리할 수 있다.
간단한 유저 에이전트 프로그램이다.
# go run http_client.go http://www.joinc.co.kr
charset UTF-8
Get Body
<head>
<meta http-equiv="refresh" content="0;URL=http://www.joinc.co.kr/modules/moniwiki/wiki.php/FrontPage">
</head>
요청 정보도 확인해 보기로 했다. 리눅스의 nc(Netcat)를 이용해서 read 전용의 간단한 서버를 만들었다.
# nc -l 8000
http_client를 이용해서 localhost:8000으로 요청을 전송했다.
# go run http_client.go http://localhost:8000
아래와 같은 요청을 확인할 수 있었다.
GET / HTTP/1.1
Host: localhost:8000
User-Agent: Go 1.1 package http
Accept-Charset: UTF-8;q=1, ISO-8859-1;q=0
Accept-Encoding: gzip
GET 방식으로 / 를 요청했다. HTTP 1.1 버전을 사용하고 있다.
Host 정보를 보여주고 있다. 서버가 Virtual host를 지원한다면, 호스트 정보를 보고 서비스를 분기할 수 있다.
Go언어로 만들어진 유저 에이전트 임을 확인할 수 있다.
처리할 수 있는 문자셋을 명시하고 있다.
gzip 압축을 처리할 수 있다는 것을 서버에게 알려주고 있다. 압축통신을 지원하는 서버라면, 컨텐츠를 압축하는 것으로 트래픽을 줄일 수 있을 거다.
사용자 정의 헤더를 추가해서 테스트해 보자. http_client 프로그램을 수정했다.
request.Header.Add("Accept-Charset", "UTF-8;q=1, ISO-8859-1;q=0")
request.Header.Add("My-Header", "Hello World") # 사용자 헤더를 추가했다.
테스트 결과.
# nc -l 8000
GET / HTTP/1.1
Host: localhost:8000
User-Agent: Go 1.1 package http
Accept-Charset: UTF-8;q=1, ISO-8859-1;q=0
My-Header: Hello World
Accept-Encoding: gzip
Proxy
간단한 Proxy
HTTP 1.1은 Proxy를 허용한다. GET 요청을 Proxy로 만들기 위해서는 "Host" 필드에 target의 호스트 이름을 설정하면 된다. 이걸로 끝이다.
Proxy 서버가 testproxy.com:8000 이고 Target host가 google.com 이라면, 아래와 같이 테스트 하면 된다.
# go run ProxyGet.go http://XYZ.com:8080/ http://www.google.com
nc를 이용해서 실제 proxy 요청 형식을 살펴보기로 했다.
# go run ProxyGet.go http://localhost:8000 http://www.joinc.co.kr
# nc -l 8000
GET http://www.joinc.co.kr/ HTTP/1.1
Host: www.joinc.co.kr
User-Agent: Go 1.1 package http
Accept-Encoding: gzip
Host가 Target host로 설정된 걸 확인할 수 있다.
이 프로그램을 테스트 하려면 forward proxy를 지원하는 서버가 있어야 한다. 직접 테스트 해야 직성이 풀리겠다면, nginx로 proxy 서버를 구축해 보자. 나는 개인 리눅스 박스에 아래와 같은 설정으로 forward proxy를 만들어서 테스트를 수행했다.
# go run http_proxy.go http://localhost:8000 http://www.joinc.co.kr
GET / HTTP/1.1
Host: www.joinc.co.kr
Read ok
Reponse ok
<head>
<meta http-equiv="refresh" content="0;URL=http://www.joinc.co.kr/modules/moniwiki/wiki.php/FrontPage">
</head>
Proxy 인증
어떤 proxy 서버들은 아이디/패스워드 기반의 인증을 요구한다. 사용하는 인증 방식은 HTTP Basic authentication으로 아이디와 패스워드를 "userid:password" 형태로 만들어서 base64 인코딩 한 후 proxy 서버에 전달한다.
nc -l 8000
GET http://www.joinc.co.kr/ HTTP/1.1
Host: www.joinc.co.kr
User-Agent: Go 1.1 package http
Proxy-Authorization: Basic eXVuZHJlYW06bXlwYXNzd29yZA==
Accept-Encoding: gzip
Proxy-Authorization 필드에 아이디/패스워드 값이 들어간 걸 확인할 수 있다.
HTTPS 연결
HTTPS 평문으로 통신하는 HTTP의 취약점을 개선하기 위해서 사용하며, TLS를 적용해서 송/수신 데이터를 암호화 한다. HTTPS라고 부르며, http://url대신에 https://url을 사용한다.
클라이언트가 서버에 연결하면, 데이터 전송전에 SSL handshake과정을 거친다. 클라이언트와 서버는 SSL의 버전을 확인하고, 클라이언트가 지원하는 cipher를 확인 하는등의 과정을 거쳐서 SSL 연결을 맺는다. 추가적으로 클라이언트는 서버의 인증서가 올바른(valid)지를 체크한다.
인증서가 올바른지를 체크하는 건 중요한데, 인증서의 사용기간이 만료되었거나 공인하지 않는 사설인증서(self-signed)를 사용하는 경우도 있기 때문이다. 웹브라우저의 경우 https 서버의 인증서가 valid 하지 않으면 "Get me out of here!"와 같은 경고 메시지를 출력하고, "그럼에도 불구하고 접속 할 것인지"를 묻는다.(분명 위험하다고 경고했으니, 이후 일어나는 문제는 니가 책임져라!!)
Server
지금까지 클라이언트 프로그램만 만들었는데, 서버 프로그램도 만들어 보자. 파일을 업로드하는 간단한 프로그램이다.
Go의 FileServer메서드를 이용하면, 간단하게 파일 서버를 만들 수 있다. 이 메서드는 매개변수로 파일을 관리할 "root 디렉토리"를 필요로 한다. 루트 디렉토르는 URL 상의 "/"와 일치한다.
Contents
HTTP 소개
HTTP 살펴보기
URL과 리소스
HTTP의 특성
HTTP 버전
HTTP 1.0의 구성
HTTP 1.1의 구성
Simple user-agents
Configuring HTTP Requests
Client 객체
Proxy
간단한 Proxy
Proxy 인증
HTTPS 연결
Server
Handler function
Recent Posts
Archive Posts
Tags