순간이 영원해 지는 곳

프로그래머입장에서 다시 보는 TCP와 UDP 본문

네트워크 프로그래밍

프로그래머입장에서 다시 보는 TCP와 UDP

nenunena 2009. 7. 4. 22:47

프로그래머 입장에서 보는 TCP와 UDP  패킷 전달

TCP는 신뢰성 있는 통신을 보장한다고 다들 배웠을 것이다.

프로그래머 입장에서 TCP를 다시 보자.


1. in-order delievery(순서대로 전송)

우리가 send(1000byte)를 여러번 호출하는 경우, send()호출 한번에 패킷 하나가 생성되어 전달된다고 가정해보자. 이 경우 네트워크 경로상의 문제로 인해 먼저 전송한 데이터가 나중에 전달될 수 있다.
하지만 프로그래머는 그것을 신경쓰지 않아도 된다. TCP(L4 : Transport Layer)에서 순서가 뒤 바뀐 패킷을 원래대로 조립하여 프로그래머가 다루는 데이터의 형태로 만들어서 상위계층( Application Layer )로 올려준다.
그러므로 송신자가 보낸 순서대로 수신자는 데이터를 받게 된다.


2. segmentation(분할 전송)

하지만 한가지 알아야 할것은 위에서 했던 가정처럼 Send()를 했다고 해서 Send()호출 한번에 항상 패킷 하나가 생성되어 전송되는 것이 아니라는 사실이다.

일단 Send()한 데이터는 MSS( Maximum Segment Size : TCP에서 한번에 전송할 수 있는 segment의 최대크기) 에 맞게 나뉜다.

또한 여러 라우터(L3 : Network Layer 장비)를  거쳐 전달되는 패킷은 프레임헤더가 추가되면서 프레임형태로 캡슐화되어 전달 되는데, 이 프레임에서 헤더부분을 제외한 데이터부분(상위계층의데이터-패킷)의 크기는 MTU( Maximum Transmission Unit )라고 해서 정해져 있다. (보통 1500byte정도의 크기를 가진다.)

* MSS는 MTU에서 IP헤더와 TCP헤더를 제외하면 크기를 계산할 수 있다.
MTU(1500byte) - IP Header(20byte) - TCP Header(20byte) = 1460byte (MSS)

* 이렇게 구한 MSS를 어떻게 쓰는가?
-> 한번 Send() 호출시 보내는 데이터의 크기를 1460byte 이하로 한다.

* 왜?
-> 그보다 크게 Send()를 해봤자 분할되어 전송될 것이므로 분할되지 않게 하는 것이 조금이라도 오버헤드를 줄이게 된다. 또한 분할되어 전송되는 패킷은 손실될 가능성이 높아진다고 한다.


이렇게 이러저리 전달되는 과정에서의 크기가 정해져 있기 때문에 MSS나 MTU보다 큰 데이터를 한번에 Send() 한다고 해도 MSS나 MTU크기에 맞게 잘라서 하나의 데이터가 여러개의 세그먼트(L3)나 프레임(L2)으로 나눠져 전달이 된다.
따라서 송신측이 Send(2000byte)를 호출하고 수신측이 Receive(2000byte)를 호출했을때 Receive()함수의 리턴값인 전달된 데이터의 크기를 확인해보면 2000byte가 되지 않을 수 있다.
반대로, 송신측이 Send(500byte)를 두번 호출하더라도 수신측에서는 Receive(1000byte)한번 호출로 송신측이 보낸 1000byte를 한번에 받을 수도 있다.

한줄로 요약하면, 반드시 Send()한 크기만큼 그대로 패킷 하나가 되어 전송 되는 것이 아니고, 따라서 Receive()한번 호출을 통해 Send한 만큼의 크기를 한번에 다 받지 못 할 수 있다. 전송되는 패킷 크기와 수신되는 패킷 크기가 그때그때 다르다는 말이다.

이럴 경우 원하는 데이터의 크기가 올때까지 받은 데이터를 버퍼링(동적할당한 공간이나 배열같은공간에 저장) 하면서 데이터가 올때마다 Receive()를 호출해야 할 것이다.


3. no loss

사실 이 부분은 프로그램을 만들 때 별로 신경쓰지 않아도 된다. No Loss라 함은 Send()를 하면 무슨일이 있어도 도착한다는 건데, 그렇기 때문에 프로그래머는 그냥 보낸 내용이 다 도착할때가지 Receive()만 호출하면 된다. 위에서 말했듯이 실제 전송이 쪼개져서 가든 한꺼번에 가든 수신이 여러번에 걸쳐서 되든 한번에 되든간에 어쨋든 결국엔 도착한다는 이야기다.

