-
4. 트랜스포트 계층공부/컴퓨터 네트워크 2022. 5. 23. 19:19반응형
이 글은 Computer Networking: A Top-Down Approach 7th를 읽고 정리한 글입니다.
1. 트랜스포트 계층 서비스 및 개요
트랜스포트 계층 프로토콜은 서로 다른 호스트에서 동작하는 애플리케이션 프로세스들 간의 논리적 통신을 제공한다.
여기서 논리적 통신이란 애플리케이션의 관점에서 볼 때 프로세스들이 동작하는 호스트들이 서로 직접 연결이 된 것처럼 보인다는 것이다.
우리가 애플리케이션 계층에서는 메시지라고 불렀지만, 트랜스포트 계층에서는 메시지를 작게 분할하여 이런 분할된 조각 한 개를 세그먼트라고 부르게 된다. 이게 트랜스포트 계층에서의 기본 단위라서 앞으로 세그먼트라고 하면 이런 뜻이라고 생각하면 된다.
참고로 네트워크 계층에서의 기본 단위는 데이터그램인데, 트랜스포트 계층에서 얻은 세그먼트를 데이터그램이라고 불리는 것에 캡슐화되어 이동하게 된다.
1.1 트랜스포트 계층과 네트워크 계층 사이의 관계
트랜스포트 계층은 프로토콜 스택에서 네트워크 계층 바로 위에 존재한다.
트랜스포트 계층 프로토콜: 서로 다른 호스트에서 동작하는 프로세스들 사이의 논리적 통신을 제공
네트워크 계층 프로토콜: 호스트들 사이의 논리적 통신을 제공
이게 무슨 소리냐면 위에서 논리적 통신이 직접적으로 연결이 된 것처럼 느끼는 것이라고 언급했었다. 그래서 네트워크 계층은 호스트와 호스트 사이를 직접 연결한 것처럼 보이게 해주는 역할을 해주고, 트랜스포트 계층은 네트워크 계층에게 받은 것을 애플리케이션 계층에 보여주며 자기들이 직접 연결한 것처럼 보이게하는 역할을 한다는 것이다.
1.2 인터넷 트랜스포트 계층의 개요
인터넷 전송 프로토콜이 애플리케이션 계층에 TCP와 UDP라는 프로토콜을 제공한다고 2장에서 언급을 했었다.
이것은 트랜스포트 계층에 있는 프로토콜이다.
- UDP: 애플리케이션에게 비신뢰적이고, 비연결형이지만 빠른 서비스를 제공함
- TCP: UDP보다 느리지만 애플리케이션에게 신뢰적이고, 연결지향성 서비스를 제공함
네트워크 애플리케이션을 설정할 때, 애플리케이션 개발자는 트랜스포트 계층 프로토콜인 UDP나 TCP 중 하나를 명시해야한다.
UDP와 TCP의 기본적인 기능은 종단 시스템 사이의 IP 전달 서비스를 시스템에서 동작하는 두 프로세스 간의 전달 서비스로 확장하는 것이다.
그니까 호스트-호스트의 큰 틀에서 프로세스-프로세스인 작은 단위로 전달하는 것으로 확장하는 것인데, 이것을 트랜스포트 다중화와 역다중화라고 부른다.
2. 다중화와 역다중화
다중화와 역다중화는 호스트-호스트 전달에서 프로세스-프로세스 전달 서비스로 확장하는 것과 관련이 있다.
먼저 2장에서 소켓이 네트워크로 메시지를 주고 받는 출입구 같은 역할을 한다고 언급한 적이 있다.
다중화는 데이터를 모아 세그먼트를 만들고, 세그먼트들을 모아 네트워크 계층으로 전달하는 작업을 뜻한다.
역다중화는 트랜스포트 계층 세그먼트의 데이터를 올바른 소켓으로 전달하는 작업을 하는 것이다.
책에 나와있는 예시로 예를 들어보면 A집과 B집이 서로 편지를 주고 받는데, A집에 있는 사람 한 명이 가족들이 보내려는 편지를 모아서 우편집배원에게 넘겨주는 것이 다중화이다. 그리고 다중화를 통해 우편집배원이 얻은 편지를 B집에 있는 사람한테 전달해주는데, 그때 B집에서 편지를 받은 사람이 가족들에게 편지를 나누어주는 것이 역다중화이다.
2.1 역다중화 기본 작동 원리
이전에 네트워크 계층에서는 세그먼트를 데이터그램으로 캡슐화해서 사용한다고 언급한 적이 있다.
호스트의 각 소켓은 포트 번호를 할당 받는데, 위 데이터그램에 있는 목적지 포트 번호와 소켓의 포트 번호가 서로 같은지 확인하여 같은 번호일 경우 세그먼트를 보낸다. 그러면 데이터는 소켓을 통해 데이터를 받아야하는 프로세스에 전달된다.
이것은 역다중화의 공통적인 작동 방식이고, 여기에 UDP와 TCP가 달라지게 된다.
2.2 비연결형 다중화와 역다중화 (UDP)
클라이언트는 9157, 5775 포트 번호를, 서버는 6428 포트 번호를 부여 받았다고 해보자
여기서 잘 봐야할 것은 서버쪽인데 클라이언트에서 서버로 보내는 데이터그램의 목적지 포트 번호가 같으면 같은 소켓으로 들어가서 동일 프로세스로 가게 된다.
2.3 연결지향형 다중화와 역다중화 (TCP)
TCP 소켓은 4개의 요소들의 집합인 출발지 IP 주소, 출발지 포트 번호, 목적지 IP 주소, 목적지 포트 번호에 의해 식별된다.
그래서 UDP에서는 목적지 포트 번호만 같으면 같은 소켓으로 같은 프로세스로 향하게 되는데, TCP는 목적지 포트 번호는 같지만 출발지 포트 번호가 다르므로 서로 다른 소켓으로 향하게 된다.
1) 같은 목적지 포트 번호라도 IP 주소가 다르면 다른 소켓으로 간다.(P4와 P5, P6)
2) 같은 클라이언트라도 출발지 포트 번호가 다르면 다른 소켓으로 간다.(P5와 P6)
3. 비연결형 트랜스포트: UDP
UDP는 트랜스포트 계층 프로토콜이 할 수 있는 최소 기능으로 동작한다. (다중화/역다중화 기능, 간단한 오류 검사 기능만 추가 제공)
거기다가 세그먼트를 송신하기 전에 송신 트랜스포트 계층 개체들과 수신 트랜스포트 계층 개체들 사이에 핸드셰이크를 사용하지 않는다. 그래서 UDP를 비연결형이라고 부른다.
왜 UDP를 사용할까?
- 연결 설정이 없다 (3-way handshake가 없다)
- 연결 상태가 없다 (TCP는 연결 상태를 유지한다)
- 작은 패킷 오버헤드
- 단순하다
UDP 헤더는 2바이트(4^2 bit)씩 구성된 4개의 필드를 가진다.
여기서 우리가 아직 안배운 내용은 체크섬인데, 세그먼트에 오류가 발생하는지 검사하기 위해 존재하는 것이다.
3.1 checksum
세그먼트가 출발지로부터 목적지로 이동 했을 때, UDP 세그먼트 안의 비트에 대한 변경사항이 있는지 검사한다.
체크섬을 사용하는 방법은 송신 측에서 모든 16비트 워드 단위로 더하고, 이에 대하여 1의 보수를 수행한다. 이때 덧셈 과정에서 발생하는 오버플로는 윤회식 자리 올림을 한다.
예제
3개의 데이터가 있다고 해보자
0110011001100000 - 1번 워드
0101010101010101 - 2번 워드
1000111100001100 - 3번 워드
송신측)
1. 먼저 1번 워드와 2번 워드의 합을 구한다.
0110011001100000
0101010101010101
1011101110110101 - 결과값
2. 과정1에서 구한 결과값에 3번 워드를 더한다.
1011101110110101
1000111100001100
0100101011000001 - 결과값 (오버플로)
3. 오버플로가 발생하면 윤회식 자리 올림을 한다.
0100101011000001
0000000000000001
0100101011000010 - 결과값
4. 1의 보수로 변환한다.
0100101011000010
1011010100111101 - 결과값
이렇게 해서 나온 값이 checksum이 된다.
수신측)
1. checksum과 수신해서 얻은 1번 워드, 2번 워드, 3번 워드를 합한 것을 더해준다.
1011010100111101 - checksum
0100101011000010 - 1번 워드 + 2번 워드 + 3번 워드 + 윤회식 자리 올림
1111111111111111 - 결과값
이렇게 모든 비트의 값이 1이 된다면 정상이라는 것이고, 하나라도 0이 나온다면 패킷에 오류가 있다는 뜻이다.
4. 신뢰성 있는 데이터의 전송 원리
신뢰성 있는 데이터는 보내는 순서대로 데이터를 전달 받는다는 것이다.
처음에는 간단한 모델로 시작해서 오류를 처리하기 위해 점점 복잡해지는 모델로 내용을 전개해나간다.
참고로 신뢰성 있는 데이터의 전송은 rdt(reliable data transfer)로 불리게 된다.
아래에 나오는 그림들은 FSM (유한상태머신)이라고 부르는데, 총 3개로 나눠진다.
- 상태(state): 해당 노드가 하는 역할이다.
- 이벤트(event): 노드의 상태에 맞는 일이 들어오면 발동한다.
- 액션(action): 노드의 상태가 변화하면서 발생하는 일이다.
그래서 state → event → action → 다른 state → ... 로 계속 이어지게 된다.
- rdt_send(): 상위 계층으로부터 데이터를 받아들이고, 데이터를 포함한 패킷을 생성한다.
- rdt_rcv(): 하위 계층으로부터 패킷을 수신한다.
- deliver_data(): rdt 프로토콜이 상위 계층에 데이터를 전달하게 해준다.
- udt_send(): 다른쪽에 패킷을 전송한다.(상위 계층, 하위 계층 상관없음)
4.1 완벽하게 신뢰적인 채널 상에서의 신뢰적인 데이터 전송: rdt 1.0
가장 간단하면서도 가장 완벽한 프로토콜이다. 오류가 생기지 않지만, 현실에서는 없는 프로토콜이다.
sender
1. Wait for call from above: state는 위에서 호출을 기다린다.
2. 위에서 호출이 들어오면 rdt_send(data): event로 데이터를 받아들인다.
3. 이후 make_pkt(data): action을 통해 패킷을 생성하고, udt_send(packet): action을 통해 다른쪽에 패킷을 전송한다.
receiver
1. Wait for call from below: state는 아래에서 호출을 기다린다.
2. 아래에서 호출이 들어오면 rdt_rcv(packet): event를 통해 패킷을 받아들인다.
3. 이후 extract(packet, data): action을 통해 패킷에서 데이터를 추출하고, deliver_data(data)를 통해 데이터를 상위 계층으로 전달한다.
4.2 비트 오류가 있는 채널 상에서의 신뢰적 데이터 전송: rdt 2.0
rdt 2.0에서는 패킷들이 송신된 순서대로 수신을 받을 수 있지만, 패킷에 오류가 생길 수도 있다는 것을 가정한다.
1. sender (송신측)
1. 상위 계층에서 호출이 들어오면 rdt_send(data)를 통해 데이터를 받아들인다.
2. sndpkt = make_pkt(data, checksum)을 통해 체크섬과 데이터를 패킷화 시킨다.
3. 이후 udt_send(sndpkt)로 2번에서 만든 패킷을 통해 전송을 한다.
4. 오른쪽에 있는 노드는 ACK 또는 NAK 응답을 기다린다. (이쪽은 receiver가 처리한다.)
5-1. ACK 패킷이 수신된다면 송신자는 가장 최근에 전송된 패킷이 오류없이 정확하게 왔다는 것을 알 수 있게 된다.
5-2. NAK 패킷이 수신된다면 프로토콜은 마지막 패킷을 재전송하고, 다시 ACK 또는 NAK 패킷을 기다린다.
여기서 우리가 모르는 기호가 있는데, 5-1에서 ACK 패킷을 받는다면 action이 Λ로 표시 되어 있는데, 아무것도 안한다는 뜻이다.
4번에서 ACK 또는 NAK 응답을 기다린다고 나와있는데, 이제 receiver쪽을 확인해보자
2. receiver (수신측)
1. 하위 계층에서 호출을 받는다.
2. 그러면 rdt_rcv(rcvpkt)을 통해 패킷을 받는다.
3-1 체크섬 값과 데이터 값을 더하여 오류가 검출되면 corrupt(rcvpkt) = True가 되고, 송신측에 NAK 패킷을 보낸다.
3-2 체크섬 값과 데이터 값을 더하여 오류가 검출되지 않으면 notcorrupt(rcvpkt) = True가 되고 4번으로 넘어간다.
4. extract(rcvpkt, data)를 통해 패킷에서 데이터를 추출한다.
5. 데이터를 상위 계층에 전달한다.
6. 송신측에 ACK 패킷을 보낸다.
그래서 sender와 receiver를 합친다면
이렇게 서로 연결이 되어 있다고 볼 수 있다.
4.3 패킷 오류 뿐만 아니라 ACK, NAK 패킷이 오류가 나왔을 때: rdt 2.1
rdt 2.0의 치명적인 결함이 있다.
ACK, NAK 패킷이 손상될 수 있다는 가능성을 고려하지 않은 것이다.
그래서 ACK, NAK 패킷들에 대한 체크섬 비트를 추가하고, has_seq0(x), has_seq1(x)라는 현재 순서번호가 0인지 1인지 판단하여 중복을 확인한다.
여기서 제일 어려운 문제는 ACK, NAK 패킷이 손상되었으면 송신자는 수신자가 전송된 데이터의 마지막 부분이 올바르게 수신됐는지 확인할 방법이 없다는 것이다.
이것을 해결하는 가장 간단한 방법은 데이터 패킷에 새로운 필드를 추가하고, 이 필드 안에 순서 번호(sequence number)를 삽입하는 것이다.
순서번호는 0과 1만 사용하는데, 처음에 순서 번호가 0인 패킷을 전달했을 때 0 ACK를 받으면 다음 패킷을 보낼 때 순서 번호인 1인 패킷을 보내면 되지만, 0 NAK 패킷을 받았으면 다시 순서 번호가 0인 패킷을 재전송해야하므로 순서 번호는 0과 1만 사용해도 충분하다.
그래서 0 → 1 → 0 → 1 → 0 → ... 순으로 순서 번호를 얻게 된다면 정상적으로 데이터가 들어왔다는 것이고, 0 → 0 → 1 → 1 → 0 → ... 순으로 순서 번호를 얻게 되면 두 번이나 데이터를 재전송 받았다는 것이다.
rdt 2.1에서는 순서 번호가 0일 때와 1일 때 각각 처리를 해야하므로 rdt 2.0에 비해 상태가 두 배 늘어나게 된다.
게다가 순서 번호를 추가해도 NAK, ACK가 ㅈ
1. sender (송신측)
1) 상위 계층에서 0번 호출을 기다린다.
2) 0번이 호출되면 rdt_send(data)를 통해 데이터를 받아들인다.
3) 이후 sndpkt = make_pkt(0, data, checksum)을 통해 순서 번호, 데이터, 체크섬을 패킷으로 만들어준다.
4) udt_send(sndpkt)를 통해 데이터를 송신한다.
-- receiver에서 ACK 혹은 NAK 패킷을 받음 --
5) rdt_rcv(rcvpkt)를 통해 데이터를 받는다.
6-1) corrupt(rcvpkt)를 통해 패킷에 오류가 있거나, isNAK(rcvpkt)를 통해 NAK 패킷을 받는다면 데이터를 다시 받는다.
6-2) notcorrupt(revpkt)를 통해 패킷에 오류가 없고, isACK(rcvpkt)를 통해 ACK 패킷을 받았다면 데이터를 제대로 전송 받았으니 상위 계층에서 1번 호출을 기다린다.
-- 1번 호출이 일어남(0번과정과 같음) --
7) 1번이 호출되면 rdt_send(data)를 통해 데이터를 받아들인다.
8) 이후 sndpkt = make_pkt(0, data, checksum)을 통해 순서 번호, 데이터, 체크섬을 패킷으로 만들어준다.
9) udt_send(sndpkt)를 통해 데이터를 송신한다.
-- receiver에서 ACK 혹은 NAK 패킷을 받음 --
10) rdt_rcv(rcvpkt)를 통해 데이터를 받는다.
11-1) corrupt(rcvpkt)를 통해 패킷에 오류가 있거나, isNAK(rcvpkt)를 통해 NAK 패킷을 받는다면 데이터를 다시 받는다.
11-2) notcorrupt(revpkt)를 통해 패킷에 오류가 없고, isACK(rcvpkt)를 통해 ACK 패킷을 받았다면 데이터를 제대로 전송 받았으니 상위 계층에서 0번 호출을 기다린다.
.....
내용이 엄청 길지만 1) ~ 6)과 7) ~ 11)은 순서 번호 값만 다르지 나머지는 같은 과정을 거치고 있다.
2. receiver (수신측)
1) 하위 계층에서 0번 호출을 기다린다.
2) rdt_rcv(rcvpkt)를 통해 패킷을 받는다.
3-1) corrupt(rcvpkt)를 통해 패킷에 오류가 있으면 sndpkt = make_pkt(NAK, chksum)을 통해 NAK과 NAK이 오류인지 아닌지 체크섬까지 추가한 패킷을 만들고, udt_send(sndpkt)를 통해 수신자가 NAK 패킷을 받을 수 있도록 한다.
3-2) not corrupt(rcvpkt)를 통해 패킷에 오류가 없고, has_seq1(rcvpkt)를 통해 현재 패킷의 순서 번호가 1인지 확인한다. 이 경우에는 원래 0번 패킷이 들어와야 하는데, 1번 패킷이 왔으므로 중복된 데이터니 패킷을 버린다. 그래서 데이터 전송하는 것 없이 sndpkt = make_pkt(ACK,chksum)을 통해 패킷을 만들고, udt_send(sndpkt)로 수신자가 ACK 패킷을 받을 수 있도록 한다.
3-3) notcorrupt(rcvpkt)를 통해 패킷에 오류가 없고, has_seq0(rcvpkt)를 통해 현재 패킷의 순서 번호가 0인지 확인한다. 그러면 중복된 데이터도 아니고, 오류도 없으니 extract(rcvpkt, data)를 통해 rcvpkt으로부터 data를 추출한다. 그리고 snd_pkt = make_pkt(ACK, chksum)을 통해 ACK를 보내고, ACK에 오류가 없는지 확인하도록 chksum까지 함께 패킷으로 만든다. 이후 udt_send(sndpkt)를 통해 패킷을 송신측에 보낸다. 이후 수신자의 상태를 Wait for 1 from below로 1번 호출을 기다리는 것으로 상태 변경을 한다.
0번뿐만 아니라 1번도 같다고 생각하면 된다.
4.3 중복 ACK를 받았을 때에 관한 대처와 NAK 없는 신뢰적인 데이터 전송 프로토콜: rdt 2.2
rdt 2.1에서는 순서가 바뀐 패킷을 받으면 ACK를 내고, 손상된 패킷을 받으면 NAK을 낸다. 그런데 NAK 대신 가장 최근에 정확하게 수신된 패킷에 대해 ACK를 송신하면 손상된 패킷 이전에 받은 패킷에 ACK를 받으니, NAK을 보낸 것과 같은 효과를 얻을 수 있게 된다.
(예를 들어서 8번 패킷 → 9번 패킷(손상) → 10번 패킷(손상) 이렇게 온다면 8번 패킷 ACK → 8번 패킷 ACK → 8번 패킷 ACK으로 나오게 된다.)
그래서 NAK이 없기 때문에 ACK에 의해 확인응답을 하는 패킷의 순서 번호를 포함해야한다.
sender, receiver
이건 rdt 2.1과 비슷한데, 차이점은 수신측에 있던 has_seq0를 송신쪽에서도 얻을 수 있게 isACK(rcvpkt)에 0을 추가하여 isACK(rcvpkt, 0)을 한 것이라고 생각하면 된다.
4.4 비트 오류와 손실 있는 채널 상에서의 신뢰적 데이터 전송: rdt 3.0
오늘날의 인터넷은 구조가 복잡하기 때문에 하위 채널에 있던 패킷을 손실하여 현재 채널에 영향을 줄 수 있다. 이것에 대해 해결을 해야하는데, 패킷을 손실했다는 것은 송신자에게 응답을 받을 수 없기 때문에 패킷을 손실했다는 것을 확신하기 위해 일정 시간만큼 기다려본다. 이후엔 패킷이 늦었지만 정상적으로 들어와도 패킷을 손실했다고 간주하여 패킷을 재전송 받는다.
1. sender (송신측)
여기서 추가된 것은 빨간 글씨인데, start_timer는 타이머를 시작해서 상태를 변경한다. 이후 timeout이 된다면 패킷이 손실됐든 안됐든 다시 패킷을 전송 받을 수 있도록한다.
2. receiver (수신측)
rdt 2.2의 receiver와 같다.
패킷 손실이 없을 때 동작하는 방식이다.
이 경우엔 패킷이 유실되어서 일정 시간이 넘어간 이후에는 송신측에서 ack 1 패킷이 들어오지 않으니 다시 패킷을 전송할 수 있도록 한다.
이 상황은 ack 1이 유실된 상황인데, 이것도 마찬가지로 송신측에서 ack 1 패킷을 받지 못하였으니 다시 패킷을 보낼 수 있도록 한다.
이것은 일정 시간 이후에 ack 1 패킷을 받았지만, 이미 timeout이 일어나서 pkt 1을 다시 전송하게 된다. 그래서 pkt1을 보내자마자 ack 1이 들어왔으니, 다시 보낸 패킷이 ack 1로 온 줄 알고 pkt 0을 보낸다. 이후 ack 1을 받았는데 중복된 데이터가 들어간 줄 알고 이전에 보냈던 pkt 1을 다시 보내게 된다. 이후 ack 0을 받아서 중복된 데이터를 보낸줄 알고 pkt 0을 다시 보내는 과정을 반복한다. rdt 3.0이 기능적으로 가장 정확하지만 저런 상황이 발생하면 성능이 굉장히 안좋게 변한다.
거기다가 또 다른 문제는 rdt 3.0은 패킷을 보내고, 패킷을 받을 때까지 대기하기 때문에 태생적으로 성능이 좋지않다.
그래서 이것을 해결한 파이프라인 프로토콜이 있는데 이것은 다음 글에 설명할 예정이다.
반응형'공부 > 컴퓨터 네트워크' 카테고리의 다른 글
6. 혼잡 제어 (0) 2022.05.24 5. 파이프라인 프로토콜, TCP (0) 2022.05.24 3. e-mail, DNS (0) 2022.05.18 2. 애플리케이션 계층 (0) 2022.05.17 1. 컴퓨터 네트워크와 인터넷 (2) 2022.05.10