TCP&IP

TCP/IP 4장 기반서버/클라이언트1

Barbarian developer 2024. 9. 3.

TCP/IP 프로토콜 스택

TCP/IP프로토콜 스택이란?

  • 인터넷 기반의 데이터 송수신을 목적으로 설계된 스택
  • 큰 문제를 작게 나눠서 계층화한 결과
  • 데이터 송수신의 과정을 네 개의 영역으로 계층화 한 결과
  • 각 스택 별 영역을 전문화하고 표준화함
  • 7계층으로 세분화가 되나 , 4계층이 있다만 알면 됨.

LINK&IP 계층

LIN 계층의 기능 및 역할

  • 물리적인 영역의 표준화 결과
  • LAN, WAN, MAN과 같은 물리적인 네트워크 표준 관련 프로토콜이 정의된 영역
  • 아래의 그림과 같은 물리적인 연결의 표준이 된다.

 

IP계층의 기능 및 역할

  • IP는 Internet protocol을 의미함
  • 경로의 설정과 관련이 있는 프로토콜 
  • 오류발생에 대한 대비가 되어 있지 않아 데이터가 손실되거나, 각종 오류가 발생할수 있다.

 

TCP 계층

TCP계층의 기능 및 역할

  • 실제 데이터의 송수신과 관련있는 계층
  • 그래서 전송(transport)계층이라고도 함
  • TCP는 데이터의 전송을 보장하는 프로토콜(신뢰성이 있는 프로토콜),
  • TCP는 신뢰성을 보장하기 때문에 UDP에 비해 복잡한 프로토콜이다.

 

 

IP만을 이용해서도 데이터를 전송할 수 는 있다. 하지만 그 신뢰성은 보장할 수 없기 때문에 TCP를 IP위에 올려서 데이터가 전송이 완료가 되었는지 지속적으로 확인할 하는 것을 TCP프로토콜이라고 한다.

 

APPLICATION 계층

  • 프로그래머에 의해서 완성되는 APPLICATION 계층
  • 응용프로그램의 프로토콜을 구성하는 계층
  • 소켓을 기반으로 완성하는 프로토콜을 의미함
  • 소켓을 생성하면, 앞서 보인 LINK, IP, TCP계층에 의한 내용은 감춰진다.
  • 그러니 응용 프로그래머는 APPLICATION 계층의 완성에 집중하게 된다.

TCP기반 서버, 클라이언트 구현

TCP 서버의 기본적인 함수호출 순서

bind 함수까지 호출이 되면 주소가 할당된 소켓을 얻게 된다.

따라서 listen함수의 호출을 통해서 연결요청이 가능한 상태가 되어야 한다.

이번 단원에서는 바로 이 listen함수의 호출이 의미하는 바에 대해서 주로 학습한다.

 

#include <sys/type.h>

int listen(int sock, int backlog);
	성공시 0, 실패시 -1반환
    
	sock        연결요청 대기상태에 두고자 하는 소켓의 파일 디스크립터 전달, 이 함수의 ㅇ니자로 전달된
    			디스크립터의 소켓이 서버 소켓(리스닝 소켓)이 된다.

	backlog		연결요청 대기큐(Queue)의 크기정보 전달, 5가 전달되면 큐의 크기가 5가 되어
    			클라이언트의 연결요청을 5개까지 대기시킬 수 있다.

 

#include <sys/socket.h>

int accept(int sock, struct sockaddr * addr, socklen_t * addrlen)
	성공시 생성된 소켓의 파일 이스크립터, 실패시 -1 반환.
    	sock 	서버 소켓파일 디스크립터 전달
        
        addr	연결요청한 클라이언트의 주소정보를 담을 변수의 주소 값 전달, 함수호출이 완료되면
        		인자로 전달된 주소의 변수에는 클라이언트의 주소정보가 채워진다.
                
        addrlen 두번째 매개변수 addr에 전달된 주소의 변수 크기를 바이트 단위로 전달,
        		단 크기정보를 변수에 저장한 다음에 변수의 주소값을 전달한다. 그리고 함수호출이 완료되면
        		크기정보로 채워져 있던 변수에는 클라이언트의 주소정보 길이가 바이트 단위로 계산되어 채워진다.

 

