수업/Network Programming

[Network Programming] UDP socket 통신

hw-ani 2023. 5. 27. 23:02

> UDP 소켓의 특성 (TCP와 비교)

    - SEQ,ACK number 같은 것을 전달하지 않는다. (Flow Control 없음)

    - 연결 설정/해제 같은 과정이 없다.

    - 데이터 분실 및 손실의 위험이 있다.

    - 확인 과정이 없으므로 데이터 전송이 빠르다.

따라서 안전성보단 성능이 중요할 때 UDP를 사용한다. 예를들어 실시간 스트리밍 서비스...

(교수님께선 실제로 해보면 TCP가 그렇게 느리지도 않다고 하심. 이론상 그렇단 것)

 

 

 

TCP는 1:1로 연결되기 때문에, socket 하나가 상대방과 연결되면 그 socket은 그 연결된 상대하고만 데이터를 주고 받을 수 있었다.(read()/write())

하지만 UDP는 연결이라는 개념이 존재하지 않으므로 서버 소켓, 클라이언트 소켓 구분이 없다. 위 그림처럼 한 소켓을 통해 host A에 데이터를 보낼 수도 있고 host B로부터 데이터를 받을 수도 있고, 자유롭다.

명시적으로 bind()를 하든, sendto()하다가 자동으로 IP/PORT가 할당되든, IP/PORT만 할당 되있으면 그쪽으로 들어오는 데이터는 자유롭게 수신 할 수도 있고, 그냥 그 소켓으로 데이터를 다른 곳으로 송신할 수도 있다.(어차피 입출력 버퍼는 나눠져 있음.)

 

 

 


UDP API 호출 순서

 

Server side에선 UDP socket을 특정 IP/PORT로 bind()한다. 그래야 그곳으로 들어오는 데이터를 받을 수 있다.

단 TCP처럼 뭐 연결은 수립하고 할게 없기때문에 그냥 bind()만 해두고, 거기로 들어오는 데이터를 그대로 읽는다.

Client side에선 UDP socket을 만들고 그냥 sendto()를 이용해서 바로 원하는 곳으로 데이터를 보내버린다. 연결의 개념이 없으므로 매번 다른 곳으로 데이터를 보내고 받을 수 있다. 즉, 위에서 Server와 Client로 나누긴했는데 그건 프로그램의 역할 측면에서 server와 client로 나뉜 것이지, TCP 처럼 socket 자체가 server용과 client용으로 구분되진 않는다. TCP에선 연결 요청을 처리하는 server socket이 따로 있었지만, UDP는 server쪽 socket이든 client쪽 socket이든 데이터를 보내고 받고 하는 역할을 다 수행할 수 있어서 구분이 안된다.(물론 server side의 socket은 약속한 port 번호를 써야한다는 건 있겠지만 딱 떼놓고 보면 차이가 없단 말임)

 

Q. UDP에선 server/client socket의 차이가 없다고 했는데, 왜 client 쪽의 socket은 bind() 함수를 호출하지 않지?

A. bind()를 호출하는 이유는 socket에 IP 주소와 PORT 번호를 할당하기 위해서이다. 그래야 통신이 될테니까... 그런데 sendto() 함수를 호출하면 자동으로 해당 socket엔 IP/PORT가 할당된다. 따라서 recvfrom() 먼저 호출하는 server 쪽 소켓은 bind()를 해줘야하지만, sendto()를 먼저 호출하는 client 쪽 소켓은 IP/PORT가 자동으로 할당되므로 bind()를 호출하지 않는 것이다.

 

 

 

 

sendto

#include <sys/socket.h>

ssize_t sendto(int sock, void *buff, size_t nbytes, int flags,
               struct sockaddr * to, socklen_t addrlen);

UDP는 연결의 개념이 없으므로 매번 4번째 인자로 목적지의 주소 정보를 전달해야 한다.

첫번째 인자로 데이터 전송에 사용할 UDP socke file descriptor를 전달한다.

두번째 인자로 전송할 데이터를 저장하고 있는 변수의 주소 값을 전달한다.

세번째 인자로 전송할 데이터의 크기를 byte 단위로 전달한다.

네번째 인자는 옵션 지정에 사용되는데, 딱히 지정할게 없다면 0을 전달한다.

다섯번째 인자로 목적지 주소 정보를 담고 있는 sockaddr 구조체의 주소를 전달한다.

