본문 바로가기

Dev.Basic/네트워크

[2장] 2. 데이터를 송 수신하는 과정

이 포스팅은 '성공과 실패를 결정하는 1%의 네트워크 원리' 책을 기반으로 작성되었습니다.

2장 두번째, 데이터를 송 수신 한다.


1. 프로토콜 스택에 HTTP 리퀘스트 메시지를 넘긴다.
접속 동작을 마치게 되면 데이터 송 수신 동작에 들어간다.
이 동작은 애플리케이션이 write를 호출하여 송신 데이터를 프로토콜 스택에 건네주는 곳부터 시작된다.

프로토콜 스택은 데이터롤 곧바로 송신하는 것이 아니라 내부이 있는 송신용 버퍼 메모리 영역에 저장을 해둔다. 그 이유는 송신 의뢰가 올 때마다 데이터를 송신하게 되면 네트워크 효율이 떨어지기 때문이다.
그렇다면 어떤 기준으로 데이터를 저장하는가?
프로토콜 스택은 MTU라는 매개변수를 바탕으로 판단한다. MTU란 한 패킷으로 운반할 수 있는 디지털 데이터의 최대 길이를 의마하며 보통 1,500바이트가 된다. MTU의 값은 패킷 앞부분에 포함되는 각종 헤더를 포함한 값이고, 이 헤더들의 크기를 제외한 값이 MSS이다. 즉, 이 MSS가 우리가 전송하고자 하는 데이터가 저장될 수 있는 최대 길이가 되는 것이다.

프로토콜 스택은 송신 의뢰가 들어온 데이터가 MSS 값과 가까워 지면 송신을 하게 된다.
MSS보다 오랜 시간 작으면 송신을 하지 않는가?
이 부분도 OS에서 결정해야할 사항이다. 송신용 버퍼 메모리에 저장된지 일정 시간이 지났음에도 불구하고 MSS 값에 가까워지지 못했다면, 네트워크 효율을 떠나서 데이터 송신에 대한 응답이 늦어지기 때문에 데이터를 송신해야 하는 것이다. 이러한 이유로 프로토콜 스택 내부에 타이머가 존재하고 일정 시간 경과하면 패킷을 송신한다.


2. 데이터가 클 때는 분할하여 보낸다.
HTTP 리퀘스트 메시지는 보통 한 개의 패킷에 들어가지만, 긴 데이터의 경우 한 개의 패킷에 들어가지 않는다. 블로그나 게시판 등에서의 긴 문장이 그 예이다. 이 경우 송신 버퍼에 저장된 데이터는 MSS 길이를 초과하므로 다음 데이터를 기다릴 필요가 없다. 따라서 송신 버퍼에 들어가 있는 데이터를 앞에서부터 MSS 크기에 맞춰 분할한 뒤 TCP헤더를 부가하고 한 개씩 패킷에 넣어 송신한다.


3. ACK 번호를 사용하여 패킷이 도착했는지 확인한다.
TCP에는 송신한 패킷이 상대에게 올바르게 도착했는지 확인하고, 도착하지 않았으면 다시 송신하는 기능이 있다. 따라서 패킷을 송신한 후에는 확인 동작으로 넘어간다.
어떻게 확인하는가?
TCP 담당 부분은 데이터를 조각으로 분할할 때, 조각이 통신 개시부터 따져서 몇 번째 바이트에 해당하는지를 세어둔다. 그리고 데이터의 조각을 송신할 때마다 세어둔 값을 TCP 헤더에 기록한다. 이 값을 시퀀스 번호(sequence number)라고 한다.
1번째 바이트 / 1,461번째 바이트 / 2,921번째 바이트 ...
n번째 바이트라고 할 때, n 이 ACK 번호가 된다. 이렇게 하면 수신측에서 패킷이 누락되었는지 확인할 수 있다. 수신측에서는 수신한 데이터들을 합쳐서 몇 번째 바이트까지 수신한 것인지 계산하고, 그 값을 TCP 헤더의 ACK 번호에 기록하여 송신측에 알려준다,
이 ACK 번호를 되돌려주는 동작을 수신 확인 응답이라고 부른다.

