네트워크 어댑터 현재, 디바이스의 네트워크 연결 상태를 보여준다. 가령 IPv4를 쓰는지, IPv6를 쓰는 지, IP는 어떻게 되는 지, MAC 주소는 어떻게 되는 지, 서브넷 마스크는 무엇인 지를 알려준다.
이를 출력하는 방법은 linux, macOS terminal에서 다음의 명령어를 치면 된다.
ifconfig
뭔가 리스트들이 쭈욱 나올 텐데 잘 읽어보면 네트워크 정보를 알려주는 부분이다. 브로드 캐스트 IP가 무엇이고, 서브넷 마스크가 무엇이고, 현재 호스트 IP주소는 어디에 접속되어 있는 지 등등 자세히 알려준다.
이 내용을 출력하는 c언어 코드를 만들어보자, linux, macOS와 window 버전의 코드가 다르다. 때문에 아래의 코드는 linux, macOS에서만 구동된다.
#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;
사용한 주소를 반납하면, 마무리 된다.
#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 프로그래밍을 기반으로 만들어졌음을 생각하며 공부해보도록 하자.