C 언어로 배우는 IPv4와 IPv6 소켓 프로그래밍 차이점

C 언어에서 소켓 프로그래밍은 네트워크 애플리케이션 개발의 핵심 기술 중 하나입니다. 본 기사에서는 소켓 프로그래밍의 기본 개념을 다루며, 특히 IPv4와 IPv6를 활용한 구현의 차이점에 초점을 맞춥니다. 이를 통해 네트워크 프로토콜의 기본 원리를 이해하고, 두 주소 체계의 특성을 활용한 소켓 애플리케이션 개발 능력을 향상시킬 수 있습니다.

소켓 프로그래밍 기초


소켓 프로그래밍은 네트워크 상에서 통신하는 두 프로그램 간 데이터를 주고받는 기술입니다. 소켓(Socket)은 네트워크 연결을 위한 인터페이스로, 프로세스 간 통신을 가능하게 합니다.

소켓의 개념


소켓은 네트워크에서 데이터를 송수신하기 위한 엔드포인트입니다. 이는 운영 체제의 API를 통해 제공되며, 클라이언트와 서버 간 연결을 설정하는 데 사용됩니다.

소켓의 주요 요소

  1. IP 주소: 통신할 대상 장치를 식별합니다.
  2. 포트 번호: 응용 프로그램의 특정 프로세스를 식별합니다.
  3. 프로토콜: TCP 또는 UDP와 같은 전송 프로토콜을 지정합니다.

소켓 프로그래밍의 절차

  1. 소켓 생성: socket() 함수를 사용해 소켓을 생성합니다.
  2. 주소 바인딩: bind()를 통해 소켓에 IP 주소와 포트를 바인딩합니다.
  3. 연결 대기/요청: 서버는 listen()으로 대기하고, 클라이언트는 connect()로 연결을 요청합니다.
  4. 데이터 송수신: send()recv() 또는 read()write()를 사용해 데이터를 주고받습니다.
  5. 연결 종료: 통신이 끝나면 close()로 소켓을 닫습니다.

이와 같은 기초는 IPv4와 IPv6 기반의 소켓 프로그래밍에서도 동일하게 적용되며, 이후 두 프로토콜의 차이점을 중심으로 심화 내용을 다룹니다.

IPv4와 IPv6의 기본 구조

IPv4의 주소 체계와 데이터 구조


IPv4(Internet Protocol Version 4)는 32비트 주소 체계를 사용하며, 총 232개(약 42억 개)의 고유 주소를 제공합니다. 각 주소는 점(.)으로 구분된 4개의 8비트 숫자로 표현됩니다(예: 192.168.1.1).

  • 헤더 구조
    IPv4 헤더는 20바이트 고정 길이로 구성되며, 다음과 같은 주요 필드를 포함합니다.
  • 버전(Version): 4비트를 사용해 프로토콜 버전을 나타냅니다.
  • 헤더 길이(IHL): IP 헤더의 길이를 지정합니다.
  • 소스 주소(Source Address): 데이터가 출발한 IP 주소입니다.
  • 목적지 주소(Destination Address): 데이터가 향하는 IP 주소입니다.

IPv6의 주소 체계와 데이터 구조


IPv6(Internet Protocol Version 6)는 128비트 주소 체계를 사용하며, 총 2128개(약 3.4×1038)의 고유 주소를 제공합니다. 각 주소는 콜론(:)으로 구분된 8개의 16비트 블록으로 표현됩니다(예: 2001:0db8:85a3:0000:0000:8a2e:0370:7334).

  • 헤더 구조
    IPv6 헤더는 40바이트 고정 길이로, IPv4에 비해 단순화되어 있습니다. 주요 필드는 다음과 같습니다.
  • 버전(Version): 4비트로 IPv6를 나타냅니다.
  • 소스 주소(Source Address): 데이터가 출발한 IPv6 주소입니다.
  • 목적지 주소(Destination Address): 데이터가 향하는 IPv6 주소입니다.

IPv4와 IPv6의 주요 차이점

  • 주소 표현: IPv4는 32비트(점-구분), IPv6는 128비트(콜론-구분).
  • 주소 공간: IPv6는 더 많은 장치와 네트워크를 지원.
  • 헤더 구조: IPv6는 간소화된 헤더로 처리 속도를 향상.
  • 기능 개선: IPv6는 IPsec, 멀티캐스트, 자동 구성과 같은 기능이 기본 내장.

