본문 바로가기

CS/네트워크

[네트워크 개념] 버클리 소켓 - 1

개요

소켓 통신을 개념을 정리하기 위해 아래 책의 버클리 소켓 파트를 읽은 뒤 이를 바탕으로 소개해보겠습니다.

https://product.kyobobook.co.kr/detail/S000001792473?utm_source=google&utm_medium=cpc&utm_campaign=googleSearch&gt_network=g&gt_keyword=&gt_target_id=aud-901091942354:dsa-1974044869918&gt_campaign_id=9979905549&gt_adgroup_id=132556570510&gad_source=1

 

멀티플레이어 게임 프로그래밍 | 조슈아 글레이저 - 교보문고

멀티플레이어 게임 프로그래밍 | 현업 개발자가 알려주는 탄탄한 멀티플레이어 게임 프로그래밍[리그 오브 레전드], [디스트로이 올 휴먼즈] 시리즈를 컨설팅하고 [로보블리츠], [맥스 액스], [스

product.kyobobook.co.kr

소켓이 무슨 개념인지 등 기본적인 네트워크 개념은 다소 생략하고 사용법을 다루며 Windows 중점적으로 설명하겠습니다.  


소켓 생성

windows OS는 WinSock2라이브러리를 통해 소켓 프로그래밍을 제공한다.

socket생성은 socket함수를 통해 생성한다.

https://learn.microsoft.com/ko-kr/windows/win32/api/winsock2/nf-winsock2-socket

 

socket 함수(winsock2.h) - Win32 apps

소켓 함수는 특정 전송 서비스 공급자에 바인딩된 소켓을 만듭니다.

learn.microsoft.com

 

SOCKET WSAAPI socket(
  [in] int af,
  [in] int type,
  [in] int protocol
);

 

af: address family 네트워크 계층 프로토콜을 어떤 것을 사용할지를 지정한다.

AF_INET: IPv4, AF_INET6: IPv6 등이 있다.

 

type: socket을 통해 주고받을 데이터 타입을 지정한다. SOCK_STREAM, SOCK_DGRAM 등이 있다.

 

protocol: socket통신에 사용할 프로토콜을 지정한다. IPPROTO_TCP, IPPROTO_UDP 등이 있다.

 

protocol을 0(NULL)으로 지정하면 type에 맞는 기본 프로토콜을 자동으로 선택해준다.

예를 들어 STREAM이라면 TCP, DGRAM이라면 UDP를 선택한다.


closesocket

https://learn.microsoft.com/ko-kr/windows/win32/api/winsock2/nf-winsock2-closesocket

 

closesocket 함수(winsock2.h) - Win32 apps

closesocket 함수는 기존 소켓을 닫습니다. (closesocket 함수(winsock2.h))

learn.microsoft.com

 

socket을 닫을 때는 closesocket을 통해 닫는다.

windows에서 socket은 HANDLE 취급이므로 closesocket을 하지않으면 리소스 누수가 일어나며 지속적으로 핸들이 증가한다.

 

shutdown 을 통해 socket을 닫기 전에 연결 종료만을 할 수도 있다.

연결을 종료하는 것이기 때문에 TCP 같은 연결지향 프로토콜을 제외하면 의미는 없다.

https://learn.microsoft.com/ko-kr/windows/win32/api/winsock/nf-winsock-shutdown

 

shutdown 함수(winsock.h) - Win32 apps

shutdown 함수(winsock.h)는 소켓에서 송신 또는 수신을 사용하지 않도록 설정합니다.

learn.microsoft.com


Windows Socket

Windows에서는 WinsSock2.h를 사용해 소켓 라이브러리를 제공한다.

Windows.h와 WinSock2.h는 충돌하기 때문에 같이 사용 시에

#define WIN32_LEAN_AND_MEAN

매크로 정의가 필요하다.

 

WSAStartup

Windows에서는 소켓 라이브러리 사용을 위해 WSAStartup()호출이 필요하다.

https://learn.microsoft.com/ko-kr/windows/win32/api/winsock/nf-winsock-wsastartup

 

WSAStartup 함수(winsock.h) - Win32 apps

WSAStartup 함수(winsock.h)는 프로세스에 의해 Winsock DLL 사용을 시작합니다.

learn.microsoft.com

WSAStartup은 사용할 소켓 라이브러리의 버전을 지정하고 불러오기 때문에 해당 함수가 실패하면 이후 Winsock 함수가 동작하지 않는다.

 

앞에는 버전, 뒤에는 라이브러리 정보를 받을 구조체 WSADATA 주소를 넘겨주면 된다.

WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);

