UDP 소켓 프로그래밍


  • UDP에서는 TCP에서 제공하는 신뢰적인 정보 전달, 순차적인 정보 전달, 흐름 제어, 혼잡 제어와 같은 서비스를 제공하지 않는다.

  • 대신 포트번호를 사용하여 데이터를 올바른 프로세스에게 전달해주는 서비스, 즉 전송 계층 프로토콜이 제공해주는 서비스 중 필 수 서비스만을 제공한다.

  • TCP는 위에서 나열한 서비스를 제공하기 위해서 TCP 모듈 간의 정보를 공유해야하고 이를 위해 결국 네트워크 자원을 소모한다.

  • 그에 비해서 UDP는 카운터 파트들 사이에 정보를 공유할 필요가 없으니 그만큼 가볍다고 볼 수 있다.

UDP


  • UDP는 전송 계층 프로토콜의 핵심 기능인 호스트 안에서의 프로세스 식별을 통한 데이터 배달만을 수행하는 프로토콜이다.

  • 따라서 TCP 처럼 안정적이고 순차적인 데이터 전달을 보장하지 않으며, 흐름제어와 혼잡 제어를 수행하지 않는다.

  • 헤더 구조가 단순하며, 주로 동영상 스트리밍, 인터넷 전화와 같은 실시간 응용에 많이 사용된다.

  • TCP는 소켓을 생성하고 연결을 한 이후에 통신이 가능하다, 또한 전송되는 TCP 세그먼트의 성공적인 전달 여부를 확인하기 위해서 수신자 TCP 모듈은 ACK 세그먼트를 추가적으로 전달한다.

  • UDP의 경우 전송할 데이터가 생기면 바로 상대방 UDP 모듈로 전송을 시도한다. 응용 계층으로부터 의뢰받은 데이터에 UDP 포트 넘버가 적혀있는 UDP 헤더만을 붙인 이후에 바로 네트워크 계층에 전송 서비스를 의뢰한다.

UDP 헤더


  • SOURCE PORT, DESTINATION PORT: 출발지 포트와, 목적지 포트인 것을 확인할 수 있다.

  • UDP 데이터 그램의 크기를 나타낸다.

  • CHECK SUM 데이터 무결성 검사를 위한 체크섬

UDP 소켓


  • UDP는 연결지향적이지 않기 때문에, 송신자와 수신자 간의 연결과 연결 종료 절차가 없을 것이고 소켓에 수신자와 송신자 쌍에 대한 정보도 없을 것이다.

  • 따라서 읽기 쓰기를 할 때는 상대방의 주소 정보를 항상 포함해야 될 것이다. 또한 주고 받는 데이터를 바이트 스트림으로 취급하는 TCP와는 다르게 UDP는 하나의 데이터그램 단위로 읽기/쓰기 작업을 진행한다.

UDP 소켓의 특징

  • 연결 / 연결 종료 절차가 없음

  • 소켓에 수신자, 송신자 쌍에 대한 정보를 저장하지 않음

  • 데이터그램 단위로 읽기/쓰기 진행

UDP 서버 클라이언트 모델


소켓 생성

  • 전송 계층 프로토콜로 UDP를 사용하려면, socket 함수의 인자를 다음과 같이 설정해야한다.
socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP);
socket (AF_INET, SOCK_DGRAM, 0);

데이터 전송

  • 소켓 전용 입출력 함수인 sendto를 이용하여 UDP 데이터그램을 전송하는 것이 가능하다.
#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto (
    int sockfd,
    const void *buf,
    size_t len,
    int flags,
    const struct sockaddr *dest_addr,
    socklen_t addrlen
    )
  • 파라미터에 대한 자세한 설명은 아래와 같다.
  • sockfd: 소켓의 파일 디스크립터
  • buf: 전송할 데이어가 저장되어 있는 곳의 첫 주소
  • len: 전송할 데이터의 최대 길이
  • flags: 부가적인 기능을 설정할 수 있는 플래그
  • dest_addr: 목적지 주소
  • addrlen: 목적지 주소 공간의 크기
  • sendto() 함수는 목적지의 주소를 인자로 받는다. 여기서 다음과 같은 의문이 생길 수 있다.
소켓의 출발지 주소는 어떻게 설정하는가?
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>
#include <arpa/inet.h>

void error_proc(const char*);

int main(int argc, char *argv[]) {
  int my_sock, read_len, n_sent;
  char buff[BUFSIZ];
  struct sockaddr_in dest_addr;
  socklen_t addr_len;

  my_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);

  if (my_sock == -1) error_proc("socket");
  memset(&dest_addr, 0, sizeof(dest_addr));
  dest_addr.sin_addr.s_addr = inet_addr(argv[1]);
  dest_addr.sin_family = AF_INET;
  dest_addr.sin_port = htons(atoi(argv[2]));
  addr_len = sizeof(dest_addr);

  while(1) {
    fgets(buff, BUFSIZ - 1, stdin);
    read_len = strlen(buff);
    n_sent = sendto(my_sock, buff, read_len, 0, (struct sockaddr *) &dest_addr,
addr_len);
    printf("%d bytes were sent. \n", n_sent);
  }
  return 0;
}

void error_proc(const char* str) {
  fprintf(stderr, "%s: %s \n", str, strerror(errno));
  exit(1);
}
  • 다음 예제를 실행하고 패킷을 캡처해본면 다음과 같은 사실을 확인할 수 있다.