이러한 차이점은 네트워크의 확장성과 효율성을 높이기 위한 IPv6 설계의 기반이 됩니다. C 언어를 활용한 구현 시 이러한 구조적 특성을 이해하는 것이 중요합니다.

C 언어에서의 IPv4 소켓 구현

IPv4 소켓 프로그래밍 개요


IPv4 기반 소켓 프로그래밍은 32비트 IP 주소를 사용하여 클라이언트와 서버 간 데이터를 송수신합니다. C 언어에서는 소켓 API를 통해 이를 구현할 수 있습니다.

IPv4 소켓 구현 단계

1. 소켓 생성


socket() 함수를 사용하여 IPv4 소켓을 생성합니다.

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("Socket creation failed");
    exit(EXIT_FAILURE);
}
  • AF_INET: IPv4 주소 체계 사용.
  • SOCK_STREAM: TCP 프로토콜 사용(UDP의 경우 SOCK_DGRAM).
  • 0: 기본 프로토콜 사용.

2. 주소 바인딩


bind() 함수로 소켓에 IP 주소와 포트를 바인딩합니다.

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 모든 인터페이스에서 수신
server_addr.sin_port = htons(8080); // 포트 번호 8080

if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    perror("Bind failed");
    exit(EXIT_FAILURE);
}

3. 연결 대기 및 요청


서버는 listen()으로 연결 요청을 대기하고, 클라이언트는 connect()로 요청을 보냅니다.

// 서버
if (listen(sockfd, 5) < 0) {
    perror("Listen failed");
    exit(EXIT_FAILURE);
}

// 클라이언트
if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    perror("Connection failed");
    exit(EXIT_FAILURE);
}

4. 데이터 송수신


send()recv()를 사용하여 데이터를 송수신합니다.

// 데이터 송신
char *message = "Hello, Client!";
send(client_sock, message, strlen(message), 0);

// 데이터 수신
char buffer[1024];
int bytes_received = recv(client_sock, buffer, sizeof(buffer), 0);
buffer[bytes_received] = '\0';
printf("Received: %s\n", buffer);

5. 연결 종료


close()로 소켓 연결을 종료합니다.

close(sockfd);

IPv4 소켓 구현 예제

서버 예제 코드:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);

    // 소켓 생성
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("Socket creation failed");
        return 1;
    }

    // 주소 바인딩
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Bind failed");
        return 1;
    }

    // 연결 대기
    listen(server_fd, 5);

    printf("Waiting for a connection...\n");
    client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
    if (client_fd < 0) {
        perror("Connection failed");
        return 1;
    }

    // 데이터 송수신
    char buffer[1024] = {0};
    recv(client_fd, buffer, sizeof(buffer), 0);
    printf("Received: %s\n", buffer);

    char *response = "Hello from server!";
    send(client_fd, response, strlen(response), 0);

    // 연결 종료
    close(client_fd);
    close(server_fd);

    return 0;
}

IPv4 소켓 프로그래밍은 네트워크 기본을 학습하는 데 중요한 단계입니다. 이후 IPv6 소켓과 비교해보면 두 프로토콜의 차이를 더 명확히 이해할 수 있습니다.

C 언어에서의 IPv6 소켓 구현

IPv6 소켓 프로그래밍 개요


IPv6 기반 소켓 프로그래밍은 128비트 IP 주소를 사용하여 클라이언트와 서버 간 데이터를 송수신합니다. 이는 IPv4의 주소 공간 제한을 해결하며, C 언어에서는 IPv4와 유사한 소켓 API를 사용해 구현됩니다.

IPv6 소켓 구현 단계

1. 소켓 생성


IPv6 소켓은 AF_INET6 주소 체계를 사용해 생성됩니다.

int sockfd = socket(AF_INET6, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("Socket creation failed");
    exit(EXIT_FAILURE);
}
  • AF_INET6: IPv6 주소 체계 사용.
  • SOCK_STREAM: TCP 프로토콜 사용.

2. 주소 구조 설정


IPv6 주소는 struct sockaddr_in6 구조체를 사용해 설정합니다.

struct sockaddr_in6 server_addr;
server_addr.sin6_family = AF_INET6;
server_addr.sin6_addr = in6addr_any; // 모든 인터페이스에서 수신
server_addr.sin6_port = htons(8080); // 포트 번호 8080

3. 주소 바인딩


