mojo's Blog

Network Programming: Part II 본문

학교에서 들은거 정리/시스템프로그래밍

Network Programming: Part II

_mojo_ 2022. 4. 8. 03:02

※ Sockets Interface : socket

 

클라이언트와 서버는 socket 함수를 사용하여 socket descriptor 를 생성합니다.

int socket(int domain, int type, int protocol)

 

Example :

int clientfd = Socket(AF_INET, SOCK_STREAM, 0);

 

  • AF_INET : 32비트 IPV4 주소를 사용 중임을 나타낸다.
  • SOCK_STREAM : 소켓이 연결의 끝점이 됨을 나타낸다.

프로토콜에 따라 다르다.

가장 좋은 방법은 getaddrinfo 를 사용하여 parameter 를 자동으로 생성하여 코드가 프로토콜에 의존하지 않도록 하는 것이다.

 

 

※ Sockets Interface :  bind

 

서버는 bind 를 사용하여 커널에 서버의 socket address 를 socket descriptor 와 연결하도록 요청한다.

int bind(int sockfd, SA *addr, socklen_t addrlen);

 

이 프로세스는 descriptor sockfd 에서 읽음으로써 endpoint 가 주소인 connection 에 도착하는 바이트를 읽을 수 있다.

마찬가지로, sockfd 에 대한 쓰기는 endpoint 가 addr 인 연결을 따라 전송된다.

가장 좋은 방법으로 getaddrinfo 를 사용하여 addr 및 addrlen 인자를 제공하는 것이다.

 

 

※ Sockets Interface : listen

 

기본적으로 커널은 소켓 함수로부터 descriptor 가 connection 의 클라이언트 끝에 있을 활성 소켓이라고 가정한다.

서버는 listen 함수를 호출하여 클라이언트가 아닌 서버가 descriptor 를 사용할 것임을 커널에 알린다.

int listen(int sockfd, int backlog);

 

활성 소켓에서 클라이언트의 연결 요청을 수락할 수 있는 수신 소켓으로 sockfd 를 변환한다.

backlog 는 커널이 요청을 거부하기 전에 대기열에 있어야 하는 미결 연결 요청 수에 대한 힌트이다.

 

 

※ Sockets Interface : accept

 

서버는 accept를 호출하여 클라이언트의 연결 요청을 기다린다.

int accept(int listenfd, SA *addr, int *addrlen);

 

listenfd 에 바인딩된 연결에 연결 요청이 도착할 때까지 기다린 다음 addrlen의 소켓 주소에 클라이언트 소켓 주소를 입력한다.

Unix I/O 루틴을 통해 클라이언트와 통신하는 데 사용할 수 있는 연결된 descriptor 를 반환한다.

 

 

※ Sockets Interface : connect

 

클라이언트는 connect 를 호출하여 서버와의 연결을 설정한다.

int connect(int clientfd, SA *addr, socklen_t addrlen);

 

소켓 주소 addr 에 대한 서버와 연결을 설정하려고 시도한다.

  • 만약 성공하면, clientfd 가 이제 읽고 쓸 준비가 된다.
  • Resulting connection 은 소켓 쌍으로 특정지어진다. (x : y, addr.sin_addr : addr.sin_port)
  • x : 클라이언트 주소
  • y : 클라이언트 호스트에서 클라이언트 프로세스를 고유하게 식별하는 ephemeral porot 이다.

 

 

※ accept Illustrated

 

 

1. 서버 블록이 수락 중, listening descriptor listenfd 에서 연결 요청을 기다리는 중이다.

2. 클라이언트가 connect 함수를 호출하여 서버와의 연결을 요청한다.

3. 서버는 수락된 connfd descriptor 값을 반환한다. 

     따라서 clientfd, connfd 사이에 연결이 성립되게 된다.

 

 

※ Connected vs Listening Descriptors

 

Listening descriptor

  • 클라이언트 연결 요청에 대한 end point
  • 한 번 생성되어 서버의 lifetime 동안 존재함

Connected descriptor

  • 클라이언트와 서버 간의 연결의 endpoint
  • 서버가 클라이언트의 연결 요청을 수락할 때마다 새로운 descriptor 가 작성된다.
  • 클라이언트에 서비스를 제공하는 데 걸리는 시간만큼만 존재한다.

Why the distinction?

  • 여러 클라이언트 연결을 통해 동시에 통신할 수 있는 동시 서버를 허용한다.
  • ex : 새로운 요청을 받을 때마다 요청을 처리할 수 있는 child 를 fork 한다.

 

 

※ Socket Interface

 

open_cilentfd, open_listenfd 함수를 보기 전에 전체적인 흐름을 한번 살펴보고 넘어가보도록 하자.

 

 

 

 

※ Sockets Helper : open_clientfd

 

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 */  //line:netp:openclientfd:closefd
    }

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

 

clientfd 를 받아오도록 하는 open_clientfd 함수이다.매개변수로 hostname 과 port 를 받아와서 수행되는 것을 알 수 있다.우선 hints 에 대한 초기화 작업이 진행된다.

  • hints.ai_socktype = SOCK_STREAM : connection 을 open 할 수 있도록 소켓 타입을 설정한다.
  • hints.at_flags = AI_NUMERICSERV | AI_ADDRCONFIG : connection 에 대한 추천을 할 수 있도록 플래그를 설정한다.

 

그리고 Getaddrinfo 함수를 호출함으로써 listp 에 family, socktype, protocol, ... 등 여러 정보들을 받아올 수 있다.

