2023. 11. 26. 01:32ㆍServer/소켓프로그래밍
개요
보통 자주 사용하는 네트워크 프로토콜 이라고 한다면, UDP/TCP ,SSL, HTTP 등, 다양한 프로토콜이 존재합니다. 하지만 4계층인 Transport Layer 에서 주로 사용하는 프로토콜은 UDP와 TCP가 존재합니다.
이 둘은 확연한 장/단점이 있기에, 사용 용도에 따라 취사 선택하여 사용하곤 합니다.
지난 포스팅에서 보았듯이, UDP는 stateless 하기 때문에, 상대방과 연결을 맺지 않는 반면
TCP는 stateful하기 때문에, 상대방과의 연결을 맺고 ( 터널을 뚫고 ) 직접 통신을 진행합니다.
이를 3-way-handshaking 이라고 합니다.
흐름도
소켓 프로그래밍의 절차도 동일합니다.
단지 네트워크 프로토콜을 프로그래밍 언어로 구현한 라이브러리인데, 다르다면 그게 더 이상할 것입니다.
UDP Client 의 흐름도
상당히 심플한 모습입니다. 순서대로 설명하자면
- 소켓을 생성합니다.
sendto()
를 통해 상대방에게 전송합니다.- 서버는
recvfrom()
을 통해 정보를 읽어들입니다. close()
를 통해 소켓을 닫습니다.
뒤에서 보게 될 TCP socket 은 send()
, recv()
인 반면, UDP 소켓은 to 와 from 이 붙습니다.
이유가 뭘까요 ?
답은 앞서 설명한 UDP의 특성과 연결됩니다. UDP는 클라이언트와 직접적으로 연결을 맺고 있지 않기 때문에, 상대방의 정보를 알지 못합니다. 즉, 데이터를 전송하고 받을 때에, 어느 주소로 주고 받는지를 명시해야만 정보를 주고 받을 수 있습니다.
UDP Server 의 흐름도
UDP 를 사용하는 서버의 모습입니다 .
클라이언트와 동일하나 작업이 한개 추가되었습니다 . bind()
입니다. 이전 포스팅에서 말씀드렸듯이, 서버는 소켓 생성 시에 bind를 통한 주소 고정이 필요합니다. (주소가 계속 바뀌는 서버를 생각 해보세요..)
(서버는 bind 가 필수 작업이지만, 클라이언트는 선택사항입니다.)
TCP Client 의 흐름도
이번에는 TCP를 사용하는 Client 의 흐름도 입니다. 흐름을 설명하자면 이렇습니다.
- 소켓 생성
- 연결 작업 ( 터널 뚫기 )
- 연결 완료
send()
,recv()
를 통한 데이터 전송- 소켓 닫기
생각보다 별 거 없어 보입니다. 단지 다른 점이 있다면, connect()
작업이 추가되었습니다. 이는 위에서 말한 TCP의 특성과 연결하여 생각하면 이해가 빠릅니다. TCP는 상대방과 연결을 형성하는 작업이 필요합니다.
게임에서의 친구 요청과 비슷하게, connect()
를 통해 연결 요청을 , 서버는 accept()
를 통해 연결을 형성합니다.
TCP Server 의 흐름도
이번에는 뭔가 많이 추가된 모습입니다 ! 흐름도를 설명해보겠습니다.
- 소켓 생성
bind()
를 통한 주소 고정listen()
을 통한 연결 요청 대기 (Passive Socket)accept()
를 통한 연결 수락 (Active Socket)send()
,recv()
를 이용한 데이터 전송- 소켓 닫기
단계가 더 늘어났습니다. 심지어 의미를 알기 어려운 단어도 사용하고 있네요.
위 작업들은 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 |
---|