UDP에서의 서버와 클라이언트의 연결은?
UDP 서버, 클라이언트는 TCP와 같이 연결된 상태로 데이터를 송수신하지 않기 때문에 TCP와 달리 연결 설정 과정이 필요가 없습니다. 따라서 TCP 서버 구현과정에서 거쳤던 listen() 함수와 accept() 함수의 호출은 불필요합니다. 소켓의 생성과 데이터의 송수신 과정만 존재할 뿐입니다.
UDP에서는 하나의 소켓만?
TCP에서는 소켓과 소켓의 관계가 일대일 이었기 때문에 서버에서 열 개의 클라이언트에게 서비스를 제공하려면 문지기의 역할을 하는 서버 소켓을 제외하고도 열 개의 데이터 송수신용 소켓이 더 필요했는데, UDP는 서버건 클라이언트건 하나의 소켓만 있으면 됩니다.
아래 그림을 보면 하나의 UDP 소켓으로 두 곳의 호스트를 대상으로 데이터의 송수신이 가능함을 볼 수 있습니다. 이렇듯 UDP 소켓은 하나만 있으면 둘 이상의 호스트와 통신이 가능합니다.
UDP 기반 데이터 입출력 함수
TCP 소켓을 생성하고 나서 데이터를 전송하는 경우에는 주소 정보를 따로 추가하는 과정이 필요없습니다. 왜냐하면 TCP 소켓은 목적지에 해당하는 소켓과 연결된 상태이기 때문입니다. 즉, TCP 소켓은 목적지의 주소정보를 이미 알고있습니다. 그러나 UDP 소켓은 연결상태를 유지하지 않으므로 데이터를 전송할 때마다 반드시 목적지의 주소정보를 별도로 추가해야 합니다. sendto 함수를 이용해서 UDP 기반 데이터 전송을 할 수 있습니다. 이 함수에는 다음과 같은 인자들을 전달해줘야 합니다.
- int sock: 데이터 전송에 사용될 UDP 소켓의 파일 디스크립터를 인자로 전달
- void* buff: 전송할 데이터를 저장하고 있는 버퍼의 주소 값 전달
- size_t nbytes: 전송할 데이터 크기를 바이트 단위로 전달
- int flags: 옵션 지정에 사용된느 매개변수, 지정할 옵션이 없다면 0 전달
- struct sockaddr* to: 목적지 주소정보를 담고 있는 sockaddr 구조체 변수의 주소 값 전달
- socklen_t addrlen: 매개변수 to로 전달된 주소 값의 구조체 변수 크기 전달
// 성공 시 전송된 바이트 수, 실패 시 -1 반환
#include <sys/socket.h>
ssize_t sendto(int sock, void* buff, size_t nbytes, int flags,
struct sockaddr* to, socklen_t* addrlen);
이어서 UDP 기반 데이터 수신에 사용되는 recvfrom 함수를 소개하겠습니다. UDP 데이터는 발신지가 일정치 않아서 발신지 정보를 얻을 수 있도록 함수가 정의되어 있습니다. 즉, 이 함수는 UDP 패킷에 있는 발신지 정보를 함께 반환합니다. 이 함수는 다음과 같은 인자들을 전달해줘야 합니다.
- int sock: 데이터 수신에 사용될 UDP 소켓의 파일 디스크립터를 인자로 전달
- void* buff: 데이터 수신에 사용될 버퍼의 주소 값 전달
- size_t nbytes: 수신할 최대 바이트 수 전달, 때문에 매개변수 buff가 가리키는 버퍼의 크기를 넘을 수 없습니다.
- int flags: 옵션 지정에 사용되는 매개변수, 지정할 옵션이 없다면 0 전달
- struct sockaddr* from: 발신지 정보를 채워 넣을 sockaddr 구조체 변수의 주소 값 전달
- socklen_t addrlen: 매개변수 from으로 전달된 주소에 해당하는 구조체 변수의 크기정보를 담고 있는 변수의 주소 값 전달
// 성공 시 수신한 바이트 수, 실패 시 -1 반환
#include <sys/socket.h>
ssize_t recvfrom(int sock, void* buff, size_t nbytes, int flags,
struct sockaddr* from, socklen_t* addrlen);
UDP 기반 에코 서버와 클라이언트
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char* message);
int main(int argc, char* argv[]) {
int serv_sock;
char message[BUF_SIZE];
int str_len;
socklen_t clnt_adr_sz;
struct sockaddr_in serv_adr, clnt_adr;
if (argc != 2) {
printf("Usage: %s <port>\n", argv[0]);
}
// UDP 소켓 생성
serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
if (serv_sock == -1)
error_handling("UDP socket creation error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
error_handling("bind() error");
while (1) {
clnt_adr_sz = sizeof(clnt_adr);
// 앞에서 bind한 주소로 전달되는 모든 데이터를 수신,
// 물론 데이터의 전달대상에는 제한이 없습니다.
str_len = recvfrom(serv_sock, message, BUF_SIZE, 0,
(struct sockaddr*)&clnt_adr, &clnt_adr_sz);
// 위에서 데이터를 전송한 이의 주소정보도 얻게 되는데,
// 이를 이용해 수신된 데이터를 역으로 전송합니다.
sendto(serv_sock, message, str_len, 0,
(struct sockaddr*)&clnt_adr, clnt_adr_sz);
}
close(serv_sock);
return 0;
}
void error_handling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
이어서 바로 위의 서버와 함께 동작하는 클라이언트를 소개하겠습니다. 클라이언트 코드에 IP와 PORT를 자동으로 할당해주는 connect 함수의 호출이 없음에 유의하며 보기 바랍니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 30
void error_handling(char* message);
int main(int argc, char* argv[]) {
int sock;
char message[BUF_SIZE];
int str_len;
socklen_t adr_sz;
struct sockaddr_in serv_adr, from_adr;
if (argc != 3) {
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_DGRAM, 0);
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(argv[2]));
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_sz = sizeof(from_adr);
str_len = recvfrom(sock, message, sizeof(message), 0,
(struct sockaddr*)&from_adr, &adr_sz);
message[str_len] = 0;
printf("Message from server: %s\n", message);
}
close(sock);
return 0;
}
void error_handling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
결과는 아래와 같습니다.
UDP 클라이언트 소켓의 주소정보 할당
UDP 프로그램에서는 데이터를 전송하는 sendto 함수호출 이전에 해당 소켓에 주소정보가 할당되어있어야 합니다. 따라서 sendto 함수호출 이전에 bind를 사용해 주소정보를 할당해야 합니다. 그리고 만약 sendto 함수호출 시까지 주소정보가 할당되지 않았다면 sendto 함수가 처음 호출되는 시점에 해당 소켓에 IP와 PORT 번호가 자동으로 할당됩니다. 이렇게 한 번 할당되면 프로그램이 종료될 때까지 주소정보가 그대로 유지되므로 다른 UDP 소켓과 데이터를 주고받을 수 있습니다. 물론 IP는 호스트의 IP로, PORT는 임의로 할당하게 됩니다.
'네트워크 프로그래밍 > C' 카테고리의 다른 글
[네트워크 프로그래밍/C] TCP 기반의 Half-close (0) | 2022.02.26 |
---|---|
[네트워크 프로그래밍/C] UDP의 데이터 송수신 특성과 UDP에서의 connect 함수호출 (0) | 2022.02.26 |
[네트워크 프로그래밍/C] UDP에 대한 이해 (0) | 2022.02.25 |
[네트워크 프로그래밍/C] TCP의 이론적 내용 (0) | 2022.02.25 |
[네트워크 프로그래밍/C] TCP 기반 서버, 클라이언트 구현 (0) | 2022.02.22 |