실제로 시퀀스 번호는 1부터 시작하지 않고 난수를 바탕으로 산출한 값이 초기값으로 설정된다. 보안 이슈 문제도 존재하고, connection을 끊지 않고, 서버가 갑자기 작동을 멈췄을 때 재 송신하는 과정이 발생하기 때문이다.

그렇다면 실제로 어떻게 동작하는가?
접속 동작을 실행할 때
클라이언트 > 서버로 보내는 데이터에 관한 시퀀스 번호의 초기값을 산출하여 서버에 통지한다.
서버서버에 시퀀스 번호 초기값이 도착했음을 알리기 위해 통지 받은 초기값으로부터 ACK 번호를 산출하여 클라이언트에게 반송한다. 그리고, 서버에서도, 클라이언트로 보내는 데이터에 관한 시퀀스 번호의 초기값을 산출하여 클라이언트에 통지한다.
클라이언트 > 마찬가지로, 통지받은 초기값으로부터 ACK 번호를 산출하여 서버에 반송한다.

데이터 송수신 동작
클라이언트> 서버에 데이터와 시퀀스 번호를 보낸다.
서버> ACK 번호를 반송한다.

TCPACK 번호를 반송하는 구조로 데이터 받은 것을 확인한다. 이 때 확인할 때까지 송신한 패킷을 송신용 버퍼 메모리 영역에 보관해둔다. 만약 송신한 데이터에 대응하는 ACK 번호가 반송되지 않으면 패킷을 다시 보낸다. 이 구조를 통해 오류를 회복하는 처리를 다른 곳에서 전혀 할 필요가 없다. 제대로 도착할 때까지 재전송하므로 오류를 전부 검출할 수 있다.
단, 서버가 다운되었다던가, 특수한 케이스의 경우 아무리 재전송해도 전송이 이루어지지 않는 경우가 발생할 수 있다. 이 경우에 대해서는 몇 번 다시 보낸 후 회복의 전망이 없는 것으로 판단하여 데이터 송신 동작을 강제 종료하고 애플리케이션에 오류를 통지한다.


4. 패킷 평균 왕복 시간으로 ACK 번호의 대기 시간을 조정한다.
ACK 번호가 돌아오기까지 대기하는 시간을 타임아웃 값이라고 한다. 네트워크가 혼잡하여 정체가 일어나면 타임아웃 값이 커지게 되는데, 이 값을 어느 정도 길게 설정해둬야 한다. 그렇지 않으면 데이터가 제대로 도착했음에도 불구하고 데이터를 재전송하는 경우가 발생하게 된다. 이것은 재전송의 낭비로만 끝나는 것이 아닌 네트워크의 혼잡을 더욱 악화시킨다.

그렇다고 타임아웃 값을 너무 크게 잡으면 속도 저하의 원인이 된다. 이 두 가지를 모두 고려하여 적절할 값을 설정하는 것은 간단한 일이 아니다. 서버와의 거리에 따라 돌아오는데까지 걸리는 시간이 차이가 나고, 정체시의 지연도 고려해야하기 때문이다. LAN이라면 금방 ACK 번호가 되돌아오지만 인터넷의 경우 엄청 늦는 경우도 있다.

이러한 이유들로 대기 시간을 일정한 값으로 설정하는 것은 적절하지 않다. 그래서 TCP는 대기 시간을 동적으로 변경하는 방법을 사용한다. ACK 번호가 돌아오는 시간을 기준으로 대기 시간을 설정하는 것이다. ACK 번호가 돌아오는 시간이 지연되면 이에 대응하여 대기시간도 늘리고 곧바로 돌아오면 대기 시간을 짧게 설정하는 것이다.


