Skip to content

Latest commit

 

History

History
250 lines (148 loc) · 17.2 KB

File metadata and controls

250 lines (148 loc) · 17.2 KB

TCP/IP 의 데이터를 전송

애플리케이션에서 서버에 데이터를 보낼 때 어떻게 데이터를 전송하게 될까요? 해당 과정을 애플리케이션 레이어, TCP레이어, IP 레이어 측면에서 알아보도록 하겠습니다.

애플리케이션(ex. 브라우저)에서 의뢰받은 프로토콜 스택이 TCP/UDP 프로토콜을 이용해 메시지를 전송할 때 가장 먼저 소켓을 만들게 됩니다.

프로토콜 스택

먼저 프로토콜 스택이 무엇인지 간략하게 알고 넘어가겠습니다.

image

  • 네트워크 애플리케이션
    • 애플리케이션 레이어에서 HTTP 리퀘스트 메시지를 만들고 Socket 라이브러리를 이용해 DNS 서버에 조회하고 커넥션을 수립합니다.
  • 프로토콜 스택
    • TCP/UDP 프로토콜을 사용해 액세스 대상과 데이터를 주고받습니다.
    • IP 프로토콜을 이용해 패킷의 송수신을 제어합니다.
  • LAN 드라이버
    • LAN 어댑터의 하드웨어를 제어합니다.
  • LAN 어댑터
    • 실제 송수신 동작, 즉 케이블에 대해 신호를 송수신하는 동작을 수행합니다.

프로토콜 스택은 소켓의 정보(ex. 상대측의 IP주소, 포트번호)를 가지고 수신측과 데이터를 주고 받습니다. 또한 데이터가 오지 않을 때를 대비해 소켓에 기록된 데이터 응답 여부와 송신 동작 후의 경과시간을 확인합니다.

socket을 이용한 통신

TCP 프로토콜을 사용해 액세스 대상과 통신하는 과정을 알아보겠습니다.

  • 소켓을 생성
    • socket() 메서드를 호출해 소켓을 생성합니다.
    • 프로토콜 스택은 소켓 한 개 분량의 메모리 공간을 확보합니다.
    • 소켓을 식별할 수 있는 디스크립터를 애플리케이션에 알려줍니다.
  • 액세스 대상과 접속
    • connect() 메서드를 호출해 액세스 대상과 접속합니다.
  • 데이터 송수신
    • write(), read() 메서드를 통해 데이터를 주고 받습니다.
  • 연결 끊기
    • close() 메서드를 호출해 연결을 끊고 소켓을 말소합니다.

액세스 대상과 접속

접속동작을 수행할 때 크게 아래 두 가지 작업을 수행하게 됩니다.

  • 통신 상대와의 사이에 제어 정보를 주고받아 소켓에 필요한 정보를 기록하고 데이터 송수신이 가능한 상태로 만드는 것
  • 송수신 데이터를 일시적으로 저장하기 위한 메모리 영역 확보
    • 해당 메모리 영역을 버퍼 메모리라고 부릅니다.

클라이언트와 서버가 데이터를 주고 받기 위한 제어 정보는 아래와 같이 TCP 프로토콜 사양으로 규정하고 있습니다.

image

이 항목들은 고정되어 있어서 클라이언트와 서버가 통신할 때마다 이 제어 정보를 부가하게 됩니다. 이 제어 정보를 TCP 헤더라고 부릅니다.

제어 정보는 소켓에 기록해 프로토콜 스택을 제어하기 위한 정보가 더 있습니다. 여기에는 애플리케이션에서 통지된 정보, 상대측으로부터 받은 정보가 수시로 기록됩니다.

그러면 connect()를 호출하는 곳부터 동작이 어떻게 진행되는지 따라가보겠습니다.

connect(<디스크립터>, <서버측의 IP주소와 포트번호>, ...)

해당 메서드를 호출하면 프로토콜 스택의 TCP 담당부분에 서버측의 IP주소와 포트번호가 전달됩니다. 그러면 전달받은 상대측과 제어 정보를 주고 받습니다. 이 대화는 아래 단계를 밟습니다.

  • 먼저 데이터 송수진 동작의 개시를 나타내는 제어 정보를 기록한 헤더를 만듭니다.
    • 컨트롤 비트SYN이라는 비트를 1로 만듭니다.
  • 만든 헤더를 IP 담당부분에 건네주어 송신을 의뢰합니다.
  • IP 담당부분은 패킷 송신 동작을 수행하고 네트워크를 통해 서버의 IP 담당 부분에 도착하게 됩니다.
  • 서버 측의 TCP 담당 부분이 수신처 포트번호에 해당하는 소켓을 찾아 동작을 수행하고 응답을 돌려줍니다.
  • 클라이언트측과 마찬가지로 송신처와 수신처의 포트번호, SYN 비트를 설정한 TCP 헤더를 생성합니다.
    • 패킷이 정상 도착했는지 알려주기 위한 ACK 비트를 1로 만듭니다.
  • 다시 클라이언트에게 패킷이 돌아오고 IP 담당부분을 경유해 TCP 담당부분에 도착하게 됩니다.
    • 이때 TCP 헤더를 조사해 접속 동작이 성공했는지 확인합니다.
    • SYN가 1이면 접속 성공이므로 소켓에 접속 완료를 나타내는 제어 정보를 기록합니다.
  • 마지막으로 클라이언트측에서 패킷이 정상도착했다는 것을 알리기 위해 ACK 비트를 1로 만든 TCP 헤더를 반송합니다.