bind() 함수로 소켓에 IPv6 주소를 바인딩합니다.

if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    perror("Bind failed");
    exit(EXIT_FAILURE);
}

4. 연결 대기 및 요청


서버는 listen()으로 연결 요청을 대기하고, 클라이언트는 connect()로 요청을 보냅니다.

// 서버
if (listen(sockfd, 5) < 0) {
    perror("Listen failed");
    exit(EXIT_FAILURE);
}

// 클라이언트
struct sockaddr_in6 server_addr;
server_addr.sin6_family = AF_INET6;
inet_pton(AF_INET6, "fe80::1", &server_addr.sin6_addr); // 서버의 IPv6 주소
server_addr.sin6_port = htons(8080);

if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    perror("Connection failed");
    exit(EXIT_FAILURE);
}

5. 데이터 송수신


send()recv()를 사용해 데이터를 주고받습니다.

// 데이터 송신
char *message = "Hello, IPv6 Client!";
send(client_sock, message, strlen(message), 0);

// 데이터 수신
char buffer[1024];
int bytes_received = recv(client_sock, buffer, sizeof(buffer), 0);
buffer[bytes_received] = '\0';
printf("Received: %s\n", buffer);

6. 연결 종료


close()로 소켓 연결을 종료합니다.

close(sockfd);

IPv6 소켓 구현 예제

서버 예제 코드:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int server_fd, client_fd;
    struct sockaddr_in6 server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);

    // 소켓 생성
    server_fd = socket(AF_INET6, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("Socket creation failed");
        return 1;
    }

    // 주소 바인딩
    server_addr.sin6_family = AF_INET6;
    server_addr.sin6_addr = in6addr_any;
    server_addr.sin6_port = htons(8080);
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Bind failed");
        return 1;
    }

    // 연결 대기
    listen(server_fd, 5);

    printf("Waiting for a connection...\n");
    client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
    if (client_fd < 0) {
        perror("Connection failed");
        return 1;
    }

    // 데이터 송수신
    char buffer[1024] = {0};
    recv(client_fd, buffer, sizeof(buffer), 0);
    printf("Received: %s\n", buffer);

    char *response = "Hello from IPv6 server!";
    send(client_fd, response, strlen(response), 0);

    // 연결 종료
    close(client_fd);
    close(server_fd);

    return 0;
}

IPv6 소켓 프로그래밍은 현대 네트워크 환경에서 필수적인 기술입니다. IPv4 소켓과의 구조적 차이점을 이해하고 응용함으로써, 더 확장된 네트워크 환경에서 애플리케이션을 개발할 수 있습니다.

IPv4와 IPv6의 주요 차이점

주소 체계의 차이

  • IPv4: 32비트 주소 체계를 사용하며 약 42억 개의 고유 주소를 제공합니다. 점으로 구분된 4개의 10진수로 표현됩니다(예: 192.168.0.1).
  • IPv6: 128비트 주소 체계를 사용하며, 2128개의 고유 주소를 제공합니다. 콜론으로 구분된 8개의 16진수 블록으로 표현됩니다(예: 2001:0db8:85a3:0000:0000:8a2e:0370:7334).

헤더 구조의 차이

  • IPv4 헤더: 20~60바이트의 가변 길이를 가지며, 필드가 많아 처리 속도가 느릴 수 있습니다.
  • IPv6 헤더: 40바이트 고정 길이를 가지며, IPv4에 비해 단순화되어 라우터와 네트워크 장비의 처리 효율을 높입니다.

기능의 차이

  • IPv4: 기본적으로 보안 및 멀티캐스트 지원이 내장되어 있지 않습니다. 추가 설정이 필요합니다.
  • IPv6:
  • IPsec: 데이터 암호화 및 인증을 기본적으로 지원하여 보안성을 강화합니다.
  • 자동 구성: DHCP 없이 네트워크 구성 가능.
  • 멀티캐스트: 대규모 네트워크에서 효율적인 데이터 전송이 가능.

주소 공간과 네트워크 확장성

  • IPv4: 주소 공간이 제한적이며, NAT(Network Address Translation) 기술을 사용하여 주소 부족 문제를 해결합니다.
  • IPv6: 거의 무제한의 주소 공간을 제공하며, NAT 없이도 대규모 네트워크 확장이 가능합니다.

