TCP 10/24

with MK·2020년 10월 24일
0

소켓 프로그래밍

목록 보기
11/13

TCP : Transmissin Control Protocol, 데이터 전송과정의 컨트롤

TCP/IP 프로토콜 스택

인터넷 기반의 효율적인 데이터 전송이라는 커다란 하나의 문제를 하나의 큰 프로토콜로 설계로 한 것이 아니라 작게 나누어 계층화 했고, 이 결과 TCP/IP 프로토콜 스택이 탄생 했다.

각각의 계층을 담당하는 것은 운영체제와 같은 소프트웨어이기도 하고, NIC와 같은 물리적인 장치이기도 하다.

링크 계층

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

IP 계층

목적지로 데이터를 전송하기 위해 중간에 어떤 경로를 거쳐갈 것인지의 문제를 해결하는 것이 IP 계층이다. IP 자체는 비 연결지향적이며 신뢰할 수 없는 프로토콜이다. 데이터를 전송할 때마다 거쳐야 할 경로를 선택해주지만, 그 경로는 일정치 않다. 특히 데이터 전송 도중에 경로상에 문제가 발생하면 다른 경로를 찾아주지만, 이 과정에서 데이터의 손실이나 오류를 해결하지 않는다.

TCP or UDP 계층

해당 계층은 IP 계층에서 알려준 경로정보를 바탕으로 데이터의 실제 송수신을 담당한다. 때문에 이 계층을 전송(Transport) 계층이라 한다.
TCP는 신뢰성 있는 데이터의 전송을 담당한다. 그런데 TCP가 데이터를 보낼 때 기반이 되는 프로토콜이 IP이다.
IP는 오로지 하나의 데이터 패킷이 전송되는 과정에만 중심을 두고 설계되었다. 따라서 여러 개의 패킷을 전송해도 각각의 패킷이 전송되는 과정은 신뢰할 수 없다. 여기에 신뢰를 더하는 역할을 TCP가 해준다.
IP의 상위 계층에서 호스트 대 호스트의 데이터 송수신 방식을 약속하는 것이 TCP 그리고 UDP이며, TCP는 확인절차를 걸쳐서 신뢰성 없는 IP에 신뢰성을 부여한 프로토콜이라 할 수 있다.

Application 계층

소켓이라는 도구를 활용해 무언가를 만드는 과정에서 프로그램의 성격에 따라 클라이언트와 서버간의 데이터 송수신에 대한 약속들이 정해지기 마련인데, 이를 Application 프로토콜이라 한다.
그리고 대부분의 네트워크 프로그래밍은 Application 프로토콜의 설계 및 구현으로 이루어진다.

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

TCP 서버의 함수호출 순서

socket() -> bind() -> listen() -> accept() -> read() / write() -> close()

연결요청 대기상태로의 진입

int listen(int sock, int backlog);
두 번째 인자에 연결요청 대기 큐의 크기정보를 전달한다. 5가 전달되면 큐의 크기가 5가 되어 클라이언트의 연결요청을 5개까지 대기시킬 수 있다.
클라이언트의 연결요청도 일종의 데이터 전송이기 때문에 소켓이 필요하며 이것이 바로 서버 소켓의 역할이다. 일종의 문지기 역할을 한다고 볼 수 있다.

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

listen 함수호출 이후에 연결요청이 들어온다면 들어온 순서대로 수락해야 한다. 서버 소켓은 연결 요청을 받는 역할을 하고 있고 때문에 소켓을 하나 더 만들어야 한다.
다음 함수의 호출 결과로 소켓이 하나 만들어지고 이 소켓은 연결요청을 한 클라이언트 소켓과 자동으로 연결된다.
int accept(int sock, struct sockaddr addr, socklen_t addrlen);
accept 함수는 호출 성공 시 내부적으로 데이터 입출력에 사용할 소켓을 생성하고, 그 소켓의 파일 디스크립터를 반환한다. 중요한 점은 소켓이 자동으로 생성되어, 연결요청을 한 클라이언트 소켓에 연결까지 이루어진다는 점이다.

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

socket() -> connect() -> read() / write() -> close()

클라이언트에 의해 connect 함수가 호출되면 다음 둘 중 한가지 상황에서만 함수가 반환된다.

  • 서버에 의해 연결요청이 접수되었다.
  • 네트워크 단절 등 오류상황이 발생해 연결요청이 중단되었다.

여기서 연결요청의 접수는 accept 함수호출을 의미하는 것이 아니라 연결요청 대기 큐에 등록된 상황을 의미하는 것이다.

클라이언트의 경우 bind 함수를 통해 직접 주소를 할당하지 않아도 connect 함수호출 시 자동으로 소켓에 IP와 PORT가 할당된다. IP의 경우 컴퓨터에 할당된 IP로, PORT는 임의로 선택된다.

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

계속해서 들어오는 클라이언트의 연결요청을 수락하기 위해 서버는 accept 함수 호출을 반복해야 한다.
echo_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 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++)
	{
		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(serv_sock);
	return 0;
}

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

echo_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 1024
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int sock;
	char message[BUF_SIZE];
	int str_len, recv_len, recv_cnt;
	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)
		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;

		str_len=write(sock, message, strlen(message));
		
		recv_len=0;
		while(recv_len<str_len)
		{
			recv_cnt=read(sock, &message[recv_len], BUF_SIZE-1);
			if(recv_cnt==-1)
				error_handling("read() error!");
			recv_len+=recv_cnt;
		}
		
		message[recv_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의 이론적인 이야기

TCP 소켓에 존재하는 입출력 버퍼

서버가 write 함수를 통해 40바이트를 전송해도 클라이언트는 여러번의 read 함수 호출을 통해 10바이트씩 데이터를 수신하는 것이 가능하다. 클라이언트가 10바이트를 먼저 수신한다면, 나머지 30바이트는 도대체 어디에 있는걸까?
write 함수를 호출하는 순간 데이터는 출력버퍼로 이동을 하고, read 함수가 호출되는 순간 입력버퍼에 저장된 데이터를 읽어 들이게 된다.

  • 입출력 버퍼는 소켓 각각에 대해 별도로 존재한다.
  • 입출력 버퍼는 소켓생성시 자동으로 생성된다.
  • 소켓을 닫아도 출력버퍼에 남아있는 데이터는 계속 전송이 이루어진다.
  • 소켓을 닫으면 입력버퍼에 있는 데이터는 소멸된다.

TCP에는 슬라이딩 윈도우라는 프로토콜이 존재한다. 따라서 입력버퍼의 크기를 초과하는 분량의 데이터 전송이 발생하지 않게 한다.

TCP의 내부 동작원리1 : 상대 소켓과의 연결

Three way handshaking을 통해 세 번의 대화를 주고 받는다.

TCP의 내부 동작원리2 : 상대 소켓과의 데이터 송수신

전송된 바이트 크기만큼 추가로 증가시킨다.
데이터를 실어 나를때 전송에 실패하는 경우 타이머가 타임 아웃 될 경우 패킷을 재전송한다.

TCP의 내부 동작원리3 : 상대 소켓과의 연결종료

FIN 메시지를 한번씩 주고 받고서 연결이 종료된다. Four way handshaking

이 모든 것을 TCP 흐름제어(Flow control)이라고 한다.

0개의 댓글