S/C based on TCP

with MK·2020년 8월 2일
0

소켓 프로그래밍

목록 보기
6/13
post-thumbnail

TCP & UDP

TCP는 Transmission Control Protocol의 약자로 데이터 흐름을 컨트롤 하는 역할을 한다. 따라서 TCP 소켓의 이해를 위해 컨트롤의 방법과 범위에 대해 이해해야 한다.

TCP/IP 프로토콜 스택

(Application) <-> (TCP, UDP) <-> (IP) <-> (LINK)
TCP/IP는 위와 같이 스택을 총 4 층으로 나눈다. 즉, 데이터 송수신의 과정을 네 개로 계층화 시킨 것이다.
각각의 계층은 소프트웨어이거나 NIC와 같은 하드웨어이다.
(통신이론에서는 데이터 통신에 사용되는 프로토콜 스택을 7계층(OSI 7 Layer)로 세분화 시키나 프로그래밍 관점에서는 4계층으로 충분하다.)

OPEN SYSTEM
프로토콜 계층화의 장점으로 표준화 작업을 통한 개방형 시스템을 들 수 있다. 표준이 존재하는 경우 어떤 특정함에 종속되지 않기 때문에 빠른 속도의 기술발전이 가능하다.

LINK 계층은 물리적인 영역의 표준화에 대한 결과이다. 가장 기본이 되는 영역으로 LAN, WAN, MAN과 같은 네트워크 표준과 관련된 프로토콜을 정의하는 영역이다. 즉, 물리적인 연결에 대한 표준을 해당 계층에서 담당한다.

IP 계층

IP(Internet Protocol)의 경우 경로의 선택을 담당한다. IP 자체는 비 연결지향적이며 신뢰할 수 없는 프로토콜이다. 데이터 전송 시 경로를 선택해 주지만, 경로가 일정치 않다. 예를 들어 경로상에 문제가 발생하면 다른 경로를 선택해 주는데, 이 과정에서 데이터가 손실되거나 오류가 발생할 수 있다.

TCP/UDP 계층

해당 계층은 IP계층에서 알려준 경로정보를 바탕으로 데이터의 실제 송수신을 담당한다. 결과적으로 해당 계층은 전송(Transport) 계층이라고 한다. TCP의 경우 연결지향으로 신뢰성 있는 데이터 전송을 담당한다.
IP의 경우 하나의 데이터 패킷이 전송되는 과정에만 중심을 두고 설계되어 여러 개의 패킷을 전송할 경우 순서는 물론 전송 그 자체를 신뢰할 수 없다. 반면에 TCP가 추가되면 호스트끼리 대화를 주고받는 형식으로 문제가 발생할 경우 그 문제를 해결하며 데이터 전송에 신뢰를 부여한다.

Application 계층

프로그램의 성격에 따라 클라이언트와 서버간의 데이터 송수신에 대한 약속들이 정해지는데, 이를 가리켜 Application 프로토콜이라고 한다. 대부분의 네트워크 프로그래밍은 Application 프로토콜의 설계 및 구현이 대부분을 차지한다.

TCP기반 서버, 클라이언트

TCP 서버 함수호출 순서

(1) socket() 소켓 생성
(2) bind() 소켓 주소할당
(3) listen() 연결요청 대기
(4) accept() 연결 허용
(5) read()/write() 데이터 송 수신
(6) close() 연결 종료

연결요청 대기상태

bind()를 통해 주소를 할당을 마쳤다면, listen()을 통해 연결요청 대기 상태로 들어가야한다. 즉 listen() 함수가 호출되어야 클라이언트가 연결요청(connect())을 할 수 있는 상태가 된다. 이전에 connect() 함수가 호출될 경우 오류가 발생한다.

#include <sys/socket.h>
int listen(int sock, int backlog);
// 성공 시 0, 실패 시 -1 반환

sock에 연결요청 대기상태에 두고자 하는 소켓의 파일디스크립터를 전달하고 backlog에 연결요청 대기 큐의 크기정보를 전달한다. 즉, 3을 주면 연결요청을 3개까지 대기시킬 수 있다.

