Socket - TCP & UDP

2023. 11. 26. 01:32Server/소켓프로그래밍

개요

보통 자주 사용하는 네트워크 프로토콜 이라고 한다면, UDP/TCP ,SSL, HTTP 등, 다양한 프로토콜이 존재합니다. 하지만 4계층인 Transport Layer 에서 주로 사용하는 프로토콜은 UDP와 TCP가 존재합니다.

이 둘은 확연한 장/단점이 있기에, 사용 용도에 따라 취사 선택하여 사용하곤 합니다.

지난 포스팅에서 보았듯이, UDP는 stateless 하기 때문에, 상대방과 연결을 맺지 않는 반면
TCP는 stateful하기 때문에, 상대방과의 연결을 맺고 ( 터널을 뚫고 ) 직접 통신을 진행합니다.
이를 3-way-handshaking 이라고 합니다.

 


흐름도

소켓 프로그래밍의 절차도 동일합니다.

단지 네트워크 프로토콜을 프로그래밍 언어로 구현한 라이브러리인데, 다르다면 그게 더 이상할 것입니다.

UDP Client 의 흐름도

상당히 심플한 모습입니다. 순서대로 설명하자면

  1. 소켓을 생성합니다.
  2. sendto()를 통해 상대방에게 전송합니다.
  3. 서버는 recvfrom() 을 통해 정보를 읽어들입니다.
  4. close()를 통해 소켓을 닫습니다.

뒤에서 보게 될 TCP socket 은 send(), recv() 인 반면, UDP 소켓은 to 와 from 이 붙습니다.
이유가 뭘까요 ?

답은 앞서 설명한 UDP의 특성과 연결됩니다. UDP는 클라이언트와 직접적으로 연결을 맺고 있지 않기 때문에, 상대방의 정보를 알지 못합니다. 즉, 데이터를 전송하고 받을 때에, 어느 주소로 주고 받는지를 명시해야만 정보를 주고 받을 수 있습니다.

UDP Server 의 흐름도

 

UDP 를 사용하는 서버의 모습입니다 .
클라이언트와 동일하나 작업이 한개 추가되었습니다 . bind() 입니다. 이전 포스팅에서 말씀드렸듯이, 서버는 소켓 생성 시에 bind를 통한 주소 고정이 필요합니다. (주소가 계속 바뀌는 서버를 생각 해보세요..)
(서버는 bind 가 필수 작업이지만, 클라이언트는 선택사항입니다.)

TCP Client 의 흐름도

이번에는 TCP를 사용하는 Client 의 흐름도 입니다. 흐름을 설명하자면 이렇습니다.

  1. 소켓 생성
  2. 연결 작업 ( 터널 뚫기 )
  3. 연결 완료
  4. send(), recv() 를 통한 데이터 전송
  5. 소켓 닫기

생각보다 별 거 없어 보입니다. 단지 다른 점이 있다면, connect() 작업이 추가되었습니다. 이는 위에서 말한 TCP의 특성과 연결하여 생각하면 이해가 빠릅니다. TCP는 상대방과 연결을 형성하는 작업이 필요합니다.
게임에서의 친구 요청과 비슷하게, connect() 를 통해 연결 요청을 , 서버는 accept() 를 통해 연결을 형성합니다.

TCP Server 의 흐름도

이번에는 뭔가 많이 추가된 모습입니다 ! 흐름도를 설명해보겠습니다.

  1. 소켓 생성
  2. bind() 를 통한 주소 고정
  3. listen() 을 통한 연결 요청 대기 (Passive Socket)
  4. accept() 를 통한 연결 수락 (Active Socket)
  5. send(), recv() 를 이용한 데이터 전송
  6. 소켓 닫기

단계가 더 늘어났습니다. 심지어 의미를 알기 어려운 단어도 사용하고 있네요.
위 작업들은 TCP 3-Way-HandShaking 을 생각하면 굉장히 이해하기 쉽습니다. 그림을 다시 한번 보겠습니다.

 

TCP 서버는 소켓 생성 후 , 클라이언트의 연결을 수락하기 위하여 listen() 을 수행합니다.
서버가 listen()시 사용하는 소켓은 passive Socket 으로, 연결 수락과 대기열 생성만을 담당하는 소켓입니다.
즉, 실제 통신을 수행하는 소켓이 아닙니다.

