C언어로 소켓 프로그래밍을 해보자 2일차 - 소켓 프로그래밍 개요

0

Socket_C

목록 보기
2/2

linux, macOS에서 네트워크 어댑터 출력

네트워크 어댑터 현재, 디바이스의 네트워크 연결 상태를 보여준다. 가령 IPv4를 쓰는지, IPv6를 쓰는 지, IP는 어떻게 되는 지, MAC 주소는 어떻게 되는 지, 서브넷 마스크는 무엇인 지를 알려준다.

이를 출력하는 방법은 linux, macOS terminal에서 다음의 명령어를 치면 된다.

ifconfig

뭔가 리스트들이 쭈욱 나올 텐데 잘 읽어보면 네트워크 정보를 알려주는 부분이다. 브로드 캐스트 IP가 무엇이고, 서브넷 마스크가 무엇이고, 현재 호스트 IP주소는 어디에 접속되어 있는 지 등등 자세히 알려준다.

이 내용을 출력하는 c언어 코드를 만들어보자, linux, macOS와 window 버전의 코드가 다르다. 때문에 아래의 코드는 linux, macOS에서만 구동된다.

  • ip_list.c
#include <sys/socket.h>
#include <netdb.h>
#include <ifaddrs.h>
#include <stdio.h>
#include <stdlib.h>

int main(){
    struct ifaddrs *addresses;

    if(getifaddrs(&addresses) == -1){
            printf("getifaddrs call failed\n");
            return -1;
    }

    struct ifaddrs *address = addresses;
    while(address){
        int family = address->ifa_addr->sa_family;
        if(family == AF_INET || family == AF_INET6){
            printf("%s\t", address->ifa_name);
            printf("%s\t", family == AF_INET ? "IPv4" : "IPv6");

            char ap[100];
            const int family_size = family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);
            getnameinfo(address->ifa_addr, family_size, ap, sizeof(ap), 0, 0, NI_NUMERICHOST);
            printf("\t%s\n", ap);
        }
        address = address->ifa_next;
    }
    freeifaddrs(addresses);
    return 0;
}

gcc를 통해 빌드해주도록 하자.

gcc ip_list.c -o ip_list
./ip_list

결과는 다음과 같다.

xxxx    IPv4            xxx.x.x.x
xxxx    IPv4            xx.xxx.xxx.xxx
xxxx    IPv4            xxx.xx.x.x
xxxx    IPv6            ::x
xxxx    IPv6            xxxx::xxxx:xxxx:xxxx:xxxx%xxxx
xxxx    IPv6            xxxx::xxxx:xxxx:xxxx:xxxx%xxxx
xxxx    IPv6            xxxx::xxxx:xxxx:xxxx:xxxx%xxxx

ip랑 이름부분을 가린 것은 각 호스트에 따라 다르기 때문에 가렸다.

하나하나 확인해보면 다음과 같다.

struct ifaddrs *addresses;

if(getifaddrs(&addresses) == -1){
        printf("getifaddrs call failed\n");
        return -1;
}

struct ifaddrs타입으로 addresses포인터를 만들었다. 이 포인터는 IP에 대한 기본적인 정보들을 담는 변수이다. getifaddrs(&addresses)함수를 통해서 주소 정보를 받을 수 있는데, 재밌게도 포인터의 포인터를 넘겨주어야 한다. 이는 아래에 보면 알 수 있는데, addresses가 링크드 리스트로 만들어졌기 때문에 데이터를 만들어주는데 있어 이중 포인터를 사용한 것이다. 정보를 받는 것에 성공하면 0이 나오고, 실패하면 -1이 나온다.

struct ifaddrs *address = addresses;
while(address){
    int family = address->ifa_addr->sa_family;
    if(family == AF_INET || family == AF_INET6){
        printf("%s\t", address->ifa_name);
        printf("%s\t", family == AF_INET ? "IPv4" : "IPv6");

        char ap[100];
        const int family_size = family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6);
        getnameinfo(address->ifa_addr, family_size, ap, sizeof(ap), 0, 0, NI_NUMERICHOST);
        printf("\t%s\n", ap);
    }
    address = address->ifa_next;
}