소켓 프로그래밍의 차이점

  1. 주소 구조체:
  • IPv4는 struct sockaddr_in을 사용합니다.
  • IPv6는 struct sockaddr_in6을 사용하며, 128비트 주소와 추가 필드를 포함합니다.
  1. 호스트 주소 지정:
  • IPv4는 inet_pton(AF_INET, ...)과 같은 함수로 주소를 변환합니다.
  • IPv6는 inet_pton(AF_INET6, ...)로 IPv6 주소를 처리합니다.
  1. 바인딩과 호환성:
    IPv6 소켓은 IPv4 주소를 포함한 Dual Stack을 지원할 수 있으나, 추가 설정(setsockopt)이 필요합니다.

응용 측면에서의 차이점

  • IPv4: 소규모 네트워크 및 레거시 시스템에 적합.
  • IPv6: IoT, 대규모 클라우드 환경, 그리고 미래의 인터넷 확장에 적합.

IPv4와 IPv6의 이러한 차이점을 이해하면, 소켓 프로그래밍을 통해 두 프로토콜을 효과적으로 다룰 수 있습니다. C 언어로 구현할 때는 이러한 차이를 고려하여 코드 작성 및 디버깅을 수행해야 합니다.

호환성을 고려한 프로그래밍

Dual Stack 소켓 프로그래밍


IPv4와 IPv6를 모두 지원하는 네트워크 애플리케이션을 개발하려면 Dual Stack 방식이 필요합니다. Dual Stack은 하나의 네트워크 장비나 애플리케이션에서 두 프로토콜을 동시에 처리할 수 있도록 구성합니다.

IPv4 및 IPv6 주소를 처리하는 소켓 설정


IPv6 소켓은 기본적으로 IPv4 주소와 호환될 수 있지만, 이를 명시적으로 활성화해야 합니다. C 언어에서는 setsockopt()를 사용하여 IPv4-mapped IPv6 주소를 활성화할 수 있습니다.

