Windows · Winsock
winsock2.h와 ws2tcpip.h를 포함하고 Winsock 사용 전 WSAStartup 성공을 확인합니다. 사용을 마치면 WSACleanup으로 초기화와 짝을 맞춥니다. MSVC의 pragma comment(lib)는 MinGW 링크 옵션을 대신하지 않으므로 MinGW에서는 -lws2_32가 필요합니다.
IP 주소와 포트를 구조체에 담고, 바이트 순서를 맞춘 뒤 주소와 이름을 변환합니다.
2-2장에서 만든 소켓은 운영체제의 통신 자원입니다. 주소 구조체는 그 소켓을 어느 주소에 묶거나 어느 상대와 연결할지 알려 주는 별도의 값입니다. 소켓 번호와 IP 주소는 같은 것이 아닙니다.
sockaddr는 여러 주소 계열을 받기 위한 공통 인터페이스입니다. IPv4 값은 sockaddr_in, IPv6 값은 sockaddr_in6에 보관하고 함수가 요구할 때 sockaddr 포인터로 전달합니다. 형변환이 IPv4를 IPv6로 바꾸거나 메모리 크기를 늘려 주지는 않습니다.
| 필드 | IPv4 | IPv6 | 역할 |
|---|---|---|---|
| 주소 계열 | sin_family = AF_INET | sin6_family = AF_INET6 | 구조체 해석 방법 |
| 포트 | sin_port | sin6_port | 16비트 서비스 번호 |
| IP | sin_addr · in_addr | sin6_addr · in6_addr | 32비트 / 128비트 주소 |
| 나머지 | sin_zero | sin6_flowinfo, sin6_scope_id | 패딩 / 흐름 정보와 범위 식별 |
in_addr의 s_addr에는 4바이트 주소가 들어갑니다. IPv6는 16바이트이며 이식 가능한 바이트 접근에는 s6_addr를 씁니다. 원본의 운영체제별 공용체 내부 이름을 그대로 복사하지 않아도 됩니다.
원본의 크기 표는 sockaddr와 sockaddr_in을 16바이트, sockaddr_in6를 28바이트, Bluetooth용 sockaddr_bth를 30바이트로 보여 줍니다. 이 크기가 모든 운영체제에서 보장되는 것은 아닙니다. 함수 인수에는 실제 객체의 sizeof를 사용합니다. 여러 주소 계열의 결과를 담아야 할 때는 sockaddr_storage를 사용할 수 있습니다.
보충 · 함수를 호출해 주소를 받아올 때는 버퍼의 길이도 준비합니다. Linux의 accept 같은 함수는 socklen_t 길이를 입력·출력 인수로 받고, Winsock은 int 길이를 사용합니다. 반환된 길이와 주소 계열을 확인한 뒤 해석합니다.
바이트 정렬은 여러 바이트로 이루어진 정수를 메모리에 놓는 순서입니다. 여기서는 메모리의 정렬 경계나 비트 순서를 바꾸는 작업을 뜻하지 않습니다. 같은 0x12345678도 낮은 주소부터 어떤 바이트를 두는지에 따라 배열이 달라집니다.
12 | 34 | 56 | 7878 | 56 | 34 | 12호스트 바이트 순서는 CPU 환경의 표현이고, 네트워크 바이트 순서는 통신 규약에서 정한 표현입니다. 포트와 IP 주소는 네트워크 순서를 따릅니다. 응용프로그램이 보내는 정수 데이터의 표현도 송신자와 수신자가 별도로 약속해야 합니다. send가 임의의 구조체를 자동으로 변환해 주지는 않습니다.
| 방향 | 16 bit | 32 bit |
|---|---|---|
| 호스트 → 네트워크 | htons | htonl |
| 네트워크 → 호스트 | ntohs | ntohl |
s는 16비트, l은 32비트 변환을 뜻합니다. 16비트 포트에는 htons를 쓰고 출력할 때 ntohs로 되돌립니다. 원본의 htonl(x1)은 16비트 변수에 32비트 변환을 적용한 잘못된 선택을 보여 줍니다. x1이 정수 승격된 뒤 변환되므로 포트용 변환과 결과가 다릅니다.
주소 계열은 호스트 순서로 둡니다. inet_pton이 만든 IP는 이미 네트워크 순서이므로 다시 htonl을 적용하지 않습니다. IPv6 예제에서는 flowinfo와 scope_id를 0으로 초기화합니다. 링크 로컬 주소의 scope_id는 인터페이스 선택과 관련 있으므로 주소의 16바이트와 구분해야 합니다.
실습 출력 주의 · htons 결과를 정수로 바로 출력한 값은 호스트 바이트 순서에 따라 달라질 수 있습니다. 바이트 배열과 원래 값으로 되돌린 결과를 함께 확인하면 혼동을 줄일 수 있습니다.
사용자가 입력하는 127.0.0.1은 문자들의 나열입니다. 소켓 API는 이 문자열 자체가 아니라 이진 주소를 사용합니다. inet_pton의 p는 표시용 문자열, n은 네트워크 형식을 뜻합니다. inet_ntop은 반대 방향으로 변환합니다.
IPv4에는 AF_INET과 in_addr를, IPv6에는 AF_INET6와 in6_addr를 대응시킵니다. 출력 버퍼는 각각 INET_ADDRSTRLEN, INET6_ADDRSTRLEN을 사용하면 문자열 종료 공간까지 확보할 수 있습니다. IPv6의 연속된 0은 ::로 줄일 수 있어서 변환 후 문자열의 표기는 입력과 달라도 같은 주소일 수 있습니다.
| API · Linux | 성공 | 실패와 처리 |
|---|---|---|
| inet_pton | 1 | 0: 잘못된 주소 문자열 / -1: 지원하지 않는 주소 계열, errno 확인 |
| inet_ntop | 출력 버퍼 포인터 | NULL: errno 확인, 작은 버퍼 등 |
반환값이 0인 inet_pton 실패에 perror만 쓰면 이전 errno의 내용을 잘못 보여 줄 수 있습니다. 입력 형식 오류와 시스템 오류를 나누어 처리합니다. 변환 성공을 확인한 뒤에만 결과 구조체를 사용합니다.
struct sockaddr_in peer = {0};
peer.sin_family = AF_INET;
peer.sin_port = htons(9000);
int status = inet_pton(AF_INET, "127.0.0.1", &peer.sin_addr);
/* Continue only when status == 1. */원본 보완 · PDF 14쪽의 IPv4 전용 입력 위젯은 과거 GUI 사용 예입니다. 이 장의 콘솔 실습은 문자열을 직접 변환합니다. inet_pton은 도메인 이름을 검색하지 않으므로 www.example.com 같은 이름에는 이름 변환 API가 필요합니다.
DNS는 도메인 이름과 IP 주소 등의 정보를 연결하는 이름 시스템입니다. 하나의 이름에 여러 IP가 연결될 수 있고, 여러 이름이 같은 IP를 가리킬 수도 있습니다. 이름을 주소로 찾는 정방향 조회와 주소에서 이름을 찾는 역방향 조회는 서로 독립적인 데이터에 의존하므로 왕복해도 원래 이름이 보장되지는 않습니다.
프로그램의 이름 변환 함수는 운영체제의 이름 해석 기능을 사용합니다. 결과가 로컬 hosts 파일이나 캐시에서 올 수도 있어 함수 호출마다 반드시 DNS 패킷이 나가는 것은 아닙니다. 외부 이름 조회는 네트워크 설정과 조회 시점에 따라 성공 여부와 결과가 달라집니다.
원본은 gethostbyname으로 이름을 조회하고 gethostbyaddr로 역조회합니다. 성공 결과인 hostent는 이름, 별명 목록, 주소 계열, 주소 하나의 길이, 주소 목록을 가리킵니다. 실패하면 NULL이므로 필드에 접근하기 전에 검사해야 합니다.
h_name → 공식 이름 문자열h_aliases → 포인터 배열: [0] 별명 문자열, [1] 별명 문자열, [2] NULLh_addrtype / h_length → AF_INET / 4h_addr_list → 포인터 배열: [0] 4바이트 주소, [1] 4바이트 주소, [2] NULLh_aliases와 h_addr_list는 포인터 배열입니다. 배열의 원소가 다음 원소를 가리키는 연결 리스트가 아닙니다. h_addr_list의 각 포인터가 가리키는 이진 주소를 주소 계열과 길이에 맞게 해석하고, 배열 끝의 NULL까지 순회합니다.
원본의 GetIPAddr는 첫 주소만 복사하므로 결과가 여러 개일 수 있다는 점을 놓치지 않아야 합니다. 반환 메모리를 임의로 free하지 않고, 다음 조회 뒤에도 필요할 값은 별도 공간에 복사합니다.
원본 보완 · PDF 20쪽은 AF_INET6와 16바이트 주소를 가진 hostent 모양을 설명합니다. 이것이 Windows gethostbyname의 IPv6 지원을 뜻하지는 않습니다. Winsock의 gethostbyname은 IPv4만 반환하며 새 코드에는 getaddrinfo가 권장됩니다.
getaddrinfo는 이름과 서비스 번호를 받아 소켓 주소 후보를 연결 리스트로 돌려줍니다. AF_UNSPEC으로 IPv4와 IPv6 후보를 허용하고 ai_next를 따라 확인합니다. 실제 연결 프로그램에서는 첫 결과만 가정하지 말고 후보별 연결 실패를 처리해야 합니다. 사용을 마치면 리스트의 처음 포인터로 freeaddrinfo를 호출합니다.
Linux에서 getaddrinfo는 성공 시 0, 실패 시 오류 코드를 반환하며 gai_strerror로 설명을 얻습니다. getnameinfo는 반대 방향으로 소켓 주소를 이름과 서비스 문자열로 바꿉니다. NI_NAMEREQD는 역조회 이름이 없을 때 실패하도록 하고, NI_NUMERICHOST는 숫자 주소 문자열을 요청합니다.
원본 GetDomainName의 strncpy(name, ..., namelen)은 길이가 부족하면 문자열 끝의 NUL을 보장하지 않습니다. 복사 전에 목적지 크기를 검사하거나 결과 길이를 확인하는 방식으로 보완해야 합니다. Linux의 옛 gethostbyname 오류는 h_errno 계열이며 일반 소켓 오류의 errno와 구분합니다.
다음은 원본 실습 3-1·3-2의 변환 과정을 합치고 오류 처리를 보완한 Ubuntu/POSIX용 예제입니다. IPv4·IPv6 루프백 주소, 포트 바이트, 32비트 정수 왕복 변환을 확인합니다. 마지막 getaddrinfo는 숫자 전용 옵션을 사용하므로 DNS 조회나 서버 연결을 하지 않습니다.
print_bytes는 메모리를 바이트 단위로 출력하는 보조 함수이고 main이 실행의 시작점입니다. 주소 준비 → 변환과 확인 → 출력 → 후보 목록 생성과 해제 순서로 읽습니다. 소켓을 생성하지 않으므로 close할 소켓도 없습니다.
#define _POSIX_C_SOURCE 200112L
#include <arpa/inet.h>
#include <inttypes.h>
#include <netdb.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
static void print_bytes(const void *data, size_t length)
{
const unsigned char *bytes = data;
for (size_t i = 0; i < length; ++i) {
printf("%s%02x", i ? " " : "", (unsigned int)bytes[i]);
}
putchar('\n');
}
int main(int argc, char **argv)
{
const char *input = argc > 1 ? argv[1] : "127.0.0.1";
struct sockaddr_in address = {0};
address.sin_family = AF_INET;
address.sin_port = htons(9000);
int converted = inet_pton(AF_INET, input, &address.sin_addr);
if (converted != 1) {
if (converted == 0) {
fprintf(stderr, "Invalid IPv4 address: %s\n", input);
} else {
perror("inet_pton IPv4");
}
return EXIT_FAILURE;
}
char text[INET_ADDRSTRLEN];
if (inet_ntop(AF_INET, &address.sin_addr, text, sizeof(text)) == NULL) {
perror("inet_ntop IPv4");
return EXIT_FAILURE;
}
printf("IPv4: %s, port: %u\n", text, (unsigned int)ntohs(address.sin_port));
printf("IPv4 bytes: ");
print_bytes(&address.sin_addr, sizeof(address.sin_addr));
printf("Port bytes: ");
print_bytes(&address.sin_port, sizeof(address.sin_port));
uint32_t host = UINT32_C(0x12345678);
uint32_t network = htonl(host);
printf("Host bytes: ");
print_bytes(&host, sizeof(host));
printf("Network bytes: ");
print_bytes(&network, sizeof(network));
printf("Round trip: 0x%08" PRIx32 "\n", ntohl(network));
struct sockaddr_in6 address6 = {0};
address6.sin6_family = AF_INET6;
address6.sin6_port = htons(9000);
if (inet_pton(AF_INET6, "::1", &address6.sin6_addr) != 1) {
fputs("IPv6 conversion failed\n", stderr);
return EXIT_FAILURE;
}
char text6[INET6_ADDRSTRLEN];
if (inet_ntop(AF_INET6, &address6.sin6_addr, text6, sizeof(text6)) == NULL) {
perror("inet_ntop IPv6");
return EXIT_FAILURE;
}
printf("IPv6: %s\n", text6);
/* Numeric input only: no DNS request or socket connection. */
struct addrinfo hints = {0};
struct addrinfo *result = NULL;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV;
int error = getaddrinfo(input, "9000", &hints, &result);
if (error != 0) {
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(error));
return EXIT_FAILURE;
}
size_t count = 0;
for (const struct addrinfo *p = result; p != NULL; p = p->ai_next) {
++count;
}
printf("Numeric address candidates: %zu\n", count);
freeaddrinfo(result);
return EXIT_SUCCESS;
}
다운로드한 address_lab.c가 있는 폴더에서 실행합니다. GCC와 C 표준 라이브러리가 필요합니다. 아래 gcc 명령은 컴파일과 링크를 함께 수행하며 생성한 address_lab을 이어서 실행합니다. Windows의 Dev-C++에 이 POSIX 소스를 그대로 넣는 방법은 아닙니다.
gcc -std=c17 -Wall -Wextra -Wpedantic -g -O0 address_lab.c -o address_lab
./address_lab
./address_lab 999.1.1.1
echo $?정상 실행의 주요 예상 출력은 다음과 같습니다. Host bytes는 CPU에 따라 다릅니다. 잘못된 주소 실행은 Invalid IPv4 address 메시지와 종료 코드 1을 출력하며, 마지막 echo는 그 종료 코드를 확인합니다.
IPv4: 127.0.0.1, port: 9000
IPv4 bytes: 7f 00 00 01
Port bytes: 23 28
Network bytes: 12 34 56 78
Round trip: 0x12345678
IPv6: ::1추가 확인 · ./address_lab 192.0.2.1로 실행하면 문서용 IPv4 주소의 바이트를 비교할 수 있습니다. 이 프로그램은 입력 주소로 통신하지 않습니다. 외부 DNS의 실시간 결과를 고정된 예상 정답으로 사용하지 않습니다.
원본 common.h는 여러 예제에서 쓰는 헤더와 오류 출력 함수를 한곳에 모은 보조 파일입니다. C 언어가 기본 제공하는 헤더 이름이 아닙니다. Windows와 Linux용 내용을 섞어 한 파일로 복사하면 안 됩니다.
winsock2.h와 ws2tcpip.h를 포함하고 Winsock 사용 전 WSAStartup 성공을 확인합니다. 사용을 마치면 WSACleanup으로 초기화와 짝을 맞춥니다. MSVC의 pragma comment(lib)는 MinGW 링크 옵션을 대신하지 않으므로 MinGW에서는 -lws2_32가 필요합니다.
주소 변환은 arpa/inet.h, 이름 조회는 netdb.h, 소켓 인터페이스는 sys/socket.h를 사용합니다. WSAStartup은 없습니다. 원본의 SOCKET typedef와 INVALID_SOCKET 매크로는 이름을 맞추는 보조 정의이며 모든 API 차이를 없애지는 않습니다.
원본 err_quit는 오류를 출력한 뒤 프로그램을 끝내고, err_display는 호출한 곳으로 돌아갑니다. Windows의 FormatMessageA는 반환값을 검사한 뒤 메시지 버퍼를 사용하고 할당 성공 시 LocalFree로 해제해야 합니다. Linux에서는 실패 직후 errno를 보존해야 다른 호출이 오류 값을 덮어쓰는 일을 피할 수 있습니다.
이 장의 실습은 필요한 헤더만 포함한 한 파일로 구성했습니다. common.h 부록의 pthread·poll·epoll 헤더가 이번 주소 변환 실습에 모두 필요하다는 뜻은 아닙니다. 이전 장의 오류 처리·자원 정리 설명과 함께 읽으세요.
아닙니다. 함수가 주소를 받는 인터페이스에 맞추는 포인터 형변환입니다. 실제 객체의 주소 계열·필드·크기는 그대로입니다.
아닙니다. 결과는 이미 네트워크 순서입니다. 정수로 준비한 포트에는 htons를 따로 적용합니다.
아닙니다. 호스트가 이미 빅엔디언이면 바이트 순서를 유지할 수 있습니다. 목적은 네트워크 순서로 표현하는 것입니다.
아닙니다. IPv4 주소 하나의 길이가 4바이트라는 뜻입니다. 주소 개수는 h_addr_list의 NULL까지 확인합니다.
아닙니다. 정방향·역방향 데이터는 별개이며 역방향 이름이 없거나 다른 이름을 반환할 수 있습니다.
아닙니다. 주소를 변환하고 숫자 주소 후보를 생성할 뿐입니다. socket·connect·send는 호출하지 않습니다.
강의 원본은 ch03_소켓 주소 구조체1.pdf입니다. 파일은 25쪽이며 한 쪽에 슬라이드 두 장을 배치합니다. 표의 슬라이드 번호는 인쇄 번호를 기준으로 하고, 번호 없는 표지·목차·구분 화면은 배치 순서로 셉니다. 마지막 감사 화면은 번호가 없습니다. PDF 쪽은 파일의 실제 페이지 위치입니다.
| 인쇄 슬라이드 | 내용 | |
|---|---|---|
| 1–2 | 1–4 | 장 목표와 목차 |
| 3–8 | 5–16 | 주소 구조체·IPv4·IPv6·함수 인수 |
| 9–13 | 17–26 | 바이트 순서·변환 함수·실습 3-1 |
| 14–17 | 27–34 | IP 입력·주소 변환·실습 3-2 |
| 18–21 | 35–42 | DNS·hostent·사용자 함수·실습 3-3 |
| 22–25 | 43–49 | Windows/Linux common.h와 main 골격 |
1장은 네트워크·주소·포트의 개념과 에코 프로그램, 2-1장은 C 자료구조, 2-2장은 소켓 생성·오류 처리를 다룹니다. 이 장에서는 주소 필드를 직접 채우고 변환 결과를 읽습니다.
sockaddr_storage·반환값 검사·getaddrinfo 비교와 독립 C 실습은 보충 내용입니다. 외부 도메인 조회 결과는 실행 시점과 네트워크 환경에 따라 달라지며, 원본의 GUI 화면은 당시 도구의 사용 예입니다.