struct ifaddrs *address를 통해서 각 주소를 뽑아오는 과정을 반복한다. 이 때 family를 가져오는데, 이를 통해서 이 주소가 IPv4인지, IPv6인지 확인한다. getnameinfo에 필요한 정보를 주면, 이쁘게 IP를 가져다 준다. getnameinfo에 대해서는 추후에 배우도록 하자.

freeifaddrs(addresses);
return 0;

사용한 주소를 반납하면, 마무리 된다.

Window에서 네트워크 리스트 출력 방법

  • win_init.c
#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0600
#endif
 
#include <winsock2.h>
#include <iphlpapi.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>

#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "iphlpapi.lib")

int main() {

    WSADATA d;
    if (WSAStartup(MAKEWORD(2, 2), &d)) {
        printf("Failed to initialize.\n");
        return -1;
    }
    DWORD asize = 20000;
    PIP_ADAPTER_ADDRESSES adapters;
    do {
        adapters = (PIP_ADAPTER_ADDRESSES)malloc(asize);

        if (!adapters) {
            printf("Couldn't allocate %ld bytes for adapters.\n", asize);
            WSACleanup();
            return -1;
        }

        int r = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, 0,
                adapters, &asize);
        if (r == ERROR_BUFFER_OVERFLOW) {
            printf("GetAdaptersAddresses wants %ld bytes.\n", asize);
            free(adapters);
        } else if (r == ERROR_SUCCESS) {
            break;
        } else {
            printf("Error from GetAdaptersAddresses: %d\n", r);
            free(adapters);
            WSACleanup();
            return -1;
        }
    } while (!adapters);

    PIP_ADAPTER_ADDRESSES adapter = adapters;
    while (adapter) {
        printf("\nAdapter name: %S\n", adapter->FriendlyName);

        PIP_ADAPTER_UNICAST_ADDRESS address = adapter->FirstUnicastAddress;
        while (address) {
            printf("\t%s",
                    address->Address.lpSockaddr->sa_family == AF_INET ?
                    "IPv4" : "IPv6");

            char ap[100];

            getnameinfo(address->Address.lpSockaddr,
                    address->Address.iSockaddrLength,
                    ap, sizeof(ap), 0, 0, NI_NUMERICHOST);
            printf("\t%s\n", ap);

            address = address->Next;
        }

        adapter = adapter->Next;
    }
    free(adapters);
    WSACleanup();
    return 0;
}

하나하나 살펴보자.

#pragma comment(lib, "ws2_32.lib")
#pragma comment(lib, "iphlpapi.lib")

이 부분은 컴파일러에게 어떤 라이브러리를 사용할 것인지 알려주는 부분인데, minGW는 이 부분을 무시하고, 컴파일을 할 때 명시적으로 어떤 라이브러리를 사용할 것인지 써줄 것이다. 만약 visual studio 컴파일러를 사용한다면 이 부분이 적용되어 알아서 라이브러리를 링킹해주는 작업을 해줄 것이다.

WSADATA d;
if (WSAStartup(MAKEWORD(2, 2), &d)) {
    printf("Failed to initialize.\n");
    return -1;
}

WSAStartup(MAKEWORD(2, 2), &d)을 통해서 WSADATA을 가져올 수 있다. 인자로 들어가는 MAKEWORD(2,2)는 버전을 의미한다. 즉, 우리는 Winsock2.2를 사용하겠다는 것이다.

WORD asize = 20000;
PIP_ADAPTER_ADDRESSES adapters;
do {
    adapters = (PIP_ADAPTER_ADDRESSES)malloc(asize);

    if (!adapters) {
        printf("Couldn't allocate %ld bytes for adapters.\n", asize);
        WSACleanup();
        return -1;
    }

    int r = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, 0,
            adapters, &asize);
    if (r == ERROR_BUFFER_OVERFLOW) {
        printf("GetAdaptersAddresses wants %ld bytes.\n", asize);
        free(adapters);
    } else if (r == ERROR_SUCCESS) {
        break;
    } else {
        printf("Error from GetAdaptersAddresses: %d\n", r);
        free(adapters);
        WSACleanup();
        return -1;
    }
} while (!adapters);

