[42Seoul] webserv - 02
[42Seoul] Webserv - 01
이전 글에 이어. [42Seoul] Webserv - 00 길고 길었던 웹서브 과제가 끝남에 따라, 그 동안 했던것들을 정리해보고자 한다. 웹서버와 HTTP 웹서브 서브젝트를 보면 이런 글귀가 있다. This is when you finally u
woongtech.tistory.com
지난 글에 이어 이번엔 HTTP 메세지에 대해 써보려한다.
HTTP Request
클라이언트(크롬, 사파리와 같은 브라우저)가 서버에 무언가 요청을 한다면, 서버는 요청에 맞도록 적절하게 응답을 해주어야 한다. 만약 우리가 구글 메인에 접근하고 싶을 때, 브라우저 검색창에 주소를 입력하는것을 브라우저에서는 구글 홈페이지 요청에 대한 적절한 HTTP 메세지를 작성해서 서버로 보내주게 된다.
www.google.com
그러면, 서버는 구글 홈페이지에 대한 적절한 응답을 보내주게 된다. 그 적절한 응답으로는 HTML 문서도 포함되는데, 우리가 보는 브라우저 화면이 HTML로 이루어진 사이트라는것을 생각해보면, 쉽게 이해할 수 있다. 요청을 통해 응답받은 메세지가 화면에 표시되는 것이다.
요청 메세지는 다음과 같은 형식으로 들어오게 된다.
// HTTP 요청 메세지
METHOD URL HTTPVERSION
HEADER1 VALUE1
HEADER2 VALUE2
...
HEADER(N) VALUE(N)
BODY // OPTIONAL
위는 요청메세지의 기본 양식이다. 우리는 요청메세지로 들어오는 값들을 적절하게 파싱해주고, 응답 메세지 작성 시, 요청 사항에 맞도록 처리해주기만 하면 된다.
가장 기본이 되는 웹 서버의 홈으로 요청을 해보았다.
HTTP REQUEST : localhost:8080/
====================
GET / HTTP/1.1
accept-encoding: gzip, deflate, br
Accept: */*
User-Agent: Thunder Client (https://www.thunderclient.com)
Host: localhost:8080
Connection: close
===================
"localhost:8080/"을 입력해보았을 때, 요청 메세지는 이런식으로 들어오게 된다. 아래에서 더 자세히 알아보도록 하자.
METHOD
먼저, METHOD는 요청이 서버에 어떤 서버에 어떤 동작을 수행하길 원하는지를 지정하는 부분이다. HTTP 프로토콜은 다양한 메서드를 제공하여 클라이언트가 서버에 대해 원하는 동작을 명시할 수 있도록 해준다.
가장 일반적으로 사용되는 HTTP 메서드는 다음과 같다.
- GET: 서버로부터 지정된 리소스의 데이터를 요청한다. 주로 리소스의 내용을 가져오는 데 사용되며, 요청 메시지에 별도의 데이터를 전송하지 않습니다. (BODY가 존재하지 않는다.)
- POST: 서버에 새로운 데이터를 제출하거나 처리를 요청한다. 주로 서버에 데이터를 전송하고 저장하는 데 사용되며, 요청 메시지의 본문(body)에 데이터를 포함한다.
- PUT: 서버에 지정된 위치에 새로운 데이터를 저장하거나 갱신하도록 요청한다. 주로 리소스를 생성하거나 업데이트하는 데 사용되며, 요청 메시지의 본문에 데이터를 포함한다.
- DELETE: 서버에서 지정된 리소스를 삭제하도록 요청한다. 주로 리소스를 삭제하는 데 사용되며, 요청 메시지에 별도의 데이터를 전송하지 않는다.
- PATCH: 서버에서 지정된 리소스의 일부를 수정하도록 요청한다. 주로 리소스의 일부를 업데이트하는 데 사용되며, 요청 메시지의 본문에 수정할 데이터를 포함합니다.
- HEAD: GET 메서드와 유사하지만, 실제 데이터를 요청하지 않고 응답 헤더만을 받는다. 주로 리소스의 메타데이터를 확인하는 데 사용된다.
이 중, 서브젝트에서 요구하는 메서드는 GET, POST, DELETE이다. 하지만, 서브젝트와 함께 주어지는 테스터를 끝까지 실행시키기 위해서는 HEAD와 PUT 메서드도 추가 구현해주어야 한다. 메서드 구현은 하다보면 추가하는것이 크게 어려운 부분은 아니므로 모두 구현해보는것을 추천한다.
URL
URL은 클라이언트가 서버에 접근하고자 하는 리소스의 위치를 나타낸다. URL은 웹 서버의 도메인 이름(호스트)과 리소스의 경로로 구성된다. 나는 우리의 구성 파일에 location 지시자를 토대로 리소스의 경로를 찾아주었다.
URL은 다음과 같은 구성 요소로 이루어진다.
- 프로토콜(Protocol): URL의 시작 부분에는 요청을 처리하는 데 사용되는 프로토콜이 지정되며, 일반적으로 HTTP나 HTTPS를 사용합니다.
- 호스트(Host): URL의 다음 부분에는 웹 서버의 도메인 이름이나 IP 주소가 지정된다. 이는 클라이언트가 접근하려는 서버를 식별하는 데 사용된다. 예를 들어, "example.com"이나 "127.0.0.1"과 같은 형식.
- 포트(Port): URL에 포트 번호가 지정되어 있으면, 해당 포트 번호로 접속하여 서버와 통신한다. 일반적으로 HTTP의 기본 포트는 80이고, HTTPS의 기본 포트는 443이다. 포트 번호가 생략되면 기본 포트가 사용된다.
- 경로(Path): URL의 호스트 다음에는 리소스의 경로가 지정된다. 경로는 서버에서 요청하는 리소스의 위치를 나타낸다. 예를 들어, "/products"이나 "/images/logo.png"과 같은 형식이다.
- 쿼리 매개변수(Query Parameters): URL에는 추가적인 매개변수를 전달하기 위해 쿼리 매개변수를 사용할 수 있다. 쿼리 매개변수는 "?"로 시작하며, 이름과 값의 쌍으로 구성된다. 예를 들어, "/search?query=example"과 같은 형식이다.
- 프래그먼트(Fragment): URL에는 페이지 내 특정 부분을 가리키는 프래그먼트를 지정할 수 있다. 프래그먼트는 "#"으로 시작하며, 주로 웹 페이지의 특정 섹션이나 앵커로 사용된다.
URL이라고 했지만, 요청 메세지를 보면 경로만 나오는것을 확인할 수 있을것이다. 호스트와 포트는 헤더를 통해 확인할 수 있다. 위의 HTTP 요청 메세지 예시에서는 "/"이라는 문자열만 나왔다. "/"도 경로가 될 수 있다는 것인데, 구성 파일에서 location 에 대한 것으로 "/"을 설정해주면, "/example/index.html"이라는 경로가 "/"을 통해서도 접근할 수 있어, 결국 "localhost:8080/"은 "localhost:8080/example/index.html"이 될 수 있다.
HTTP_VERSION
이 부분은 예시에서 "HTTP/1.1"이라고 되어있는 것을 볼 수 있다. HTTP/1.1 버전에서는 이전 버전인 0.9와 1.0 버전과 비교했을 때, 성능상 많은 개선점이 존재하는데, 성능 외에도 요청 메세지 양식도 조금씩 달라지는것을 확인할 수 있다. 이 차이는 여기서 한번 확인해보길 바란다.
우리는 1.1 버전의 요청이 들어온다는 것을 가정하고 진행했다. 지금 생각해보면 아쉬운 점으로 남는데, 1.1 버전인지 아닌지 검증하는 코드도 추가하면 좋을것 같다는 생각이 든다.
HEADER
HTTP 요청의 대부분의 옵션은 헤더로 전달된다. 헤더는 요청과 관련된 추가 정보를 담고 있고, 일반적인 헤더 필드에는 다음과 같은 것들이 있다.
- Host: 요청하는 호스트의 도메인 이름 또는 IP 주소를 포함한다.
- User-Agent: 클라이언트 애플리케이션의 정보를 포함한다. 서버는 이 정보를 사용자 에이전트의 유형 또는 버전을 확인하는 데 사용할 수 있다.
- Content-Type: 요청 본문의 데이터 타입을 지정한다. 예를 들어, JSON 데이터를 전송하는 경우 "application/json"으로 설정할 수 있다.
- Content-Length: 요청 본문의 길이를 바이트 단위로 나타낸다.
- Authorization: 요청을 인증하기 위한 정보를 포함합니다. 일반적으로 토큰이나 사용자 이름과 비밀번호의 조합이 포함된다.
- Transfer-Encoding: HTTP 프로토콜에서 사용되며 메시지 본문을 전송하기 위한 인코딩 방식을 명시한다. 즉, 메시지가 클라이언트와 서버 간에 어떻게 전송될 것인지를 결정한다.
모든 헤더가 중요한 역할을 수행하겠지만, 이 중 우리가 제일 자주 사용했던것은 Content-length, Host, Transfer-Encoding이었다.
먼저, Content-length는 요청 본문에 해당하는 내용의 길이를 의미한다. Content-length와 요청 본문의 길이는 반드시 같아야하며, 다르다면 컨텐츠 길이 오류에 대한 상태 코드를 반환해야한다.
그리고 Host헤더를 통해 우리는 클라이언트에서 접속한 포트번호를 알 수 있었다. 같은 서버일지라도 여러개의 포트번호를 가질 수 있는데, 어떤 포트번호로 요청했는지 알 필요가 있기 때문이다.
마지막으로 Transfer-Encoding에 따라 요청 본문이 달라질 수도 있어서 주의해야한다. 그 중 "chunked"라는 플래그의 유무로 본문의 형식이 달라진다. 서브젝트에서 주어지는 테스터에서는 기본적으로 Transfer-Encoding: "chunked"
라는 헤더가 포함되어있기 때문에, 처리해주는 것이 좋다.
예를 들어, 요청 메세지 본문이 다음과 같다고 해보자.
Content-length: 50
// 요청 메세지 본문
aaaaaaaaaa
bbbbbbbbbb
cccccccccc
dddddddddd
eeeeeeeeee
이렇게 10자씩 a, b, c, d, e가 본문 내용이라면, 헤더에서 Content-length는 50이 되어야 할 것이다.
이것을 chunked로 적용한다면, 다음과 같이 적용된다.
Transfer-Encoding: chunked
10
aaaaaaaaaa
10
bbbbbbbbbb
10
cccccccccc
10
dddddddddd
10
eeeeeeeeee
0
이렇게 변경된다. chunked일 경우, 헤더에 Content-length가 없으며 본문이 조각형식으로 문자열의 길이가 먼저 나온 뒤, 그 뒤에 길이에 맞는 문자열이 나오게 된다.
즉, 문자열 10자가 나오는데 그게 "aaaaaaaaaa"이라는 것이고 그 뒤에 또 10자가 나오며 "bbbbbbbbbb", .... , 10자 + "eeeeeeeeee"가 오게 된다.
chunked는 마지막에 "0"을 통해 본문이 끝남을 알려준다.
주의할 점
요청 메세지를 받을 때 주의할 점이 있는데, 굉장히 큰 사이즈의 요청이 들어오게 되면 버퍼를 한번에 다 받지 못할 수 있다는 것이다. 테스터에서는 최대 1억자까지의 본문을 요청하게 되는데, 이 1억자 본문에서 끝까지 요청 메세지를 받지 못해서 터지는 경우가 있었다. 그래서 요청 메세지는 마지막이 개행인지 아닌지 검사해주면 될 것이다. 이 때, 개행문자는 CRLF로, "\r\n"이다.