본문 바로가기
개인공부/윤성우의 열혈 TCP&IP 소켓 프로그래밍

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

by 하고싶은건많은놈 2023. 2. 24.

05-1 : 에코 클라이언트의 완벽 구현!

에코 클라이언트의 경우 클라이언트가 수신해야 할 데이터의 크기를 미리 알고있음
따라서 데이터가 전부 전송될 타이밍에 맞추어 read() 함수를 호출하는 것으로 기존의 문제점을 해결

// echo_client2.cpp

...

    while (1)
    {
        cout << "Input message(Q to quit) : ";
        cin.getline(message, BUF_SIZE);

        if (!strcmp(message, "q") || !strcmp(message, "Q"))
            break;

        str_len = write(sock, message, srlen(message));

        recv_len = 0;
        while(recv_len < str_len) // 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[str_len] = 0;
        cout << "Message from server : " << message << endl;
    }

    close(sock);
    return 0;
}

단, 에코 클라이언트와 달리 수신할 데이터의 크기를 이전에 파악할 수 없는 경우가 많음
이 경우 어플리케이션 프로토콜의 정의를 활용

  • 데이터 송수신 과정에서 데이터의 끝을 파악할 수 있는 약속(프로토콜)을 별도로 정의
  • 또는 송수신될 데이터의 크기를 미리 알려줌

계산기 서버 예시

// op_server.c

#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUF_SIZE 1024
int        calculate(int num, int *opset);
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;

    if(argc != 2)
    {
        cout << "Usage : " << argv[0] << " <port>\n";
        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, (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);

    for (int i = 0; i < 5; i++)
    {
        clnt_sock = accept(serv_sock, (sockaddr*)&clnt_addr, &clnt_addr_size);
        if (clnt_sock == -1)
            error_handling("accept() error");
        else
            cout << "Connected client " << i + 1 << endl;

        int *opset = new int[BUF_SIZE];

        int num;
        read(clnt_sock, &num, sizeof(int));

        int recv_len = 0;
        while (recv_len < num * sizeof(int) + 1)
            recv_len += read(clnt_sock, opset, BUF_SIZE - 1);

        int result = calculate(num, opset);
        write(clnt_sock, &result, sizeof(int));
        close(clnt_sock);
    }

    close(serv_sock);
    return 0;
}

int        calculate(int num, int *opset)
{
    int result = opset[0];
    char op = static_cast<char>(opset[num]);

    switch(op)
    {
        case '+':
            for (int j = 1; j < num; j++)
                result += opset[j];
            break;
        case '-':
            for (int j = 1; j < num; j++)
                result -= opset[j];
            break;
        case '*':
            for (int j = 1; j < num; j++)
                result *= opset[j];
            break;
    }

    delete opset;
    return (result);
}

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

계산기 클라이언트 예시

// op_client.cpp

#include <iostream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

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

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

    struct sockaddr_in    serv_addr;

    if(argc != 3)
    {
        cout << "Usage : " << argv[0] << " <IP> <port>\n";
        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, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error");
    else
        cout << "Connected.........\n";

    int *opset = new int[BUF_SIZE];
    int num;

    cout << "Operand count : ";
    cin >> num;

    opset[0] = static_cast<char>(num);
    cout << "num : " << num << endl;
    for (int i = 1; i < num + 1; i++)
    {
        cout << "Operand " << i << " : ";
        cin >> opset[i];
    }

    cout << "Operator : ";
    char op;
    cin >> op;
    opset[num + 1] = static_cast<char>(op);

    write(sock, opset, sizeof(int) * (num + 2));

    int result;
    read(sock, &result, sizeof(int));

    cout << "Operation result : " << result << endl;

    close(sock);
    return 0;
}

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

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

write() 함수 호출시 데이터는 먼저 출력버퍼에 전송되고, 이후 적절하게 상대방의 입력버퍼로 옮겨짐
read() 함수 호출시에는 입력버퍼에 저장된 데이터를 가져옴

  • 입출력 버퍼는 TCP 소켓에 대해 각각 별도로 존재
  • 입출력 버퍼는 소켓 생성시 자동으로 생성
  • 소켓을 닫더라도 출력버퍼에 남아있는 데이터는 그대로 전송됨
  • 단, 소켓을 닫을 경우 입력버퍼에 남아있는 데이터는 소멸됨