“출발지 주소를 설정하지 않고 sendto 함수를 호출한 경우, 소켓에 자동으로 IP 주소와 포트번호가 할당된다.”

데이터 수신


#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom (
  int socketfd,
  void *buf,
  size_t len,
  int flags,
  struct sockaddr *src_addr,
  socklen_t *addrlen
)
  • 파라미터에 대한 자세한 설명은 아래와 같다.
  • sockfd: 소켓의 파일 디스크립터
  • buf: 수신할 데이어가 저장될 곳의 첫 주소
  • len: 수신할 데이터의 최대 길이
  • flags: 부가적인 기능을 설정할 수 있는 플래그
  • src_addr: 출발지 주소를 저장할 구조체의 주소
  • addrlen: 출발지 주소 공간의 크기
  • 앞에서 작성한 데이터 전송 프로그램과 통신이 가능한 데이터 수신 프로그램을 만들기 위해서는 수신 프로그램의 포트번호와 전송 프로그램의 목적지 포트가 일치해야 한다.

  • 따라서 수신 측에서는 소켓에 주소를 설정해야한다.

  • 주소의 설정을 위해서 TCP 소켓과 마찬가지로 bind 함수를 이용할 수 있다.

  • bind 함수를 이용하여 소켓에 주소 정보를 설정한 후 recvfrom 함수를 호출하는 형태로 프로그램을 진행해야 한다.

전송 프로그램이 sendto 함수의 인자로 전달하는 포트번호 = 수신 프로그램이 bind 함수의 인자로 전달하는 포트번호

  • 따라서 수신 프로그램은 다음의 흐름으로 진행된다.

소켓 생성(socket) -> 소켓에 주소 설정(bind) -> recvfrom 함수 호출(recvfrom)

수신 프로그램 예제

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <string.h>
#include <errno.h>
#include <stdlib.h>

void error_proc(const char*);

int main(int argc, char **argv) {
  int my_sock, read_len, n_recv, res;
  char buff[BUFSIZ];

  struct sockaddr_in src_addr, dest_addr;
  socklen_t addr_len;

  if (argc != 2) {
    fprintf(stderr, "usage: %s port", argv[0]);
    return 0;
  }

  my_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
  if (my_sock == -1) error_proc("socket");

  memset(&src_addr, 0, sizeof(src_addr));
  src_addr.sin_addr.s_addr = htonl(INADDR_ANY);
  src_addr.sin_family = AF_INET;
  src_addr.sin_port = htons(atoi(argv[1]));

  res = bind(my_sock, (struct sockaddr *)&src_addr, sizeof(src_addr));

  if (res == -1) error_proc("bind");

  addr_len = sizeof(dest_addr);

  while(1) {
    n_recv = recvfrom(my_sock, buff, BUFSIZ - 1, 0, (struct sockaddr *)&dest_addr, &addr_len);
  if (n_recv == -1) error_proc("recv_from");
  printf("%d bytes were recv. \n", n_recv);
  }
  return 0;
}

void error_proc(const char *str) {
  fprintf(stderr, "%s: %s\n", str, strerror(errno));
  exit(1);
}

전송 측 화면

수신측 화면

다수의 클라이언트 처리


  • recvfrom 함수의 특징에 대해서 좀 더 살펴보자면, recvfrom함수를 호출하면 프로그램은 데이터그램이 도착할 때 까지 대기 상태가 된다.

  • 데이터그램이 도착하면 recvfrom 함수는 다음 코드로 제어권을 넘기며 전달받은 데이터의 출발지 주소를 알려준다.

  • 즉 출발지 주소에 관계없이 자신에게 도착한 데이터그램을 가져온다는 특징이 있다.

  • 여기서 알 수 있는 것은 sendto 함수의 목적지도 한 곳으로 정해져 있는 것은 아니라는 점이다.

  • sendto 함수의 목적지는 호출 시마다 바뀔 수 있다.

  • 다시 정리하면 sendto 함수의 목적지와 recvfrom 함수의 출발지는 호출 시마다 바뀔 수 있다. 이 말은 하나의 소켓으로 여러 호스트 또는 여러 프로세스와 통신할 수 있다는 것을 뜻한다.

connect 함수의 역할


  • UDP 소켓은 sendto 함수나 recvfrom 함수를 호출했을 때만 커널과 연결되므로 함수 호출이 끝나면 소켓과 커널의 연결이 해제된다.

  • 커널과 소켓이 연결되고 다시 해제되는 과정에서도 무시하지 못할 만큼의 컴퓨팅 자원이 소모된다.

  • connect 함수는 소켓의 목적지 주소를 설정하여 커널과 소켓을 연결시키는 역할을 한다.

  • 따라서 connect 함수를 사용하면 read 함수와 write 함수를 사용할 수 있다. (해당 소켓을 통해서 통신할 수 있는 목적지가 하나로 한정되기 때문에, sendto, recvfrom) 을 사용하지 않아도 된다.

  • 데이터 전송 시마다 커널과 소켓 사이의 연결과 연결 해제 과정이 없어서 효율이 좋아진다.

  • ICMP 메시지에 대한 통지를 받을 수 있다. (즉 UDP 다이어그램을 수신하는 상대방의 정상동작 여부를 파악할 수 있다.)

참고 문헌


>> Home