메모내용

Socket Option

Socket 옵션

Socket 의 입출력에 관한 옵션은, 말 그대로 Socket IO 전체에 개입하는 옵션이다.

옵션 개념

소켓의 옵션을 설정하고나 값을 가져올때는 아래의 개념이 필요합니다.

  • SOCKET s : 설정의 대상이 되는 Socket
  • int level : 이 레벨값에 따라 접근할수 있는 옵션이 다르다.
    • SOL_SOCKET
    • IPPROTO_TCP : IPv4 & IPv6 용으로 생성된 소켓이면서 TCP 에 적용되는 옵션레벨
    • NSPROTO_IPX : IPX 프로토콜 옵션, 하지만 요즘 IPX 프로토콜은 사용하지 않는다.
  • int optname : 설정할 옵션의 이름
  • const char* optval : 설정할 옵션 값
  • int optlen : 옵션의 값 사이즈

Socket options

level optname optval get set meaning
SOL_SOCKET SO_TYPE int yes no 소켓 유형 ( 예 : SOCKET_STREAM )
SOL_SOCKET SO_RCVBUF int yes yes 수신을 위해 예약된 버퍼공간
SOL_SOCKET SO_SNDBUF int yes yes 전송을 위해 예약된 버퍼공간
SOL_SOCKET SO_REUSEADDR bool yes yes 소켓을 이미 사용중인 주소에 바인딩 할수 있도록 합니다.
( Server Listen Socket 에 사용 )
IPPROTO_TCP TCP_NODELAY DWORD(Boolean) yes yes TCP 소켓에 대한 Nagle 알고리즘 활성화 여부를 설정할 수 있습니다.
(default : false)
활성화시 buffer 에 차는것을 기다리지 않고 바로 보낼수 있도록 설정할 수 있는것 같다. (검증필요)

Socekt Option 관련함수


    int getsockopt(
        [in]      SOCKET s,
        [in]      int    level,
        [in]      int    optname,
        [out]     char   *optval,
        [in, out] int    *optlen
    );

    int setsocketopt(
        [in] SOCKET        s,
        [in] int           level,
        [in] int           optname, // TCP_NODELAY 로 설정하면, Buffer 에 꽉 채워지도록 기다리지 않는다.
        [in] const char    *optval,
        [in] int           optlen
    )

                    

TCP_NODELAY 옵션