슬라이딩 윈도우(Sliding Window) 프로토콜으로 인해 입력버퍼의 크기를 초과하는 분량의 데이터전송은 발생하지 않음
즉, TCP에서의 write() 또는 send() 함수는 데이터의 전송이 완료되기 전에는 반환되지 않음


TCP 소켓은 연결설정 과정에서 총 세번의 대화를 주고받는 'Three-way-handshaking' 과정을 거침

  1. SYN(Synchronization) : 데이터 송수신에 앞서 전송되는 동기화 메세지, SEQ에 전송하는 패킷의 번호 부여(1000)
  2. SYN + ACK : 1번에서 전송받은 패킷에 대한 다음번 패킷을 ACK로 요구(1001), 전송하는 패킷을 SEQ에 보내는 패킷 번호 부여(2000)
  3. ACK : 2번에서 전송받은 패킷에 대한 다음번 패킷을 ACK로 요구(2001), SEQ에 전송하는 패킷의 번호 부여(2001)
    패킷에 번호를 부여하여 확인하는 절차를 통해 손실된 데이터의 확인 및 재전송이 가능하기 때문에 손실없는 데이터의 전송이 보장됨

'Three-way-handshaking' 이후 데이터 송수신 과정을 거침

  • 이 때 SEQ에 부여된 패킷번호 + 전송된 바이트 크기 + 1이 ACK로 응답하는 번호가 됨
  • 일정시간이 지나도 ACK 응답이 오지 않는 경우 이전 패킷을 재전송함

소켓 통신 종료시에는 'Four-way handshaking' 과정을 거침

  1. 전송하는 패킷에 FIN 메시지를 삽입하여 통신 종료를 요구
  2. 이 때 전송받은 패킷에 대한 응답은 그대로 보냄
  3. 상대방도 FIN 메시지를 보냄
  4. 그에 대한 응답을 보내며 연결을 종료함

05-3 : 윈도우 기반으로 구현하기

윈도우 기반 계산기 서버 예제

// op_server_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

#define BUF_SIZE 1024
int        calculate(int num, int *opset);
void    ErrorHandling(char* message);

