데이터의 경계가 존재하는 UDP 소켓
UDP는 데이터의 경계가 존재하는 프로토콜이므로 데이터 송수신 과정에서 호출하는 입출력 함수의 호출횟수가 큰 의미를 가집니다. 따라서 입력함수의 호출횟수와 출력함수의 호출횟수가 완벽히 일치해야 송신된 데이터 전부를 수신할 수 있습니다. 간단한 예제를 보며 살펴보겠습니다.
#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];
struct sockaddr_in my_adr, your_adr;
socklen_t adr_sz;
int str_len;
if (argc != 2) {
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_DGRAM, 0);
if (sock == -1)
error_handling("socket() error");
memset(&my_adr, 0, sizeof(my_adr));
my_adr.sin_family = AF_INET;
my_adr.sin_addr.s_addr = htonl(INADDR_ANY);
my_adr.sin_port = htons(atoi(argv[1]));
if (bind(sock, (struct sockaddr*)&my_adr, sizeof(my_adr)) == -1)
error_handling("bind() error");
for (int i = 0; i < 3; i++) {
sleep(5); // delay 5 sec
adr_sz = sizeof(your_adr);
str_len = recvfrom(sock, message, BUF_SIZE, 0,
(struct sockaddr*)&your_adr, &adr_sz);
printf("Message %d: %s\n", i + 1, message);
}
close(sock);
return 0;
}
void error_handling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
이어서 보이는 코드는 앞서 보인 코드에 데이터를 전송하는 예제입니다. 이 예제에서는 총 3회의 sendto 함수호출을 통해 문자열 데이터를 전송합니다.
#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 msg1[BUF_SIZE] = "HI!";
char msg2[BUF_SIZE] = "I'm another UDP host!";
char msg3[BUF_SIZE] = "Nice to meet you";
struct sockaddr_in your_adr;
socklen_t your_adr_sz;
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(&your_adr, 0, sizeof(your_adr));
your_adr.sin_family = AF_INET;
your_adr.sin_addr.s_addr = inet_addr(argv[1]);
your_adr.sin_port = htons(atoi(argv[2]));
sendto(sock, msg1, sizeof(msg1), 0,
(struct sockaddr*)&your_adr, sizeof(your_adr));
sendto(sock, msg2, sizeof(msg2), 0,
(struct sockaddr*)&your_adr, sizeof(your_adr));
sendto(sock, msg3, sizeof(msg3), 0,
(struct sockaddr*)&your_adr, sizeof(your_adr));
close(sock);
return 0;
}
void error_handling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
결과는 아래와 같습니다.
결국 보내는 쪽에서는 sendto 함수호출로 3회 데이터를 전송하고, 수신하는 쪽에서는 3회의 recvfrom 함수호출로 데이터를 수신합니다. 그런데 수신하는 쪽에는 각 수신마다 5초의 지연시간이 있으므로, recvfrom 함수가 호출되기 전에 이미 3회의 sendto 함수호출이 끝나서 데이터는 이미 수신하는 쪽에 전송된 상태에 놓입니다. TCP라면 이 상황에서 데이터를 모두 읽어들일 수 있겠지만, UDP는 이 상황에서도 3회의 recvfrom 함수호출이 필요합니다.
connected UDP 소켓, unconnected UDP 소켓
TCP 소켓에는 데이터를 전송할 목적지의 IP와 PORT 번호를 등록하는 반면, UDP 소켓에는 전송할 목적지의 IP와 PORT를 등록하지 않습니다. 때문에 sendto 함수호출을 통한 데이터의 전송 과정은 다음과 같이 세 단계로 나눌 수 있습니다.
- 1단계: UDP 소켓에 목적지의 IP번호와 PORT 번호 등록
- 2단계: 데이터 전송
- 3단계: UDP 소켓에 등록된 목적지 정보 삭제
즉, sendto 함수가 호출될 때마다 위의 과정을 반복하게 됩니다. 이렇듯 목적지의 주소정보가 계속해서 변경되기 때문에 하나의 UDP 소켓을 이용해 다양한 목적지로 데이터 전송이 가능한 것입니다. 이렇게 목적지 정보가 등록되어있지 않은 소켓을 가리켜 unconnected 소켓이라고 하고, 목적지 정보가 등록되어있는 소켓을 가리켜 connected 소켓이라 합니다.물론 UDP 소켓은 기본적으로 unconnected 소켓입니다. 그런데 같은 곳으로 여러 번 데이터를 보낼 때, sendto를 호출할 때마다 위의 1, 3단계를 거쳐야 한다면 이는 매우 비합리적입니다. 이런 경우에는 connected 소켓을 사용하는 것이 더 합리적이겠죠.
connected 소켓을 만드는 방법은 간단합니다. 그냥 UDP 소켓을 대상으로 connect 함수를 호출해주면 됩니다. 이렇게 connected 소켓으로 만들어주고 나면 sendto 함수가 호출될 때마다 위의 2단계만 수행해서 데이터 전송을 하게 됩니다. 또한 송수신의 대상이 정해졌기 때문에 write, read 함수로도 데이터를 송수신할 수 있습니다. 간단한 예제를 살펴보겠습니다. 이 코드는 앞서 이 글에서 구현한 에코 서버와 함께 동작합니다.
#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]));
connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
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));
// 위의 주석친 코드가 아래의 코드로 대체 가능해집니다.
write(sock, message, strlen(message));
// adr_sz = sizeof(from_adr);
// str_len = recvfrom(sock, message, sizeof(message), 0,
// (struct sockaddr*)&from_adr, &adr_sz);
// 위의 주석친 코드가 아래의 코드로 대체 가능해집니다.
str_len = read(sock, message, sizeof(message) - 1);
message[str_len] = 0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char* message) {
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
결과는 아래와 같습니다.
'네트워크 프로그래밍 > C' 카테고리의 다른 글
[네트워크 프로그래밍/C] Domain Name System (0) | 2022.02.26 |
---|---|
[네트워크 프로그래밍/C] TCP 기반의 Half-close (0) | 2022.02.26 |
[네트워크 프로그래밍/C] UDP 기반 서버, 클라이언트의 구현 (0) | 2022.02.25 |
[네트워크 프로그래밍/C] UDP에 대한 이해 (0) | 2022.02.25 |
[네트워크 프로그래밍/C] TCP의 이론적 내용 (0) | 2022.02.25 |