정글

에코 서버(1) - 호스트와 서비스 변환

nkdev 2025. 5. 5. 17:23

에코 서버는 클라이언트가 표준 입력으로 받은 데이터를 서버가 받아서 그대로 돌려주면 클라이언트가 받은 데이터를 표준 출력하는 간단한 프로그램이다. CSAPP 책에서는 CGI 프로그램을 이해하기 전에 에코 서버를 구현해본다. 

소켓 인터페이스 기반 네트워크 응용 프로그램의 개요

소켓 인터페이스 기반 응용프로그램은 위와 같은 절차를 거쳐 통신한다.

 

이번 포스팅에서는 getaddrinfo() 함수가 어떻게 연결 가능한 소켓 주소 리스트들을 반환하는지 알아보자.

그리고 그 리스트를 해석하는 getnameinfo(), 메모리 누수를 방지하기 위해 리스트를 반환할 때 쓰이는 freeaddrinfo()도 함께 알아보자.

 

호스트와 서비스 변환

1. getaddrinfo()

  • 도메인 주소, 포트번호를 받아 연결 가능한 소켓 주소 정보를 연결 리스트로 제공하는 함수
  • 함수 인자
    • host : 도메인 이름 ("www.domain.com") 또는 숫자로 된 IP 주소 ("127.0.0.1") 
    • service : 포트 번호 ("80") 또는 서비스 이름 ("http")
    • hints : 어떤 주소를 원하는지 설정하는 부분 (옵션 구조체)
    • res : 주소 정보를 담을 연결 리스트의 시작 포인터 
  • host로 도메인 이름이 들어오면 IP로 바꿔주고, IP로 들어오면 바로 sockaddr 구조체로 파싱한다.
  • 함수 인자 hints는 옵션이며, getaddrinfo()가 리턴하는 소켓 주소들에 대한 상세한 설정을 하는 부분이다.
  • 반환값은 int타입이며 성공하면 0, 실패하면 오류 코드를 리턴한다. 
  • 인자로 받은 이중 포인터 'struct addrinfo** result'를 통해 연결 가능한 소켓 주소 정보 구조체의 리스트를 넘겨준다.
  • result는 함수 호출 후 [addrinfo구조체1] -> [addrinfo구조체2] -> ... 이렇게 연결 리스트 형태로 주소 정보를 갖게 된다.

getaddrinfo()가 리턴하는 자료구조

  • getaddrinfo()로 소켓 주소 정보를 여러 개 반환받아 그 중에서 고르는 이유?
    • 하나의 도메인이 여러 IP를 가질 수도 있고(IPv4, IPv6) 시스템 환경 마다 지원 가능한 주소 체계가 다르기 때문에 특정 주소가 현재 시스템이나 네트워크에서 지원되지 않는 경우 connect나 binding이 실패할 수 있다.
    • 예전처럼 우리가 직접 IP, Port를 지정해주는 것보다 getaddrinfo()와 같이 모든 프로토콜에 대해 동작하는 이식성 있는 함수를 사용하는 것이 좋다.
/*******************************
 * Protocol-independent wrappers
 *******************************/

int getaddrinfo(const char *host, const char *service, 
                 const struct addrinfo *hints, struct addrinfo **result)
더보기

addrinfo 구조체

  • getaddrinfo()는 도메인이나 IP를 받아서 그에 해당하는 연결 가능한 소켓 주소 후보들을 반환한다.
  • 이 때 반환되는 리스트는 addrinfo 구조체가 리스트로 쭉 이어져있는 형태이다.
  • hints라는 인자로 해당 구조체 변수값을 설정하여 getaddrinfo()가 리턴하는 구조체 리스트에 대한 상세한 설정을 할 수 있다.
    • ai_family, ai_sockettype, ai_protocol, ai_flags 필드만 설정될 수 있으며 다른 필드는 0 또는 NULL로 설정되어야 함
    • 실제로 우리는 memset을 이용하여 전체 구조체를 0으로 설정한 후 일부 필드만 값을 줌
    • ai_family : 리스트의 ip주소를 제한
      • AF_INET : IPv4
      • AF_INET6 : IPv6
    • ai_socktype : 인터넷 연결의 끝점이 될 것
      • end point = ip주소 + port(접속 지점)
      • 나중에 getaddrinfo함수를 이용해서 자동으로 생성할 것 
      • SOCK_STREAM : TCP
      • SOCK_DGRAM : UDP
    • ai_protocol : 구체적인 프로토콜 지정
      • 0 : default (TCP/UDP)
      • IPPROTO_TCP : TCP
      • IPPROO_DUP : UDP
    • sockaddr : 실제 소켓 주소 정보 -> 이 부분은 나중에 설명 예정
