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

Chapter 09 - 소켓의 다양한 옵션

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

09-1 : 소켓의 옵션과 입출력 버퍼의 크기

소켓의 옵션은 계층별로 존재

  • SOL_SOCKET - 소켓에 대한 일반적인 옵션들
  • IPPROTO_IP - IP 프로토콜에 관련된 사항
  • IPPROTO_TCP - TCP 프로토콜에 관련된 사항

대부분의 옵션은 설정상태의 참조(Get) 및 변경(Set)이 가능

#include <sys/socket.h>

int getsockopt(int sock, int level, int optname, void *optval, socklen_t *optlen);
int setsockopt(int sock, int level, int optname, const void *optval, socklent_t optlen);
// 성공시 0, 실패시 -1 반환

getsockopt() 함수를 사용한 예시

// sock_type.cpp

#include <iostream>
#include <sys/socket.h>

using namespace std;

void    error_handling(char *message);

int main(int argc, char *argv[])
{
    int            tcp_sock, udp_sock;
    int            sock_type;
    int            state;
    socklen_t    optlen;

    optlen = sizeof(sock_type);
    tcp_sock = socket(PF_INET, SOCK_STREAM, 0);
    udp_sock = socket(PF_INET, SOCK_DGRAM, 0);
    cout << "SOCK_STREAM : " << SOCK_STREAM << endl;
    cout << "SOCK_DGRAM : " << SOCK_DGRAM << endl;

    state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen);
    if (state)
        error_handling("getsockopt() error!");
    cout << "Socket type one : " << sock_type << endl;

    state = getsockopt(udp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen);
    if (state)
        error_handling("getsockopt() error!");
    cout << "Socket type two : " << sock_type << endl;

    return 0;
}

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

입출력 버퍼와 관련된 옵션 SO_SNDBUFSO_RCVBUF를 사용하여 입출력 버퍼의 크기를 확인한 예시

// get_buf.cpp

#include <iostream>
#include <sys/socket.h>

using namespace std;

void    error_handling(char *message);

int main(int argc, char *argv[])
{
    int            sock;
    int            snd_buf, rcv_buf, state;
    socklen_t    len;

    sock = socket(PF_INET, SOCK_STREAM, 0);

    len = sizeof(snd_buf);
    state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
    if (state)
        error_handling("getsockopt() error!");

    len = sizeof(rcv_buf);
    state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, &len);
    if (state)
        error_handling("getsockopt() error!");

    cout << "Input buffer size : " << rcv_buf << endl;
    cout << "Output buffer size : " << snd_buf << endl;

    return 0;
}

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

SO_SNDBUFSO_RCVBUF를 사용하여 입출력 버퍼의 크기를 변경한 예시

// set_buf.cpp

#include <iostream>
#include <sys/socket.h>

using namespace std;

void    error_handling(char *message);

int main(int argc, char *argv[])
{
    int            sock;
    int            snd_buf = 1024*3, rcv_buf = 1024*3;
    int            state;
    socklen_t    len;

    sock = socket(PF_INET, SOCK_STREAM, 0);

    state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, sizeof(rcv_buf));
    if (state)
        error_handling("setsockopt() error!");

    state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, sizeof(snd_buf));
    if (state)
        error_handling("setsockopt() error!");

    len = sizeof(snd_buf);
    state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
    if (state)
        error_handling("getsockopt() error!");

    len = sizeof(rcv_buf);
    state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, &len);
    if (state)
        error_handling("getsockopt() error!");

    cout << "Input buffer size : " << rcv_buf << endl;
    cout << "Output buffer size : " << snd_buf << endl;

    return 0;
}

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

09-2 : SO_REUSEADDR

주소할당 에러(Binding Error) 발생 예시

// reuseadr_eserver.cpp
// 클라이언트는 이전의 echo_client.cpp 사용

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

#define TRUE 1
#define FALSE 0
using namespace std;

void    error_handling(char *message);

int main(int argc, char *argv[])
{
    int                    serv_sock, clnt_sock;
    int                    option, str_len;
    char                message[30];
    socklen_t            optlen, clnt_adr_sz;
    struct sockaddr_in    serv_adr, clnt_adr;

    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");
    /*
    optlen = sizeof(option);
    option = TRUE;
    setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, optlen);
    */

    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)))
        error_handling("bind() error");

    if (::listen(serv_sock, 5) == -1)
        error_handling("listen error");

    clnt_adr_sz = sizeof(clnt_adr);
    clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);

    while ((str_len = read(clnt_sock, message, sizeof(message))) != 0)
    {
        write(clnt_sock, message, str_len);
        write(1, message, str_len);
    }

    close(clnt_sock);
    close(serv_sock);

    return 0;
}

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

클라이언트에서 먼저 close() 함수를 호출하거나 CTRL+C로 강제종료하여 서버측으로 FIN 메시지를 전송할 경우 Four-way handshaking 과정을 거침
이는 매우 일반적인 상황으로 별다른 상황이 발생하지 않아 서버의 재실행이 가능

반면 서버 프로그램을 강제 종료하여 클라이언트 측으로 먼저 FIN 메시지를 전달한 경우 서버를 재실행하면 bind() 에러가 발생, 일정시간이 지나면 정상적으로 재실행 가능
이는 Four-way handshaking에서 먼저 FIN 메시지를 보낸 호스트가 과정이 끝난 후에도 바로 소멸되지 않고 Time-wait 상태를 일정시간 유지하기 때문
따라서 해당 소켓의 포트번호가 사용중인 상태이기 때문에 곧바로 재실행이 불가능함

  • Time-wait는 마지막 메시지가 정상적으로 전달되지 못하는 경우를 대비하기 위해 존재
  • Time-wait가 없다면 마지막 ACK 메시지 전송 실패시 영원히 정상적인 종료가 불가능함

