rdt 개념
TCP는 신뢰성을 보장하는 프로토콜이다.
TCP에 대해 정리하기 전에 사전 개념으로서 rdt에 대해 정리한다.
rdt는 reliable data transfer 의 줄임말이다.
즉, 신뢰성있는 데이터 전송을 어떻게 할 수 있는지에 대한 개념이다.
이 그림을 보자.
어플리케이션에서 데이터를 만들어 transport 계층으로 보낸다.
그러면 transport 계층에 있는 프로토콜을 거쳐 네트워크 계층을 통해 데이터가 전송된 후 receiver 가 transport 계층을 거쳐 데이터를 받는다.
그런데 네트워크 계층을 포함한 그 밑의 데이터 전송 과정은 unreliable 하다.
unreliable 하다는 말은 보내는 중간에 데이터가 사라지거나(loss), 훼손되거나(corrupt), 순서가 바뀔 수 있다(reorder)는 뜻이다.
따라서 내가 보낸 데이터에 손실, 훼손, 순서 변경이 없이 온전히 목적지로 도착하도록 하려면 transport 계층에서 그 신뢰성을 보장해주어야 한다.
그런데 신뢰성을 보장할 때 구현하기 제일 어려운 부분은 내가 보낸 데이터가 잘 도착했는지 확인하는 것이다.
sender와 receiver는 메세지가 잘 도착했는지와 같은 정보, 즉, 서로의 상태(state)를 알 수 없다.
마치 sender는 커튼 뒤에 숨어있는 receiver를 향해 데이터를 보내는 것과 같다.
receiver도 마찬가지로 sender 가 커튼 뒤에 가려진 것처럼 보이기 때문에 정확한 상태를 알 수 없다.
그래서 서로의 상태를 알려주는 과정이 추가적으로 필요한데, 이 개념이 rdt의 핵심 개념이다.
그리고 rdt의 개념을 실제로 프로토콜로 구현한 것이 TCP이다.
rdt는 아래와 같은 동작을 정의한다.
rdt_send() 는 어플리케이션 계층에서 트랜스포트 계층으로 데이터를 보낸다.
이 기능은 TCP에서 그대로 구현되어 tcp_send() 로 역할을 할 것이다.
트랜스포트계층은 받은 데이터를 네트워크 계층으로 udt_send() 로 보낸다.
udt는 unrelicable data transfer 의 줄임말이다. (UDP가 아니다!)
받는 쪽은 데이터를 rdt_rcv() 로 받는다. 받아서 신뢰성을 보장하는 작업을 수행하기 때문에 rdt 이다.
그리고 신뢰성이 보장된 데이터를 어플리케이션 계층에 보내는 기능이 deliver_data() 이다.
위 그림에서 data 는 어플리케이션 계층의 전송단위, message 라고 볼 수 있다.
data에 해더가 붙은 형태의 데이터는 네트워크 계층에 전송되는 패킷으로 볼 수 있다.
네트워크 계층에 존재하는 unreliable channel 은 양방향 채널이라고 생각한다.
FSM을 이용한 rdt 프로토콜 설계
이제 rdt 라는 프로토콜을 한번 설계해보자.
우선 지금 설계하는 rdt 프로토콜은 unidirectional, 단방향 데이터 전송을 가정한다.
(실제로 제어정보는 양쪽에서 왔다갔다한다. 하지만 단방향으로 설계한 과정을 거꾸로 뒤집으면 반대 방향도 묘사가 되므로 단방향으로 설계한다.)
설계를 할 때는 FSM (Finite State Machine, 유한 상태 기계) 을 이용한다.
FSM 프로그램의 흐름을 특정 시점의 상태와 상태의 변경으로 묘사한다.
상태(state)는 동그라미를 그리고 그 안에 구체적인 상태를 적는다.
그리고 상태가 변하는 것은 그 이전 상태에서 다음 상태를 향해 화살표를 그리는 것으로 묘사한다.
그리고 화살표 위에 직선을 긋고, 직선 위에는 상태 변화를 일으키는 이벤트, 직선 아래에는 상태 변화 과정에서 실제로 수행하는 행동을 적는다.
이제 본격적으로 FSM으로 rdt 프로토콜을 설계해보자.
rdt 2.0 (stop and wait)
rdt를 1.0부터 차근차근 설계해서 발전시켜나가면 좋겠지만, 시간이 없어 수업에서는 2.0부터 진행했다.
먼저 상위 계층 (어플리케이션 계층)으로부터 call 을 기다리는 상태에서 시작한다.
이 상태에서 rdt_send(data) 라는 함수 호출 이벤트가 발생하면, 트랜스포트 계층은 데이터로부터 패킷을 만들고, udt_send() 함수를 호출해 패킷을 전송한다.
그 뒤, sender 는 receiver로부터 ACK, 또는 NAK 를 기다리는 상태로 변화한다.
ACK는 잘 받았다는 뜻의 신호 (acknowledge),
NAK는 메세지가 훼손되었을 때 보내는 신호이다. (no acknowledge)
만약 NAK를 받으면 데이터를 다시 전송해야 한다.
만약 ACK 또는 NAK를 기다리는 상황에서 reciever로부터 응답을 수신했고, 그 응답의 내용이 ACK 라면 잘 전송한 것이니 sender는 역할을 다 했다. 따라서 action 은 아무것도 수행하지 않는다. (^ 기호가 아무것도 안한다는 의미다.)
만약 ACK가 아니라면 ACK를 받을 때까지 계속 대기한다.
만약 리시버로부터 받은 패킷에 NAK였다면, 이 상태를 유지하면서 udt_send() 액션을 취한다.
이전에 만들어둔 sndpkt 을 그대로 다시 보내는 것이다. (새로운 패킷을 만들지 않음)
이제 receiver 입장을 보자.
리시버는 네트워크 계층의 rdt_rcv() 함수 호출을 기다린다.
만약 함수가 호출되어서 데이터를 수신했는데, 받은 패킷 데이터가 훼손되었다면 NAK를 송신하고, call을 기다리는 상태는 유지한다.
만약 수신한 데이터가 훼손되지 않았다면, data를 추출해서 어플리케이션 계층 (상위 계층)으로 보내고, sender에게는 ACK를 보낸다.
이 과정을 보면 알겠찌만 sender와 reciever는 서로의 상태를 바로 알지 못한다.
reciever가 NAK, ACK를 보내는 것으로 확인할 뿐이다.
그래서 이렇게 상태를 따로 보내라는 약속, 즉 프로토콜이 필요한 것이다.
그런데 rdt 2.0 에는 심각한 문제가 하나 있다.
바로 ACK, NAK 라는 신호 자체가 corrupt 되지 않았다는 상황을 가정하고 있다는 것이다.
ACK, NAK 자체가 만약 corrupt 되었다면 재전송해야 하는데 재전송을 하지 않거나,
재전송을 할 필요가 없는데 재전송하는 문제가 발생한다. (duplicated 데이터가 수신자에 도착한다.)
따라서 무작정 재전송을 할 수는 없다.
그래서 중복 전송 문제를 해결하기 위해 패킷에 sequence number 를 추가하는 아이디어가 등장했다.
패킷을 보낼 때, 순서를 부여해서 항상 0 1 0 1 순으로 패킷을 보내는 것이다.
만약 0번 패킷을 이미 수신한 상황에서 또 0번 패킷이 들어오면 중복 데이터가 들어온 것이니 reciever는 이를 무시할 수 있다.
시퀀스 넘버를 부여하는 방식 자체는 TCP에서도 동일하게 사용한다.
그리고 이렇게 sender가 한번 데이터를 보내고, reciever의 ACK, NAK 신호를 기다렸다가 다시 데이터를 보내는 방식을 stop and wait 방식이라고 한다. 그래서 효율로 따지면 매우 비효율적인 프로토콜이다.
그래서 TCP는 이 방식을 그대로 사용하지 않는다.
rdt 2.1
rdt 2.1 에서는 ACK, NAK가 훼손된 경우를 처리하는 방식이 추가되었다.
먼저 sender 에 대해 다음과 같이 state가 주어진다.
위에서 화살표가 가리키는 것이 처음 시작하는 상태를 의미한다.
처음 sender는 0번 시퀀스로 보낼 데이터 전송 요청을 기다린다.
상위 계층이 데이터를 보내주면 이 데이터를 0번 패킷으로 만들어 전송한다.
그리고 0번 패킷에 대한 ACK, NAK를 기다린다.
만약 수신측으로부터 0번에 대한 ACK를 받았고, ACK에 훼손이 없다면, 액션을 추가적으로 하지는 않는다.
그리고 1번 데이터 전송 요청을 기다리는 상태로 변환한다.
그 상태에서 데이터 전송 요청을 받으면 1번 시퀀스 넘버를 붙인 패킷을 전송하고 1번 시퀀스에 대한 ACK, NAK를 기다린다.
만약 수신 패킷이 훼손되지 않은채로 잘 받았으며, ACK라면 0번 데이터 전송 요청을 기다리는 상태로 바꾼다.
만약 수신 데이터에 문제가 없는 상태에서 NAK를 받았다면, 2.0 에서 한 대로 같은 데이터를 재전송할 것이다.
이제 reciever 입장을 생각해보자.
reciever는 0번 데이터 수신을 기다리거나, 1번 데이터 수신을 기다리는 상태 2가지가 존재한다.
처음에는 0번 데이터 수신 대기 상태에서 시작한다.
만약 데이터를 수신했고, 문제가 없으면서, 0번 시퀸스에 대한 데이터라면 상위 계층에 데이터를 전달하고, 1번 데이터를 수신 대기하는 상태로 변화한다.
1번에서도 마찬가지로, 훼손되지 않은 1번 데이터를 잘 받았다면 ACK를 응답하면서 0번 시퀀스 데이터를 기다리는 상태로 변화한다.
만약 수신 패킷이 훼손되었다면 NAK를 보내고 다시 1번 시퀀스 패킷을 기다린다.
또는 이런 경우도 있다.
훼손되지 않은 패킷을 잘 받았는데, 자신이 기다리는 시퀀스 번호가 아닌 패킷을 받은 상황이다.
이 상황에서는 일단 받기는 잘 받았으니 ACK를 보내지만, 수신한 패킷에서 데이터를 추출해 어플리케이션으로 보내는 작업은 하지 않는다.
당연히 이 과정은 0번 시퀀스 데이터를 기다리는 상태에서도 발생할 수 있고, 같은 방식으로 처리한다.
이제 지금까지 정리한 rdt 2.1을 rdt 2.0 과 비교해보자.
sender 는 패킷에 시퀀스 넘버를 추가해서, ACK, NAK 자체가 훼손된 상황에 대처한다.
ACK, NAK가 훼손되었다면, 이전에 보낸 시퀀스 데이터를 다시 전송한다.
이때 시퀀스 번호는 0, 1 두 가지만 있으면 충분하다.
그 이유는 rdt 2.1 이 stop and wait 프토로콜이기 때문이다.
하나의 패킷을 보내고, 잘 보내졌는지 기다렸다가 다음 패킷을 보내기 때문에, 하나의 패킷에 대한 상태를 확인하는 0, 1만 있으면 충분하다.
(현재 상태가 0일 때, 0을 잘 보냈다면 1로 바꾸고, 1을 잘 보내면 다시 0으로 바꾸고..)
따라서 시퀀스 넘버가 늘어난 만큼, sender, reciever의 state 숫자는 2배로 증가한다.
(sender는 2개에서 4개, reciever는 1개에서 2개)
각각의 state는 지금 받아야하는 패킷이 0인지 1인지를 기억해야 하기 때문이다.
reciever는 지금 수신한 패킷이 이미 받은 패킷인지 확인해야 한다.
그래서 현재 받아야 하는 패킷의 상태(state)를 seq 넘버를 통해서 확인한다.
단, reciever는 자신이 마지막으로 보낸 ACK/NAK가 잘 도착했는지는 알 수 없다.
그저 sender가 중복된 데이터를 보내거나, 보낸 데이터가 훼손되면 이에 대해 ACK, NAK로 처리할 뿐이다.
rdt 2.2
rdt 2.2는 기능적으로는 rdt 2.1과 동일하다.
다만 NAK를 사용하지 않는 차이점이 있다.
(실제로 TCP도 ACK만 사용한다.)
그렇다면 ACK만 가지고 어떻게 NAK를 표현할 수 있을까?
바로 ACK의 번호를 부여하는 것이다.
그 번호는 자신이 마지막으로 ACK를 받은 패킷의 seq 번호를 사용한다.
만약 sender가 보낸 0번 데이터가 훼손되었다면, reciever는 1번 데이터까지는 잘 받았었다는 의미이므로, ACK에 1번을 담아서 보낸다.
sender는 ACK에 있는 seq 번호가 자신이 보냈던 데이터의 seq 번호와 다르므로 내가 보낸 것은 잘 받지 못했다고 생각해서 자신의 seq 번호를 다시 보낸다.
구체적으로 어떻게 작동하는지 다음 예시를 보자.
sender 에서는 NAK인지 ACK인지 체크하는 부분을, ACK인데 자신이 보낸 시퀀스 번호와 같은지를 체크한다.
receiver는 올바르게 수신한 패킷을 만들 때, NAK 대신 ACK1, ACK0 과 같은 식으로 새로운 패킷을 만든다.
그리고 만약 올바르게 수신하지 않았다면, 이전에 만들어둔 패킷을 그대로 다시 보낸다.
'CS > 컴퓨터 네트워크' 카테고리의 다른 글
[컴퓨터 네트워크] 19. Transport Layer (5) : Go-Back-N (GBN), Selective Repeat (SR) (0) | 2024.05.24 |
---|---|
[컴퓨터 네트워크] 18. Transport Layer (4) : rdt 3.0 (0) | 2024.04.24 |
[컴퓨터 네트워크] 16. Transport Layer (2) : UDP (0) | 2024.04.20 |
[컴퓨터 네트워크] 15. Transport Layer (1) : 트랜스포트 계층 개요 (0) | 2024.04.20 |
[컴퓨터 네트워크] 14. Application Layer (7) : Socket Programming (0) | 2024.04.19 |