kakasoo

[Network] TCP/IP의 데이터를 전기 신호로 만들어 보낸다 (2) 본문

프로그래밍/네트워크

[Network] TCP/IP의 데이터를 전기 신호로 만들어 보낸다 (2)

카카수(kakasoo) 2021. 1. 31. 17:20
반응형

이 글은 성공과 실패를 결정하는 1%의 네트워크 원리를 읽고, 스터디한 결과를 토대로 작성했다.


 

03. 데이터를 송수신한다

1. 프로토콜 스택에 HTTP 리퀘스트 메시지를 넘긴다

socket connect 이후 write를 호출하여 데이터 송수신 단계로 이행한다. 프로토콜 스택은 데이터를 바이너리 데이터가 1바이트 단위로 이어져 있다는 것만 알 뿐 데이터의 내용은 알지 못한다.

프로토콜 스택은 일단 데이터를 버퍼 메모리 영역에 저장한다. 그 이유는,

  • 데이터의 길이는 애플리케이션의 종류나 만드는 방법에 따라 결정 ( 데이터를 한 번에 보내는지, 몇 분할을 해야 하는지, 애플리케이션에서 결정 ) 하기 때문이다.
    • 만약 데이터 저장 없이 바로 보낸다고 한다면 만약을 대비하여 짧은 길이로 나누어 보내야 하는데, 이는 데이터 전송 효율이 낮다.

데이터를 송수신할 때에는 OS나 버전에 따라 달라지는데, 다음과 같은 요소를 바탕으로 한다.

  • 한 패킷에 저장할 수 있는 데이터의 크기
    • MTU : 한 패킷으로 운반할 수 있는 디지털 데이터의 최대 길이 ( 이더넷 기준 1500바이트 )
      • MSS : MTU에서 IP 헤더와 TCP 헤더를 제외한 데이터의 길이
  • 타이밍
    • MSS에 가깝게 ( 데이터 사이즈에 최대한 꽉 차게 ) 데이터를 저장하면 송신 동작이 지연되기 때문에 버퍼에 어느 정도 데이터가 쌓이면 바로 전송하도록 해야 한다.
    • 이것을 위해 프로토콜 스택 내부에는 타이머가 있다.

이렇게, 공간적인 것과 타이밍 두 가지가 속도에 영향을 주지만, 서로 상반된 면도 있다. 전자를 중시하면 패킷 길이가 길어져서 효율적일 수 있지만, 버퍼에 머무는 시간이 길어져 비효율적일 수도 있다.

2. 데이터가 클 때는 분할하여 보낸다

HTTP 리퀘스트 메시지는 보통 길지 않으므로 한 개의 패킷에 들어가지만 폼을 이용하여 긴 데이터를 보낼 경우에는 한 개의 패킷에 들어가지 않을 수도 있다. 블로그나 게시판에 글 쓰는 경우가 그러하다.

이 경우에는 버퍼에 저장될 데이터의 길이가 MSS의 길이를 초과하므로 다음 데이터를 기다릴 필요가 없다. 따라서 송신 버퍼에 들어간 데이터를 맨 앞부터 차례대로 MSS 크기에 맞게 분할한다.

이렇게 분할한 데이터들의 맨앞에 TCP 헤더를 추가하여 전송한다. 헤더에는 양측 포트번호와 같이 필요한 항목을 기록한다. 이후 IP 담당 부분에 건네주면 송신 동작을 시작한다.

요점 정리

⇒ 택배를 보내는 것으로 설명한다면, 화물의 양이 많을 경우 택배를 나눠서 보내는 것이 유리하다. 다만, 너무 잘게 나누면 전체 도착 시간이 오히려 느려질 수도 있다. 따라서 포장 사이즈를 잘 고민해야 한다...

⇒ TCP/UDP가 이런 택배 포장 과정이면, TCP는 그중에서도 지하철이나 버스처럼 지점이 명확한 경우이며, UDP는 택시처럼 보내는 사람이 지점을 정할 수 있는 경우라고 생각하면 될 것으로 보인다.

⇒ IP는 실제로 보내는 부분이다. TCP/UDP가 운송 방법이라면 IP는 실제 화물을 보내는 것에 가깝다.

3. ACK 번호를 사용하여 패킷이 도착하였는지 확인한다

TCP에는 송신한 패킷이 올바르게 도착하였는지 확인하고, 그렇지 않은 경우에 다시 보내는 기능이 있다.

TCP는 전송 시에 각 패킷의 시작 지점이 몇번째 바이트인지를 세어 기록하는데, 이를 시퀀스 번호라고 한다. 이 시퀀스 번호와 데이터의 길이를 같이 보낸다. ACK는 전체 데이터 길이에 1을 더한 값이다.