Time-wait는 상황에 따라 길어질 수 있어 서버를 즉시 재실행해야할 때 불필요한 존재가 됨

  • Four-way handshaking의 마지막 데이터가 손실되면 상대 호스트는 FIN 메시지를 재전송
  • 이 때 FIN 메시지를 수신한 호스트는 Time-wait를 재가동
  • 따라서 네트워크 상황이 원할하지 못하면 Time-wait 상태가 지속될 수 있음

이를 해결하기 위해 SO_REUSEADDR 옵션을 사용하여 Time-wait 상태에 있는 소켓에 할당된 포트번호를 새로 시작하는 소켓에 할당되게끔 할 수 았음

  • SO_REUSEADDR의 디폴트 값은 0 : Time-wait 상태의 소켓 포트번호는 할당 불가능
  • 이를 1(TRUE)로 변경할 경우(위 예제에서 주석을 해제할 경우) 포트번호를 새롭게 할당이 가능

09-3 : TCP_NODELAY

Nagle 알고리즘 : 앞서 전송한 데이터에 대한 ACK 메시지를 받아야만 다음 데이터를 전송하는 알고리즘

  • TCP 소켓에는 기본적으로 Nagle 알고리즘이 적용되어있음
  • Nagle 알고리즘이 적용되지 않으면 네트워크 트래픽(Traffic, 네트워크에 걸리는 부하나 혼잡의 정도)이 증가할 수 있음
  • 단, 용량이 큰 파일 데이터와 같은 경우 출력버퍼를 거의 꽉 채운 상태에서 패킷을 전송하므로 Nagle 알고리즘을 적용하지 않는 것이 속도가 향상됨
  • 그러므로 데이터의 특성을 판단하여 Nagle 알고리즘의 사용여부를 결정해야함

Nagle 알고리즘을 끄기 위해서는 소켓 옵션인 TCP_NODELAY를 1(TRUE)로 변경


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

윈도우 기반 소켓 옵션의 변경 및 참조에 사용되는 함수

#include <winsock2.h>

int getsockopt(SOCKET sock, int level, int optname, char *optval, int *optlen);
int setsockopt(SOCKET sock, int level, int optname, const char *optval, int optlen);
// 성공시 0, 실패시 SOCKET_ERROR 반환

리눅스에서 optvalvoid형 포인터였던 것과 다르게 char형 포인터임에 유의

윈도우 기반 입출력 버퍼 옵션 사용 예시

// buf_win.cpp

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

using namespace std;

void    ErrorHandling(char* message);
void    ShowSocketBufSize(SOCKET sock);

int main(int argc, char *argv[])
{
    WSADATA            wsaData;
    SOCKET            hSock;
    int                sndBuf, rcvBuf, state;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHandling("WSAStartup() error");

    hSock = socket(PF_INET, SOCK_STREAM, 0);
    ShowSocketBufSize(hSock);

    sndBuf = 1024*3, rcvBuf = 1024*3;
    state = setsockopt(hSock, SOL_SOCKET, SO_SNDBUF, (char*)&sndBuf, sizeof(sndBuf));
    if (state == SOCKET_ERROR)
        ErrorHandling("setsockopt() error!");

    state = setsockopt(hSock, SOL_SOCKET, SO_RCVBUF, (char*)&rcvBuf, sizeof(rcvBuf));
    if (state == SOCKET_ERROR)
        ErrorHandling("setsockopt() error!");

    ShowSocketBufSize(hSock);
    closesocket(hSock);
    WSACleanup();

    return 0;
}

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

void    ShowSocketBufSize(SOCKET sock)
{
    int    sndBuf, rcvBuf, state, len;

    len = sizeof(sndBuf);
    state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&sndBuf, &len);
    if (state == SOCKET_ERROR)
        ErrorHandling("getsockopt() error");

    len = sizeof(rcvBuf);
    state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&rcvBuf, &len);
    if (state == SOCKET_ERROR)
        ErrorHandling("getsockopt() error");

    cout << "Input buffer size : " << rcvBuf << endl;
    cout << "Output buffer size : " << sndBuf << endl;
}

내용 확인문제

  1. 다음 중 Time-wait 상태에 대해서 잘못 설명한 것을 모두 고르면?
  • Time-wait 상태는 서버 프로그램에서 생성한 소켓에서만 발생한다. (X)
  • 연결종료의 Four-way handshaking 과정에서 먼저 FIN 메시지를 전달한 소켓이 Time-wait 상태가 된다. (O)
  • 연결요청 과정에서 전송하는 SYN 메시지의 전송순서에 따라서 Time-wait 상태는 연결종료와 상관없이 일어날 수 있다. (X)
  • Time-wait 상태는 불필요하게 발생하는 것이 대부분이므로, 가급적이면 발생하지 않도록 소켓의 옵션을 변경해야한다. (X)

  1. 옵션 TCP_NODELAY는 Nagle 알고리즘과 관련이 있다. 이 옵션을 이용해서 Nagle 알고리즘을 해제할 수도 있는데, 그렇다면 어떠한 경우에 한해서 Nagle 알고리즘의 해제를 고민해 볼 수 있겠는가? 이를 송수신하는 데이터의 특성과 관련해서 설명해보자.

용량이 큰 파일 데이터와 같이 출력버퍼가 거의 꽉 채워진 상태에서 패킷이 전송될 경우
즉, Nagle 알고리즘의 적용 여부에 따른 트래픽의 차이가 크지 않으면서도 Nagle 알고리즘을 적용하는 것보다 데이터의 전송이 빠른 경우
게임과 같이 반응 속도가 중요한 경우

댓글