패킷 전송지연을 사용하지 않도록 하여 recv, send 시 Buffer에 데이터가 충분히 채워질때까지 기다리지 않고, 바로보내도록 설정한다.

    
        int opt = 1;
        ::setsocketopt (hSocket, IPPROTO_TCP, TCP_NODELAY, (char*)&nOpt, sizeof(nOpt))
    
                    

    // HelloTcpipClient.cpp : 콘솔 응용 프로그램에 대한 진입점을 정의합니다.
    
    #include "stdafx.h"
    #include <winsock2.h>
    #pragma comment(lib, "ws2_32")
    
    
    int _tmain(int argc, _TCHAR* argv[])
    {
        //※윈속 초기화
        WSADATA wsa = { 0 };
        if (::WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
        {
            puts("ERROR: 윈속을 초기화 할 수 없습니다.");
            return 0;
        }
    
        //1. 접속대기 소켓 생성
        SOCKET hSocket = ::socket(AF_INET, SOCK_STREAM, 0);
        if (hSocket == INVALID_SOCKET)
        {
            puts("ERROR: 소켓을 생성할 수 없습니다.");
            return 0;
        }
    
        //2. 포트 바인딩 및 연결
        SOCKADDR_IN	svraddr = { 0 };
        svraddr.sin_family = AF_INET;
        svraddr.sin_port = htons(25000);
        svraddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
        if (::connect(hSocket,
            (SOCKADDR*)&svraddr, sizeof(svraddr)) == SOCKET_ERROR)
        {
            puts("ERROR: 서버에 연결할 수 없습니다.");
            return 0;
        }
    
        //2.1
        int nOpt = 1;
        ::setsockopt(hSocket, IPPROTO_TCP, TCP_NODELAY,
            (char*)&nOpt, sizeof(nOpt));
    
        //3. 채팅 메시지 송/수신
        char szBuffer[128] = { 0 };
        while (1)
        {
            //사용자로부터 문자열을 입력 받는다.
            gets_s(szBuffer);
            if (strcmp(szBuffer, "EXIT") == 0)		break;
    
            //사용자가 입력한 문자열을 서버에 전송한다.
            //::send(hSocket, szBuffer, strlen(szBuffer) + 1, 0);
            int nLength = strlen(szBuffer);
            for (int i = 0; i < nLength; ++i)
                ::send(hSocket, szBuffer + i, 1, 0);
    
            //서버로부터 방금 보낸 문자열에 대한 에코 메시지를 수신한다.
            memset(szBuffer, 0, sizeof(szBuffer));
            ::recv(hSocket, szBuffer, sizeof(szBuffer), 0);
            printf("From server: %s\n", szBuffer);
        }
    
        //4. 소켓을 닫고 종료.
        ::shutdown(hSocket, SD_BOTH);
        ::closesocket(hSocket);
        //※윈속 해제
        ::WSACleanup();
        return 0;
    }                        

                

SO_REUSEADDR 옵션

서버쪽 특화 옵션 IP 주소를 재 사용하겠다는 옵션이다. socket 이라고 하는것은 자원이다. 그리고 이 소켓자원에 접근하는 process 는, socket 에 ip, port 를 묶게 되고, 만약 다른 Process 가 이미 사용중인 IP, Port 는 묶을 수 없다. 근데. 이미 사용된 IP 주소와 Port 번호를 이것을 사용해야 할때가 있다. Socket의 종료를 요청하는 쪽에서는 필연적으로 TIME_WAIT 가 발생하고 TIME_WAIT 가 짧게는 수초에서 길게는 몇분동안 일어난다. Server 에서 문제가 발생하여 Server 쪽에서 연결을 끊자, TIME_WAIT 가 발생하였고, 다시 Server 를 재시작해야하는데, 아직까지 Socket 이 회수가 안되어, 서버가 재시작을 못하는경우가 발생할 수 있다. 그 시간동안 Client 가 접속을 하지 못한다는 것이 되는것이다., 그것은 큰 문제가 될 수 있다. Server 의 생명은 1번이 안정성, 2번이 성능이다. 안정성을 최우선으로 생각해야 한다. 게이머들이 게임하다 팅겼는데 2분동안 접속이 안된다면,, 엄청난 문제가 될것이다.


    // 바인딩 전에 IP 주소와 포트를 재사용 하도록 소켓 옵션을 변경한다.
    BOOL bOption = TRUE;
    if(SOCKET_ERROR ==::setsockopt(hSocket, SOL_SOCKET, SO_REUSEADDR, (char*)&bOption, sizeof(BOOL))  )
    {
        puts("소켓 옵션을 변경할 수 없습니다.");
        return 0;
    }

                    

위 옵션을 사용하면 똑같은 IP, PORT 번호를 사용하는 Server 를 n 개 실행할 수 있다. 그렇다면 만약 똑같은 주소를 사용하는 Server1, Server2 총 2개가 있고, Client 가 해당 주소로 연결하려고 할때, 어느쪽으로 연결이 될까. 이렇게 되면, 먼저 실행된 Server1 에 우선적으로 연결이 되고, 만약 그 Server1 이 죽으면, Client 재시작시, 그 다음으로 실행된 Server2 에 연결이 된다.


// SockReuse.cpp : 콘솔 응용 프로그램에 대한 진입점을 정의합니다.

#include "stdafx.h"
#include <winsock2.h>
#pragma comment(lib, "ws2_32")

int _tmain(int argc, _TCHAR* argv[])
{
    WSADATA wsa = { 0 };
    if (::WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
    {
        puts("ERROR: 윈속을 초기화 할 수 없습니다.");
        return 0;
    }

    SOCKET hSocket = ::socket(AF_INET, SOCK_STREAM, 0);
    if (hSocket == INVALID_SOCKET)
    {
        puts("ERROR: 접속 대기 소켓을 생성할 수 없습니다.");
        return 0;
    }

    //※ 바인딩 전에 IP주소와 포트를 재사용하도록 소켓 옵션을 변경한다.
    BOOL bOption = TRUE;
    if (::setsockopt(hSocket, SOL_SOCKET,
        SO_REUSEADDR, (char*)&bOption, sizeof(BOOL)) == SOCKET_ERROR)
    {
        puts("ERROR: 소켓 옵션을 변경할 수 없습니다.");
        return 0;
    }

    //바인딩
    SOCKADDR_IN	svraddr = { 0 };
    svraddr.sin_family = AF_INET;
    svraddr.sin_port = htons(25000);
//	svraddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
    svraddr.sin_addr.S_un.S_addr = inet_addr("192.168.77.2");
    if (::bind(hSocket,
        (SOCKADDR*)&svraddr, sizeof(svraddr)) == SOCKET_ERROR)
    {
        puts("ERROR: 소켓에 IP주소와 포트를 바인드 할 수 없습니다.");
        return 0;
    }

    if (::listen(hSocket, SOMAXCONN) == SOCKET_ERROR)
    {
        puts("ERROR: 리슨 상태로 전환할 수 없습니다.");
        return 0;
    }

    puts("Echo 서버를 시작합니다.");

    SOCKADDR_IN clientaddr = { 0 };
    int nAddrLen = sizeof(clientaddr);
    SOCKET hClient = 0;
    char szBuffer[128] = { 0 };
    int nReceive = 0;

    while ((hClient = ::accept(hSocket,
        (SOCKADDR*)&clientaddr, &nAddrLen)) != INVALID_SOCKET)
    {
        puts("새 클라이언트가 연결되었습니다.");
        while ((nReceive = ::recv(hClient,
            szBuffer, sizeof(szBuffer), 0)) > 0)
        {
            ::send(hClient, szBuffer, sizeof(szBuffer), 0);
            puts(szBuffer);
            memset(szBuffer, 0, sizeof(szBuffer));
        }

        ::closesocket(hClient);
        puts("클라이언트 연결이 끊겼습니다.");
    }

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

                    
그림1

127.0.0.1 과 원래 주소 192.168.0.4 는 다른 주소로 처리가 된다. 가급적 htonl(INADDR_ANY) 를 사용하지말고, inet_addr ("192.168.0.4") 로 사용하라 Secure 관련 편의성과 보안성을 반비례한다.