int listen(sock, {backlog 숫자});

와 같은 형태를 띄고 있습니다. Backlog 은, 연결 완료를 기다리는 다른 클라이언트들이 대기하는 대기열의 크기입니다. 상대가 연결 요청을 했는데, 아직 완료되지 않은 것을 몇 개 까지 기억하고 있을지에 대한 숫자입니다.

그 후 서버는 accept() 를 수행하여 클라이언트를 대기합니다.
이후 포스팅에서 설명하겠지만, accept()는 클라이언트의 연결을 기다리는 blocking 함수입니다. accept() 를 실행한 서버는 다른 클라이언트의 통신을 처리할 수 없게 되기 때문에, 연결 유실을 방지하기 위해 listen() 을 통해 대기열을 생성하게 됩니다.

이후 클라이언트는 Connect() 를 실행하여 서버에게 연결 요청을 보냅니다. connect() 또한 blocking 함수입니다.

서버가 connect() 를 통해 연결을 받으면, 커널 레벨에서 3 way handShaking 의 일환인 SYN, ACK 비트를 주고 받습니다. 응답을 받은 클라이언트의 Connect() 는 새로운 소켓을 반환하게 되고, 이는 직접적인 데이터 통신에 사용되는 Active Socket입니다.

서버 또한 accpet() 가 완료되고, socket을 반환합니다. 이 소켓을 데이터 통신에 사용하고, Active socket 의 역할을 하게 됩니다.


Network ByteOrder

2바이트 메모리 공간에 정수 0x1234 를 저장할 경우,
메모리 주소는 X 번지와 (X+1) 번지를 사용할 수 있습니다. 이 때 , 시작 주소 X에 높은 바이트의 수 0x12 를 저장할지 , 낮은 바이트의 수 0x34 를 저장할지 선택할 수 있습니다.

Big Endian / Little Endian

)

Byte Order 는 두가지 방법이 존재합니다.

  • Big Endian : 시작 주소에 높은 바이트 수 저장
  • Little Endian : 시작 주소에 낮은 바이트 수 저장

 

왜 필요할까요 ?

CPU 아키텍쳐 마다 사용하는 ByteOrder 가 다르기 때문에, 네트워크 환경에서 통신 시, 통일된 ByteOrder 를 사용하는게 중요합니다. 제가 보낸 1234가 4321로 읽히는 네트워크가 있다면 누구도 사용하지 않을 것입니다.
따라서 네트워크 환경에서는 byte order 를 Big endian 으로 고정하여 전송합니다.

이를 위해서 소켓 라이브러리에서도 지원하는 함수들이 있습니다. 같이 보겠습니다.

  • htons() : host - to - network - short (2bytes)
  • ntohs() : network - to - host - short (2bytes)
  • htonl() : host - to - network - long (4bytes)
  • ntohl() : network - to - host - long (4bytes)

이름이 꽤 복잡해 보이지만, 약자를 풀어본다면 어렵지 않습니다. network 는 big endian 으로 통신한다는 사실을 기억하세요 .


Host to network short -> 네트워크로 short 타입을 전송한다는 뜻으로, 어떤 형식인지는 몰라도 Big endian 으로 전송한다는 사실을 유추할 수 있습니다.

 

Network to host Short -> 네트워크에서 받은 데이터를 host 가 읽을 수 있는 형식으로 전송한다는 뜻으로, 어떤 내 CPU 아키텍쳐가 어떤 형식을 사용하는지는 몰라도, 읽을 수 있도록 변환합니다.

 

Host to network long -> 동일하지만 자료형의 크기가 다릅니다.


Network to host long -> 동일하지만 자료형의 크기가 다릅니다.

 

이렇게 소켓 프로그래밍에서 TCP/UDP 소켓의 절차에 대해서 알아보았습니다! 점점 재밌어지네요

다음 포스팅에서는 TCP/UDP 소켓 코드 예제를 통해 소켓 통신을 익혀보겠습니다.

'Server > 소켓프로그래밍' 카테고리의 다른 글

Socket - Introduction  (1) 2023.11.03