5. 윈도우 제어 방식으로 효율적으로 ACK 번호를 관리한다.
한 개의 패킷을 보내고 ACK 번호를 기다리고 도착하면 또 다른 패킷을 보내는 것은 이해하기 쉽지만 ACK 번호를 기다리는 시간은 낭비가 된다. TCP는 이러한 낭비를 줄이기 위해 윈도우 제어라는 방식에 따라 송신과 ACK 번호 통지의 동작을 실행한다.

윈도우 제어는 한 개의 패킷을 보낸 후 ACK 번호를 기다리지 않고 차례대로 연속해서 복수의 패킷을 보내는 방법이다. 이렇게 하면 시간 낭비를 줄일 수는 있지만 수신측에 과부하가 걸릴 수 있다.
과부하의 경우를 조금 자세히 살펴보면, 수신측 TCP는 패킷을 수신하면 일단 수신용 버퍼 메모리에 데이터를 일시 보관한다. 그리고 ACK 번호를 계산하고, 조각을 연결하여 데이터 복원을 마친 뒤 애플리케이션에게 전달한다.
이 과정을 진행하는 동안 아직 처리가 끝나지 않은 상태에서 다음 패킷이 도착해도 문제가 되지 않도록 수신 버퍼를 따로 설치하는데, 이 수신 버퍼가 패킷으로 넘치게 되는 것이다.

넘친 패킷은 사라져버리므로(packet loss) 패킷이 도착해도 오류가 발생하는 것처럼 된다. 이러한 문제를 해결하기 위해 윈도우 제어 방식은 먼저 수신측에서 송신측에 수신 가능한 데이터 양을 통지하고, 수신측은 이 양을 초과하지 않도록 송신동작을 실행한다. 데이터 양을 통지할 때는 TCP 헤더의 윈도우 필드를 통해서 통지하며 수신 가능한 데이터 양의 최대값을 윈도우 사이즈라고 부른다.


6. ACK 번호와 윈도우를 합승한다.
윈도우 통지 동작은 언제 일어날까
윈도우 통지가 필요한 것은 수신측이 수신 버퍼에서 데이터를 추출하여 애플리케이션에 건네주었을 때다. 수신 버퍼에 있던 데이터가 빠졌으므로 그 만큼 공간이 생겼으니 이를 송신측에 알려줘야 하는 것이다. 즉, 애플리케이션에 데이터를 건네주었을 때, 이 때가 윈도우 통지의 타이밍이다. (ACK 번호는  데이터를 수신을 완료한 후 즉시 보낸다.)

서버측(수신측)에서는 이 두 가지 경우에 대해 패킷을 따로따로 보내야 하므로 효율성이 저하된다.
이 문제를 어떻게 해결할 것인가
결론부터 말하자면, '최후의 것만을 통지하자' 이다.
데이터 수신이 연속적으로 일어나는 경우에는 매번 ACK 번호를 반송할 필요없이 일정 시간 기다리다가 수신한 데이터의 끝에 대한 ACK 번호만 통지하고 도중의 것은 생략한다. 클라이언트 입장에서는 데이터를 어디까지 받았는지가 중요하기 때문이다.

윈도우 통지의 경우도 마찬가지이다. 어떠한 단계로 데이터가 애플리케이션에게 전달되어 윈도우 사이즈가 늘어나는지는 관심없다. 결과적으로 내가 지금 송신할 수 있는 사이즈가 궁금한 것이다. 그러므로 연속적으로 애플리케이션에 데이터를 전달하여 수신 버퍼에 빈 영역이 연속적으로 증가해도 매번 윈도우 통지를 하는 것이 아닌, 최후의 것만 통지하면 되는 것이다.

윈도우 통지와 ACK 번호 통지를 함께 보내도 된다. ‘일정 시간’을 대기하다가 보내기 때문에 이 대기 시간에 두 가지 통지를 보내할 경우가 발생한다면 두 통지를 하나의 패킷에 담아 전송할 수 있는 것이다. 패킷의 양을 줄여 네트워크 혼잡을 줄이는 방법이다.