보충 설명

  • 1번 데이터
    • 시퀀스 번호 : 1, 크기 : 1,460바이트, ACK 번호 : 1,461
  • 2번 데이터
    • 시퀀스 번호 : 1461, 크기 1,460바이트, ACK 번호 : 2,921

보다시피, ACK 번호는 다음 데이터 전송 시의 시퀀스 번호와 같다. 받는 측에서는 데이터를 차곡차곡 받아 더한 값에 1을 더해 ACK 값을 구해놓으면, 상대방의 시퀀스 번호와 맞춰볼 수 있는 셈이다.

이를 통해 상대방이 누락한 정보가 있는지 확인할 수 있다.

누락한 게 없으면 현재까지 수신한 데이터가 어느 정도 크기인지를, TCP 헤더의 ACK 번호에 기록하여, 송신 측에게 다시 알려준다.

⇒ 아래 그림이 이해가 되는지 설명해보자.

 

ACK를 돌려주는 것을 수신 확인 응답이라고 한다.

예시에서는 시퀀스 번호가 1부터 시작하거나 100부터 시작하는 등 특정 숫자로 시작하고 있지만, 시퀀스 번호는 사실 난수로 초기화된다. 항상 1부터 시작되면 악의적인 공격을 당할 우려가 있다.

이 때, 난수로 초기화되면 상대방은 초기 값이 몇인지 알 수 없다는 문제가 있기 때문에, 송수신을 시작하기 전에 초기 값을 상대방에게 알리게 되어 있다.

위 그림에서 100과 200이라는 값들은, 클라이언트와 서버가 서로 초기 값을 알려주는 과정이라고 볼 수 있다.

⇒ 이걸 이해한 게 이번이 처음인데, 매우 명쾌하다.

추가적으로, 상대방이 받았다는 응답 (ACK)가 돌아오기 전까지는 TCP는 버퍼를 비우지 않는다. 응답이 없으면 다시 보내야 하기 때문에 대기하는 것.

만약 서버가 다운 되는 등 TCP가 아무리 전송하여도 응답이 돌아오지 않으면 TCP는 회복의 전망이 없는 것으로 간주하고, 데이터 송신을 강제 종료, 애플리케이션에 오류를 통지한다.

4. 패킷 평균 왕복 시간으로 ACK 번호의 대기 시간을 조정한다

ACK가 돌아오는 것을 기다리는 대기 시간을 타임아웃 값이라고 한다.

네트워크가 혼잡해지면 ACK가 돌아오는 것이 지연될 수 있으므로, 대기 시간을 너무 짧게 해서는 안 된다. ( ACK가 돌아오는 시간 보다 타임 아웃이 짧으면, 응답 전에 계속 패킷을 전송할 것이다. )

대기 시간은 너무 짧지도 길지도 않게 적당하게 설정해야 하는데, 지연은 수 밀리초에서 수백 밀리초 사이를 오가므로 적절한 값을 찾는 것이 쉬운 일이 아니다.

따라서 TCP는 동적으로 대기 시간을 변경한다. ACK 번호가 돌아오는 시간을 기준으로, 대기 시간을 판단한다. 따라서 ACK 번호가 돌아오는 시간을 측정하고, 이것에 따라 대기시간을 늘리고 줄이고 반복한다.

5. 윈도우 제어 방식으로 효율적으로 ACK 번호를 관리한다

하나의 ACK가 돌아올 때까지 대기하는 것은 시간 낭비이다. 이러한 낭비를 줄이기 위해서 윈도우 제어 방식에 따라 송신과 ACK 번호 통지 동작을 실행한다.

윈도우 제어는 한 개의 패킷을 보낸 후 ACK 번호를 기다리지 않고 차례대로 연속해서 복수의 패킷을 보내는 방법이다. 그러면 ACK 번호가 돌아올 때까지의 시간이 낭비되지 않는다.

아래 그림을 참고하자.

 

그렇지만 여기에도 문제가 있는데, 바로 버퍼에 데이터가 쌓이는 시간이 방출하는 시간보다 빠르다는 점이다.

TCP에서 수신측은 해당 패킷 데이터를 일시 보관하고, ACK 번호 계산 등의 데이터 처리 과정을 한 후에 애플리케이션에 데이터를 전달하는데,

만일 이 때의 처리보다 새로운 데이터가 쌓이는 시간이 빠르면 버퍼 크기를 초과할 수가 있다. 따라서 개선이 필요하다.

 

윈도우 제어 방식에 수신용 메모리를 관리하는 것을 추가한 모습이다. 수신 측은 자신이 가용 가능한 데이터 사이즈를 상대에게 알려서 그 이상을 보내지 않도록 강제한다.

