UDP 소켓은 신뢰할 수 없는 전송방법을 제공하나, TCP 보다 훨씬 간결한 구조로 설계되어 있다. 신뢰성보다는 성능이 중요시되는 상황에서는 UDP가 좋은 선택이 될 수 있다.
TCP의 경우 신뢰성 없는 IP를 기반으로 신뢰성 있는 데이터의 송수신을 위해서 플로우 컨트롤을 하는데, UDP에는 그러한 기능이 존재하지 않는다.
호스트 B를 떠난 UDP 패킷이 호스트 A에게 전달되도록 하는 것은 IP의 역할이다. 전잘된 UDP 패킷을 호스트 A 내에 존재하는 UDP 소켓 중 하나에게 최종 전달하는 것이 UDP 역할이다. 즉, 호스트로 수신된 패킷을 PORT 정보를 참조해 최종 목적지인 UDP 소켓에 전달하는 것이다.
압축파일의 경우 반드시 TCP 기반으로 송수신이 이루어져야 한다. 압축파일을 일부만 손실되어도 해제가 어렵기 때문이다. 그러나 인터넷 기반 스트리밍 서비스의 경우 일부 데이터가 손실되어도 크게 문제가 되지 않는다.
TCP가 언제나 느린 것은 아니다. TCP가 UDP에 비해 느린 이유는 다음과 같다.
따라서 송수신하는 데이터의 양은 작으면서 잦은 연결이 필요한 경우 UDP가 훨씬 효율적이다.
즉, listen과 accept 함수의 호출이 불필요하다. UDP 소켓의 생성과 데이터의 송수신 과정만 존재할 뿐이다.
편지를 주고받기 위해서 필요한 우체통을 UDP 소켓에 비유할 수 있다. 우체통이 근처에 하나 있다면, 이를 통해 어디건 편지를 보낼 수 있는 것과 마찬가지이다.
TCP 소켓의 경우 연결의 과정에서 주소를 입력하므로 데이터를 보낼 때 마다 주소 정보를 넣을 필요가 없다. 그러나 UDP는 데이터를 전송할 때마다 반드시 목적지의 주소정보를 추가해야 한다.
ssize_t sendto(int sock, void *buff, size_t nbytes, int falgs,
struct sockaddr *to, socklen_t addrlen);
해당 함수는 성공 시 전송된 바이트 수를 실패 시 -1을 반환한다.
UDP 데이터는 발신지가 일정하지 않기 때문에 발신지 정보를 얻을 수 있도록 함수가 정의되어 있다.
ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags,
struct sockaddr *from, socklen_t *addrlen);
해당 함수는 성공 시 수신한 바이트 수를 실패 시 -1을 반환한다.
uecho_server.c
#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 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]);
exit(1);
}
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);
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);
}
uecho_client.c
#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, BUF_SIZE, 0,
(struct sockaddr*)&from_adr, &adr_sz);
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);
}
UDP의 경우 sendto 이전에 bind를 통해 주소 정보를 할당할 수 있다. 그러나 그 이전까지 주소 정보를 할당하지 않는다면 sendto 함수가 처음 호출되는 시점에 해당 소켓에 IP와 PORT 번호가 자동으로 할당되어 종료시까지 유지된다. 이 방법이 일반적인 UDP 주소 할당 방법이다.
TCP 기반에서 송수신하는 데이터에는 경계가 존재하지 않는다. 즉, 데이터 송수신 과정에서 호출하는 입출력함수의 호출횟수는 큰 의미를 지니지 않는다.
반면 UDP의 경우 데이터의 경계가 존재하므로 입출력함수의 호출횟수가 큰 의미를 지닌다.
입력함수의 호출횟수와 출력함수의 호출횟수가 완벽히 일치해야 한다.
하나의 주소에 UDP 소켓으로 여러번 데이터를 전송하는 경우 연결을 시키는 것에 이점이 있다. 또한, 연결이 된 상태에서는 read, write를 쓸 수 있다는 이점 또한 있다.
#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, BUF_SIZE, 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);
}
TCP에서는 연결과정보다 종료과정이 중요하다. 예상치 못한 일이 발생할 수 있기 때문에 종료과정이 명확해야 한다.
리눅스의 close 함수는 완전 종료를 의미하고 데이터의 수신, 발신이 불가능한 상황을 만든다. 즉, 오고있던 데이터가 도중에 잘려버릴 수 있다.
이러한 문제를 해결하기 위해, 데이터의 송수신에 사용되는 스트림의 일부만 종료하는 Half-close를 사용한다.
두 호스트가 연결된 상태를 스트림이 형성된 상태라고 한다. 스트림은 물의 흐름을 의미하고, 물의 흐름은 한쪽 방향으로만 형성된다. 마찬가지로 소켓 스트림 역시 한 방향으로만 데이터의 이동이 가능하기 때문에, 양방향 통신을 위해서는 두 개의 스트림이 필요하다.
Half-close는 이 두 스트림 중 하나의 스트림만을 끊는 것이다.
int shutdown(int sock, int howto);
해당 함수는 성공 시 0을, 실패 시 -1을 반환한다.
클라이언트는 언제까지 데이터를 받아야 할지 모른다. 이러한 문제의 해결을 위해 서버는 파일의 전송이 끝났음을 알리는 목적으로 EOF를 전송해야 한다.
그렇다면 서버는 어떻게 EOF를 전달할 수 있을까?
출력 스트림을 종료하면 상대 호스트로 EOF가 전송된다.
Half-close를 통해 EOF를 전달하고 상대측에서 데이터 수신을 완료했다는 메시지를 받을 수 있게 Half-close를 사용한다.
file_server.c
#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 serv_sd, clnt_sd;
FILE * fp;
char buf[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_sz;
if(argc!=2) {
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
fp=fopen("file_server.c", "rb");
serv_sd=socket(PF_INET, SOCK_STREAM, 0);
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]));
bind(serv_sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
listen(serv_sd, 5);
clnt_adr_sz=sizeof(clnt_adr);
clnt_sd=accept(serv_sd, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
while(1)
{
read_cnt=fread((void*)buf, 1, BUF_SIZE, fp);
if(read_cnt<BUF_SIZE)
{
write(clnt_sd, buf, read_cnt);
break;
}
write(clnt_sd, buf, BUF_SIZE);
}
shutdown(clnt_sd, SHUT_WR);
read(clnt_sd, buf, BUF_SIZE);
printf("Message from client: %s \n", buf);
fclose(fp);
close(clnt_sd); close(serv_sd);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
file_client.c
#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 sd;
FILE *fp;
char buf[BUF_SIZE];
int read_cnt;
struct sockaddr_in serv_adr;
if(argc!=3) {
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
fp=fopen("receive.dat", "wb");
sd=socket(PF_INET, SOCK_STREAM, 0);
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(sd, (struct sockaddr*)&serv_adr, sizeof(serv_adr));
while((read_cnt=read(sd, buf, BUF_SIZE ))!=0)
fwrite((void*)buf, 1, read_cnt, fp);
puts("Received file data");
write(sd, "Thank you", 10);
fclose(fp);
close(sd);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}