UDP기반 서버, 클라이언트 구현

UDP 서버, 클라이언트는 TCP와 같이 연결된 상태로 데이터를 송수진하지 않는다. 때문에 TCP와 달리 연결 설정의 과정이 필요 없다. 따라서 TCP 서버 구현과정에서 거쳤던 listen 함수와 accept 함수의 호출은 불필요하다. UDP 소켓의 생성과 데이터의 송수신 과정만 존재할 뿐이다.

UDP에서는 서버건 클라이언트건 하나의 소켓만 있으면 된다.

UDP 기반의 데이터 입출력 함수

#include <sys/socket.h>

ssize_t sendto(int sock, void *buff, size_t nbytes, int flags, 
               struct sockaddr *to, socklen_t addrlen);
// 성공 시 전송된 바이트 수, 실패 시 -1 반환
  • sock: 데이터 전송에 사용될 UDP 소켓의 파일 디스크립터를 인자로 전달
  • buff: 전송할 데이터를 저장하고 있는 버퍼의 주소 값 전달
  • nbytes: 전송할 데이터 크기를 바이트 단위로 전달
  • flags: 옵션 지정에 사용되는 매개변수, 지정할 옵션이 없다면 0 전달
  • to: 목적지 주소정보를 담고 있는 sockaddr 구조체 변수의 주소 값 전달
  • addrlen: 매개변수 to로 전달된 주소 값의 구조체 변수 크기 전달

TCP 기반의 출력함수와 가장 비교되는 것은 목적지 주소정보를 요구한다는 점이다.

#include <sys/socket.h>

ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags,
                 struct sockaddr *from, socklen_t *addrlen);
// 성공 시 수신한 바이트 수, 실패 시 -1 반환
  • sock: 데이터 수신에 사용될 UDP 소켓의 파일 디스크립터를 인자로 전달
  • buff: 데이터 수신에 사용될 버퍼의 주소 값 전달
  • nbytes: 수신할 최대 바이트 수 전달, 때문에 매개변수 buff가 가리키는 버퍼의 크기를 넘을 수 없다
  • flags: 옵션 지정에 사용되는 매개변수, 지정할 옵션이 없다면 0 전달
  • from: 발신지 정보를 채워 넣을 sockaddr 구조체 변수의 주소 값 전달
  • addrlen: 매개변수 from으로 전달된 주소에 해당하는 구조체 변수의 크기정보를 담고 있는 변수의 주소값 전달

UDP 데이터는 발신지가 일정치 않기 때문에 발신지 정보를 얻을 수 있도록 함수가 정의되어 있다.

UDP 클라이언트 소켓의 주소정보 할당

TCP 클라이언트의 경우에는 connect 함수 호출 시 IP와 PORT 주소정보가 자동으로 할당되는데, UDP 클라이언트의 경우에는 그러한 기능을 대신 할만한 함수호출문 조차 보이지 않는다. 어느 시점에 IP와 PORT가 할당되는 것일까?

UDP 프로그램에서는 데이터를 전송하는 sendto 함수호출 이전에 해당 소켓에 주소정보가 할당되어 있어야 한다. 따라서 sendto 함수호출 이전에 bind 함수를 호출해서 주소정보를 할당해야 한다. 물론 bind함수는 TCP 서버의 구현에 호출되었던 함수이다. 그러나 이 함수는 TCP/UDP를 가리지 않으므로 UDP에서도 호출 가능하다.

⚠️만약 첫 번째 sendto 호출 시점에 소켓에 IP, PORT가 할당되지 않았을 경우, 자동으로 할당된다. 따라서 꼭 bind를 호출할 필요는 없다.

UDP의 데이터 송수신 특성과 UDP에서의 connect 함수호출

TCP 기반에서 송수신하는 데이터에는 경계가 존재하지 않는다고 하였는데, 이는 다음의 의미를 지닌다.

“데이터 송수신 과정에서 호출하는 입출력함수의 호출횟수는 큰 의미를 지니지 않는다.”

반대로 UDP는 데이터의 경계가 존재하는 프로토콜이므로, 데이터 송수신 과정에서 호출하는 입출력 함수의 호출횟수가 큰 의미를 지닌다. 때문에 입력함수의 호출횟수와 출력함수의 호출횟수가 완벽히 일치해야 송신된 데이터 전부를 수신할 수 있다.

connected UDP 소켓, unconnected UDP 소켓

TCP 소켓에는 데이터를 전송할 목적지의 IP와 PORT번호를 등록하는 반면, UDP 소켓에는 데이터를 전송할 목적지의 IP와 PORT 번호를 등록하지 않는다. 때문에 sendto 함수호출을 통한 데이터의 전송과정은 다음과 같이 크게 세 단계로 나눌 수 있다.

  • 1단계 : UDP 소켓에 목적지의 IP와 PORT 번호 등록
  • 2단계 : 데이터 전송
  • 3단계 : UDP 소켓에 등록된 목적지 정보 삭제

그런데, 같은 IP, PORT로 n번의 데이터를 n번의 sendto 함수호출을 통해 전송한다고 하면, 너무 비효율적이다. 1단계와 3단계가 UDP 전송과정의 1/3에 해당한다고 하니, UDP소켓을 connected소켓으로 만들어 이 시간을 줄임으로 성능향상을 얻을 수 있다.

connected UDP 소켓 생성

connected UDP 소켓을 생성하는 방법은 의외로 간단하다. UDP 소켓을 대상으로 connect 함수만 호출해주면 된다.

sock = socket(PF_INET, SOCK_DGRAM, 0);
memset(&adr, 0, sizeof(adr));
adr.sin_family=AF_INET;
adr.sin_addr.s_ddr = . . . .
adr.sin_pot = . . . .
connect(sock, (strut sockaddr*)&addr, sizeof(adr));

이렇게 connect로 연결이 완료되면, read, write 함수도 호출이 가능하다. connect는 실제로 연결을 구현하는 것이 아니라, 내부적으로 데이터 전송의 대상을 지정하는 역할을 한다.

윈도우 기반으로 구현하기

sendto와 readfrom 함수는 사실상 리눅스와 차이는 없다.

#include <winsock2.h>

int sendto(SOCKET s, const char* buf, int len, int flags, 
           const struct sockaddr* to, int tolen)
// 성공 시 전송된 바이트 수, 실패 시 SOCKET_ERROR 반환
#include <winsock2.h>

int recvfrom(SOCKET s, char* buf, int len, int flags, 
             struct sockaddr* from, int* fromlen);
// 성공 시 수신한 바이트 수, 실패 시 SOCKET_ERROR 반환