struct addrinfo {
    int ai_flags;
    int ai_family;      // AF_INET, AF_INET6
    int ai_socktype;    // SOCK_STREAM, SOCK_DGRAM
    int ai_protocol;
    size_t ai_addrlen;
    struct sockaddr *ai_addr;  // 핵심 주소 정보
    char *ai_canonname;
    struct addrinfo *ai_next;  // 연결 리스트!
};

 

 

getaddrinfo() 함수가 어디서 쓰이는지 알아보자.

1. 서버 입장에서는 성공적으로 바인딩할 수 있는 소켓의 후보들을 구할 때 쓰인다.

서버측의 open_listenfd()함수를 보면 getaddrinfo()를 호출하고 바인딩 가능한 소켓 주소 정보를 struct addrinfo* listp에 리스트로 반환받은 뒤 for문으로 리스트를 순회하며 바인딩할 수 있는지 시도해본다.

int open_listenfd(char *port) 
{
    struct addrinfo hints, *listp, *p;
    int listenfd, rc, optval=1;

    /* Get a list of potential server addresses */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;             /* Accept connections */
    hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
    hints.ai_flags |= AI_NUMERICSERV;            /* ... using port number */
    if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port, gai_strerror(rc));
        return -2;
    }

    /* Walk the list for one that we can bind to */
    for (p = listp; p; p = p->ai_next) { //listp를 순회하면서 바인딩 가능한 소켓의 주소를 찾음
        /* Create a socket descriptor */
        if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) 
            continue;  /* Socket failed, try the next */

        /* Eliminates "Address already in use" error from bind */
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,    //line:netp:csapp:setsockopt
                   (const void *)&optval , sizeof(int));

        /* Bind the descriptor to the address */
        if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
            break; /* Success */
        if (close(listenfd) < 0) { /* Bind failed, try the next */
            fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
            return -1;
        }
    }


    /* Clean up */
    freeaddrinfo(listp);
    if (!p) /* No address worked */
        return -1;

    /* Make it a listening socket ready to accept connection requests */
    if (listen(listenfd, LISTENQ) < 0) {
        close(listenfd);
	return -1;
    }
    return listenfd;
}

 

2. 클라이언트 입장에서는 성공적으로 연결 가능한 소켓의 후보를 구할 때 쓰인다. 

클라이언트측의 open_clientfd()함수를 보면 getaddrinfo()를 호출하고 연결 가능한 소켓 주소 정보를 struct addrinfo* listp에 리스트로 반환받은 뒤 for문으로 리스트를 순회하며 연결할 수 있는지 시도해본다.

int open_clientfd(char *hostname, char *port) {
    int clientfd, rc;
    struct addrinfo hints, *listp, *p;

    /* Get a list of potential server addresses */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;  /* Open a connection */
    hints.ai_flags = AI_NUMERICSERV;  /* ... using a numeric port arg. */
    hints.ai_flags |= AI_ADDRCONFIG;  /* Recommended for connections */
    if ((rc = getaddrinfo(hostname, port, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo failed (%s:%s): %s\n", hostname, port, gai_strerror(rc));
        return -2;
    }
  
    /* Walk the list for one that we can successfully connect to */
    for (p = listp; p; p = p->ai_next) { //listp를 순회하면서 연결 가능한 소켓의 주소를 찾음
        /* Create a socket descriptor */
        if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) 
            continue; /* Socket failed, try the next */

        /* Connect to the server */
        if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1) 
            break; /* Success */
        if (close(clientfd) < 0) { /* Connect failed, try another */  //line:netp:openclientfd:closefd
            fprintf(stderr, "open_clientfd: close failed: %s\n", strerror(errno));
            return -1;
        } 
    } 

    /* Clean up */
    freeaddrinfo(listp);
    if (!p) /* All connects failed */
        return -1;
    else    /* The last connect succeeded */
        return clientfd;
}

 getaddrinfo()함수가 하는 일은 여기까지 알아보자. 더 깊게 파면 목적에 맞지 않는 공부 방향이 될 것 같다.

 

(참고)

  • 메모리 누수를 피하기 위해서 freeaddrinfo()를 호출하여 getaddrinfo()를 통해 얻은 리스트를 반환해야 한다. 
  • getaddrinfo()가 0이 아닌 에러코드를 리턴하면 gai_strerror을 호출하여 이 코드를 메시지 스트링으로 변환할 수 있다.

 

