UDP 기반 S/C, 소켓의 연결종료

with MK·2020년 10월 26일
0

소켓 프로그래밍

목록 보기
12/13

UDP 소켓의 특성

UDP 소켓은 신뢰할 수 없는 전송방법을 제공하나, TCP 보다 훨씬 간결한 구조로 설계되어 있다. 신뢰성보다는 성능이 중요시되는 상황에서는 UDP가 좋은 선택이 될 수 있다.
TCP의 경우 신뢰성 없는 IP를 기반으로 신뢰성 있는 데이터의 송수신을 위해서 플로우 컨트롤을 하는데, UDP에는 그러한 기능이 존재하지 않는다.

UDP의 내부 동작원리


호스트 B를 떠난 UDP 패킷이 호스트 A에게 전달되도록 하는 것은 IP의 역할이다. 전잘된 UDP 패킷을 호스트 A 내에 존재하는 UDP 소켓 중 하나에게 최종 전달하는 것이 UDP 역할이다. 즉, 호스트로 수신된 패킷을 PORT 정보를 참조해 최종 목적지인 UDP 소켓에 전달하는 것이다.

UDP의 효율적 사용

압축파일의 경우 반드시 TCP 기반으로 송수신이 이루어져야 한다. 압축파일을 일부만 손실되어도 해제가 어렵기 때문이다. 그러나 인터넷 기반 스트리밍 서비스의 경우 일부 데이터가 손실되어도 크게 문제가 되지 않는다.
TCP가 언제나 느린 것은 아니다. TCP가 UDP에 비해 느린 이유는 다음과 같다.

  • 데이터 송수신 이전, 이후에 거치는 연결설정 및 해제과정
  • 데이터 송수신 과정에서 거치는 신뢰성 보장을 위한 흐름제어

따라서 송수신하는 데이터의 양은 작으면서 잦은 연결이 필요한 경우 UDP가 훨씬 효율적이다.

UDP 기반 구현

UDP에서 서버와 클라이언트는 연결되어 있지 않다.

즉, listen과 accept 함수의 호출이 불필요하다. UDP 소켓의 생성과 데이터의 송수신 과정만 존재할 뿐이다.

UDP에서는 서버건 클라이언트건 하나의 소켓만 있으면 된다.

편지를 주고받기 위해서 필요한 우체통을 UDP 소켓에 비유할 수 있다. 우체통이 근처에 하나 있다면, 이를 통해 어디건 편지를 보낼 수 있는 것과 마찬가지이다.

UDP 기반의 데이터 입출력 함수

TCP 소켓의 경우 연결의 과정에서 주소를 입력하므로 데이터를 보낼 때 마다 주소 정보를 넣을 필요가 없다. 그러나 UDP는 데이터를 전송할 때마다 반드시 목적지의 주소정보를 추가해야 한다.

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

해당 함수는 성공 시 전송된 바이트 수를 실패 시 -1을 반환한다.

  • sock 데이터 전송에 사용될 DUP 소켓의 파일 디스크립터 전달
  • buff 전송할 데이터를 저장하고 있는 버퍼의 주소 값 전달
  • nbytes 전송할 데이터 크기를 바이트 단위로 전달
  • flags 옵션 지정에 사용되는 매개변수, 옵션이 없다면 0 전달
  • to 목적지 주소정보를 담고 있는 sockaddr 구조체 변수의 주소 값 전달
  • addrlen 주소 값의 구조체 변수 크기 전달

UDP 데이터는 발신지가 일정하지 않기 때문에 발신지 정보를 얻을 수 있도록 함수가 정의되어 있다.

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

해당 함수는 성공 시 수신한 바이트 수를 실패 시 -1을 반환한다.

  • sock 데이터 수신에 사용될 UDP 소켓의 파일 디스크립터 전달
  • buff 데이터 수신에 사용될 버퍼의 주소 값 전달
  • nbytes 수신할 최대 바이트 수 전달
  • flags 옵션 지정 변수
  • from 발신지 정보를 채워 넣을 sockaddr 구조체 변수의 주소 전달
  • addrlen 해당 구조체 변수의 크기 정보를 담고있는 변수의 주소값 전달

UDP 기반의 에코 서버와 에코 클라이언트

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 주소 할당 방법이다.

UDP의 데이터 송수신 특성과 connect함수 호출

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 기반의 Half-close

TCP에서는 연결과정보다 종료과정이 중요하다. 예상치 못한 일이 발생할 수 있기 때문에 종료과정이 명확해야 한다.

일방적인 연결종료의 문제점

리눅스의 close 함수는 완전 종료를 의미하고 데이터의 수신, 발신이 불가능한 상황을 만든다. 즉, 오고있던 데이터가 도중에 잘려버릴 수 있다.
이러한 문제를 해결하기 위해, 데이터의 송수신에 사용되는 스트림의 일부만 종료하는 Half-close를 사용한다.

소켓과 스트림

두 호스트가 연결된 상태를 스트림이 형성된 상태라고 한다. 스트림은 물의 흐름을 의미하고, 물의 흐름은 한쪽 방향으로만 형성된다. 마찬가지로 소켓 스트림 역시 한 방향으로만 데이터의 이동이 가능하기 때문에, 양방향 통신을 위해서는 두 개의 스트림이 필요하다.

Half-close는 이 두 스트림 중 하나의 스트림만을 끊는 것이다.

우아한 종료를 위한 shutdown 함수

int shutdown(int sock, int howto);

해당 함수는 성공 시 0을, 실패 시 -1을 반환한다.

  • sock 종료할 소켓의 파일 디스크립터 전달
  • howto 종료방법에 대한 정보를 전달
    SHUT_RD 입력 스트림의 종료
    SHUT_WR 출력 스트림의 종료
    SHUT_RDWR 입출력 스트림의 종료

Half-close가 필요한 이유

클라이언트는 언제까지 데이터를 받아야 할지 모른다. 이러한 문제의 해결을 위해 서버는 파일의 전송이 끝났음을 알리는 목적으로 EOF를 전송해야 한다.
그렇다면 서버는 어떻게 EOF를 전달할 수 있을까?
출력 스트림을 종료하면 상대 호스트로 EOF가 전송된다.
Half-close를 통해 EOF를 전달하고 상대측에서 데이터 수신을 완료했다는 메시지를 받을 수 있게 Half-close를 사용한다.

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);
}

0개의 댓글

관련 채용 정보