위와 같이 선언할 수 있다.

 

현재는 2.2버전을 사용하며, wsa는 실제로 프로그래밍 시에 사용할만한 정보는 없다.


WSACleanup

WSACleanup은 사용중인 모든 소켓을 종료시키고 리소스를 소멸시킨다.

https://learn.microsoft.com/ko-kr/windows/win32/api/winsock2/nf-winsock2-wsacleanup

 

WSACleanup 함수(winsock2.h) - Win32 apps

WSACleanup 함수(winsock2.h)는 WS2_32.dll 사용을 종료합니다.

learn.microsoft.com

 

따라서 비정상적인 종료를 원하지 않으면 소켓이 정상적으로 닫혔는지 확인이 필요하다.

추가로 WSAStartup은 내부적으로 ref count를 계산하기 때문에 WSAStartup을 n번했다면 WSACleanup을 n번 호출해야 정상적으로 마무리 작업이 진행된다.


WSAGetLastError

소켓 라이브러리 함수 호출 시에는 다양한 에러가 발생할 수 있다.

 

에러가 발생한 후 이후에는 다른 소켓 함수 호출에도 영향을 줄 수 있기 때문에 에러체크 후 적절한 조치가 필요하다.

 

기본적으로 windows의 소켓 함수가 실패하면

#define SOCKET_ERROR            (-1)

SOCKET_ERROR가 반환된다. 에러에 대한 자세한 정보를 얻기 위해서는 WSAGetLastError 호출이 필요하다.

 

WSAGetLastError함수는 GetLastError와 같이 가장 최근에 발생한 오류에 대한 정보를 가지기 때문에 다른 함수 호출 직후 즉시 호출해야 한다.

https://learn.microsoft.com/ko-kr/windows/win32/api/winsock/nf-winsock-wsagetlasterror

 

WSAGetLastError 함수(winsock.h) - Win32 apps

WSAGetLastError 함수(winsock.h)는 실패한 마지막 Windows 소켓 작업에 대한 오류 상태 반환합니다.

learn.microsoft.com

https://learn.microsoft.com/ko-kr/windows/win32/winsock/windows-sockets-error-codes-2

 

Windows 소켓 오류 코드(Winsock2.h) - Win32 apps

WSAGetLastError 함수에서 반환된 Windows 소켓(Winsock) 오류 코드입니다.

learn.microsoft.com


소켓 주소 

소켓을 만들었으니 이제는 전송 수신을 위해 주소를 기입해야 한다.

소켓 라이브러리는 sockaddr을 통해 주소를 표현한다.

struct sockaddr {
        ushort  sa_family;
        char    sa_data[14];
};

struct sockaddr_in {
        short   sin_family;
        u_short sin_port;
        struct  in_addr sin_addr;
        char    sin_zero[8];
};

sa_family: af와 같은 값을 기입

sa_data: af에 대한 데이터 삽입

 

sockaddr은 총 16바이트인데, IPv4에서 사용하기 위해서는 같은 크기의 sockaddr_in으로 만들고 sockaddr로 래핑해서 사용하고는 한다.