여섯번째 인자로 다섯번째 인자의 크기 정보를 전달한다.

 

성공 시 전송된 byte 수, 실패 시 -1을 반환한다.

 

 

 

 

recvfrom

#include <sys/socket.h>

ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags,
                 struct sockaddr * from, socklen_t *addrlen);

UDP는 연결의 개념이 없으므로 매번 4번째 인자로 전송지의 정보를 "받아온다."

첫번째 인자로 데이터 수신에 사용할 UDP socke file descriptor를 전달한다.

두번째 인자로 데이터 수신에 사용할 변수의 주소 값을 전달한다.

세번째 인자로 수신할 최대 byte 수를 전달한다.

네번째 인자는 옵션 지정에 사용되는데, 딱히 지정할게 없다면 0을 전달한다.

다섯번째 인자로 전송지(발신지) 주소정보를 담을 sockaddr 구조체 변수의 주소를 전달한다.

여섯번째 인자로 다섯번째 인자의 크기 정보를 전달한다.

 

성공 시 수신한 byte 수, 실패 시 -1을 반환한다.

 

 

 

UDP echo Server/Client Example

//server.c 일부분

if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
    error_handling("bind() error");
    
while(1) {
    clnt_adr_size = sizeof(clnt_adr);
    str_len = recvfrom(serv_sock, message, BUF_SIZE, 0,
                       (struct sockaddr*) &clnt_adr, &clnt_adr_size);
    sendto(serv_sock, message, str_len, 0,
           (struct sockaddr*) &clnt_adr, &clnt_adr_size);
}
while(1) {
    fputs("Insert message(q to quit): ", stdout);
    fgets(message, sizeof(message), stdin);
    if (!strcmp(message, "q\n" || !strcmp(message, "Q\n"))
        break;
    
    sendto(sock, message, strlen(message), 0,
           (struct sockaddr*)&serv_adr, sizeof(serv_adr));
    
    adr_size = sizeof(from_adr);
    str_len = recvfrom(sock, message, BUF_SIZE, 0,
                       (struct sockaddr*)&from_adr, &adr_size);
    message[str_len] = 0;
    printf("Message from server: %s", message);
}

 

TCP와 달리 UDP는 데이터가 한 덩어리씩 보내진다.(UDP는 message-based) 즉, 데이터의 경계가 존재하므로 한번의 recvfrom으로 하나의 메시지를 완전히 읽어 들인다. 따라서 TCP에서처럼 보낸 메시지 byte 수만큼 채워질 때까지 받아오는 번거로운 작업은 할 필요 없다.

 

위에서도 말했듯, sendto()를 사용하면 자동으로 IP 주소와 PORT 번호가 그 socket에 할당되므로, sendto() 함수를 먼저 호출하는 client에선 bind()를 사용하지 않은 것이다.

 

 

 

 

connected UDP socket

UDP를 이용한 데이터 전송은 아래 세 과정을 거친다.

1. UDP socket에 목적지의 IP와 PORT 번호 등록

2. 데이터 전송

3. UDP socket에 등록된 목적지 정보 삭제

 

하지만, 일반적인 통신에서는 한번 데이터를 보내면 당번간은 그놈이랑 통신할 확률이 높기때문에 매번 위 3단계를 거치기보다 목적지 정보를 등록해둘 수 있다. 그게 connected UDP이다.

 

connected() 함수를 호출해 connected UDP socket을 만들 수 있다.

그럼 매번 위의 1/3 과정을 거치지 않으며, read/write를 이용할 수도 있다. 매번 목적지 정보를 인자로 주지 않아도 된다.

 

//connected UDP socket 생성

sock = socket(PF_INET, SOCK_DGRAM, 0);  //일반 UDP 소켓 생성
if (sock == -1)
    error_handling("socket() error");

//목적지 주소 정보 설정
memset(&serv_adr, 0, sizeof(serv_adr);
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
serv_adr.sin_port = htons(atoi(arv[2]));

//connect() 함수로 connected UDP 만들기
connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr));

 

 

 


getsockname  &  getpeername

#include <sys/socket.h>

int getsockname(int sockfd, struct sockaddr * localaddr, socklen_t * addrlen);
int getpeername(int sockfd, struct sockaddr * peeraddr, socklen_t * addrlen);

두 함수 다 두번째와 세번째 인자로 반환값을 받아온다.