네트워크 및 보안/윈도우즈 소켓 통신 프로그램

2. 윈도우즈 소켓 [TCP/IP 소켓 통신 프로그래밍 with 윈도우즈]

언제나휴일 2016. 4. 7. 12:18
반응형

2. 윈도우즈 소켓

 

 네트워크 통신 프로그래밍을 하기 위해서 사용하는 입출력 인터페이스를 소켓이라 부릅니다. 버클리 대학에서 시작한 BSD 유닉스가 최초의 소켓이며 흔히 소켓이라 부르면 버클리 소켓을 말합니다.

 

 그리고 버클리 소켓을 기반으로 마이크로 소프트 사에서 윈도우즈 운영체제에서 사용할 수 있게 만든 소켓을 윈도우즈 소켓이라 부르며 흔히 윈속이라 줄여서 불리고 있습니다. 윈속은 윈도우즈 운영체제의 버전 업그레이드와 함께 변화하였는데 95년에 윈속 2.0을 발표하였으며 이 책에서는 윈속 2.2 버전을 사용합니다.

 

 이번 장에서는 윈속에서 제공하는 기본적인 함수와 자료형들에 관하여 살펴봅시다.

 

2.1 윈속 초기화

 윈속을 사용하려면 먼저 WinSock2.h 파일을 포함하고 ws2_32.dll을 동적 링크하여야 합니다.

#include <WinSock2.h>

#pragma comment(lib,"ws2_32")

 

 그리고 윈속을 사용하려면 초기화 과정을 거쳐야 합니다. 그리고 더 이상 윈속을 사용하지 않을 때 이를 해제화합니다. 따라서 윈속을 이용하는 응용 프로그램의 시작 위치에 윈속 초기화를 수행하고 프로그램이 끝나기 전에 해제화를 수행합니다.

 

int WSAStartup(WORD wVersionRequested,LPWSADATA lpWSAData);

wVersionRequested: 사용할 윈속 버전으로 상위 2바이트에는 주 버전 번호 하위 2바이트에는 부 버전 번호

                           만약 2.2 버전을 사용하려면 0x0202 혹은 매크로 함수를 이용하여 MAKEWORD(2,2)

lpWSAData: 초기화 과정에서 윈속의 속성을 설정에 필요, 함수 내부에서 설정해 준다.

int WSACleanup(void);

typedef struct WSAData {

    WORD                wVersion; // 버전

    WORD                wHighVersion; //사용할 수 있는 상위 버전으로 wVersion과 일치한다.

    char                   szDescription[WSADESCRIPTION_LEN+1]; //윈속 설명

    char                   szSystemStatus[WSASYS_STATUS_LEN+1]; //상태 문자열

    unsigned short     iMaxSockets; //최대 소켓

    unsigned short     iMaxUdpDg; //데이터 그램의 최대 크기

    char FAR *           lpVendorInfo; //벤더 정보( 큰 의미 없음)

#endif

} WSADATA, FAR * LPWSADATA;

 

 윈속을 초기화할 때 WSADATA 형식 변수의 주소를 전달하며 초기화 함수 내부에서 윈속의 속성 정보로 설정해 줍니다. 하지만 실제 값을 확인해 보면 별다른 내용도 없으며 소켓 프로그래밍에서 이를 활용할 필요도 없습니다. 이러한 멤버가 남아있는 것은 하위버전과의 호환성을 위해서 남아있는 것입니다.

 

 

//윈속 초기화 및 해제

#include <WinSock2.h>//윈속 헤더파일 포함문

#include <stdio.h>

#pragma comment(lib,"ws2_32")//윈속 라이브러리 참조

int main()

{

    WSADATA wsadata;

    WSAStartup(MAKEWORD(2,2),&wsadata);//윈속 초기화

   

    printf("wHighVersion:%#x\n",wsadata.wHighVersion);//윈속상위버전

    printf("wVersion:%#x\n",wsadata.wVersion);//윈속버전

    printf("szDescription:%s\n",wsadata.szDescription);//윈속 설명

    printf("szSystemStatus:%s\n",wsadata.szSystemStatus);//윈속상태

 

    //의미상으로 최대 소켓 개수이나 확인해 보면 언제나 0(하위버전과 호환성을 위해 남겨둠)

    printf("iMaxSocket:%d\n",wsadata.iMaxSockets);

    //의미상으로 데이터 그램의 최대 크기이나 확인해 보면 언제나 0(하위버전과 호환성을 위해 남겨둠)

    printf("iMaxUdpDg:%d\n",wsadata.iMaxUdpDg);

   

    WSACleanup();//윈속 해제

             return 0;

}

