[열혈TCP/IP] 05 TCP 기반 서버 / 클라이언트 2

3 minute read

05 TCP 기반 서버 / 클라이언트 2

챕터 4의 마지막에 언급했던 문제는 echo_server에는 없고, echo_client에만 있다. 에코_클라는 read() 호출을 통해서 잣니이 전송한 문자열 데이터를 한방에 수신하기를 원하고 있다. 이를 문자열 데이터가 전송되었을 때 이를 모두 읽어서 출력하는 것으로 해결하면 된다. 즉, 클라가 수신해야할 데이터의 크기를 미리 알고 있기 때문에 이를 활용하면 된다. 전체적으로는 똑같고 달라지는 부분만 입력한다.

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

전송한 데이터 크기만큼 데이터를 수신하기 위해서 read 함수를 반복하고 있다.

아래의 예제 코드는 131페이지에서 설명하고 있는 코드이다. 1바이트 피연산자의 수 - 피연산자 N개 - 연산자를 char형 배열에 저장해서 한 번에 write를 한다.

//opclient.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
#define RLT_SIZE 4
#define OPSZ 4
void error_handling(char *message);

int main(int argc, char *argv[])
{
	int sock;
	char opmsg[BUF_SIZE];
	int result, opnd_cnt, i;
	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...........");

	fputs("Operand count: ", stdout);
	scanf("%d", &opnd_cnt);
	opmsg[0]=(char)opnd_cnt;
	
	for(i=0; i<opnd_cnt; i++)
	{
		printf("Operand %d: ", i+1);
		scanf("%d", (int*)&opmsg[i*OPSZ+1]);
	}
	fgetc(stdin);
	fputs("Operator: ", stdout);
	scanf("%c", &opmsg[opnd_cnt*OPSZ+1]);
	write(sock, opmsg, opnd_cnt*OPSZ+2);
	read(sock, &result, RLT_SIZE);
	
	printf("Operation result: %d \n", result);
	close(sock);
	return 0;
}

void error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}
//opserver.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
#define OPSZ 4
void error_handling(char *message);
int calculate(int opnum, int opnds[], char oprator);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	char opinfo[BUF_SIZE];
	int result, opnd_cnt, i;
	int recv_cnt, recv_len;	
	struct sockaddr_in serv_adr, 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++)
	{
		opnd_cnt=0;
		clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);	
		read(clnt_sock, &opnd_cnt, 1);
		
		recv_len=0;
		while((opnd_cnt*OPSZ+1)>recv_len)
		{
			recv_cnt=read(clnt_sock, &opinfo[recv_len], BUF_SIZE-1);
			recv_len+=recv_cnt;
		}
		result=calculate(opnd_cnt, (int*)opinfo, opinfo[recv_len-1]);
		write(clnt_sock, (char*)&result, sizeof(result));
		close(clnt_sock);
	}
	close(serv_sock);
	return 0;
}

int calculate(int opnum, int opnds[], char op)
{
	int result=opnds[0], i;
	
	switch(op)
	{
	case '+':
		for(i=1; i<opnum; i++) result+=opnds[i];
		break;
	case '-':
		for(i=1; i<opnum; i++) result-=opnds[i];
		break;
	case '*':
		for(i=1; i<opnum; i++) result*=opnds[i];
		break;
	}
	return result;
}

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

05-2 TCP의 이론적인 이야기!

양쪽 호스트의 read() / write()에 대해서는 각각 출력버퍼와 입력버퍼가 있어서 경계가 없는 데이터 송수신이 가능하다. (write와 read의 호출 횟수가 일치하지 않을 수 있다.)

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

입력버퍼의 크기를 초과하는 분량의 데이터 전송을 TCp에서는 일어나지 않는다. 슬라이딩 윈도우라는 프로토콜을 사용한다. 입력 버퍼가 50바이트이고 데이터가 100바이트이면, 입력버퍼의 20바이트가 처리됐다면 70바이트까지는 보낼 수 있도록 통신을 한다는 뜻이다.

TCP의 내부 동작원리1: 상대 소켓과의 연결(3-way handshake)

TCP 소켓 생성에서 소멸의 과정을 거치면서 아래의 단계를 지난다.

  1. 상대 소켓과의 연결
  2. 상대 소켓과의 데이터 송수신
  3. 상대 소켓과의 연결종료

3-way handshake를 간단하게 표현하면 다음과 같다.

  1. A : 연결하자 : [SYN] SEQ: 1000, ACK : -
    • 내가 지금 보내는 패킷에 1000이라는 번호를 부여하니, 잘 받았으면 다음에는 1001번 패킷을 전달하라고 내게말해줘
  2. B : 준비됐어 : [SYN+ACK] SEQ: 2000, ACK : 1001
    • 내가 지금 보내는 패킷에 2000이라는 번호를 부여하니, 잘 받았으면 다음에는 2001번 패킷을 전달하라고 내게말해줘
    • 좀 전에 전송한 SEQ가 1000인 패킷은 잘 받았으니, 다음 번에는 SEQ가 1001인 패킷을 전송하기 바란다.
  3. A : 알겠어 : [ACK] SEQ: 1001, ACK : 2001
    • 1001 보낼게
    • 2000 잘 받았고 다음부터 2001 보내

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

호스트 A가 호스트 B에게 총 200바이트를 두 번에 나눠서 보내는 경우

  1. A: [SEQ] SEQ: 1200, 100 byte data
  2. B: [ACK] 1301
  3. A: [SEQ] SEQ: 1301, 100 byte data
  4. B: [ACK] 1402
  • ACK 번호 = SEQ 번호 + 전송된 바이트 크기 + 1이다.
  • +1은 다음번에 전달될 SEQ의 번호를 알리기 위함이다.
  • A: [SEQ] SEQ: 1301, 100 byte data를 B가 제대로 받지 못하면 timeout이 걸리고 A는 같은 패킷을 재전송한다.

TCP의 내부 동작원리3: 상대 소켓과의 연결종료(Four-way handshaking)

  1. A: 연결 끊고싶습니다. FIN SEQ 5000, ACK -
  2. B: 잠시만요 ACK SEQ 7500, ACK 5001
  3. B: 저도 끊을 준비가 됐습니다. 끊으세요. FIN SEQ 7501, ACK 5001
  4. A: 네 연결 끊겠습니다. ACK SEQ 5001, ACK 7502