클라이언트의 연결요청도 인터넷을 통해 들어오는 일종의 데이터이기 때문에, 이를 받아들이기 위해서는 당연히 소켓이 필요하고 이 역할을 맡는 것이 바로 서버 소켓이다. 서버 소켓은 문지기 역할로 클라이언트의 연결요청이 들어올 경우 대기큐에 요청을 넣고 이 과정이 모두 끝나면 연결요청을 받아들일 수 있는 상태인 '연결요청 대기상태'가 된다.

클라이언트의 연결요청 수락

대기큐에 있는 순서대로 연결요청을 수락할 경우 데이터를 주고받을 수 있는 상태가 된다. 이미 서버 소켓의 경우 연결 요청의 문지기 역할로 사용했으므로 또 다른 소켓이 필요하다.
그러나 이를 위해서 소켓을 직접 생성할 필요는 없고 함수를 통해서 자동으로 생성되고 클라이언트 소켓과 연결되게 된다. 즉, accept 함수를 호출할 경우 내부적으로 데이터 입출력에 사용되는 소켓이 생성되고 그 소켓의 파일디스크립터를 반환한다.

#include <sys/socket.h>
int accept(int sock, struct sockaddr * addr, socklen_t * addrlen);
// 성공 시 소켓의 디스크립터, 실패 시 -1 반환

sock에는 서버 소켓을, addr에는 연결요청을 한 클라이언트의 주소정보를 담을 변수의 주소 값을 전달한다. 함수호출이 완료되면 인자로 전달된 주소의 변수에는 클라이언트의 주소정보가 채워진다. addrlen에는 addr에 전달된 주소의 변수 크기를 바이트 단위로 전달한다. 단, 크기정보를 변수에 저장한 다음 변수의 주소 값을 전달한다. 함수호출 완료 시 크기정보로 채워져 있던 변수에는 클라이언트 주소정보 길이가 바이트 단위로 계산되어 채워진다.

// listen() 호출 이후..
clnt_addr_size = sizeof(clnt_addr);
clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);

연결요청

#include <sys/sockey.h>
int connect(int sock, struct sockaddr * servaddr, socklen_t addrlen);
// 성공 시 0, 실패 시 -1 반환

sock에는 클라이언트 소켓의 디스크립터를 전달한다. servaddr에는 연결요청 할 서버의 주소정보를 담은 변수의 주소 값을, addrlen에는 servaddr에 전달된 주소의 변수 크기를 바이트 단위로 전달한다.

connect 함수 호출 시 함수가 반환되는 경우는 다음 두가지이다.
(1) 서버에 의한 연결요청 수락
(2) 오류상황으로 연결요청 중단

listen() 후 그리고 accept() 이전에 connect()를 호출해야 하는 것은 맞으나, 서버가 listen() 후 먼저 accept()를 호출하고 뒤이어 connect()가 호출될 수도 있다. 다만 이 경우 클라이언트가 connect()를 호출하기 전까지 서버는 accept 함수가 호출된 위치에서 블로킹 상태에 놓인다.

클라이언트는 왜 bind()를 호출하지 않는가?
클라이언트의 경우도 IP와 PORT가 반드시 할당되어야 한다. 그러나 코드에서는 bind()를 통한 주소할당을 찾아볼 수 없다. 그렇다면 어디서 할당이 내부적으로 되는 것이라 짐작할 수 있다. 클라이언트의 경우는 connect 호출 시 커널에서 IP는 컴퓨터(호스트)에 할당된 IP로 PORT는 임의로 선택에서 할당을 한다.

서버, 클라이언트 코드 복습

여기까지 학습한다면 다음의 코드들을 전부 이해할 수 있다.

#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"); 
	
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");
	
	clnt_addr_size=sizeof(clnt_addr);  
	clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_addr,&clnt_addr_size);
	if(clnt_sock==-1)
		error_handling("accept() error");  
	
	write(clnt_sock, message, sizeof(message));
	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));
	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!");
	
	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);
	return 0;
}

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

0개의 댓글