typedef struct in_addr {
        union {
                struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
                struct { USHORT s_w1,s_w2; } S_un_w;
                ULONG S_addr;
        } S_un;
#define s_addr  S_un.S_addr /* can be used for most tcp & ip code */
#define s_host  S_un.S_un_b.s_b2    // host on imp
#define s_net   S_un.S_un_b.s_b1    // network
#define s_imp   S_un.S_un_w.s_w2    // imp
#define s_impno S_un.S_un_b.s_b4    // imp #
#define s_lh    S_un.S_un_b.s_b3    // logical host
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;

in_addr은 ip를 편하게 접근하기 위해 사용하는 구조체이다.

 

sin_zero는 패딩 값이다.


호스트의 바이트 순서와 네트워크의 바이트 순서

일반적으로 프로그램에서는 리틀 엔디안을 사용하지만, 네트워크 장비는 빅 엔디안을 사용하기 때문에 네트워크에서 사용하는 정보는 변환이 필요하다.

https://learn.microsoft.com/ko-kr/windows/win32/api/winsock2/nf-winsock2-htons

 

htons 함수(winsock2.h) - Win32 apps

htons 함수(winsock2.h)는 u_short 호스트에서 TCP/IP 네트워크 바이트 순서(big-endian)로 변환합니다.

learn.microsoft.com

htons(host to network short)는 host의 바이트 순서를 network의 바이트 순서로 바꿔준다.

 

이와 비슷한 함수로는, htonl, ntohs, ntohl가 있다.


문자열로 in_addr(ip)설정

in_addr을 직접 설정하기에는 불편한 점이 있기 때문에 문자열 "192.168.1.0"과 같이 초기화할 수 있다면 매우 편할 것이다.

Windows에서는 InetPton 함수를 사용하면 sockaddr_in의 sin_addr 필드를 편하게 초기화할 수 있다.

https://learn.microsoft.com/ko-kr/windows/win32/api/ws2tcpip/nf-ws2tcpip-inetptonw

 

InetPtonW 함수(ws2tcpip.h) - Win32 apps

InetPton 함수는 표준 텍스트 프레젠테이션 형식의 IPv4 또는 IPv6 인터넷 네트워크 주소를 숫자 이진 형식으로 변환합니다. 이 함수의 ANSI 버전은 inet_pton. (InetPtonW)

learn.microsoft.com

InetPton(AF_INET, L"127.0.0.1", &addr.sin_addr);

위와 같이 sockaddr_in구조체를 넘기면 확인해보았을 때 (&addr.sin_addr)

빅엔디안 바이트 순서로 값이 들어갔음을 확인할 수 있다.


도메인 문자열로 IP 받아오기

InetPton은 사용자가 IP를 알고있어야 사용할 수 있다. 

만약 도메인만을 알고 있다면 DNS 서버에 query를 보내 도메인에 대응하는 IP를 획득해야 한다.

 

이를 위해 getaddrinfo 함수를 사용한다.

https://learn.microsoft.com/ko-kr/windows/win32/api/ws2tcpip/nf-ws2tcpip-getaddrinfo

 

getaddrinfo 함수(ws2tcpip.h) - Win32 apps

ANSI 호스트 이름에서 주소로 프로토콜 독립적 변환을 제공합니다.

learn.microsoft.com

INT WSAAPI getaddrinfo(
  [in, optional] PCSTR           pNodeName,
  [in, optional] PCSTR           pServiceName,
  [in, optional] const ADDRINFOA *pHints,
  [out]          PADDRINFOA      *ppResult
);

pNodeName: 조회할 도메인 문자열이다.

pServiceName: 포트 번호 혹은 서비스 이름을 문자열로 지정한다. "80", "http" 등으로 지정할 수 있다.

pHints: 넘겨받길 원하는 정보를 지정할 수 있다. 모든 정보를 원하면 nullptr을 넘긴다.

ppResult: 결과를 넘겨받는 구조체이다.

 

    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        std::cerr << "WSAStartup failed\n";
        return 1;
    }

    struct addrinfo hints = {};
    struct addrinfo* result = nullptr;

    hints.ai_family = AF_INET;        // IPv4
    hints.ai_socktype = SOCK_STREAM;  // TCP
    hints.ai_protocol = IPPROTO_TCP;

    const char* hostname = "www.shiftup.co.kr";
    const char* port = "80";

    int ret = getaddrinfo(hostname, port, &hints, &result);
    if (ret != 0) {
        std::cerr << "getaddrinfo failed: " << gai_strerrorA(ret) << "\n";
        WSACleanup();
        return 1;
    }

    // 주소 출력
    for (struct addrinfo* ptr = result; ptr != nullptr; ptr = ptr->ai_next) {
        struct sockaddr_in* sockaddr_ipv4 = (struct sockaddr_in*)ptr->ai_addr;
        char ipStr[INET_ADDRSTRLEN] = { 0 };

        // IP 주소를 문자열로 변환
        inet_ntop(AF_INET, &(sockaddr_ipv4->sin_addr), ipStr, INET_ADDRSTRLEN);

        std::cout << "Resolved IP: " << ipStr << "\n";
    }

