HTTP 통신도 결국 소켓 통신이다.
사용자는 URL을 통해 웹사이트에 접속한다. 카페에서 커피를 주문하듯, 사용자는 URL로부터 페이지 정보를 받아볼 수 있다. 이렇게 URL을 통해 오고 가는 애플리케이션 계층의 데이터를 '메시지'라고 부른다.
웹사이트는 사실 주체적인 존재가 아니라 어떤 서버 컴퓨터에서 만들어진 소스코드와 파일에 불과하다.
누군가 웹사이트에 접속하면 서버가 만들어둔 웹페이지 파일을 요청하는 셈이다. 서버 컴퓨터가 생성한 파일은 이진수를 거쳐 전기신호로 변환되어 클라이언트의 컴퓨터를 찾아간다. 클라이언트의 컴퓨터에 도착한 전기신호는 이진수로, 이진수는 다시 의미를 가진 메시지로 변환되어 사용자의 눈에 보인다. 걸리는 시간은 지극히 짧지만, 그 과정은 여러 계층으로 나누어져 있고 각각의 계층이 갖는 역할은 엄밀하게 구분된다.
네트워크 통신이 이루어지는 과정은 신호가 끊임없이 변환되는 과정이다. 이 과정을 추상적인 레이어로 구분해놓은 두 개의 모델인 OSI 7 모델, TCP/IP 모델이 존재한다. 두 모델은 유사하며, 오늘날의 네트워크는 TCP/IP 모델로 구성되었다. 각각의 레이어에서 데이터를 처리할 때는 지켜야할 규격(프로토콜)이 존재한다.
HTTP 프로토콜은 최상위 레이어인 어플리케이션 레이어에서 사용자가 요청하는 데이터 혹은 서버가 응답하는 데이터를 메시지라는 단위로 래핑하고 HTTP 헤더를 붙일 때 지켜져야 할 규칙을 말한다.
TCP/IP 프로토콜은 그 아래 레이어인 트랜스포트 레이어에서, 위에서부터 받아온 데이터를 세그먼트라는 단위로 래핑하고 세그먼트 헤더를 붙일 때 지켜져야 할 규칙을 말한다.
엄밀히 말해 HTTP 프로토콜만 사용하는 통신이라는 것은 없다. 클라이언트에서 HTTP 프로토콜을 지켜서 만들어진 메시지는 아래 계층들을 거쳐서 서버에 도착하고 요청한 메시지에 대한 응답을 받는다.
HTTP 메시지는 아래 계층의 프로토콜들을 모두 거치면서 데이터가 인크립트되고 디크립트되는 과정을 겪는다. HTTP는 1989년에서 1991년 사이에 팀 버너스 리와 팀원들에 의해 개발되었다.
조금 서치해보면 이 시기에는 이미 글로벌 네트워크가 만들어져있는 상태였다는 것을 알 수 있다. 상식적으로 생각해보더라도 인터넷이 이미 만들어지고 나서 메시지를 주고 받을 수 있는 통신프로토콜이 정립되는 것은 매우 자연스럽다. 이후에 설명할 소켓이라는 구조체(소켓 주소를 담은 구조체) 역시 HTTP 가 개발되기 훨씬 전에 리눅스 커널에서 제공하는 인터페이스로 이미 만들어져 있었다. 즉 HTTP 통신은 이미 글로벌 네트워크 생태계가 형성되어 있고 소켓 통신 인터페이스도 만들어져있는 상태에서 웹브라우저라는 클라이언트를 통해 자유롭게 웹서핑을 할 수 있도록 고안된 통신방식이다.
HTTP 메시지에 대한 응답을 준 이후에 서버는 이전에 왔던 HTTP 요청에 대해서는 잊어버린다. (stateless). 하지만 만약 소켓연결이 아직 이어져있다면 서버와 클라이언트는 서로에 대해서 여전히 인식하고 있다.
클라이언트와 서버는 각자의 소켓주소를 통해 서로가 원할 때 연결을 시작하고 서로가 원할 때 연결을 끊을 수 있으며, 이 연결은 소켓 디스크립터를 통해 해당 프로세스 혹은 해당 포트 내에서 고유성이 유지된다. 그러나 일반적인 HTTP 요청과 응답은 한번 주고받고 나면 바로 잊어버리기 때문에, 서로의 존재를 인식하면서도 이전에 무슨 데이터가 오고갔는지는 모르는 상태가 되는 것이다.
사실 이러한 설계는 매우 효율적이다. 클라이언트가 요청하는 대부분의 정보는 서로 독립적이므로 서버가 클라이언트에 응답을 준 이후에 계속해서 이전 요청을 기억하도록 하는 것은 불필요하기 때문이다. 인터넷 세상이 커지고 실제세상을 옮겨담기 시작하면서 점차 연결이 기억되는(stateful) 채팅이나, 유저 정보 기억 등의 편리하고 재미있는 기능들이 필요해졌고 HTTP 는 페이로드에 붙이는 헤더에 쿠키나 WebSocket-Key 등을 추가해서 보낼 수 있도록 개선되었다. 이 헤더에 정보를 추가함으로써 HTTP 연결은 stateful 해질 수 있게 되었다.
정리해보자면, 기본적으로 HTTP 연결은 stateless 하다. 그러나 필요에 의해 HTTP 헤더에 정보를 추가함으로써 stateful 한 통신도 할 수 있게 되었다. 이것이 가능한 이유는 HTTP 통신이 소켓통신에 의존하기 때문이다. HTTP 메시지와 헤더(페이로드)가 만들어지는 어플리케이션 계층보다 더 깊은 계층인 트랜스포트 계층에서는 소켓이라는 인터페이스를 제공하고, 클라이언트의 소켓과 서버의 소켓은 연결을 맺은 후부터 끊을 때까지 서로를 고유한 소켓 디스크립터를 통해 인식하고 있다.
https://gist.github.com/yeonwooz/4d669b9f043ea963f1eee85e1e9319b4
이번 주차 과제의 첫번째 파트는 클라이언트의 간단한 요청을 처리하는 웹서버를 구현하는 것이다. 클라이언트의 요청은 socket 을 통해 들어오며, 일반 파일처럼 취급되고 정적파일 요청과 동적파일 요청으로 구분된다. 33라인의 while(1)을 보면 알겠지만, 웹서버 프로그램은 항상 실행되고 있어야 한다. 40번 라인에서 서버가 클라이언트의 요청을 수행하고 응답을 보낸 뒤에는, 41번 라인에서 Close해주어야 한다. 처음에는 서버가 클라이언트의 요청을 수행한 이후에 계속 대기하고 있어야 한다는 생각에 Close를 해주지 않았는데, 서버의 대기는 listenfd를 통해 이루어지므로, 한번의 요청에 대해서는 응답 후 Close해주는 것이 맞다.
tiny 웹서버의 전체코드는 컴퓨터 시스템이라는 책에서 제공해주고 있으며 카네기멜론 대학에서 수업자료로 코드를 올려두었다.
다음번 포스팅은 tiny 웹서버 코드를 세부적으로 들여다보는 내용이거나, 아니면 이번 주 과제의 다음 파트인 프록시 서버에 대한 내용이 될 것이다.
Photo by Lewis Keegan on Unsplash
TMI 1.
이번 과제에서 가장 어이없고 속상했던 부분은 AWS EC2로 할당받은 우분투 머신에서 서버 프로그램을 실행해놓고 localhost(127.0.0.1) 로 접속하려고 시도하느라 시간을 낭비한 점이다. 당연히 AWS EC2에서 할당받은 주소와 미리 허용해둔 포트로 접속해야 한다. 이전에 혼자서 작업할 때 항상 로컬호스트로 접속하던 습관이 남아있었던 데다가, 정신을 똑바로 차리지 않고 있었기 때문인 것 같다. 타성에 젖어서 코딩하는 일이 없도록 주의해야겠다.
TMI 2.
혹시 이 포스팅을 계속 읽어주신 분이 계시다면 눈치채셨겠지만, 여섯째주 일곱째주 포스팅이 없다!
각각 레드블랙트리 와 메모리 동적 할당이었는데, 미처 글로 정리하지 못했다. 트리를 구현하고 동적할당기를 구현하는 과정은 재미있었지만, 솔직하게 말해보자면 글로 정리할 만큼 애착이 생기는 내용은 아니었다. 이번주 과제는 정말 재미있고 애착이 많이 가서 계속 깊이 파고들게 된다. 완전히 다 이해해보고 싶다.