이 수신용 메모리 공간을 알리기 위해서 TCP 헤더의 윈도우 필드를 사용한다. 이 때, 수신 가능한 데이터 양의 최대 값을 윈도우 사이즈 라고 한다.

6. ACK 번호와 윈도우를 합승한다

윈도우 통지가 발생하는 시점

윈도우 통지는 수신 측이 버퍼에서 데이터를 추출하여 애플리케이션을 건네주었을 때마다 필요하다.

이 동작은 수신 측의 애플리케이션에 따라 일어나기 때문에 송신 측에서는 알 수가 없다. 따라서 수신 측에서 애플리케이션에 데이터를 건네주고 수신 버퍼의 빈 영역이 늘어나면 통지해야 한다.

⇒ 수신 버퍼의 빈 영역이 줄어드는 것은, 송신 측이 알아서 판단할 수 있다. ( 자기가 보낸 만큼 줄어들 것이기 때문. )

ACK 번호 응답이 발생하는 시점

데이터를 수신한 후에, 수신 받은 쪽이 즉시 보낸다.

윈도우 통지와 ACK 번호 응답을 동시에

패킷이 하나 씩 따로 따로 송신되는 것은 비효율적이기 때문에, ACK 번호나 윈도우 통지를 하기 전에 소켓을 바로 보내지 않고 잠시 대기한다. 이 때 다음 통지 동작이 일어나면 두 개를 하나로 합쳐서 보낸다.

복수의 ACK 번호 응답이 일어날 때에도 패킷의 수를 줄이기 위해서, 마지막 데이터의 번호만 통지하고 중간 것은 생략한다.

7. HTTP 응답 메시지를 수신한다

리퀘스트 메시지를 보내는 것은 이상으로 설명이 끝난다. 다음은 응답을 받는 과정이다.

브라우저는 송신을 의뢰하고, 이것이 끝나면 서버에서 오는 데이터를 받기 위해 read 를 호출한다. 다시 프로토콜 스택이 움직이고, 수신 버퍼를 사용하여 데이터를 응답을 받게 된다.

프로토콜 스택은 데이터를 추출하여 애플리케이션에게 전송한다.

단, 애플리케이션에게 전송하기 전에 클라이언트도 서버로부터 데이터를 잘 받았다는 확인을 받아야 하기 때문에 버퍼를 바로 비우지 않는다. ( 버퍼를 비운다는 것은 애플리케이션에게 넘긴다는 뜻과 같다. )

04. 서버에서 연결을 끊어 소켓을 말소한다

1. 데이터 보내기를 완료했을 때 연결을 끊는다

프로토콜 스택은 어느 쪽이 먼저 연결을 끊어도 되게끔 만들어져 있다. 여기서는 서버와 클라이언트의 구분을 두지 않지만, 편의 상 서버가 끊었다고 가정한다.

  1. 서버 측에서 Socket 라이브러리의 close를 호출한다. 서버 측의 프로토콜 스택이 TCP 헤더를 만들고, 연결 끊기를 나타내는 정보를 담는다. ⇒ 컨트롤 비트의 FIN 비트에 1을 설정하고 송신한다.
  2. 클라이언트 측은 위 통신이 도착하면 자신의 소켓에 서버 측이 연결 끊기 동작에 들어간 것을 기록하고 ACK번호와 FIN 번호를 보낸다. ( 이제 시퀀스 번호는 필요없다. )
  3. 이후 애플리케이션이 데이터를 읽으려고 들 때, ( 데이터를 이미 다 전달하고 없다면 ) 데이터를 건네는 대신에 서버 측 데이터를 모두 수신했음을 통지한다.
  4. 클라이언트도 종료한다.
    • 클라이언트 측도 1부터 3을 반복한다.

2. 소켓을 말소한다

대화가 끝나면 더 이상 소켓은 필요없지만, 바로 말소하기 전에 잠시간 대기 시간을 가진 후 말소한다. 이는 오작동을 방지하기 위함인데, 오작동의 이유는 많으니 간단한 예 하나만을 든다.

소켓 오작동

  1. 클라이언트가 FIN을 송신
  2. 서버가 ACK를 송신
  3. 서버가 FIN을 송신
  4. 클라이언트가 ACK를 송신

위와 같이 과정을 진행할 때, 클라이언트가 보낸 ACK는 말소된다. ( 이미 클라이언트는 닫았기 때문에, 서버는 받을 수가 없다. )

문제는 클라이언트의 포트는 임의로 할당되는 것이기 때문에, 이 짧은 시간에 혹시라도 포트가 재사용되어 다른 곳과 연결되면, TCP 통신이 서로 섞일 위험이 있다.

3. 데이터 송수신 동작을 정리한다

최종 정리는 생략한다.

반응형