그래서 connect 가 성공이 될 때까지 listp 를 next 함으로써 탐색한다.

  • socket(p->ai_family, p->ai_socktype, p->ai_protocol) : socket descriptor 를 만들며 실패할 경우 다음 주소로 이동한다.
  • connect(clientfd, p->ai_addr, p->ai_addrlen) : 연결이 이뤄질 경우 for 문을 break 하고 연결을 실패할 경우 해당 descriptor 를 close 해준다.

 

위와 같은 과정을 걸친 후, 메모리 누수를 방지하기 위해 서버의 여러 주소들의 정보들을 동적 할당하여 만든 listp 를 free 해준 후에 clientfd 값을 반환하게 된다.

 

 

※ Sockets Helper : open_listenfd

 

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,    //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 */
        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() 함수와 별 차이는 없어보인다.

그러나 for 문에서 socket 함수를 호출하는 부분까지는 동일하지만 그 이후로 다르다. (bind, listen 등)

우선, socket 함수를 호출해서 정상적으로 socket descriptor 를 할당받을 경우 Setsockopt 함수를 호출하는데 소켓의 옵션등을 설정하는 함수로 이해할 수 있겠다.

 

그 후에 bind 함수를 server descriptor 값을 인자로 하여 호출하는데 0 이 반환하게 되면 성공적으로 바인딩되는 것을 알 수 있고, 0 이외의 값이 반환되면 바인딩이 실패되므로 다음 주소들을 탐색해야 하므로 socket descriptor 값을 close 해줘야 한다.

 

for 문이 종료되면, 더 이상 탐색하지 않으므로 open_clientfd 함수와 동일하게 free 를 진행한다.

그리고 socket() => bind() 과정이 정상적으로 이뤄진다면, listen 함수를 socket 함수에서 호출하여 얻었던 socket descriptor 를 인자로 하여 호출하고 클라이언트 측의 connection request 를 수락할 것인지에 대한 여부를 따지게 된다.

만약 listen 함수를 호출하여 음수를 반환하게 된다면, socket descriptor 를 종료하고 -1 을 반환하고 listen 함수까지 정상적으로 이뤄지게 된다면 그 때 listenfd 값을 반환하게 된다.

 

여기서 핵심은 open_clientfd 와 open_listenfd 는 모두 IP의 특정 버전과 독립적이다.

 

 

※ Echo Clinet : Main Routine

 

#include "csapp.h"

int main(int argc, char **argv)
{
    int clientfd;
    char *host, *port, buf[MAXLINE];
    rio_t rio;

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

    clientfd = Open_clientfd(host, port);
    Rio_readinitb(&rio, clientfd);

    while (Fgets(buf, MAXLINE, stdin) != NULL) {
        Rio_writen(clientfd, buf, strlen(buf));
        Rio_readlineb(&rio, buf, MAXLINE);
        Fputs(buf, stdout);
    }
    Close(clientfd); //line:netp:echoclient:close
    exit(0);
}
/* $end echoclientmain */

 

echo 를 하도록 할 수 있는 client 코드이다.

parameter 로 서버의 host, port 를 받아서 Open_clientfd() 를 호출하여 clientfd 값을 받는다.

clientfd 라는 descriptor 값을 통해 Fgets() 함수를 호출하여 터미널에서 문구를 입력하면 clientfd 와 연결된 곳에서 buf 를 담아주고 Rio_writen() 함수를 통해 descriptor 의 clientfd 에 buf 에 담긴 내용을 작성한다.

그 후에 서버 측에서 read 하고 write 하는 작업이 일어날 때 까지 blocking 이 일어난다.

blocking 이 끝나면 서버측에서 write 을 하였기 때문에 그걸 Rio_readlineb() 함수를 통해서 buf 에 담아와서 Fputs 함수를 통해 서버측에서 write 한 결과를 출력하게 된다.

 

 

※ Iterative Echo Server : Main Routine

 

#include "csapp.h"

void echo(int connfd);

int main(int argc, char **argv)
{
    int listenfd, connfd;
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;  /* Enough space for any address */  //line:netp:echoserveri:sockaddrstorage
    char client_hostname[MAXLINE], client_port[MAXLINE];

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

    listenfd = Open_listenfd(argv[1]);
    while (1) {
        clientlen = sizeof(struct sockaddr_storage);
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
        Getnameinfo((SA *) &clientaddr, clientlen, client_hostname, MAXLINE,
                    client_port, MAXLINE, 0);
        printf("Connected to (%s, %s)\n", client_hostname, client_port);
        echo(connfd);
        Close(connfd);
    }
    exit(0);
}
/* $end echoserverimain */

 

서버측은 Accept 이 되면 client 측과 data 교환이 일어날 수 있도록 하는 descriptor connfd 가 생긴다.

그러면 Getnameinfo 함수를 통해서 현재 클라이언트의 주소를 호스트 이름으로 받아와서 프린트가 찍힌다.

그리고 echo() 함수를 통해서 client 와 연결되어진 connfd 값을 통해 echo 작업을 취한 후 해당 descriptor 를 close 한다.

 

실행 결과를 보면 다음과 같다.

 

'학교에서 들은거 정리 > 시스템프로그래밍' 카테고리의 다른 글

Synchronization: Basics  (0) 2022.04.28
Concurrent Programming  (0) 2022.04.14
Network Programming: Part I  (0) 2022.04.01
System-Level I/O  (0) 2022.04.01
Exceptional Control Flow: Signals and Nonlocal Jumps  (0) 2022.03.25
Comments