연결요청 정보를 참조하여 클라이언트 소켓과의 통신을 위한 별도의 소켓을 추가로 하나 더 생성한다. 그리고 이렇게 생성된 소켓을 대상으로 데이터의 송수신이 진행된다. 실제로 서버의 코드를 보면 실제로 소켓이 추가로 생성되는 것을 확인할수 있다.

 

TCP 클라이언트의 기본적인 함수호출순서

#include <sys/socket.h>

int connect(int sock, const struct sockaddr * servaddr, socklen_t addrlen);

	성공 시 생성된 소켓의 파일 디스크립터, 실패시 -1 반환
    
    sock 클라이언트 소켓의 파일 디스크립터 전달]
   
	servaddr 연결요청 한 클라이언트의 주소정보를 담을 변수의 주소 값 전달, 함수호출이 완료되면 
    		 인자로 전달된 주소의 변수에는 클라이언트의 주소정보가 채워진다.
             
    addrlen 두번째 매개변수 servaddr에 전달된 주소의 변수 크기를 바이트 단위로 전달, 단, 크기정보를
    		변수에 저장한 다음에 변수의 주소값을 전달한다. 그리고 함수호출이 완료되면 크기정보로 채워져 있던
    		변수에는 클라이언트의 주소정보 길이가 바이트 단위로 계산되어 채워진다.

 