데이터를 송수신

프로토콜 스택은 받은 HTTP 리퀘스트 메시지의 내용을 내부에 있는 송신용 버퍼 메모리에 저장하고 애플리케이션이 다음 데이터를 건네주기를 기다립니다.

물론 받은 데이터를 바로 전송할 수 있겠지만, 이는 네트워크의 이용 효율이 저하되기 때문에 어느 정도 데이터를 저장하고 난 후 데이터를 전송하게 됩니다.

그러면 “어느 정도까지” 데이터를 저장할까요? 이를 결정하는 요소 중 하나는 한 패킷에 저장할 수 있는 데이터의 크기입니다.

어느 정도까지 데이터를 저장? : 데이터의 크기

  1. MTU (Maximum Transmission Unit)
    • 패킷 한 개로 운반할 수 있는 디지털 데이터의 최대 길이.
  2. MSS (Maximum Segment Size)
    • 헤더를 제외하고 한 개의 패킷으로 운반할 수 있는 TCP 데이터의 최대 길이

MTU는 한 패킷으로 운반될 수 있는 데이터의 최대 길이가 됩니다. 이더넷에서는 보통 1500 바이트가 됩니다. 이 MTU에는 패킷의 맨 앞부분에 헤더가 포함되어 있는데, 이를 제외한 것이 하나의 패킷으로 운반할 수 있는 최대 데이터의 길이가 되고 이를 MSS라고 합니다.

image

나머지 요소는 타이밍입니다.

어느 정도까지 데이터를 저장? : 타이밍

애플리케이션의 송신 속도가 느려 MSS에 가깝게 데이터를 저장하게 되면 여기에서 시간이 거려 송신 동작이 지연되게 됩니다. 따라서 적당한 타이밍에 데이터를 전송해야 합니다. 프로토콜 스택 내부에는 타이머가 존재하기 때문에 해당 시간이 경과하면 데이터를 전송하게 됩니다.

💡 데이터가 클 때는 분할!

HTTP 리퀘스트 메시지의 크기는 그렇게 길지 않지만 긴 글을 작성할 때는 데이터의 크기가 MSS를 초과하므로 다음 데이터를 기다릴 필요 없이 데이터를 MSS 크기에 맞게 분할해 패킷에 넣어 전송합니다.

데이터 전송의 신뢰성

데이터를 입력한 패킷이 상대에게 올바르게 도착했는지 확인하기 위해 시퀀스 번호라는 항목을 이용합니다. TCP 프로토콜에서 데이터를 조각으로 분할할 때 해당 조각이 통신 시작 시점부터 몇 번째 바이트에 해당하는지 세어두는데 이를 시퀀스 번호라고 합니다.

image

  1. 시퀀스 번호 : 1, 크기 : 669 바이트
    • 수신측은 669번째 바이트까지 수신 완료한 상태에서 몇 번째 바이트까지 읽었는지 ACK 번호에 기록한다.
  2. 시퀀스 번호 : 670, 크기 : 1460 바이트
    • ACK : 2130
  3. 시퀀스 번호 : 2130, 크기 : 1460 바이트
    • ACK : 3590

흐름제어

수신측과 송신측의 속도차이가 있다면 문제가 생길 수 있습니다.

수신측의 버퍼를 초과한 데이터가 손실될 수 있으며 만약 손실 된다면 불필요한 응답과 데이터 전송 송/수신측간에 빈번이 발생한다.

이를 해결하기 위해 아래와 같은 방법을 사용할 수 있습니다.

  • Stop and Wait
  • Sliding Window (Go Back N ARQ)

Stop and Wait

Stop and Wait는 매번 전송한 패킷에 대한 응답을 받아야지만 다음 패킷을 전송하는 방법입니다.

image

Sliding Window (Go Back N ARQ)

위의 Stop and Wait 방식은 ACK 번호가 돌아올 때까지 아무 일도 하지 않기 때문에 시간이 낭비됩니다. 이를 해결하기 위해 슬라이딩 윈도우 방식을 이용합니다.