7. HTTP 응답 메시지를 수신한다
수신한 데이터 조각과 TCP 헤더의 내용을 조사하여 도중에 누락된 데이터가 없는지 검사하고, 문제가 없으면 ACK 번호를 반송한다. 그리고 데이터 조각을 수신 버퍼에 일시 보관하고, 조각을 연결하여 데이터를 원래 모습으로 복원한 뒤 애플리케이션에게 건네준다.
이 건네주는 과정을 조금 구체적으로 살펴보자면, 수신 데이터를 애플리케이션이 지정한 메모리 영역에 옮겨 기록한 후 애플리케이션에 제어를 돌려주면 애플리케이션이 이 메모리 영역을 참조하는 과정이다. 그리고 애플리케이션에 데이터를 건네주고나서는 타이밍을 가늠하여 윈도우를 송신측에 통지한다.

브라우저는 리퀘스트 메시지를 송신해 달라고 의뢰하고 이것이 끝나면 응답 메시지를 받기 위해 read 프로그램을 호출한다.




8. 데이터 보내기를 완료했을 때, 연결을 끊는다.
송신을 완료한 측이 연결 끊기 단계로 들어간다. 웹의 경우, 브라우저가 웹 서버에 리퀘스트 메시지를 보내고, 서버에서 응답 메시지를 반송하면 데이터 송신이 끝이므로 서버측이 연결 끊기 단계에 들어간다. HTTP 1.1에서는 서버가 응답메시지를 반송한 후 계속 클라이언트가 다음 리퀘스트 메시지를 보내도 좋게 되어 있다. 따라서 이 경우 리퀘스트 메시지가 없을 때 클라이언트 측에서 먼저 연결 끊기 단계에 들어갈 수 있다.

서버측에서 연결 끊기 단계에 들어갔다고 가정하자.
서버>
애플리케이션은 Socket 라이브러리의 close를 호출한다. 프로토콜 스택이 TCP헤더를 만들고, 여기에 연결 끊기를 나타내는 정보를 설정한다.(FIN 비트에 1을 설정한다) 그리고 IP 담당 부분에 의뢰하여 클라이언트에 송신해달라고 한다. 이와 동시에 서버측 소켓에 연결 끊기 동작에 들어갔다는 정보를 기록한다.

클라>
서버에서 FIN에 1을 설정한 TCP 헤더가 도착하면 프로토콜 스택은 자신의 소켓에 서버측이 연결 끊기 동작에 들어갔다는 것을 기록한다. 패킷을 받았다는 것을 알리기 위해 ACK 번호를 반송하고, 애플리케이션이 데이터를 가지러 올 때까지 기다린다. 서버에서 보낸 데이터를 전부 수신 완료했다는 사실을 브라우저에게 알린다. 웹의 동작은 서버가 응답을 반송하면 끝이난다.
브라우저가 서버에서 보낸 데이터를 전부 수신했다는 것을 알게 되면 close를 호출하여 데이터 송수신 동작을 끝낸다. 프로토콜 스택도 FIN비트에 1을 설정한 TCP 헤더를 만들고 IP 담당 부분에 의뢰하여 서버에 송신한 후 서버에서 ACK 번호가 돌아오면 서버와의 대화가 끝난다.



9. 소켓을 말소하다.
말소하긴 하는데 잠시 기다린 뒤에 말소한다. 일반적으로 몇 분 정도 기다리고나서 소켓을 말소한다. 이는 오동작을 막기 위해서이다. 위의 경우를 짤막하게 정리해보자
(1) 서버가 FIN 송신
(2) 클라이언트가 ACK 번호 송신
(3) 클라이언트가 FIN 송신
(4) 서버가 ACK 번호 송신

이 경우 서버가 소켓을 바로 말소하여  클라이언트에 ACK 번호가 돌아오지 않았다면 FIN을 다시 한 번 송신할 것이다. 어디로 갈지 모르는 패킷이 방황을 하다가 그 사이 서버에서 새로운 소켓이 만들어졌는데 우연히 포트번호가 같다면, 새로 생긴 소켓에는 FIN 이 도착할 것이다. 이러한 오동작이 발생할 수 있다.



2장 두번째 end