하나하나 쉽게 설명할 수 있게끔 정리하면서 읽다보니 속도가 안붙는다..ㅎㅎ
천천히 꾸준히 읽어나가는걸 목표로해보자~
OS에 내장된 프로토콜 스택과 LAN 어댑터가 브라우저에서 받은 메시지를 서버에 송출하는 흐름에 대해 알아본다.
인터넷에서 데이터를 운반할 때 데이터를 패킷이라는 형태로 작게 나누어 운반한다.
위 패킷을 상대방(클라이언트 혹은 서버)에게 운반하는 것이 IP의 주된 역할이다.
IP 내부에서는 ICMP, ARP 라는 프로토콜을 다룬다.
ICMP : 패킷을 운반할 때 발생하는 오류를 알리거나 제어한다.
ARP : IP 주소에 대응하는 이더넷의 MAC 주소를 확인할 때 사용한다.
LAN 드라이버는 LAN 어댑터의 하드웨어를 제어한다.
즉 다시말해 LAN 케이블에 대해 데이터를 송, 수신하는 동작을 실행하는 역할을 한다.
위에서 설명한 프로토콜 스택은 내부에 제어 정보를 기록하는 메모리 영역을 가지고있다.
여기에는 대표적으로 통신 상대의 IP주소, 포트번호, 통신 동작상태 등의 정보가 포함되어있다.
소켓은 개념에 가까운 느낌이라 구체적으로 지정하기가 어렵다.
여기서는 위에서 말한 프로토콜 스택 내부의 제어 정보 소켓의 실체라고 볼 수 있다.
위 문장을 다음과 같이 정리해볼 수 있다.
프로토콜 스택은 소켓에 기록된 제어정보를 참조하여 동작한다.
추상적인 개념만 봐서 아직 어떤 개념을 전하고싶은지 잘 이해가 안될 수도 있다.
실제 사용예시를 보면서 소켓에 대한 이해를 높여보자.
netstat
명령어를 통해 소켓의 구체적인 이해를 해보자.
netstat 명령어를 통해 소켓의 내용을 확인해볼 수 있다.
*.*
로 표기된다.소켓의 생김새를 알았으니 동작에 대해 알아보자.
브라우저가 socket, connet와 같은 함수를 호출했을 때 프로토콜 스택의 내부 동작을 알아보자.
알아보기 쉽게 애플리케이션 = 브라우저 로 칭하겠습니다.
아직 서버랑 통신하지 않은 상황입니다!!
프로토콜 스택과 브라우저는 동일한 컴퓨터 위에서 동작하고있다는것을 인지해주세요
브라우저에서 다음과 같은 형태를 통해 프로토콜 스택에게 소켓 생성을 요청한다.
디스크립터(식별자) = socket(IPv4, TCP, ... 기타 정보들)
프로토콜 스택은 위와 같은 요청을 받아 한 개의 소켓을 생성한다.
이 때 프로토콜 스택은 소켓 한 개 분량의 메모리 영역을 확보하고, 이곳에 소켓이 초기 상태임을 기록한다.
소켓이 만들어지면 소켓의 식별자인 디스크립터를 애플리케이션에게 알려준다.
이렇게 만들어진 디스크립터(식별자)를 통해 소켓의 다른 정보를 모르는 상태여도 소켓호출이 가능해진다.
소켓을 생성하기만 해서는 통신 상대가 누군지 모른다.
소켓을 생성한 이후 브라우저는 connect를 호출한다.
프로토콜 스택은 이 요청을 받아 자신의 소켓을 서버측 소켓에 접속한다.
접속이라는 행위에 대해서 다음과 같은 과정들이 진행된다.
서버에게 connect 요청 시 자신이 연결할 서버측의 주소를 넣고, 자신의 IP, 포트번호도 같이 알려준다.
프로토콜 스택은 접속을 수행할 때 데이터 송, 수신 시 해당 데이터를 일시적으로 저장하는 메모리영역인 버퍼 메모리를 확보한다.
위 과정을 마치면 데이터를 송, 수신할 수 있는 상태가 된다.
위에서 제어 정보라고 잠시 언급했던 내용에 대한 세부적인 이야기다.
통신 과정에서 어떤 정보가 필요한지 검토하는 내용을 TCP 프로토콜의 사양으로 규정하고있다.
TCP 헤더의 포멧에는 다음과 같은 정보들이 담겨있다.
아크
라고 읽는다.접속 및 연결 종료 요청에 대해서는 실제 데이터는 없고 위와 같은 TCP 헤더 정보만 담겨져있다.
정리하자면 데이터를 저장한 패킷은 소켓에 기록된 정보(이더넷, IP) + TCP에 대한 제어정보 + 데이터 조각으로 구성되어있고, 이와 별개로 접속, 접속종료 시 사용되는 제어정보만 있는 패킷도 존재한다.
이제 접속 과정에서 사용되는 개념에 대해 얼추 이해했으니 접속 흐름을 따라가보자.
접속은 다음과 같이 브라우저에서 connect 함수를 수행하는 부분부터 시작된다.
connect(디스크립터, 서버측 IP와 포트, 등등…)
이후 프로토콜 스택은 서버와 TCP 제어정보를 주고받는다.
이 때 헤더에는 위에서 설명한 제어필드와 같이 많은 정보를 담고 있지만 가장 신경써야할 부분은 송신처와 수신처의 포트번호다. 이 정보를 통해 클라이언트와 서버의 소켓을 지정하고 SYN 컨트롤비트를 1로 만들어 접속을 완료한다.
위 과정을 거쳐 TCP 헤더를 만들고 IP를 담당하는 부분에 건네줘서 송신 동작을 수행하도록 요청한다.
그러면 IP 담당부분이 패킷 송신 동작을 실행하고, 네트워크를 통해 패킷이 서버에 도착한다.
이 때 서버측의 IP 담당부분이 이를 받아 TCP 담당부분에 건네준다.
서버측의 TCP 담당 부분이 TCP 헤더에 작성된 정보 중 수신포트에 해당하는 정보를 가지고 소켓을 찾는다. 소켓을 찾은 뒤 소켓에 필요한 정보를 기록하고 접속 상태가 진행중인 상태가 된다.
이 과정이 끝나면 서버의 TCP 담당부분은 응답을 반환한다.
서버가 응답을 반환하는 과정에서 패킷을 정상적으로 수신했다는 것을 알리기 위한 작업으로 TCP 헤더를 만든다.
클라이언트와 비슷하게 송신처와 수신처의 포트번호, SYN 비트 등을 설정한 TCP 헤더를 만드는데 ACK 컨트롤 비트를 1로 만든다는 차이가 존재한다.
해당 컨트롤 비트는 서버측이 패킷을 정상적으로 수신했음을 확인하는 비트다.
위와 이어지는 맥락으로 클라이언트에서 서버로 패킷을 전송할 때는 ACK 비트가 0이다.
위 과정을 통해 패킷이 클라이언트에게 돌아오고 IP 담당 부분을 거쳐 TCP 담당 부분에 도착한다.
이 때 TCP 헤더를 확인하여 SYN 컨트롤 비트가 1인지 (접속이 성공했는지) 확인하여 소켓에 접속 완료를 나타내는 제어정보를 기록한다.
클라이언트는 ACK 컨트롤 비트에 대한 작업을 추가로 수행해야한다.
패킷이 클라이언트에 잘 도착했음을 서버에 알리기 위해 ACK 비트가 1인 TCP 헤더를 서버로 반송한다. 이 정보가 서버에 도착하면 접속 동작이 끝난다.
위와 같은 과정을 거쳐 소켓은 데이터를 송, 수신 할 수 있는 상태가 된다.
우리는 이렇게 송, 수신할 수 있는 상태를 어떤 통로로 연결되었다고 이해할 수 있다.
이 통로를 커넥션이라고 한다. (흔히 말하는 세션도 해당 개념과 대체로 같은 의미로 사용된다.)
close 함수를 호출하여 연결을 끊을 때 까지 커넥션은 계속 존재한다.
커넥션이 이루어지면서 프로토콜 스택의 접속 동작이 끝났다. 지금부터 우리는 애플리케이션을 제어할 수 있다!! (먼길을 왔다…)
머나먼 길을 거쳐 connect 동작을 마친 우리는 애플리케이션(브라우저)에 제어권이 되돌아왔다.
애플리케이션이 write 함수를 호출하여 송신 데이터를 프로토콜 스택에 건네준다.
이 데이터를 받은 프로토콜 스택은 송신 동작을 실행하는 순서로 데이터 송신 흐름이 진행된다.
이 때 프로토콜 스택은 데이터 내용이 무엇인지 알지 못한다.
write 함수를 호출할 때 송신 데이터의 길이를 지정하는데 이 과정에서 프로토콜 스택은 해당 길이만큼의 이진 데이터가 1Byte씩 나열되어있다고만 인식한다.
프로토콜 스택은 받은 데이터를 즉시 송신하지 않는다.
일단 내부의 송신용 버퍼 메모리 영역에 데이터를 저장하고 애플리케이션이 다음 데이터를 건네주기를 기다린다.
바로 데이터를 보내면 되는데 프로토콜 스택은 왜 굳이 애플리케이션의 다음 데이터를 기다릴까?에 대한 의문점이 들 수 있다.
애플리케이션에서 건네주는 데이터의 길이는 애플리케이션의 구현 방식에 따라 결정되기 때문에 프로토콜 스택에서 해당 길이를 결정할 수 없다.
애플리케이션이 데이터를 한꺼번에 보낼 수도 있고, 1바이트씩 혹은 1행씩 나눠서 송신 요청을 하는 경우도 있다.
만약 애플리케이션이 작은 단위의 데이터를 여러개 보내는데 프로토콜 스택이 받는 족족 패킷을 보내버린다면 작은 단위의 패킷을 무수히 많이 보내버리는 상황이 일어날 수 있다. 이 경우 네트워크의 사용 효율이 저하되므로 프로토콜 스택은 버퍼 메모리 영역에 데이터를 저장했다가 송, 수신 동작을 수행한다.
얼마나 저장하고 송신 동작을 하냐에 대한 기준은 OS의 종류나 버전에 따라 달라지므로 간략하게 판단할수는 없지만, 통상적으로 판단하는 요소는 다음과 같다.
프로토콜 스택은 MTU(Maximum Transmission Unit)라는 매개변수를 바탕으로 송신 여부를 판단한다.
MTU는 하나의 패킷이 운반할 수 있는 디지털 데이터의 최대 길이를 의미한다.
이더넷에서는 보통 1,500 바이트가 된다.
MTU에서 패킷의 맨 앞부분에 헤더가 포함되어있으므로 헤더를 제외한 것이 하나의 패킷으로 운반할 수 있는 데이터의 최대 길이가 된다.
이를 MSS(Maximum Segment Size)라고 한다.
프로토콜 스택은 데이터의 사이즈 이외에도 타이밍을 기준으로 데이터를 전송하기도 한다.
애플리케이션의 송신 속도가 느려지는 경우 MSS의 용량이 채워질때 까지 기다려주기엔 송신 동작이 지연될 수 있다.
프로토콜 스택은 내부에 타이머가 있어서 이 타이머를 기준으로 일정 시간 이상 경과하면 패킷을 송신한다. (해당 시간은 보통 밀리초 단위의 시간이다)
프로토콜 스택이 버퍼에 쌓인 데이터를 송, 수신하기 위해 판단하는 요소로 데이터의 길이, 타이머 두가지 판단요소를 알아봤다.
데이터의 길이를 중시하면 네트워크의 이용 효율은 높아지나 송신 동작이 지연될 우려가 있다.
시간(타이머)을 중시한다면 송신 동작의 지연은 적어지나 네트워크 이용 효율이 떨어질 수 있다.
따라서 위 두 요소를 절충하여 적당한 시간을 가늠하여 송신 동작을 수행해야한다.
아쉽게도 TCP 프로토콜 사양에는 해당 절충안에 대한 기준이 없다. 어떤 기준으로 판단할지는 프로토콜 스택을 만드는 개발자가 정해야한다.
이러한 이유 때문에 OS의 종류나 버전에 따라 관련 동작이 달라진다.
이처럼 프로토콜 스택에만 데이터 송, 수신을 판단한다면 동일한 애플리케이션이라도 OS별로 소켓 송신에 대한 규칙이 다르게 동작한다는 상황이 발생할 수 밖에 없다.
위와 같은 방식 때문에 애플리케이션 측에서 송신 타이밍을 제어할 수 있는 방법도 제공하고있다.
버퍼에 머물지 않고 즉시 송신하는 옵션을 추가할 경우 프로토콜 스택은 버퍼에 값을 머물게 하지 않고 즉시 데이터 송신을 수행한다.
브라우저와 같은 대화형 애플리케이션이 서버에 메시지를 보낸다면 응답 지연을 막기 위해 위와 같은 옵션을 사용할 수 있을 것이다.
보통 HTTP 리퀘스트 메시지는 그렇게 길지 않기 때문에 하나의 패킷에 들어간다.
하지만 블로그 글을 작성하는 것과 같이 폼을 사용하여 긴 데이터를 보내는 경우 데이터가 하나의 패킷에 들어가지 못할 수 도 있다.
따라서 송신 버퍼에 들어있는 데이터를 앞에서부터 차례대로 MSS의 크기에 맞게 분할하고, 분할한 데이터 조각을 하나씩 패킷에 넣어서 송신한다.
이 때 분할한 데이터 조각 맨 앞부분에 TCP 헤더를 추가한다.
이후 소켓 정보를 기반으로 송, 수신처 포트번호 등 필요한 항목을 기록하고 IP 담당 부분에 전달하여 패킷을 송신한다.
초기에 송신 버퍼에 저장된 데이터는 MSS의 길이를 초과하기 때문에 다음 데이터를 기다릴 필요 없이 즉시 송신한다.
데이터를 입력한 패킷이 서버로 송신되는데 TCP에서는 송신한 패킷이 올바르게 도착했는지 확인하고 도착하지 않았다면 다시 송신하는 기능이 있다.
위와 같은 이유로 패킷 송신 후 확인 동작으로 이어진다.
패킷에는 헤더가있고 위에서 설명했듯이 패킷이 쪼개져서 전달되는 경우 어떤 데이터가 누락되었는지 알아야한다.
데이터를 조각으로 분할할 때 통신 시작부분부터 몇번째 바이트에 해당하는지 계산한다. 이후 데이터 조각을 송신할 때 계산한 값을 TCP 헤더에 기록하는데 이 값을 시퀀스 번호라고한다.
송신하는 데이터의 크기같은 경우 수신측에서 패킷 전체의 길이에서 헤더의 길이를 빼면 데이터 크기를 계산할 수 있기 때문에 수신측에 별도로 데이터의 길이를 알려줄 필요가 없다.
수신측에서는 시퀀스 번호와 데이터의 길이를 이용하여 송신된 데이터가 몇 번째 바이트부터 시작되는 값인지 알 수 있다.
송신측에서 “1번째 바이트부터 시작되는 데이터를 1460Byte만큼 보냅니다!”라고 보냈다면 수신측은 “1460번째 바이트까지 수신했습니다!” 라고 응답하는 과정이다.
다시 말해 송신측에서 말하는 1번째 바이트부터 시작되는 데이터
가 시퀀스 데이터, 1460Byte만큼
에 대한 정보는 수신측이 패킷크기 - 헤더크기 로 계산하는 값이다.
수신측이 응답하는 1460번째 바이트까지 수신했습니다
에 대한 정보가 ACK 번호다.
실제로 시퀀스 번호는 1로 시작하지 않고 난수를 바탕으로 산출한 초기값으로 시작한다. 1로 시작하면 악의적인 공격에 노출될 수 있기 때문이다.
시퀀스 번호를 난수로 초기값을 결정하기 위해서는 수신측이 초기값을 알아야하는데, 위에서 이야기했던 접속 동작부분에서의 SYN 컨트롤 비트값이 바로 시퀀스 번호 값을 나타낸다.
시퀀스 번호와 ACK 번호를 사용한 데이터 송, 수신 확인 방법을 알아봤다. 하지만 위 방법은 클라이언트에서 서버측으로 데이터를 보내는 상황만 고려했기 때문에 반대로 서버에서 클라이언트로 데이터를 전송하는 과정에 대해서는 고려하지 못하고있다.
하지만 이 과정은 방향만 역전시켜서 생각하면 편하다.
송신 측을 서버, 수신 측을 클라이언트로 생각하고 동일하게 동작한다고 보면 된다.
위와 같이 패킷이 정상적으로 도착했는지 확인하는 방식 덕분에 네트워크 통신 상에서 오류가 발생하더라도 그 상황에 대해 회복 처리(패킷 재전송)를 할 수 있기 때문에 다른 곳에서 오류를 회복할 필요가 없다.
시퀀스 번호와 ACK 번호를 통해 패킷이 수신측에 도착했다는 것을 확인하는 과정이 있기 때문에 TCP 자체적으로 오류검출 및 회복조치가 가능하다.
이 때문에 LAN 어댑터, 버퍼, 라우터에서 별도의 회복 조치를 취하지 않는다.
TCP 잘만들었네..
실제 오류 검출과 회복은 꽤나 복잡한 구조를 가지고있다.
송신 후 ACK 번호가 돌아오는것을 기다리는 시간을 타임아웃 값이라고 한다.
네트워크 상태가 좋지 않은 경우 정상 송신 후 ACK 번호가 반송되지 않아 재요청을 보는 상황이 있을 수 있다. ACK가 반송되지 않는 상황에서 패킷을 다시 보내게 되면 혼잡도 또한 많이 증가할 것이다.
때문에 대기시간을 적절한 값으로 설정해야한다.
그런데 서버의 거리, 네트워크의 상태 등등 환경적인 요인에 따라 대기시간이 천차 만별이 될 것 같은데 어떻게 대기시간을 정할까?
TCP는 ACK 번호가 돌아오는 시간을 기준으로 대기시간을 판단한다. 구체적으로 이야기하면 데이터 송신 동작을 실행하고 있을 때 항상 ACK 번호가 돌아오는 시간을 계측해두고있다가 이에 대한 값을 참조하여 대기 시간을 정한다.
ACK 반환시간이 지연되면 대기시간이 늘어나고 ACK 반환시간이 짧으면 대기시간도 짧아진다.
하나의 패킷을 보내고 ACK 번호를 기다리는 방법은 단순하지만 ACK 번호가 돌아올 때 까지의 시간이 낭비된다는 단점이 존재한다.
TCP는 이러한 낭비를 줄이기 위해 윈도우 제어 방식을 사용한다.
윈도우 제어방식은 한개의 패킷을 보낸 후 ACK 번호를 기다리지 않고 여러개의 패킷을 보내는 방법이다.
이러한 윈도우 제어 방식은 수신측의 용량을 고려하지 않기 때문에 패킷을 수신하는 측이 감당하기 어려운 양의 패킷을 보내게 될 수 도 있다.
위와 같은 상황은 패킷을 수신하는 쪽에서 송신측에게 수용 가능한 패킷의 양을 미리 통지하는 방식으로 해결할 수 있다.
TCP 헤더의 윈도우 필드를 이용해 수신측에서 송신측에게 수신할 수 있는 데이터 양을 알려준다.
수신 가능한 데이터 양의 최대값을 윈도우 사이즈라고 한다.
2장은 크게 소켓의 생성부터 소멸까지 내용을 다루고있다.
이번주에 소켓의 소멸까지 읽었지만 내용을 모두 정리하기에는 시간적으로 부족했다..ㅎㅎ
여전히 쉽게 설명하기 위해 문장을 해석하는데 많은 시간이 들고있다 😅
그리고 이 책 그림 재탕이 너무 심하다…
책을 넘기다가 3~4페이지 전에있는 그림 2-3이 갑자기 튀어나오면 다시 돌아가서 보고 와야하는 부분이 접근성을 너무 떨어지게 만든다
무차별 그림공격은 소켓의 소멸 이후 정리하는 부분에서 그 진가를 발휘한다
책을 오려서 그림만 따로 보고싶을지경이다...
그림 진짜 가위로 오려버릴까 🤔
다음은
2-3. 소켓의 데이터 송, 수신
내용 중ACK 번호와 윈도우
부터 정리해볼 계획이다.