수신측에서 설정한 윈도우 크기만큼 송신측에서 확인 응답없이 패킷을 전송할 수 있게 해서 데이터 흐름을 동적으로 제어하는 기법입니다.

먼저 수신측에서 송신측에 수신 가능한 데이터의 양을 통지하고 수신측은 이 양을 초과하지 않도록 송신 동작을 수행합니다. 수신 처리가 끝나고 수신 버퍼에 빈 부분이 생기면 그 분량만큼 수신할 수 있는 데이터의 양을 늘리므로 TCP 헤더의 윈도우 필드에 이것을 송신측에 알립니다.

image

연결 끊기

TCP 프로토콜에서 연결을 종료할 때 특별한 과정을 거쳐서 연결을 종료해야 합니다.

그냥 연결을 끊어버리면 연결이 끊기기 전에 남아있는 데이터를 전송할 수 없기 때문에 양쪽 모두 연결을 종료할 준비가 되었는지 확인합니다.

이 과정을 4-way handshake라고 합니다.

3-way handshake보다 4-way handshake를 신경써야 합니다. 이는 연결이 정상적으로 종료되지 않으면 연결이 그대로 남아있게 되기 때문입니다. 또한 상대방이 응답을 줄 때까지 대기하는 과정이 포함되어 있어 잘못하면 데드락이 발생할 수 있습니다.

물론 일정시간이상 응답이 없으면 연결을 강제 종료할 수 있겠지만 그 시간동안 메모리와 포트를 점유하고 있어 병목발생 가능성이 존재합니다.

image

FIN_WAIT-1

먼저 연결을 종료하고자 하는 요청자가 FIN 패킷을 상대방에게 보내면서 FIN_WAIT-1 상태로 들어가게됩니다. 이때 FIN 플래그를 1로 설정하고 보내게 됩니다.

💡 Half-close 요청자가 FIN 플래그만 포함된 패킷을 보내는 것이 아니라 FIN + ACK 패킷을 보내는 경우도 존재합니다. 이는 `Half-Close` 기법을 사용하고 있기 때문입니다. Half-Close를 사용하면 요청자가 처음 보내는 FIN 패킷에 승인 번호를 함께 담아 보내는데 이는 연결은 종료하지만 남은 데이터가 있다면 마저 보내라는 의미가 됩니다.

즉, 수신스트림과 전송스트림 중 하나를 우선 닫겠다는 의미가 됩니다.

이후 수신자는 미처 보내지 못한 데이터를 보내고 요청자는 아직 살아있는 수신 스트림을 이용해 데이터를 처리한 후 ACK 패킷을 응답으로 보낼 수 있게 됩니다. 이후 수신자가 다시 모든 요청을 처리하였다는 FIN 패킷을 보내게 됩니다.

CLOSE_WAIT

요청자로부터 FIN 패킷을 받으면 수신자는 요청자가 보낸 시퀀스 번호 + 1값으로 ACK를 보내게 되면서 CLOSE_WAIT 상태로 들어가게 됩니다. 이때 수신자는 전송할 데이터가 남아있다면 계속 전송한 후, 모든 전송이 끝나야지 다음 단계로 넘어갈 수 있게 됩니다.

요청자는 언제 수신자의 데이터 처리가 끝날지 모르기 때문에 연결 종료를 승인하는 FIN 패킷을 보내줄 때까지 대기해야 하는 문제가 있습니다.

이 상태를 수동 폐쇄(Passive Close)라고 합니다.

FIN_WAIT-2

요청자는 수신자로부터 ACK를 받고 자신이 보냈던 시퀀스번호와의 차가 1이 맞는지 확인합니다. 하지만 이때 수신자의 데이터 전송이 완료되지 않을 수도 있기에 FIN_WAIT-2 상태로 들어가서 수신자가 연결을 종료하는 것을 허락하는 FIN 패킷을 보내줄 때까지 기다립니다.

이때 무한정 대기하는 것이 아니라 커널 파라미터로 타임아웃이 정해져 있는 경우 일정 시간이 초과하면 자동으로 다음 단계로 넘어가게 됩니다.

LAST_ACK

수신자는 자신이 처리할 데이터가 없다면 연결을 종료하는 함수를 명시적으로 호출합니다. 이때 요청자의 연결 종료 요청을 승낙한다는 의미로 FIN 패킷을 보내게 됩니다.

이후 수신자는 LAST_ACK 상태로 들어가게 되고 다시 승인번호를 줄 때까지 대기합니다.

TIME_WAIT