int        main(int argc, char* argv[])
{
    WSADATA        wsaData;
    SOCKET        hServSock, hClntSock;
    SOCKADDR_IN    servAddr, clntAddr;
    int            clntAddrSize;

    if (argc != 2)
    {
        cout << "Usage : " << argv[0] << " <port>\n";
        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(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servAddr.sin_port = htons(atoi(argv[1]));

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

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

    clntAddrSize = sizeof(clntAddr);

    for (i = 0; i < 5; i++)
    {
        hClntSock = accept(hServSock, (SOCKADDR*)&clntAddr, &clntAddrSize);
        if (hClntSock == INVALID_SOCKET)
            ErrorHandling("accept() error");
        else
            cout << "Connected client " << i + 1 << endl;

        int *opset = new int[BUF_SIZE];

        int num;
        recv(hClntSock, &num, sizeof(int), 0);

        int recvLen = 0;
        while (recvLen < num * sizeof(int) + 1)
            recvLen += recv(hClntSock, opset, BUF_SIZE - 1, 0);

        int result = calculate(num, opset);

        closesocket(hClntSock);
    }

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

int        calculate(int num, int *opset)
{
    int result = opset[0];
    char op = static_cast<char>(opset[num]);

    switch(op)
    {
        case '+':
            for (int j = 1; j < num; j++)
                result += opset[j];
            break;
        case '-':
            for (int j = 1; j < num; j++)
                result -= opset[j];
            break;
        case '*':
            for (int j = 1; j < num; j++)
                result *= opset[j];
            break;
    }

    delete opset;
    return (result);
}

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

윈도우 기반 계산기 클라이언트 예제

// op_client_win.cpp

#include <iostream>
#include <winsock2.h>

using namespace std;

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

int        main(int argc, char *argv[])
{
    WSADATA        wsaData;
    SOCKET        hSocket;
    SOCKADDR_IN    servAddr;

    if(argc != 3)
    {
        cout << "Usage : " << argv[0] << " <IP> <port>\n";
        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(&servAddr, 0, sizeof(servAddr));
    servAddr.sin_family = AF_INET;
    servAddr.sin_addr.s_addr = inet_addr(argv[1]);
    servAddr.sin_port = htons(atoi(argv[2]));

    if (::connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
        error_handling("connect() error");
    else
        cout << "Connected..........\n";

    int *opset = new int[BUF_SIZE];
    int num;

    cout << "Operand count : ";
    cin >> num;

    opset[0] = static_cast<char>(num);
    cout << "num : " << num << endl;
    for (int i = 1; i < num + 1; i++)
    {
        cout << "Operand " << i << " : ";
        cin >> opset[i];
    }

    cout << "Operator : ";
    char op;
    cin >> op;
    opset[num + 1] = static_cast<char>(op);

    send(hSocket, opset, sizeof(int) * (num + 2), 0);

    int result;
    recv(hSocket, &result, sizeof(int), 0);

    cout << "Operation result : " << result << endl;

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

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

내용 확인문제

  1. TCP 소켓의 연결설정 과정인 Three-way handshaking에 대해서 설명해보자. 특히 총 3회의 데이터 송수신이 이뤄지는데, 각각의 데이터 송수신 과정에서 주고받는 데이터에 포함된 내용이 무엇인지 설명해보자.

'Three-way handshaking'이란 TCP 소켓의 연결설정 과정에서 데이터를 주고받는 세번의 과정을 뜻하며 이 때 주고받는 데이터에는 동기화 메시지인 SYN 패킷(현재 전송하는 패킷 번호인 SEQ 포함)과 응답 메시지인 ACK 패킷(다음에 보낼 패킷 번호 포함)이 있음


  1. TCP는 데이터의 전송을 보장하는 프로토콜이다. 그러나 인터넷을 통해서 전송되는 데이터는 소멸될 수 있다. 그렇다면 TCP는 어떠한 원리로 중간에 소멸되는 데이터의 전송까지 보장하는 것인지 ACK와 SEQ를 대상으로 설명해보자.

전송하고자 하는 데이터를 SEQ로 보낸 뒤, 해당 데이터의 크기 + 1만큼 증가한 만큼의 번호가 ACK로 응답되지 않는 경우 데이터가 제대로 전송되지 않았다고 판단하여 패킷 재전송 진행


  1. TCP 소켓을 기반으로 write 함수와 read 함수가 호출되었을 때의 데이터 이동을 입력버퍼와 출력버퍼의 상태와 더불어서 설명해보자.

write() 함수 호출시 데이터는 출력버퍼에 전송됨
이 때 출력버퍼가 차있는경우 write() 함수는 반환되지 않고 대기
송신 소켓의 출력버퍼에 있는 데이터는 수신 소켓의 입력버퍼로 이동하며, read() 함수는 입력버퍼의 데이터를 읽어들임


  1. 데이터를 수신할 상대 호스트의 입력버퍼에 남아있는 여유공간이 50byte인 상황에서 write 함수호출을 통해서 70byte의 데이터 전송을 요청했을 때, TCP는 어떻게 이를 처리하는지 설명해보자.

송신 소켓의 출력버퍼에 충분한 여유가 있는 경우 70바이트의 데이터가 전송되고, 이 데이터는 수신 소켓의 입력버퍼로 이동
그러나 수신 소켓의 입력버퍼의 남아있는 여유공간이 70바이트보다 작기 때문에 여유분인 50바이트만이 입력버퍼로 이동되고 나머지는 출력버퍼에 남아있게됨


  1. Chapter 02에서 보인 예제 tcp_server.c(Chapter 01의 hello_server.c)와 tcp_clinet.c에서는 서버가 전송하는 문자열을 클라이언트가 수신하고 끝낸다. 그런데 이번에는 서버와 클라이언트가 한번씩 문자열을 주고받는 형태로 예제를 변경해보자. 단! 데이터의 송수신이 TCP 기반으로 진행된다는 사실을 고려하여 문자열 전송에 앞서 문자열의 길이 정보를 4바이트 정수의 형태로 먼저 전송하기로 하자. 즉, 연결이 된 상태에서 서버와 클라이언트는 다음의 유형으로 데이터를 송수신해야한다. [0006][Hello?]
    그리고 문자열의 전송순서는 상관이 없으며 문자열의 종류도 여러분이 임의로 결정해도 된다. 단, 총 3회 문자열을 주고받아야한다.
// q5_tcp_server.cpp

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUFFER_SIZE 1024
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;


    if(argc != 2)
    {
        cout << "Usage : " << argv[0] << " <port>\n";
        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, (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, (sockaddr*)&clnt_addr, &clnt_addr_size);
    if (clnt_sock == -1)
        error_handling("accept() error");

    for (int i = 0; i < 3; i++)
    {
        char    str[BUFFER_SIZE];
        int        write_len, read_len;

        cout << "Write message to client : ";
        cin.getline(str, BUFFER_SIZE);
        write_len = strlen(str);
        write(clnt_sock, &write_len, sizeof(int));
        if (write(clnt_sock, str, write_len) != write_len)
            error_handling("write() error!");
        memset(str, 0, write_len);

        cout << "Read message from client : ";
        read(clnt_sock, &read_len, sizeof(int));
        if (read(clnt_sock, str, read_len) != read_len)
            error_handling("read() error!");
        cout << str << endl;
    }

    close(clnt_sock);
    close(serv_sock);
    return 0;
}

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

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUFFER_SIZE 1024
void    error_handling(char *message);

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

    struct sockaddr_in    serv_addr;

    if(argc != 3)
    {
        cout << "Usage : " << argv[0] << " <IP> <port>\n";
        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, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error");

    for (int i = 0; i < 3; i++)
    {
        char    str[BUFFER_SIZE];
        int        write_len, read_len;

        cout << "Read message from server : ";
        read(sock, &read_len, sizeof(int));
        if (read(sock, str, read_len) != read_len)
            error_handling("read() error!");
        cout << str << endl;
        memset(str, 0, read_len);

        cout << "Write message to server : ";
        cin.getline(str, BUFFER_SIZE);
        write_len = strlen(str);
        write(sock, &write_len, sizeof(int));
        if (write(sock, str, write_len) != write_len)
            error_handling("write() error!");
    }

    close(sock);
    return 0;
}

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

  1. 파일을 송수신하기 위해 클라이언트와 서버를 구현하되, 다음 순서의 시나리오를 기준으로 구현해보자.
  • 클라이언트는 프로그램의 사용자로부터 전송받을 파일의 이름을 입력받는다.
  • 클라이언트는 해당 이름의 파일전송을 서버에게 요청한다.
  • 파일이 존재할 경우 서버는 파일을 전송하고, 파일이 존재하지 않을경우 그냥 연결을 종료한다.
// file_transfer_server.cpp

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUFFER_SIZE 1024
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;

    if(argc != 2)
    {
        cout << "Usage : " << argv[0] << " <port>\n";
        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, (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, (sockaddr*)&clnt_addr, &clnt_addr_size);
    if (clnt_sock == -1)
        error_handling("accept() error");

    char    filename[BUFFER_SIZE];
    int        filename_len;
    if (read(clnt_sock, &filename_len, sizeof(int)))
    {
        read(clnt_sock, filename, filename_len);

        cout << "filename : " << filename << endl;
        if (access(filename, F_OK) == -1)
            error_handling("file doesn't exist");
        else
        {
            FILE    *sendfile = fopen(filename, "r");
            char    buf[1];

            while (fread(buf, sizeof(buf), 1, sendfile))
                write(clnt_sock, buf, 1);
        }
    }

    close(clnt_sock);
    close(serv_sock);
    return 0;
}

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

#include <iostream>
#include <fstream>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>

using namespace std;

#define BUFFER_SIZE 1024
void    error_handling(char *message);

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

    struct sockaddr_in    serv_addr;

    if(argc != 3)
    {
        cout << "Usage : " << argv[0] << " <IP> <port>\n";
        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, (sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error");

    string filename;
    cout << "Write filename to recv : ";
    cin >> filename;

    char        fbuf[BUFFER_SIZE];

    if (access(("new" + filename).c_str(), F_OK) == 0)
        error_handling("file already exist");
    else
    {
        FILE    *recvfile = fopen(("new" + filename).c_str(), "w");

        int filename_len = filename.length();
        write(sock, &filename_len, sizeof(int));
        write(sock, filename.c_str(), filename.size());

        int    readbyte;
        while ((readbyte = read(sock, fbuf, BUFFER_SIZE)) != 0)
        {
            cout << "readbyte : " << readbyte << endl;
            fwrite(fbuf, sizeof(char), readbyte, recvfile);
        }
        cout << "File transfer success" << endl;
    }

    close(sock);
    return 0;
}

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

댓글