다만 윈도우MFC에서 CAsyncSocket 처럼 비동기 소켓을 사용하는 경우 NIC(Network Interface Card)의 송신버퍼가 꽉찼을때 발생하는 WSAEWOULDBLOCK 에러가 나타날 수 있다. 이때는 보내려던 데이터를 다른 공간(사용자가만든메모리공간)에 저장해 두었다가 OnSend()와 같이 버퍼에 여유공간이 생기면 시스템이 호출해주는 콜백함수에서 다시 Send()를 호출하는식으로 적절한 순간에 Send()를 다시 호출하여 보내지 못한 데이터를 다시 보내야 한다.

참고 : 2009/06/08 - [네트워크 프로그래밍] - CAsyncSocket 콜백함수 호출 시기




이번엔 UDP다!


1. no segmentation?

용어가 이게 맞는지는 잘 모르겠다. 어쨋든... 이 말은 우리가 SendTo(1000byte)하면 ReceiveFrom(1000byte) 에서 1000byte를 그대로 받는 다는 말이다. 위에 TCP처럼 Receive()에서 받은 크기가 보낸 크기보다 작은 경우가 생기지 않는다. 데이터가 쪼개지지 않고 한번에 전송된다고 볼 수 있다.
하지만 또 위에서 말한 MTU가 있기 때문에 사실, 그 보다 큰 데이터는 데이터링크층(L2)에서는 쪼개져서 전송이 된다.

하지만 수신측에서 ReceiveFrom()했을때는, 보낸 크기 그대로 한번에 받을 수 있다는 말이 되겠다.

이론적인 UDP 패킷의 최대 크기는 64KB(65535byte) 이다.
왜냐하면 UDP 헤더에 패킷 크기를 나타내는 부분이 16비트라서 2의 16승으로 나타낼 수 있는 최대값이 한번에 보낼 수 있는 최대 데이터 크기가 되기 때문이다. 그보다 큰 데이터를 보내봤자 그렇게 보낸 줄 알수가 없다.


2. out-of-order delievery, loss

순서가 뒤 바뀐 패킷.. 뭐 사실 요세 우리나라 인터넷 환경에서는 이런 경우가 거의 생기지 않는다고 한다. 패킷 손실역시 잘 생기진 않지만, TCP처럼 재전송을 통해 손실없는 전송을 보장하는 것이 아니기 때문에 UDP자체는 패킷손실에 대한 대책이 없다. 나는 SendTo() 했는데, 상대방은 ReceiveFrom()으로 받지 못하는 경우가 발생한다.
따라서 음악이나 동영상 같은 중간에 몇개 빠져도 되는 데이터가 아닌데, UDP를 사용하고자 한다면 Reliable UDP를 구현해야 할 것이다.
Reliable UDP는 응용프로그램(Application Layer)에서 TCP에서 사용하는 순서번호, ACK를 이용하여 TCP를 흉내내는 식으로 구현할 수 있다. 프로그래머가 UDP패킷에 순서번호 붙이고 ACK패킷 만들고 해야 한다는 말이다.


3. error

reliable UDP를 써보려고 인터넷을 뒤져보니 전달 받은 데이터에 오류가 있을 수 있다는 말이 있었다. 그 이유는 UDP헤더에도 TCP헤더처럼 checksum필드가 있는데, UDP의 경우 이 checksum필드의 사용이 옵션이라는 것이다. checksum필드는 전송중에 데이터 비트값이 변경되는 경우 데이터의 내용이 달라지는 일(error)이 발생할 수 있으므로 그런 일이 있었는지 확인하고 이상이 있으면 받은 패킷을 버리는 데 사용한다.
이 사실을 알고 '아놔~ 오류 확인 까지 프로그램에서 구현해야 하나' 했는데, 결과적으로 그럴 필요 없었다.

wireshark라는 패킷 스니핑 프로그램을 이용해 떠돌아 다니는 UDP 패킷과 Visual C++ 로 구현한 간단한 UDP에코 프로그램의 UDP 패킷의 checksum필드를 보니 모두 checksum필드를 사용하고 있었다. (사용하지 않으면 모두 0으로 채운다고 한다.)

사용하는 장비가 PC라면 UDP는 운영체제에서 구현되는 것이므로, 뭐 확신할 수는 없지만 UDP의 경우 checksum을 확인해서 오류가 있는 패킷(데이터그램)은 버린다고 생각하면 되겠다. 그리고 이 경우 당연히 패킷 손실(loss)이 발생한 경우가 된다.




소켓 프로그래밍책 예제 보며 TCP와 UDP 에코 서버/클라이언트 만들어서 TCP랑 UDP가 별거 아니네 했던 사람들은 파일전송 프로그램을 만들어 보면 이게 별거가 아닌게 아니구나(?!) 하게 될 것이다.

예제는 그냥 함수사용법과 호출순서정도를 알려주는 수준이라고 생각된다.

역시 좀 많이 쓰고 유용한 프로그램을 짜봐야 진짜 핵심에 다가설 수 있는 것 같다.

Comments