[TCP/IP Socket]Chapter 04 - TCP 기반 서버/클라이언트 1

Lee Jeong Min·2021년 1월 29일
0

네트워크

목록 보기
4/17
post-thumbnail

04-1 TCP와 UDP에 대한 이해

TCP/IP 프로토콜 스택

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

TCP/IP 프로토콜의 탄생배경

'인터넷을 통한 효율적인 데이터의 송수신'을 위해 문제를 영역별로 나누어서 해결하다보니 프로토콜이 여러개가 만들어졌다. 이들을 계층 구조를 통해 상호간에 관계를 맺게 되었으며 이렇게 계층화해서 얻게 되는 장점은 표준화 작업을 통한 개방형 시스템의 설계이다.

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

IP 계층

목적지로 데이터를 전송하기 위해서 중간에 어떠한 경로를 거칠지와 관련한 문제를 해결하는 계층이 IP계층이다. 이 계층에서 사용하는 프로토콜이 IP(Internet Protocol)이다. 이 IP 자체는 비 연결지향적이며 신뢰할 수 없는 프로토콜이라 오류발생에 대한 대비가 되어있지 않다고 말한다.

TCP/UDP 계층

IP 계층에서 알려준 경로정보를 바탕으로 데이터의 실제 송수신을 담당한다. 여기서 TCP의 역할에 대해서 좀 더 설명하면 데이터를 주고받는 과정에서 서로 데이터의 주고 받음을 확인하고, 분실된 데이터에 대해서 재전송해주어 데이터 전송을 신뢰하도록 만들어준다.

APPLICATION 계층

위에서 설명한 내용은 소켓을 만들면 데이터 송수신과정에서 자동으로 처리되는 것이다. 최종적으로 소켓이라는 도구가 프로그래머에게 주어졌으며, 이 도구를 통해 무엇인가를 만드는 과정에서 프로그램의 성격에 따라 클라이언트와 서버간의 데이터 송수신에 대한 약속들이 정해지는데, 이를 APPLICATION 프로토콜이라고 한다.


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

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

hello_server.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
void error_handling(char * meassage);

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("accep() error");
    
    write(clnt_sock, message, sizeof(message));
    close(clnt_sock);
    close(serv_sock);
}

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

위에서 하나하나 살펴보면 우선 소켓을 생성한뒤 구조체 변수를 초기화한 후 bind함수를 호출한다. 그리고 연결 요청 대기상태로 들어가기 위해 listen함수를 호출하고, 연결요청 대기 큐의 크기를 5로 설정하고있다. 그 이후에 accept함수가 호출된다. 마지막으로 write함수 호출을 통해서 클라이언트에게 데이터를 전송하고 맞미ㅏㄱ으로 close함수호출을 통해 연결을 끊고 있다.

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

hello_client.c

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

클라이언트 예제에서는 socket을 생성한 후 구조체 정보를 초기화한 후에 connect함수를 호출한다. 여기서 connect는 서버의 accept함수 호출이 아닌 대기 큐에 등록된 상황을 의미하는 것이므로 connect함수가 반환했더라고 당장에 서비스가 이루어지는 것은 아니다. 또한 서버의 bind와 같이 IP와 PORT가 커널에 의해 자동적으로 할당된다. 그래서 bind함수를 명시적으로 호출할 필요가 없다. 마지막으로 데이터를 송수신 한후에 close를 통해 소켓을 닫아준다.

TCP기반 서버, 클라이언트의 함수호출 관계


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

에코 서버와 에코 클라이언트를 여기서 구현할 것이며, 에코 서버는 클라이언트가 전송하는 문자열 데이터를 그대로 재전송하는 것이다.

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, 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(int 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;
    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;
        
        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);
    return 0;
    
}

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

위의 에코 서버, 클라이언트 예제는 1가지 문제점이 있다. 그것은 바로 read, write함수가 호출될 때마다 문자열 단위로 실제 입출력이 이뤄진다라는 가정을 하고 있다. TCP클라이언트는 둘 이상의 write 함수호출로 전달된 문자열의 정보가 묶여서 한번에 서버로 전송할 수 있다. 이러한 TCP의 특성을 생각한다면 문제가 존재하지만 위 예제는 오류없이 정상적으로 작동은 한다. (But 오류 발생 가능성 존재!)


04-4 윈도우 기반으로 구현하기

echo_server_win.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>

#define BUF_SIZE 1024
void ErrorHandling(char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hServSock, hClntSock;
	char message[BUF_SIZE];
	int strLen, i;

	SOCKADDR_IN servAdr, clntAdr;
	int clntAdrSize;

	if (argc != 2)
	{
		printf("Usage: %s <port> \n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error!");

	hServSock = socket(PF_INET, SOCK_STREAM, 0);
	if (hServSock == INVALID_SOCKET)
		ErrorHandling("socket() error!");

	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
	servAdr.sin_port = htons(atoi(argv[1]));

	if (bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
		ErrorHandling("bind() error!");

	if (listen(hServSock, 5) == SOCKET_ERROR)
		ErrorHandling("listen() error!");

	clntAdrSize = sizeof(clntAdr);

	for (i = 0; i < 5; i++)
	{
		hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSize);
		if (hClntSock == -1)
			ErrorHandling("accept() error!");
		else
			printf("Connected client %d \n", i + 1);

		while ((strLen = recv(hClntSock, message, BUF_SIZE, 0)) != 0)
			send(hClntSock, message, strlen, 0);

		closesocket(hClntSock);
	}

	closesocket(hServSock);
	WSACleanup();
	return 0;
}

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

echo_client_win.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <WinSock2.h>

#define BUF_SIZE 1024
void ErrorHandling(char* message);

int main(int argc, char* argv[])
{
	WSADATA wsaData;
	SOCKET hSocket;
	char message[BUF_SIZE];
	int strLen;
	SOCKADDR_IN servAdr;

	if (argc != 3)
	{
		printf("Usage: %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error!");

	hSocket = socket(PF_INET, SOCK_STREAM, 0);
	if (hSocket == INVALID_SOCKET)
		ErrorHandling("socket() error!");

	memset(&servAdr, 0, sizeof(servAdr));
	servAdr.sin_family = AF_INET;
	servAdr.sin_addr.s_addr = inet_addr(argv[1]);
	servAdr.sin_port = htons(atoi(argv[2]));

	if (connect(hSocket, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
		ErrorHandling("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;

		send(hSocket, message, strlen(message), 0);
		strLen = recv(hSocket, message, BUF_SIZE - 1, 0);
		message[strLen] = 0;
		printf("Message from server: %s", message);

	}
	closesocket(hSocket);
	WSACleanup();
	return 0;
}

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

리눅스 기반 예제를 윈도우 기반으로 변경할때에는 다음 네 가지를 기억하면된다.

  • WSAStartup, WSACleanup 함수호출을 통한 소켓 라이브럴의 초기화와 해제
  • 자료형과 변수의 이름을 윈도우 스타일로 변경
  • 데이터 송수신을 위해 read,write 함수 대신 recv, send함수 호출하기
  • 소켓의 종료를 위해 close 대신 closesocket함수 호출하기
profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글