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