위와 같이 코드를 작성한다면

DNS를 통해 hostname의 IP를 얻을 수 있다.

 

주의해야 할 점은 getaddrinfo는 내부적으로 자원을 할당하기 때문에 반드시 freeaddrinfo를 통해 자원 할당을 해제해야 한다.

        freeaddrinfo(result);

 

어디서 자원을 할당하냐면 addrinfo 구조체 내에서 할당한다.

typedef struct addrinfo
{
    int                 ai_flags;       // AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST
    int                 ai_family;      // PF_xxx
    int                 ai_socktype;    // SOCK_xxx
    int                 ai_protocol;    // 0 or IPPROTO_xxx for IPv4 and IPv6
    size_t              ai_addrlen;     // Length of ai_addr
    char *              ai_canonname;   // Canonical name for nodename
    _Field_size_bytes_(ai_addrlen) struct sockaddr *   ai_addr;        // Binary address
    struct addrinfo *   ai_next;        // Next structure in linked list
}

해당 구조체를 보면, ai_next를 가짐을 볼 수 있다. 이를 보아 연결 리스트임을 추측할 수 있는데, 실제로 DNS 질의 시에 여러 IP주소가 돌아올 수 있기 때문에, 연결리스트로 만든다. 따라서 이를 위해서 별도로 공간을 할당한다.

 

결론적으로는 addrinfo를 모두 사용한 뒤에는 반드시 freeaddrinfo호출이 필요하다.

 

https://learn.microsoft.com/ko-kr/windows/win32/api/ws2tcpip/nf-ws2tcpip-freeaddrinfo

 

freeaddrinfo 함수(ws2tcpip.h) - Win32 apps

getaddrinfo 함수가 addrinfo 구조에서 동적으로 할당하는 주소 정보를 해제합니다.

learn.microsoft.com


소켓 바인딩

앞에서 얻은 주소를 통해서 이제 생성한 소켓을 바인딩 해보자.

 

bind를 위해서는 bind 함수를 호출하면 된다.

https://learn.microsoft.com/ko-kr/windows/win32/api/winsock/nf-winsock-bind

 

bind 함수(winsock.h) - Win32 apps

bind 함수(winsock.h)는 로컬 주소를 소켓과 연결합니다.

learn.microsoft.com

int bind(
  [in] SOCKET         s,
       const sockaddr *addr,
  [in] int            namelen
);

인자로는 바인딩할 SOCKET, 그리고 해당 소켓과 대응하는 주소(보낼 주소가 아닌 받는 주소) 와 sockaddr의 길이인 namelen을 받는다.

 

받는 주소는 어짜피 나의 PC의 주소인데 왜 직접 지정하는 지에 대해 생각할 수 있지만, PC에 NIC가 여러개라면 IP도 여러개이다.

만약 addr을 지정하면 해당 NIC에 대응하는 소켓을 바인딩하는 것이고, INADDR_ANY를 지정하면 각 IP의 모든 port가 해당 소켓에 바인딩 된다고 볼 수 있다.

 

일반적으로 주소, port쌍 하나로 소켓이 바인딩될 수 있다. 

 

데이터 전송, 수신을 위해서는 반드시 소켓이 바인딩 되어있어야 한다.

서버 측은 특정 port로 연결을 받아야하기 때문에 바인딩 시에 port 지정이 필요하지만,

클라이언트 측에서는 OS를 통해 동적으로 포트를 받아 바인딩할 수 있다.


길어져서 나머지는 나중에