. [C언어, 웹서버] open_clientfd / listen_clientfd
본문 바로가기
프로그래밍 공부/CSPP

[C언어, 웹서버] open_clientfd / listen_clientfd

by 불냥이_ 2021. 1. 24.

그림 1. 네트워크 응용프로그램 개요

 이번 시간에는 그림 1의 왼편에 나타나있는 open_clientfd를 알아보겠다. open_clientfd는 getaddrinfo부터 connect까지 담당해주는 파일이다.

 

우선 개요는 다음과 같다.

#include "csapp.h"

int open_clientfd(char *hostname, char *port);
	Returns: descriptor if OK, −1 on error

 이 함수는 호스트명;char hostname 을 사용하는 클라이언트가 서버 포트;char port 와 연결하고, 클라이언트 디스크립터를 반환한다.

 

함수의 상세 코드는 다음과 같다.

int open_clientfd(char *hostname, char *port)
{
    int clientfd;
    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 */
    Getaddrinfo(hostname, port, &hints, &listp);

    /* Walk the list for one that we can successfully connect to */
    for (p = listp; p; p = p->ai_next)
    {
        /* 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 */
        Close(clientfd); /* Connect failed, try another */
    }

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

 6~11줄 : hostinfo와 비슷하다. 우선 호스트명;hostname과 접속할 포트;port를 입력해서, addrinfo 구조체의 리스트를 생성한다. (이 addrinfo 구조체의 정보는 서버의 정보를 담고있다.) 

 

힌트의 구성은 다음과 같다.

hints.ai_socktype = SOCK_STREAM : 소켓이 인터넷 연결의 끝점이 될 것을 나타낸다. 

hints.ai_flags = AI_NUMERICSERV : getnameinfo 함수 (getaddrinfo의 역함수로서, 소켓으로부터 호스트와 서비스 이름 등을 얻어낸다.) 는 서비스이름을 리턴하나, AI_NUMERICSERV를 통해 서비스이름 대신에 포트번호를 리턴하도록 한다.

hints.ai_flags = AI_ADDRCONFIG : connect()를 사용할 경우, 이 플래그를 쓰는 것을 추천한다. (라고 되어있는데 왜 추천하는지는 모르겠다. 단지, 로컬 호스트가 IPv4로 배정된 경우에만 getaddrinfo가 IPv4를 반환하도록 한다. 라고 되어있다.)

 

 13줄~24줄 : addrinfo를 하나씩 방문하면서 socket()함수를 실행한다. ([C언어, 웹서버] client 관련 함수 (tistory.com) 참조) 도메인 형식(IPv4/6 등), 서비스 타입 (TCP/UDP 등), protocol(역시 TCP/UDP 등의 프로토콜) 을 입력한다. (getaddrinfo로 생성한 addrinfo에 이미 들어있다.)

 

 socket을 만드는 데 성공하면 클라 디스크립터를 반환하며, 이는 connect()에서 사용된다.

connect()는 클라 디스크립터, 서버의 주소 정보 (ai_addr), 주소 길이를 인자로 가진다. connect()가 성공하면 서버로 연결요청이 들어가서 서버는 accept()함수를 실행시킬 것이고, 연결요청이 실패하면 클라 디스크립터를 폐기한다. (close())

 

 26~32줄 : 접속하는데 성공하든, 모든 시도가 무산되든 서버에 접속하기 위해 사용했던 addrinfo는 폐기해야한다. (단지메모리 할당만 해제해주는 것이다.) 그리고 p가 NULL 값을 가리킨다면 (13~24줄 에서 행했던, addrinfo를 가지고 했던 모든 연결 시도가 실패했다면) -1값을 반환하여 실패했음을 알린다. 만약 그렇지않다면 clientfd를 반환한다. (이는 socket() 으로 생성하고, connect()까지 완료한 클라 디스크립터이다.)

 

 

 그렇다면 서버측에서는 어떻게 기동할것인가? 

그림 1의 우측에 있는 open_listenfd()는 다음과 같다.

#include "csapp.h"

int open_listenfd(char *port);
	Returns: descriptor if OK, −1 on error

  open_listenfd()는 받을 준비가 된 listenfd (파일 디스크립터)를 생성한다. 인자는 오직 port(서비스 종류)만 받으며

상세는 아래와 같다.

 

int open_listenfd(char *port)
{
    struct addrinfo hints, *listp, *p;
    int listenfd, 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 */
    Getaddrinfo(NULL, port, &hints, &listp);

    /* Walk the list for one that we can bind to */
    for (p = listp; p; p = p->ai_next)
    {
        /* 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,
                   (const void *)&optval, sizeof(int));

        /* Bind the descriptor to the address */
        if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
            break;       /* Success */
        Close(listenfd); /* Bind failed, try the next */
    }

    /* 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;
}

 기본적으로 open_clientfd와 유사한 것을 알 수 있다. 다른 부분만 체크해보자.

 

 9번 줄 : open_clientfd()에서는 AI_ADDRCONFIG만 있었으나, open_listenfd()에서는 AI_PASSIVE도 사용할 수 있다.

getaddrinfo는 기본적으로 connect()의 clinetfd (클라 디스크립터) 를 위한 socket을 만들도록 활동한다. 그러나 이 플래그를 사용하면 getaddrinfo가 listen()을 위한 socket을 만들 수 있도록 활동한다.

이 플래그를 사용함으로서 서버는 요청하는 클라의 IP 주소에 관계없이 요청을 수락하도록 할 수 있다.

 

 20~22번 줄 : Setsockopt는 책에서도 넘어감으로 자세하게는 알 수 없다. 단지, 서버가 종료된 뒤, 재시작해도 즉각적으로 연결 요청을 받아들일 수 있게 한다. (Setsockopt()을 쓰지 않는다면 재시작한 서버는 연결 요청을 거절한다고 한다.)

 

 25~28번 줄 : bind()를 활용하여 socket()으로 생성된 listenfd와 포트 번호를 ip주소에 묶는다. 이렇게해서 클라와 서버에 접속하면 그들만의 소켓으로 소통할 수 있게 된다.

 

 36번 줄 : listen()함수를 호출하여, 서버가 connect() 요청을 받을 수 있도록 대기상태에 들어간다. LISTENQ는 서버가 어떤 클라와 연결 중일때, 또다른 클라로부터 연결 요청이 있다면, 그 요청을 대기시키는 대기열이다. 

 

 

 

댓글