PIP_ADAPTER_ADDRESSES adapters;어댑터 변수에 주소 정보를 담을 것이다. 이를 위해서 메모리 크기를 할당해주어야하므로 asize를 설정해주었다.

int r = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_INCLUDE_PREFIX, 0, adapters, &asize);함수를 통하여 네트워크 어댑터 정보를 받는데, 첫번째 인자인 AF_UNSPEC는 IPv4든, IPv6든 다 가져오라는 것이다. GAA_FLAG_INCLUDE_PREFIX는 주소들의 리스트를 요청하기위해서 설정해야하는 파라미터이다. 3번째 파라미터는 0 또는 NULL이어야 하고, 그 다음 우리의 어댑터 버퍼와 사이즈를 넣어준다.

여기서 에러가 발생하면 r변수에 에러 status code가 날라오는데, 그 값에 따라 설정해주어야 할 것들이 있다. ERROR_BUFFER_OVERFLOW는 asize가 작기 때문에 발생한 에러로, asize를 더 큰 것으로 바꾸어주어 다시 시도한다. asize는 우리가 바꾸는 것이 아닌,GetAdaptersAddresses에 들어갈 때 자동으로 바뀐다. ERROR_SUCCESS는 성공한 경우이다. else는 생각지 못한 에러로 이 경우는 window socket 정보들이 할당된 상태이다. 이 경우는 adapter는 물론 window sock관련 메모리 정보들도 할당을 해제해주어야 한다.

PIP_ADAPTER_ADDRESSES adapter = adapters;
while (adapter) {
    printf("\nAdapter name: %S\n", adapter->FriendlyName);

    PIP_ADAPTER_UNICAST_ADDRESS address = adapter->FirstUnicastAddress;
    while (address) {
        printf("\t%s",
                address->Address.lpSockaddr->sa_family == AF_INET ?
                "IPv4" : "IPv6");

        char ap[100];

        getnameinfo(address->Address.lpSockaddr,
                address->Address.iSockaddrLength,
                ap, sizeof(ap), 0, 0, NI_NUMERICHOST);
        printf("\t%s\n", ap);

        address = address->Next;
    }

    adapter = adapter->Next;
}

다음은 위의 linux, macOS code와 마찬가지로, 링크드 리스트로 된 address정보를 뽑아오는 부분이다. 잘 보면 리눅스 부분과 별반 다를게 없다.

getnameinfo(address->Address.lpSockaddr, address->Address.iSockaddrLength, ap, sizeof(ap), 0, 0, NI_NUMERICHOST); 함수를 통해서, address 정보들을 ap 버퍼에 문자열로 채워넣는 것이다.

이제 빌드해보고 실행해보도록 하자.

gcc win_init.c -o win_init.exe -liphlpapi -lws2_32
./win_init.exe

결과는 다음과 같다.

Adapter name:   IPv6    xxxx::xxxx:xxxx:xxxx:xxxx
        IPv4    xxx.xxx.xxx.xxx

Adapter name:   IPv6    xxxx::xxxx:xxxx:xxxx:xxxx
        IPv4    xxx.xxx.xxx.xxx

Adapter name:   IPv6    xxxx::xxxx:xxxx:xxxx:xxxx
        IPv4    xxx.xxx.xxx.xxx

Adapter name: Bluetooth         IPv6    xxxx::xxxx:xxxx:xxxx:xxxx
        IPv4    xxx.xxx.xxx.xxx

Adapter name: Loopback Pseudo-Interface 1
        IPv6    xxxx::xxxx:xxxx:xxxx:xxxx
        IPv4    xxx.xxx.xxx.xxx

Adapter name: Wi-Fi
        IPv6    xxxx::xxxx:xxxx:xxxx:xxxx
        IPv4    xxx.xxx.xxx.xxx

이제 소켓 프로그래밍을 통해서, 어떻게 네트워크가 동작하고 어떤 식으로 구성이 되었는 지 확인해보도록하자, 우리가 사용하는 다양한 라이브러리들도 결국 이 socket 프로그래밍을 기반으로 만들어졌음을 생각하며 공부해보도록 하자.

0개의 댓글