wHighVersion:0x0202

wVersion:0x0202

szDescription:WinSock 2.0

szSystemStatus:Running

iMaxSocket:0

iMaxUdpDg:0

[예제 2.1] 윈속 초기화


2.2 바이트 정렬(Byte Order)

 바이트 정렬은 2 바이트 이상의 데이터를 메모리에 표현할 때 높은 주소부터 시작할 지 낮은 주소부터 시작할 지를 결정하는 것을 말합니다. 시스템에 따라 바이트 정렬 방식은 다른데 Sparc Motorola는 높은 주소부터 시작하는 big endian 바이트 정렬 방식을 사용하며 Intel 호환 시스템은 little endian 바이트 정렬 방식을 채택하고 있습니다.

 

 이처럼 네트워크 통신의 호스트에 따라 정렬 방식이 다를 수 있어서 네트워크 통신 표준을 정하였는데 이를 네트워크 바이트 정렬이라 부르며 big endian 방식을 채택하였습니다.

 그리고 윈속에서는 호스트 데이터를 네트워크 바이트 정렬 방식의 데이터로 바꾸거나 네트워크 데이터를 호스트 바이트 정렬 방식의 데이터로 바꾸는 함수를 제공하고 있습니다.

 

 만약 이 함수를 호출했을 때 내용의 변화가 없으면 호스트 바이트 정렬 방식과 네트워크 정렬 방식은 같은 방식을 사용하는 것입니다. 반대로 변화가 있으면 서로 다른 방식을 사용하는 것입니다.

호스트 바이트 정렬 방식의 4바이트 데이터를 네트워크 바이트 정렬 방식으로 변환

u_long htonl(u_long hostlong);

호스트 바이트 정렬 방식의 2바이트 데이터를 네트워크 바이트 정렬 방식으로 변환

u_short htons(u_short hostshort);

네트워크 바이트 정렬 방식의 4바이트 데이터를 호스트 바이트 정렬 방식으로 변환

u_long ntohl(u_long netlong);

네트워크 바이트 정렬 방식의 2바이트 데이터를 호스트 바이트 정렬 방식으로 변환

u_short ntohs(u_short netshort);

 

//바이트 정렬

#include <WinSock2.h>

#include <stdio.h>

#pragma comment(lib,"ws2_32")

int main()

{

    WSADATA wsadata;

    WSAStartup(MAKEWORD(2,2),&wsadata);

 

    unsigned int idata = 0x12345678;

    unsigned short sdata = 0x1234;

 

    //4바이트 정수를 네트워크 바이트 정렬로 변환 후 출력

    printf("host:%#x network:%#x\n",idata, htonl(idata));

    //2바이트 정수를 네트워크 바이트 정렬로 변환 후 출력

    printf("host:%#x network:%#x\n",sdata, htons(sdata));

   

    WSACleanup();

    return 0;

}

host:0x12345678 network:0x56781234

host:0x1234 network:0x3412

[예제 2.2] 바이트 정렬

 

 htonl 함수를 호출했을 때 원래 값이 바뀌었다면 호스트 바이트 정렬 방식이 little endian 방식이기 때문입니다. 이 때 ntohl 함수를 호출하면 htonl과 마찬가지로 동작할 것입니다. 그럼에도 불구하고 논리적인 코드를 작성할 때 왜 해당 함수를 사용했는지 이해할 수 쉽게 작성하라고 제공하고 있습니다.

 

2.3 주소 변환

 IPv4 주소는 최종 사용자가 기억하기 쉽게 "127.0.0.1"처럼 0에서 255사이의 4개의 정수를 점(.)으로 구분하여 문자열로 표현하고 있습니다. 하지만 이는 기억하기 쉽게 만든 표현 방식으로 실제 IPv4 주소는 4바이트 정수 값입니다.

 

 윈속에서는 문자열로 표현한 주소를 네트워크 바이트 정렬 방식의 4바이트 정수로 변환하는 함수와 역으로 네트워크 바이트 정렬 방식의 4바이트 정수를 문자열로 변환하는 함수를 제공하고 있습니다.

 

