CSAPP 11. Network Programming

Matthew Woo·2021년 9월 24일
0

Computer Science

목록 보기
2/12


정글에서 c언어로 웹서버를 구현하게 되었는데 코드들과 CSAPP 책의 11장. 정글에서 웹서버 주차에 공부했던 내용을 정리해보고자 한다.

패드에다 정리했다가 들고오는 부분들이 좀 있어서 이미지가 깔끔하지 못하거나 필기가 꽤나 날림체입니다..

1. The Client-Server Programming Model

A server manages some resource, and it provides some service for its clients by manipulating that resource.

서버는 자원을 관리하고 그 자원을 manipuate하여 클라이언트에게 서비스를 제공한다.

Client 란?

서비스를 사용하는 사용자 혹은 사용자의 단말기를 가리킨다.

Server 란?

서비스를 제공하는 컴퓨터이며, 다수의 클라이언트를 위해 존재하기 때문에 일반적으로 매우 큰 용량과 성능을 가지고 있었다. 허나 최근들어 서버의 역할을 하면서 동시에 클라이언트 기능을 하는 환경들이 많이 생겨나고 있다.

클라이언트-서버의 기본적인 트랜잭션을 살펴보면,

  1. 클라이언트(하나 혹은 다수)가 서버에 request을 보내 transaction 을 시작합니다.
  2. 서버는 요청을 받고, 특정 함수를 실행 하는 등의 방법으로 서버의 resource를 manipulate 합니다.
  3. 서버는 2의 결과로 response를 클라이언트로 보내고 그 다음 요청을 기다립니다.
  4. 클라이언트는 response 받은 결과를 처리합니다. (ex. 받은 결과물을 스크린에 띄우기 등등)

네트워크

네트워크란?

A computer network is a set of computers sharing resources located on or provided by network nodes.

네트워크 노드(endpoint)들에 의해 제공되는 자원들을 공유(share)하기 위한 컴퓨터들의 set(집합)들을 의미한다.

각 호스트들에게 네트워크는 하나의 I/O 디바이스로 인식된다. I/O 버스의 확장 슬롯에 꽂혀있는 어댑터는 네트워크에 물리적인 인터페이스를 제공한다. 네트워크에서 수신한 데이터는 I/O와 메모리 버스를 거쳐서 어댑터에서 메모리로, 대개 DMA 방식으로 복사된다.

물리적으로 네트워크는 계층구조를 갖는 시스템이다. 네트워크는 공부할게 많은 영역이지만 이 책에서는 짧게 소개하고 있는데 Host - Hub - Bridge의 계층구조를 갖고 있음을 보여준다. 브릿지와 브릿지 사이에는 라우터들을 통해 데이터가 전달된다.

군 복무할 때 옆 중대에서 철수하면서 대청소를 하더니 노란색 굵은 네트워크가 어뎁터가 잔뜩 꼽혀있는 브릿지의 전원코드를 뽑아놓고 가버려서 한동안 인터넷이 끊기고 지휘관 분들이 난리났던 기억이 난다.

이렇게 네트워크에서 Host와 Host들 간에 통신을 중계해주면 전송 계층에서 Host내의 Process와 Process 간 통신이
가능하게 해준다. IP주소는 고유의 단말기 주소. 집주소의 개념이고, 해당 컴퓨터에서 여러개의 프로세스가 실행 중일 수 수 있을텐데 IP주소 뒤에 붙은 Port로 전달받을 process에게 전달한다.

Socket Interface

socket 이란?

소켓은 네트워크 상에서 두 개의 프로세스가 양방향 통신이 가능하도록 해주는 장치이다.
커널에게 소켓은 하나의 open되어 있는 file이다. 해당 파일을 read/write 함으로서 다른 프로세스와 통신할 수 있다.

그럼 소켓 인터페이스란?

소켓 인터페이스는 application들 간의 네트워크를 구성하기위해 사용되는 함수들의 집합이다.


Client에서는 getaddrinfo 를 통해 해당 서버의 ip 주소를 upd 통신 방식으로 받아온 다음,

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

해당 주소를 인자로 주고 socket을 생성한 다음 connect 함수가 실행된다.

 int connect(int clientfd, const struct sockaddr *addr, socklen_t addrlen);

첫 번째 인자로는 socket 함수로 return 받은 clientfd를 인자로 받아서 서버와 연결을 시도한다.
connect 함수는 연결이 성공할 때까지 block되어 있다가 성공한다면 clientfd 와 서버의 connfd가 연결되고 해당 파일을 read/write하게 되지만 연결이 실패하면 -1을 return한다.


Server에서는 소켓을 생성 후 해당 소켓에 서버의 IP, Port 주소를 bind 함수로 bind(붙여야) 한다.

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

처음에는 클라이언트가 알아서 서버 주소로 request를 보낼텐데 서버는 왜 본인의 주소를 bind하는거지 라는 생각이 들었으나 서버 컴퓨터 입장에서 여러 프로세스를 실행하며 각기 다른 주소를 구분해야하기 위해 다른 포트번호가 필요하기에 unique한 소켓을 만든다.

int listen(int sockfd, int backlog);

이후 첫번째 인자로는 bind함수로 주소를 할당한 socketfd를 받아 listen 함수를 실행하게 되는데 서버가 클라이언트의 request를 보내는걸 기다리게 되고 request가 와서 연결에 성공하면 server 쪽에서는 connfd를 생성하게 되고, client 쪽에서는 clientfd 가 생성되게 된다.
backlog로 전달받은 두 번째 인자는 client에서 보낸 패킷들이 queue로 들어와 backlog의 크기만큼 최대한 보관할 수 있다. tcp 방식에서는 backlog만큼 queue가 가득 차 있으면 패킷이 소실되고 다시 서버쪽에 request를 보내야한다. (그만큼 느려짐)


코드

클라이언트와 서버의 주요 메인 코드들

Client

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

    /* potential server addresses 리스트 설정 */
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;  /* 연결 설정 */
    hints.ai_flags = AI_NUMERICSERV;  /* port를 숫자로 받기. */
    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;
    }
  
    /* 연결될 때 까지 addr 가 나온 구조체를 돌면서 연결 요청 */
    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; 
            
        /* Connect to the server */
        if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1) // 서버에 연결을 요청 
            break; 
        if (close(clientfd) < 0) { /* 연결 닫기 */
            fprintf(stderr, "open_clientfd: close failed: %s\n", strerror(errno));
            return -1;
        } 
    } 

    /* 구조체 정리 */
    freeaddrinfo(listp);
    if (!p) /* All connects failed */
        return -1;
    else    /* The last connect succeeded */
        return clientfd;
}

server

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

    /* server addresses  설정*/
    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM;             /* SOCK_STREAM  */
    hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* IP Address */
    hints.ai_flags |= AI_NUMERICSERV;            
    if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
        fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port, gai_strerror(rc));
        return -2;
    }

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

        /* "Address already in use" error 해제 */
        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) // 해당 소켓을 listenfd 에 bind 처리함
            break; /* Success */
        if (close(listenfd) < 0) { /* Bind failed, try the next */
            fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
            return -1;
        }
    }

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

    /* listen 상태로 client 기다리기 */
    if (listen(listenfd, LISTENQ) < 0) {
        close(listenfd);
	return -1;
    }
    return listenfd;
}
profile
지속가능하고 안정적인 시스템을 만들고자 합니다.

0개의 댓글