int opt = 0; // Dual Stack 활성화를 비활성화(기본값)
setsockopt(sockfd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
  • IPV6_V6ONLY: 0으로 설정하면 IPv4-mapped IPv6 주소를 허용합니다.

Dual Stack 서버 구현


Dual Stack 서버는 하나의 소켓에서 IPv4와 IPv6 클라이언트를 모두 처리하거나, 별도의 소켓을 생성하여 각각의 요청을 처리할 수 있습니다.

1. 단일 소켓으로 처리


IPv6 소켓을 생성하고, IPV6_V6ONLY 옵션을 비활성화하여 IPv4-mapped IPv6 주소를 허용합니다.

struct sockaddr_in6 server_addr;
server_addr.sin6_family = AF_INET6;
server_addr.sin6_addr = in6addr_any; // IPv6 및 IPv4 주소 모두 수신
server_addr.sin6_port = htons(8080);

if (bind(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
    perror("Bind failed");
    exit(EXIT_FAILURE);
}

2. 별도의 소켓으로 처리


하나는 IPv4 소켓, 다른 하나는 IPv6 소켓을 생성하여 각각 요청을 처리합니다.

int ipv4_sockfd = socket(AF_INET, SOCK_STREAM, 0);
int ipv6_sockfd = socket(AF_INET6, SOCK_STREAM, 0);

// 각각 다른 주소로 바인딩
bind(ipv4_sockfd, (struct sockaddr*)&ipv4_addr, sizeof(ipv4_addr));
bind(ipv6_sockfd, (struct sockaddr*)&ipv6_addr, sizeof(ipv6_addr));

// 각 소켓에서 연결 대기
listen(ipv4_sockfd, 5);
listen(ipv6_sockfd, 5);

호환성 테스트 및 디버깅


Dual Stack 애플리케이션의 안정성을 보장하려면 다양한 네트워크 환경에서 테스트해야 합니다.

1. IPv4 및 IPv6 연결 테스트

  • IPv4 테스트: IPv4 주소를 사용하는 클라이언트에서 연결 요청을 보냅니다(예: 127.0.0.1).
  • IPv6 테스트: IPv6 주소를 사용하는 클라이언트에서 연결 요청을 보냅니다(예: ::1).

2. Wireshark를 활용한 네트워크 패킷 분석


Wireshark와 같은 네트워크 분석 도구를 사용하여 IPv4 및 IPv6 데이터 패킷을 캡처하고 확인합니다.

호환성 유지의 이점

  • 레거시 시스템 지원: 기존 IPv4 기반 클라이언트와의 호환성을 유지합니다.
  • 미래 지향적 설계: IPv6 환경으로의 원활한 전환을 준비할 수 있습니다.
  • 유연성 향상: 다양한 네트워크 환경에서 동작 가능한 애플리케이션을 개발합니다.

Dual Stack 프로그래밍은 IPv4와 IPv6 네트워크의 경계를 허물고, 다양한 환경에서 애플리케이션을 실행할 수 있는 강력한 방법입니다. 이를 통해 프로토콜 전환기의 네트워크 요구를 충족시킬 수 있습니다.

디버깅과 문제 해결

소켓 프로그래밍에서 발생하는 일반적인 문제


소켓 프로그래밍은 네트워크 환경, 코드 오류, 설정 문제로 인해 다양한 문제를 발생시킬 수 있습니다. 이를 해결하기 위해 주요 이슈와 디버깅 방법을 이해하는 것이 중요합니다.

1. 소켓 생성 오류

  • 문제: socket() 함수 호출 시 반환값이 -1인 경우.
  • 원인: 잘못된 주소 체계(AF_INET 또는 AF_INET6) 또는 프로토콜 설정.
  • 해결:
  if ((sockfd = socket(AF_INET6, SOCK_STREAM, 0)) < 0) {
      perror("Socket creation failed");
      exit(EXIT_FAILURE);
  }

2. 주소 바인딩 실패

  • 문제: bind() 함수가 실패하여 “Address already in use” 오류 발생.
  • 원인: 동일한 포트 번호를 이미 다른 소켓이 사용 중이거나, 소켓 옵션이 설정되지 않음.
  • 해결: 소켓 옵션 활성화.
  int opt = 1;
  setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

3. 연결 실패

  • 문제: 클라이언트가 connect() 호출 시 “Connection refused” 오류 발생.
  • 원인: 서버가 실행 중이지 않거나, 잘못된 IP 주소와 포트 번호를 사용.
  • 해결: 서버와 클라이언트의 IP 주소 및 포트가 일치하는지 확인.

4. 데이터 송수신 문제

  • 문제: send()recv() 호출 시 반환값이 음수 또는 0.
  • 원인: 네트워크 연결 중단, 데이터 크기 초과, 버퍼 설정 오류.
  • 해결: 버퍼 크기를 확인하고, 데이터 전송 상태를 확인.
  char buffer[1024];
  int bytes_received = recv(sockfd, buffer, sizeof(buffer), 0);
  if (bytes_received > 0) {
      buffer[bytes_received] = '\0';
      printf("Received: %s\n", buffer);
  }

디버깅 도구와 방법

1. 로그와 디버깅 출력


소켓 상태를 확인하는 로그를 코드에 추가합니다.

printf("Socket created successfully\n");
printf("Binding to port 8080\n");

2. 네트워크 트래픽 분석


Wireshark와 같은 네트워크 트래픽 분석 도구를 사용하여 패킷 송수신 상태를 확인합니다.

3. Telnet을 사용한 연결 테스트


서버 포트가 열려 있는지 확인하기 위해 Telnet을 사용합니다.

telnet <server_ip> <port>

트러블슈팅 사례

사례 1: IPv6 연결 문제

  • 문제: IPv6 주소를 사용하는 클라이언트가 서버와 연결되지 않음.
  • 원인: 서버가 IPV6_V6ONLY로 설정되어 IPv4-mapped IPv6 주소만 허용.
  • 해결: setsockopt()로 Dual Stack 활성화.

사례 2: 데이터 전송 중 일부 손실

  • 문제: 클라이언트가 보낸 데이터의 일부만 수신됨.
  • 원인: TCP 스트림에서 데이터가 분할되어 전송.
  • 해결: 수신 루프를 작성하여 모든 데이터를 받을 때까지 반복.
  int total_bytes = 0;
  while ((bytes_received = recv(sockfd, buffer, sizeof(buffer), 0)) > 0) {
      total_bytes += bytes_received;
  }

문제 예방을 위한 팁

  1. 항상 소켓 호출의 반환값을 확인하고, 적절히 에러를 처리합니다.
  2. 소켓 옵션을 설정하여 포트 재사용 및 네트워크 설정을 최적화합니다.
  3. 네트워크 환경(방화벽, 라우터 설정)을 점검하여 외부 장애를 최소화합니다.

소켓 프로그래밍 디버깅은 정확한 원인 분석과 적절한 도구 활용으로 복잡한 문제를 해결할 수 있습니다. 이를 통해 안정적인 네트워크 애플리케이션을 구현할 수 있습니다.

연습 문제와 코드 예제

학습을 심화하기 위한 연습 문제

문제 1: IPv4 클라이언트-서버 애플리케이션 작성

  • 목표: TCP 기반의 IPv4 클라이언트와 서버를 구현하여 문자열을 주고받는 프로그램 작성.
  • 요구 사항:
  • 서버는 포트 8080에서 클라이언트 요청을 수신.
  • 클라이언트는 사용자 입력 문자열을 서버로 전송.
  • 서버는 받은 문자열을 그대로 클라이언트에게 반환.

문제 2: IPv6 기반의 멀티클라이언트 서버

  • 목표: IPv6 주소를 사용하는 서버가 여러 클라이언트와 동시 통신하는 프로그램 작성.
  • 요구 사항:
  • 서버는 쓰레드를 사용하여 클라이언트 요청을 병렬 처리.
  • 각 클라이언트는 서버와 3번의 메시지를 주고받아야 함.

문제 3: Dual Stack 서버

  • 목표: IPv4와 IPv6 클라이언트 모두를 지원하는 Dual Stack 서버 작성.
  • 요구 사항:
  • 단일 소켓으로 IPv4와 IPv6 요청을 처리.
  • 클라이언트 연결 시 서버가 클라이언트의 IP 버전을 콘솔에 출력.

코드 예제

예제 1: IPv4 에코 서버

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int server_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_len = sizeof(client_addr);
    char buffer[1024] = {0};

    // 소켓 생성
    server_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_fd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 주소 바인딩
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(8080);
    if (bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Bind failed");
        exit(EXIT_FAILURE);
    }

    // 연결 대기
    listen(server_fd, 5);

    printf("Waiting for a connection...\n");
    client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len);
    if (client_fd < 0) {
        perror("Connection failed");
        exit(EXIT_FAILURE);
    }

    // 데이터 송수신
    recv(client_fd, buffer, sizeof(buffer), 0);
    printf("Received: %s\n", buffer);
    send(client_fd, buffer, strlen(buffer), 0);

    // 연결 종료
    close(client_fd);
    close(server_fd);
    return 0;
}