문자열로 표현한 IPv4 주소를 네트워크 바이트 정렬 방식의 4바이트 정수로 변환

unsigned long inet_addr(const char *cip);

네트워크 바이트 정렬 방식의 4바이트 정수의 IPv4 주소를 문자열 주소로 표현

char *inet_ntoa(struct in_addr in);

네트워크 바이트 정렬 방식의 4바이트 정수의 IPv4 주소를 표현할 때 사용하는 구조체

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 // TCP/IP에서 가장 많이 사용

#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 형식 이름은 구조체 struct in_addr을 타입 재지정한 것입니다. 그리고 내부의 매크로로 s_addr을 정의하였는데 TCP/IP 통신에서 가장 많이 사용하는 매크로 멤버입니다.

 

 예를 들어 네트워크 바이트 정렬 방식의 4바이트 정수로 표현한 IPv4 주소가 0x12345678일 때 IN_ADDR 형식 변수에 표현하면 다음처럼 표현할 수 있습니다.

IN_ADDR addr;

addr.s_addr = htonl(0x12345678);

 

//IPv4 주소 변환

#include <WinSock2.h>

#include <stdio.h>

#pragma comment(lib,"ws2_32")

int main()

{

    WSADATA wsadata;

    WSAStartup(MAKEWORD(2,2),&wsadata);   

   

    IN_ADDR addr;

    addr.s_addr = htonl(12<<24 | 34<<16 | 56<<8 | 78);//12.34.56.78

    printf("%s\n",inet_ntoa(addr));//IPv4 주소를 문자열로 변환

 

    u_int naddr = inet_addr("192.168.34.0"); //문자열을 IPv4 주소로 변환

    u_int haddr = ntohl(naddr); //호스트 바이 정렬로 변환

    printf("%d.%d.%d.%d\n",haddr>>24, (u_char)(haddr>>16),(u_char)(haddr>>8),(u_char)(haddr));

 

    WSACleanup();

    return 0;

}

12.34.56.78

192.168.34.0

[예제 2.3] 주소 변환

 

2.4 자신의 디폴트 IP 주소 얻어오기

 윈속에서는 자신의 디폴트 IP 주소를 얻어올 수 있는 함수를 제공하고 있습니다.

 

호스트 이름을 얻어오는 함수

int gethostname(char * name,int namelen);

실패 시: -1(SOCKET_ERROR) 반환

호스트 엔트리를 얻어오는 함수

struct hostent *gethostbyname(const char *name);

호스트 엔트리 구조체

struct  hostent {

        char    * h_name;           // 호스트 이름

        char    ** h_aliases;         // 호스트 별칭 목록

        short   h_addrtype;         //하드웨어 타입, IPv4 PF_INET

        short   h_length;            //하드웨어 주소 길이

        char    ** h_addr_list;      //하드웨어 주소 목록

};

 

//디폴트 IPv4 주소 얻어오기

#include <WinSock2.h>

#include <Windows.h>

#include <stdio.h>

 

#pragma comment(lib,"ws2_32")

IN_ADDR GetDefaultMyIP();

int main()

{

    WSADATA wsadata;

    WSAStartup(MAKEWORD(2,2),&wsadata);   

    IN_ADDR addr;

 

    addr = GetDefaultMyIP();//디폴트 IPv4 주소 얻어오기

    printf("%s\n",inet_ntoa(addr));//IPv4 주소를 문자열로 출력

   

    WSACleanup();

    return 0;

}

 

IN_ADDR GetDefaultMyIP()

{

    char localhostname[MAX_PATH];

    IN_ADDR addr={0,};

 

    if(gethostname(localhostname, MAX_PATH) == SOCKET_ERROR)//호스트 이름 얻어오기

    {

        return addr;

    }

    HOSTENT *ptr = gethostbyname(localhostname);//호스트 엔트리 얻어오기

    while(ptr && ptr->h_name)

    {

        if(ptr->h_addrtype == PF_INET)//IPv4 주소 타입일 때

        {

            memcpy(&addr, ptr->h_addr_list[0], ptr->h_length);//메모리 복사

            break;//반복문 탈출

        }

        ptr++;

    }

    return addr;

}

192.168.34.50(호스트 IP 주소)

[예제 2.4] 자신의 디폴트 IP 주소 얻어오기

반응형