수신자가 보낸 FIN 패킷을 받은 요청자는 다시 수신자가 보낸 시퀀스 번호 + 1 로 승인번호를 생성해 ACK 패킷으로 응답합니다. 이때 요청자는 TIME_WAIT 상태로 들어가게 됩니다.

이때 TIME_WAIT의 역할은 의도하지 않은 에러로 인해 연결이 데드락에 빠지는 것을 방지합니다.

TIME_WAIT에서 대기하는 시간은 2 MSL(Maximum Segment Lifetime) 으로 정의되어 있습니다. 정확한 값은 커널 파라미터로 정의되어 있습니다.

이는 TIME_WAIT 상태에서 어느정도 대기할 것인지 나타내는 값입니다.

CLOSED(수신자)

요청자가 보낸 ACK 패킷을 받은 수신자는 CLOSED 상태로 들어가며 연결을 완전히 종료합니다.

CLOSED(요청자)

TIME_WAIT 상태에서 2 MSL 만큼 시간이 지나면 요청자도 CLOSED 상태로 전환됩니다.

IP와 이더넷의 패킷 송수신

image

패킷 송수신 동작의 개요

패킷 송수신 동작의 출발은 TCP 담당부분이 IP 담당 부분에 패킷 송신을 의뢰하는 것부터 시작합니다. 의뢰 동작을 수행할 때 TCP 담당부분은 데이터 조각에 TCP 헤더 를 부가한 것을 IP 담당부분에 건네 줍니다.

이 의뢰를 받은 IP 담당부분은 내용물을 한 덩어리의 디지털 데이터로 간주하고 그 앞에 IP 헤더MAC 헤더 를 부가합니다.

💡 IP 헤더와 MAC 헤더 MAC 헤더 : 이더넷용 헤더, MAC 주소를 사용합니다. IP 헤더 : IP용 헤더, IP 주소를 사용합니다.

이렇게 만든 패킷을 네트워크용 하드웨어에게 전달합니다. (여기서 하드웨어는 이더넷이나 무선 LAN등을 말합니다.)

IP 헤더와 MAC 헤더 생성

IP 담당부분에서 IP 헤더와 MAC 헤더를 생성합니다.

IP 헤더에는 패킷을 어디로 전달할지 나타내는 수신처 IP 주소와 송신처 IP 주소를 넣습니다. 그렇게 패킷을 만들고 건네줄 상대를 결정해야 하는데 이는 라우팅 테이블을 이용하여 다음 라우터를 결정합니다.

IP 헤더를 만들었으면 IP 담당부분 앞에 MAC 헤더를 붙입니다. 이더넷에서는 TCP/IP 개념이 통용되지 않고 다른 구조로 패킷의 수신처를 판단합니다. 여기서 이더넷의 수신처 판단 구조로 사용하는 것이 MAC 헤더 입니다.

MAC 헤더의 맨 앞에 있는 수신처 MAC 주소와 송신처 MAC 주소는 각각 패킷을 전달하는 상대와 패킷을 송신한 송신처의 MAC 주소를 나타냅니다. IP 주소와 같은 역할을 한다고 생각하면 됩니다. 다른 점은 IP 주소는 32 비트이지만 MAC 주소는 48비트라는 점과 MAC 주소는 48비트를 한 개의 값으로 생각한다는 점입니다.

송신처의 MAC 주소를 결정하는 것은 LAN 어댑터에 할당된 MAC 주소로 설정하면 되지만 수신처 MAC 주소 는 다소 복잡합니다. 패킷을 건네주는 상대의 MAC 주소를 설정해 이더넷에 의뢰한 후 상대에게 패킷이 전달되므로 여기에는 패킷을 건네주는 상대의 MAC 주소를 기록해야 합니다. 그러나 이때 누구에게 패킷을 건네주어야 할 지 모르기 때문에 IP 주소에서 MAC 주소를 조사하는 동작을 수행합니다.

ARP로 수신처 라우터의 MAC 주소를 조사

수신처의 MAC 주소를 조사하기 위해 사용되는 것이 ARP(Address Resolution Protocol)입니다. 이더넷에는 연결되어 있는 전원에게 패킷을 전달하는 브로드캐스트 구조가 있습니다.

상대가 자신과 같은 네트워크에 존재하면 이와 같은 구조로 MAC 주소를 알 수 있습니다. 그런데 패킷을 보낼때 마다 이 동작을 수행하면 패킷이 불어나기 때문에 ARP 캐시 라는 메모리 영역에 이를 보존해 다시 이용합니다.

ARP 캐시를 이용해 ARP의 패킷을 줄일 수 있지만 ARP의 캐시 내용과 실제가 일치하지 않는 문제가 발생할 수도 있습니다. 이를 막기 위해 ARP 캐시에 저장된 값은 시간이 되면 삭제하게 되어 있습니다.