클라이언트의 경우 소켓을 생성하고, 이 소켓을 대상으로 연결의 요청을 위해서 connect 함수를 호출하는 것이 전부니다. 그리고  connect함수를 호출할 때 연결할 서버의 주소 정보도 함께 전달한다.

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char *argv[])
{
	int serv_sock;
	int clnt_sock;

	struct sockaddr_in serv_addr;
	struct sockaddr_in clnt_addr;
	socklen_t clnt_addr_size;

	char message[]="Hello World!";
	
	if(argc!=2)
    {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	
	serv_sock=socket(PF_INET, SOCK_STREAM, 0); //서버 소캣의 생성
	if(serv_sock == -1)
		error_handling("socket() error");
	
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_addr.sin_port=htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr))==-1 )
		error_handling("bind() error"); //bind 함수를 호출하고 있다.
	
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");연결요청 대기상태로 들어가기 위해 listen함수를 호출. 27에서 생성한 소켓을 가르켜 서버 소켓이 됨.
	
	clnt_addr_size=sizeof(clnt_addr);  
	clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size); accept 함수 호출. 대기큐에서 첫 번째로 대기중에 있는 연결요청을 참조하여 클라이언트와의 연결을 구성. 디스크립터를 반환. 이 함수가 호출되었을 때, 대기큐가 비어있다면, 클라이언트의 연결요청이 올때까지 accept함수는 값을 반환하지 않음.
	if(clnt_sock==-1)
		error_handling("accept() error");  
	
	write(clnt_sock, message, sizeof(message)); //클라이언트에게 데이터를 정송중 rmflrh close함수 호출을 통해서 연결을 끝음.
	close(clnt_sock);	
	close(serv_sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

void error_handling(char *message);

int main(int argc, char* argv[])
{
	int sock;
	struct sockaddr_in serv_addr;
	char message[30];
	int str_len;
	
	if(argc!=3)
    {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	sock=socket(PF_INET, SOCK_STREAM, 0); //소켓생성
	if(sock == -1)
		error_handling("socket() error");
	
	memset(&serv_addr, 0, sizeof(serv_addr)); //IP PORT정보 초기화
	serv_addr.sin_family=AF_INET;
	serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
	serv_addr.sin_port=htons(atoi(argv[2]));
		
	if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1) 
		error_handling("connect() error!"); // connect 함수호출을 통해서 서버로 부터 전송되는 데이터를 수신하고 있다.
	
	str_len=read(sock, message, sizeof(message)-1); //연결요청을 성공한 후에 서버로부터 전송되는 데이터를 수신하고 있다.
	if(str_len==-1)
		error_handling("read() error!");
	
	printf("Message from server: %s \n", message);  
	close(sock); //데이터 수신 이후에 close 함수 호출을 통해서 소켓을 닫고 있다. 따라서 서버와의 연결은 종료가 된다.
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

Iterative 기반의 서버, 클라이언트 구현

Iterative 서버의 구현

accept 함수가 호출 된 다음에 입출력 함수인 read, write함수를 호출하고 있다. close함수를 호출하고 있는데, 이는 서버 소켓을 대상으로 하는 것이아니라 accept 함수의 호출과정에서 생성된 소켓을 대상으로 하는 것이다. 

close 함수까지 호출되었다면 한 클라이언트에 대한 서비스가 완료된 것. 그럼 이어서 또다른 클라이언트에게 서비스 하기 위해서 accept함수부터 반복을 돌린다.

 

Iterative에코 서버, 에코 클라이언트

앞서 설명한 형태의 서버를 가리켜 Iterative 서버라 한다. 그리고 서버가 Iterative 형태로 동작한다해도 클라이언트 코드에는 차이가 없음을 이해할 수 있을 것이다. 그럼 이번에는 Iteratibe 형태로 동작하는 에코서버, 그리고 이와 함께하는 에코 클라이언트를 작성해 보겠다. 먼저 프로글매의 기본 동작방식을 정리해 보겠다.

  • 서버는 한 순간에 하나의 클라이언트와 연결되어 에코서비스를 제공한다.
  • 서버는 총 다섯 개의 클라이언트에게 순차적으로 서비스를 제공하고 종료한다.
  • 클라이언트는 프로그램 사용자로부터 문자열 데이터를 입력 받아서 서버에 전송한다. 
  • 서버는 전송 받은 문자열 데이터를 클라이언트에게 재전송한다. 즉, 에코 시킨다.
  • 서버와 클라이언트간의 문자열 에코는 클라이언트가 Q를 입력할 때까지 계속한다.
#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, clnt_sock;
	char message[BUF_SIZE];
	int str_len, i;
	
	struct sockaddr_in serv_adr;
	struct sockaddr_in clnt_adr;
	socklen_t clnt_adr_sz;
	
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);   
	if(serv_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=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");
	
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");
	
	clnt_adr_sz=sizeof(clnt_adr);

	for(i=0; i<5; i++) 5개의 클라이언트에게 서비를 제공하기 위한 반복문
	{
		clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
		if(clnt_sock==-1)
			error_handling("accept() error");
		else
			printf("Connected client %d \n", i+1);
	
		while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)//실제 에코서비스가 이뤄지는 부분
			write(clnt_sock, message, str_len);

		close(clnt_sock);   //소켓을 대상으로 close함수가 호출되면, 연결되어 있던 상대방 소켓에게 EOF가 전달된다.
        					//즉, 클라이언트 소켓이 close함수를 호출하면 50행의 조건은 거짓이 되어 실행한다.
	}

	close(serv_sock);	//5개의 클라이언트에게 서비스를 제공하고 나면, 마지막으로 서버 소켓을 종료하면서 프로그램을 종료한다.
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

#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 sock;
	char message[BUF_SIZE];
	int str_len;
	struct sockaddr_in serv_adr;

	if(argc!=3) {
		printf("Usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}
	
	sock=socket(PF_INET, SOCK_STREAM, 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]));
	
	if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1) //connect 함수가 정상적으로 호출을 완료하더라도, accept 함수를 호출하지 않으면 실제 서비스가 이뤄지지 않는다.
		error_handling("connect() error!");
	else
		puts("Connected...........");
	
	while(1) 
	{
		fputs("Input message(Q to quit): ", stdout);
		fgets(message, BUF_SIZE, stdin);
		
		if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
			break;

		write(sock, message, strlen(message));
		str_len=read(sock, message, BUF_SIZE-1);
		message[str_len]=0;
		printf("Message from server: %s", message);
	}
	
	close(sock); //close 함수가 호출되면 상대 소켓으로는 EOF가 전송된다. 즉 EOF는 연결의 끝을 의미한다.
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

에코 클라이언트의 문제점

writd(sock, message, strlen(message));
str_len=read(sock, message, BUF_SIZE-1);
message[str_len]=0;
printf("Message from sever: %s", message);

 

위의 코드는 다음과 같은 잘못된 가정이 존재한다.

"read, write 함수가 호출될 때마다 문자열 단위로 실제 입출력이 이뤄진다."

댓글