예제 2: IPv6 클라이언트

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>

int main() {
    int sockfd;
    struct sockaddr_in6 server_addr;
    char *message = "Hello, Server!";
    char buffer[1024] = {0};

    // 소켓 생성
    sockfd = socket(AF_INET6, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("Socket creation failed");
        exit(EXIT_FAILURE);
    }

    // 서버 주소 설정
    server_addr.sin6_family = AF_INET6;
    inet_pton(AF_INET6, "::1", &server_addr.sin6_addr); // 로컬호스트 IPv6 주소
    server_addr.sin6_port = htons(8080);

    // 서버 연결
    if (connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Connection failed");
        exit(EXIT_FAILURE);
    }

    // 데이터 송신 및 수신
    send(sockfd, message, strlen(message), 0);
    printf("Message sent to server\n");
    recv(sockfd, buffer, sizeof(buffer), 0);
    printf("Received from server: %s\n", buffer);

    // 소켓 닫기
    close(sockfd);
    return 0;
}

연습 문제 풀이를 통한 이해 심화


위의 문제와 예제를 통해 IPv4와 IPv6 소켓 프로그래밍의 차이점과 구현 방법을 실습할 수 있습니다. 이를 통해 다양한 네트워크 환경에서의 실용적인 프로그램을 작성할 능력을 갖출 수 있습니다.

요약


IPv4와 IPv6 소켓 프로그래밍은 네트워크 애플리케이션 개발의 핵심 요소입니다. 본 기사에서는 C 언어를 활용하여 두 프로토콜의 차이점과 구현 방법, 호환성을 고려한 Dual Stack 프로그래밍, 디버깅과 문제 해결 방법을 다뤘습니다.

IPv4는 레거시 시스템에 적합하며, IPv6는 확장성과 보안이 강화된 현대 네트워크 환경에 최적화되어 있습니다. 두 프로토콜을 활용한 소켓 프로그래밍 기술은 다양한 네트워크 요구를 충족시키는 강력한 도구가 될 것입니다.