2. getnameinfo()

  • getaddrinfo()의 반대 역할을 하는 함수
  • 소켓 주소 구조체를 대응되는 호스트와 서비스 이름 스트링으로 변환
  • 서버 입장에서는 클라이언트가 접속했을 때 그 주소가 들어있는 구조체 sockaddr을 받는다. 이 구조체를 사람이 알아보려면 변환이 필요하다. 
  • sockaddr 구조체(바이너리 형태)에 해당하는 소켓 주소를 사람이 알아들을 수 있는 형태로 해석해주는 함수
  • 즉, getaddrinfo()가 addrinfo 구조체 리스트 안의 sockaddr 구조체 형태로 실제 소켓 정보를 포장했는데, 그 정보를 다시 역으로 해석해주는 역할을 함 
  • 함수 인자
    • sa : 변환 대상 소켓 주소. IP/Port 정보가 들어있는 sockaddr 포인터
    • salen : sockaddr 구조체(sa)의 Byte size
    • host : 변환된 도메인 이름/IP 주소를 저장할 버퍼
    • hostlen : host 버퍼의 크기
    • serv : 포트번호 또는 서비스 이름을 저장할 버퍼
    • servlen : serv 버퍼의 크기
    • flags : 동작 방식을 조절하는 옵션 플래그
  • NI_NUMERICHOST : 기본적으로는 도메인 이름을 리턴하지만 이 플래그를 활성화하면 도메인 이름이 아닌 IP 주소를 리턴
  • NI_NUMERICSERV : 기본적으로는 /etc/services를 찾아가서 서비스 이름을 리턴하지만 이 플래그를 활성화하면 탐색 과정을 생략하고 간단히 포트번호를 리턴
  • NI_NAMEREQD : DNS 이름이 없으면 오류 발생
  • NI_NOFQDN : 짧은 호스트 이름만 반환
  • 보통 서버에서는 NI_NUMERICHOST를 사용한다고 한다. DNS가 느리고 로그에 IP주소만 있어도 충분하기 때문이다.
  • 반환값은 int타입이며 성공 시 0, 실패 시 오류 코드가 리턴된다.
void getnameinfo(const struct sockaddr *sa, socklen_t salen, char *host, 
                 size_t hostlen, char *service, size_t servlen, int flags)
더보기

책 11.17에 소개된 hostinfo.c

  • 도메인 이름(또는 IP주소)를 실제 주소 정보로 바꿔서 출력하는 것이 목적인 간단한 프로그램이다.
  • 이 프로그램을 통해서 getaddrinfo(), getnameinfo()가 어떻게 동작하는지 살펴볼 수 있다.
  • getaddrinfo("www.naver.com", NULL, &hints, &result);
    • "www.naver.com"에 해당하는 도메인 이름을 IP 주소로 바꾸기만 하면 되므로 포트 정보는 필요 없어서 두 번째 인자인 서비스 이름을 NULL로 준다.
  • getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags)
    • getaddrinfo()로 반환받은 addrinfo 구조체 리스트를 순회하면서 어떤 주소를 반환 받았는지 하나씩 출력해본다.
/* $begin hostinfo */
#include "csapp.h"

int main(int argc, char **argv) 
{
    struct addrinfo *p, *listp, hints;
    char buf[MAXLINE];
    int rc, flags;

    if (argc != 2) {
	fprintf(stderr, "usage: %s <domain name>\n", argv[0]);
	exit(0);
    }

    /* Get a list of addrinfo records */
    //hints를 통해 반환 받을 주소값을 커스텀하기
    memset(&hints, 0, sizeof(struct addrinfo));                         
    hints.ai_family = AF_INET;       /* IPv4 only */       
    hints.ai_socktype = SOCK_STREAM; /* Connections only */ 
    
    //첫 번째 인자로 argv[1]즉 domain name을 전달, 두 번째 인자로 NULL을 준다.(domain name을 변환할 것만을 요구하고 있으므로 NULL) 
    if ((rc = getaddrinfo(argv[1], NULL, &hints, &listp)) != 0) { 
        fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(rc));
        exit(1);
    }

    /* Walk the list and display each IP address */
    flags = NI_NUMERICHOST; /* Display address string instead of domain name */
    for (p = listp; p; p = p->ai_next) {
    	//getaddrinfo()로 반환받은 addrinfo 구조체 리스트를 순회하면서 어떤 주소를 반환 받았는지 하나씩 출력해본다.
        Getnameinfo(p->ai_addr, p->ai_addrlen, buf, MAXLINE, NULL, 0, flags);
        printf("%s\n", buf);
    } 

    /* Clean up */
    Freeaddrinfo(listp);

    exit(0);
}
/* $end hostinfo */

 

hostinfo.c 실행 결과

  • hostinfo.c 실행을 통해 twitter.com이 NSLOOKUP을 이용해서 보았던 네 개의 IP주소에 매핑되는 것을 알 수 있다.
linux> ./hostinfo twitter.com
199.16.156.102
199.16.156.230
199.16.156.6
199.16.156.70

 

3. freeaddrinfo()

  • 메모리 누수 방지를 위해 getaddrinfo()가 반환한 addrinfo 구조체 리스트를 다시 반환하는 함수
void freeaddrinfo(struct addrinfo *result)

 

 

'정글' 카테고리의 다른 글

gcc 컴파일 하는 법  (0) 2025.05.06
OSI 7 Layers/TCP/UDP/HTTP  (4) 2025.05.06
CGI  (0) 2025.05.05
정적 전역 변수  (2) 2025.04.29
RBTree 정리  (0) 2025.04.18