클라이언트와 서버간에 네트워크 통신시, OS 레벨에서 어떠한 일이 일어나는지 궁금해서 "성공과 실패를 결정하는 1%의 네트워크 원리" 라는 책을 읽고 정리한 글입니다.
URL 구조
URL은 다음과 같은 구조를 갖습니다.
HTTP 프로토콜
Hyper Text Transfer Protocol로, 클라이언트와 서버가 주고받는 메시지의 내용이나 순서를 정한 것이라고 할 수 있습니다. 일종의 클라이언트와 서버 사이에 통신하기 위한 약속이라고 할 수 있습니다.
클라이언트에서 서버를 향해 요청 메시지를 보냅니다.
이 요청 메시지 안에는 "무엇을", "어떻게" 하겠다는 내용이 포함되어 있는데요, "무엇을"에 해당하는 것은 URI이라고 하고, "어떻게"에 해당하는 즉 동작에 해당하는 것은 HTTP Method라고 할 수 있습니다.
해당 관련 자세한 내용은 아래 링크를 참고해주세요.
- URL → https://developer.mozilla.org/ko/docs/Web/API/URL
- Method → https://developer.mozilla.org/ko/docs/Web/HTTP/Methods
1. 브라우저에서 URL을 해독하고 HTTP 요청 메시지를 생성.
먼저, 브라우저가 웹 서버에 보내는 요청 메시지를 작성하기 위해서 URL을 해독합니다.
브라우저가 URL을 해독하면, 브라우저는 HTTP 프로토콜을 이용해서 웹 서버와 파일명을 판단하면 브라우저는 이것을 바탕으로 HTTP 요청 메시지를 생성합니다.
요청 메시지의 첫 번째 행에 있는 Request Line을 사용하는데, 이 행에서 중요한 것은 맨 앞에 있는 Method입니다. 그리고 한 칸 띄운 다음 URI를 씁니다.
두 번째 행부터는 메시지 헤더라는 행이 이어집니다. 메시지 헤더란 부가적인 정보를 즉 메타정보들을 넘겨주는 곳이라고 할 수 있습니다.
메시지 헤더를 쓰면 그 위에 한 줄의 공백 행을 넣고, 송신할 데이터를 작성합니다. 이 부분을 메시지 본문(Body)라고 하며, 메시지의 실제 내용이 됩니다. (단 GET 메소드의 경우에는 메소드와 URI만으로 웹 서버가 무엇을 할지 판단할 수 있으므로 메시지 본문에 쓰는 송신 데이터는 아무것도 없습니다)
POST /api/v1/product HTTP/1.1
Accept: application/json
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 83
Content-Type: application/json
Host: intropython.com
User-Agent: HTTPie/0.9.3
{
"imp_uid": "imp_1234567890",
"merchant_uid": "order_id_8237352",
"status": "paid"
}
2. 웹 서버의 IP 주소를 DNS 서버에 조회
HTTP의 요청 메시지를 만들면 이것을 OS에 의뢰하여 웹 서버에게 송신합니다. 브라우저는 URL을 해독하고 HTTP 메시지를 생성하지만, 메시지를 네트워크에 송출하는 역할은 OS에서 처리합니다.
이때 URL 안에 쓰여있는 서버의 도메인명에서 IP 주소를 조사해야 합니다. 왜냐하면 OS에 송신을 의뢰할 때는 도메인명이 아니라 IP 주소로 상대를 지정해야 하기 때문입니다.
도메인 주소와 IP주소를 구분하는 이유
- 사람 입장에서 IP주소는 기억하기 어렵습니다. 이러한 문제로 이름을 통해서 상대를 지정할 수 있도록 도메인 주소를 추가하였습니다.
그럼 OS에 IP주소가 아닌 도메인 주소를 이용해서 통신하면 되지 않나요?
- IP 주소는 4바이트(32비트)인 반면에, 도메인 명은 최소 수십 바이트에서 최대 255바이트에 해당합니다. 따라서 좀 더 효율적인 통신을 위해서 IP주소를 사용한다고 할 수 있습니다.
- 게다가 도메인 주소는 길이도 결정되어 있지 않아서, 길이가 결정되어 있는 IP주소를 취급하는 것보다 복잡하므로 효율성이 떨어지는 문제도 있습니다.
그럼 도메인 주소와 IP주소 사이에서 변환을 해주는 역할은 누가 할까요? 바로 DNS입니다.
DNS
DNS는 Domain Name System으로, 도메인 명과 IP 주소를 대응시키기 위해 사용합니다.
OS에서 DNS를 이용하는 과정
OS에서 도메인에 해당하는 IP주소를 찾기 위해 가장 가까운 DNS 서버에게 DNS 요청합니다.
DNS 서버에 조회한다는 것은 DNS 서버에 조회 메시지를 보내고, 반송되는 응답 메시지를 받는 것입니다.
이때 DNS 서버로 조회하는 역할을 수행하는 것은 “DNS 리졸버”라고 하는 친구인데요. 이 리졸버는 Socket 라이브러리에 포함되어 있습니다.
리졸버를 호출하면 리졸버가 DNS 서버에 조회 메시지를 보내고, DNS 서버에서 응답 메시지가 돌아오면 IP 주소를 추출해서 브라우저에서 지정한 메모리 영역에 써넣습니다.
DNS 리졸버 내부의 작동
DNS 리졸버 내부에서는 먼저 DNS 서버에 문의하기 위한 메시지를 생성합니다. 그리고 생성된 메시지를 DNS 서버에 보내는데요.
이때 메시지 송신 동작은 리졸버가 하는 것이 아니라 OS의 내부에 포함된 프로토콜 스택을 호출해서 실행을 의뢰합니다. 그러면 DNS 서버에서 해당하는 IP를 찾아 응답 메시지가 네트워크를 통해 클라이언트 측에 도착하고, 프로토콜 스택을 경유하여 리졸버에 건네 져서 리졸버가 내용을 해독한 후 여기에서 IP주소를 추출하여 애플리케이션에 IP주소를 건네줍니다.
DNS 서버는 어떻게 동작할까?
DNS 서버의 기본 동작은 클라이언트에서 조회 메시지를 받고 조회의 내용에 응답하는 형태로 정보를 회답하는 일입니다. DNS 조회 메시지에는 세 가지 정보가 포함되어 있습니다.
- 이름 (www.google.com)
- 클래스 (IN)
- 타입 (A)
DNS 서버에는 세 가지 정보를 토대로 등록된 정보를 찾아서 해당하는 IP 주소 값을 클라이언트에 회답합니다.
DNS 도메인의 계층
인터넷에는 막대한 수의 서버가 있으므로 이것을 1대의 DNS 서버에 등록하는 것은 불가능합니다. 이러한 문제를 해결하기 위해 정보를 분산시켜서 다수의 DNS 서버에 등록하고, 다수의 DNS 서버가 연대하여 어디에 정보가 등록되어 있는지를 찾아내는 구조로 이루어져 있습니다.
DNS 서버에 등록한 정보에는 모든 도메인명이라는 계층적 구조를 가진 이름이 붙어있습니다.
예를 들어 www.google.com 인 경우 상위 계층부터 루트→ com → google → www 구조로 계층적으로 구성되어 있습니다. (참고로 com 도메인 위에 .이라는 루트 도메인이 존재합니다)
인터넷에는 DNS 서버가 수만 대나 있으므로 일일이 찾는 것은 불가능합니다. 그래서 먼저 하위의 도메인을 담당하는 DNS 서버의 IP 주소를 그 상위의 DNS 서버에 등록하는 방식으로 구성되고, 루트 도메인의 DNS 서버를 인터넷에 존재하는 DNS 서버에 전부 등록해서 어느 DNS 서버도 루트 도메인에 액세스 할 수 있게 됩니다. 그 결과 클라이언트에서 특정 DNS 서버에 액세스하면 루트 도메인을 경유하여 도메인 계층 아래로 찾아가서 최종적으로 원하는 DNS 서버에 도착해서 도메인에 해당하는 IP 주소 값을 알아옵니다.
그리고 클라이언트로 IP 주소를 회답해서 결과적으로 클라이언트는 IP 주소를 알고 액세스 할 수 있게 됩니다.
DNS 서버는 캐시 기능이 적용되어 있다.
DNS 서버는 한 번 조사한 이름을 캐시에 기록할 수 있는데, 조회한 이름에 해당하는 정보가 캐시에 있으면 그 정보를 회답하기 때문에 성능 및 DNS 서버 부하면에서 이점을 얻을 수 있습니다.
IP 주소만 캐싱해 두는 것이 아니라, 만약 조회한 이름이 도메인에 등록되어 있지 않은 경우에도 그 정보를 캐시에 보존할 수도 있습니다.
주의사항
캐시의 원리에는 주의사항이 있습니다. 캐시에 정보를 저장한 후 등록 정보가 변경되는 경우도 있으므로 캐시 안에 저장된 정보는 올바르다고 단언할 수 없습니다.
따라서 DNS 서버에 등록하는 정보에는 유효 기간을 설정하고, 캐시에 저장한 데이터의 유효 기간이 지나면 캐시에서 삭제하도록 구성되어 있습니다.
3. 프로토콜 스택에 메시지 송신을 의뢰한다.
DNS 리졸버를 통해 DNS 서버로부터, IP 주소를 알았으면 IP 주소의 상대인 웹 서버에 메시지를 송신하도록 OS의 내부에 있는 프로토콜 스택에 의뢰합니다.
데이터를 송수신하는 컴퓨터 사이에는 데이터의 통로 같은 것이 있고, 이것을 통해 데이터가 흐르면서 상대측에 도착하는데, 여기는 통로는 파이프라고 할 수 있습니다. 그래서 한쪽 끝에서 파이프에 데이터를 쏟아부으면 파이프 안을 통해 반대쪽 끝까지 도착하고, 거기에서 데이터를 추출할 수 있습니다.
먼저 파이프에 데이터를 송수신하기 전에 클라이언트와 서버 사이에 파이프를 연결하는 동작이 필요합니다. 이 부분의 요점은 파이프의 양끝에 있는 데이터의 출입구로, 소켓이라고 부릅니다.
먼저 서버측에서 소켓을 만들고, 소켓에 클라이언트가 파이프를 연결하기를 기다립니다. 이렇게 해서 서버 측이 기다리고 있는 동안에 클라이언트 측에서 소켓을 만들고, 소켓에서 파이프를 늘려 서버 측의 소켓에 연결합니다. 이렇게 양쪽의 소켓이 연결되면 준비가 완료된 것으로, 이제 소켓에서 데이터를 쏟아붓듯이 데이터 송수신 동작을 실행합니다. 송수신 동작이 끝나면 연결했던 파이프가 분리됩니다.
정리하면
- 소켓을 생성합니다.(소켓 작성)
- 서버측의 소켓에 파이프를 연결합니다. (접속)
- 데이터를 송수신합니다. (송수신)
- 파이프를 분리하고 소켓을 말소합니다. (연결 끊기)
이 네 가지 동작을 실행하는 것은 OS 내부의 프로토콜 스택으로, 브라우저 등의 애플리케이션은 자체에서 파이프를 연결하거나 데이터를 전송하지 않고, 프로토콜 스택에 의뢰해서 파이프를 연결하거나 데이터를 전송합니다.
데이터 송수신 과정 정리
1. 소켓의 작성 단계
클라이언트에서 소켓 라이브러리의 socket를 이용해서 소켓을 생성합니다.
소켓이 생성되면 소켓을 식별하는 디스크립터라는 것이 돌아오는데 애플리케이션은 이것을 메모리에 기록해둡니다.
2. 파이프를 연결하는 단계
만든 소켓을 서버 측의 소켓에 접속하도록 프로토콜 스택에 의뢰합니다. 그러면 Socket 라이브러리의 connect라는 것을 호출해서 의뢰 동작을 실행하는데, 이때 지정하는 값은 디스크립터, 서버의 IP 주소, 포트 번호 세 가지 값입니다.
먼저 디스크립터는 클라이언트 내부의 소켓을 식별하기 위해 사용되고, 포트 번호는 상대측에서 소켓을 식별하기 위해 사용됩니다.
포트 번호는 URL에 포함되어 있거나 혹은 Well-Known 포트로 애플리케이션의 종류에 따라 미리 결정된 값을 사용합니다. 반면 클라이언트의 소켓 번호는 프로토콜 스택이 적당한 값을 골라서 (랜덤 포트) 할당합니다.
3. 메시지를 주고받는 송수신 단계
소켓이 상대측과 연결되면 소켓에서 데이터를 쏟아부으면 상대측의 소켓에 데이터가 도착합니다.
우선 애플리케이션은 송신 데이터를 메모리에 준비합니다. 그리고 Socket 라이브러리의 write를 호출할 때 디스크립터와 송신 데이터를 지정합니다. 그러면 프로토콜 스택이 송신 데이터를 서버에게 송신합니다. (소켓에는 연결된 상대가 기록되어 있으므로 디스크립터로 소켓을 지정하면 연결된 상대가 판명되어 그곳을 향해 데이터를 송신합니다.)
그러면 서버는 수신 동작을 실행해서 받은 데이터의 내용을 조사하고 적절한 처리를 실행하여 응답 메시지를 반송합니다. 이 메시지를 read를 통해 받아서 수신 버퍼에 저장하고, 메시지를 저장한 시점에서 메시지를 애플리케이션에 건네줍니다.
4. 연결 끊기 단계에서 송수신이 종료된다
브라우저가 데이터 수신을 완료하면 송수신 동작은 끝나고, 그 후 라이브러리의 close를 호출하여 연결 끊기를 의뢰하면 소켓 사이를 연결한 파이프와 같은 것이 분리되고 소켓도 말소됩니다.
이러한 흐름으로 HTTP 프로토콜은 1개의 데이터를 읽을 때마다 접속, 요청 메시지 송신, 응답 메시지 수신, 연결 끊기라는 동작을 반복합니다.
감사합니다.
'공통 > Web & Network' 카테고리의 다른 글
[CORS] CORS 정리 (4) | 2021.11.22 |
---|---|
네트워크의 흐름 2단계 (0) | 2021.08.05 |