[Jungle] Week7. Socket, echo 서버 구현

somi·2024년 5월 6일
0

[Krafton Jungle]

목록 보기
47/68

클라이언트-서버 프로그래밍 모델

1개의 server process, 1개 이상의 client process


1. 클라이언트가 서비스 필요로 할 때 클라이언트는 1개의 요청(request)를 서버에 보내는 것으로 트랜잭션 개시
(예를 들어 웹 브라우저가 파일을 필요로 할 때 웹 서버로 요청 보냄)
2. 서버는 요청을 받고 해석하고 자신의 자원들을 조작. (예를 들어 웹 서버가 브라우저로부터 요청을 받을 때 디스크 파일 읽음)
3. 서버는 응답(Response)를 클라이언트로 보내고, 그 후 다음 요청 기다림.
4. 클라이언트는 응답을 받고 이를 처리.


Socket

: 연결의 종단점. 네트워크 통신을 위한 엔드 포인트
각 소켓은 인터넷 주소와 16 비트 정수 비트로 이루어진 소켓 주소를 가짐. address : port
-> 클라이언트 소켓 주소 내의 포트는 -> 클라이언트가 연결 요청할 때 커널이 자동으로 할당.(단기 ephemeral port).
서버 소켓 주소에 있는 포트는 대개 영구적으로 이 서비스에 연결되는 잘 알려진 포트

=> 웹 서버: 포트 80, 이메일 서버: 포트 25

  • 소켓 쌍: cliaddr: cliport, servaddr, servport)

  • 애플리케이션에게 소켓은 네트워크로부터 읽기/쓰기를 가능하게 하는 파일 디스크립터
    (모든 Unix의 I/O 장치들은, 네트워크를 포함해서 파일로 모델링 되어 있음!)
  • 유닉스에서 네트워크를 포함한 모든 I/O 장치들이 파일로 취급되기 때문에 소켓 역시 파일로 다루어짐. 파일을 읽고 쓰듯이 소켓을 통해 데이터 송수신 할 수 있음. -> 하지만 일반 파일을 열고 다루는 것과 달리 소켓을 사용할 때는 다른 방식으로 소켓을 열어야 한다.


소켓 주소 구조체: 일반적인 형태의 소켓 주소 제공

  • connect, bind, accept에서 주소 인자로 사용됨
  • sa_family: 프로토콜 패밀리(주소 체계) -> AF_INET = IPv4 프로토콜, AF_INET6 = IPv6 프로토콜
  • sa_data[14]: 실제 주소 데이터 포함. 예를 들어, IPv4 주소면, 포트 번호와 IP 주소가 이 배열에 포함됨.

다양한 주소 구조체 처리를 위해 struc sockaddr가 사용도미 -> 함수에 구체적인 주소 구조체 전달할 때는 그 주소 구조체 자체를 struct sockaddr *로 형변환하여 전달하게 됨


IPv4 특정 소켓 주소 구조체 - 인터넷 기반 통신 설정할 때 사용.
sin_family: 주소 체계
sin_port;: 포트 번호를 네트워크 바이트 순서로 나타냄. 소켓이 연결을 듣거나, 연결을 시작할 특정 포트를 지정
sin_addr: IP 주소를 네트워크 바이트 순서로 나타냄. 이 필드는 실제 IPv4 주소를 포함
sin_zero[8]: struct sockaddr_in 구조체의 크기를 struct sockaddr와 동일하게 맞추기 위한 패딩. 주로 0으로 초기화


The Sockets Interface

헷갈렸던 점
: Client Socket과 Connection이 만들어지는 소켓은 Server Socket이 아니라 accept API 내부에서 새로 만들어지는 소켓임 (여기서는 connfd)
실질적인 데이터 송수신은 서버 listenfd가 아니라 connfd로 생각하면 된다!

connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);

Listening descriptor vs. connected descriptor

  • listening descriptor: 클라이언트 연결 요청의 종착점.
    서버가 클라이언트의 연결 요청을 기다리도록 설정된 소켓,
    서버의 수명 동안 한번 생성되어 지속적으로 존재, 서버가 실행되는 동안 듣고 있음.
    클라이언트로부터 들어오는 연결 요청을 감지하고 수락하는 역할
  • connected descriptor: 클라이언트와 서버 간의 실제 연결의 종착점
    서버가 클라이언트의 연결 요청을 수락할 때마다 새로 생성.
    이 디스크립터를 통해 클라이언트와 서버 간의 데이터 송수신이 이루어짐
    클라이언트를 서비스를 하는 동안만 존재하며 작업이 완료되면 소멸됨.

RIO(Robust Input/Output)

rio_t 구조체

#define RIO_BUFSIZE 8192
typedef struct {
    int rio_fd;                /* Descriptor for this internal buf 내부 버퍼와 연결된 디스크립터*/
    int rio_cnt;               /* Unread bytes in internal buf 내부 버퍼에 아직 읽히지 않은 바이트 수
                                -> 사용할 수 있는 데이터의 양*/
    char *rio_bufptr;          /* Next unread byte in internal buf
                                내부 버퍼 내에서 다음에 읽을 바이트를 가리키는 포인터. */
    char rio_buf[RIO_BUFSIZE]; /* Internal buffer 실제 내부 버퍼 - 현재 8192 바이트(데이터를 임시로 저장하는 공간으로 사용)*/
} rio_t;

=> 일반적인 Read, write 시스템 콜은 전체 요청된 바이트 수를 읽거나 쓰지 못할 수 있음.
입출력 장치를 견고하게 만들어준다는 의미

RIO는 2가지 형태를 띠고 있음.

1) 2진 데이터로 이루어진 Unbuffered input / output
-> rio_readn, rio_writen 함수로 이를 처리할 것
2) 2진 데이터와 텍스트로 이루어진 Buffered input
-> rio_readlineb, rio_readnb 함수로 처리할 것


ssize_t rio_readn(int fd, void *usrbuf, size_t n) 

ssize_t rio_writen(int fd, void *usrbuf, size_t n) 

static ssize_t rio_read(rio_t *rp, char *usrbuf, size_t n)

void rio_readinitb(rio_t *rp, int fd) 

ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n) 

ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen) 

rio_readn

: 식별자 fd의 현재 파일 위치에서 메모리 위치 usrbuf로 최대 n바이트 전송.

  • read가 -1을 리턴하는 경우
  • 파일을 끝까지 전부 읽었을 때
    를 제외하고는 저장할 버퍼 크기만큼 read하는 것을 보장
  • Signal에 의해 읽기가 중단되지 않음 -> call read() again

rio_writen

: usrbuf에서 식별자 fd로 n바이트 전송

  • 언제나 버퍼 크기 만큼 쓰기 동작 수행하려고 함
  • Signal에 의해 쓰기 중단 되지 않음 -> call write() again

buffered I/O


RIO에서 버퍼를 관리
rio_buf는 버퍼가 시작된 지점
rio_bufptr은 현재 읽는 부분을 나타내는 포인터,
rio_cnt는 읽을 부분의 수

코드를 입력하세요

rio_readinitb: rio_t 타입의 구조체 초기화 함수

rio_readlineb: 한 줄 단위로 입력 받음

rio_t 구조체, 저장할 버퍼, 그리고 읽어들일 최대 바이트 크기를 인자로 받음. 함수는 개행 문자('\n')를 만나거나 지정된 최대 바이트 크기에 도달할 때까지 데이터를 읽음.

rio_readnb: 파일 fd에서 n바이트 크기를 읽어옴

rio_t 구조체, 저장할 버퍼, 그리고 읽어들일 바이트 수를 인자로 받음. 함수는 지정된 바이트 수만큼 데이터를 읽어 버퍼에 저장


open_clientfd

int open_clientfd(char *hostname, char *port)
: 호스트 hostname에서 돌아가고 포트번호 port에 연결 요청을 듣는 서버와 연결을 설정
-> 입력과 출력에 대해 준비된 열린 소켓 식별자를 리턴

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 TCP 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; //getaddrinfo 오류 시 -2 반환
    }
    
    //연결 가능한 주소 찾아서 순회
    //Socket()과 connect()성공할 때까지 linked list 탐색
    /* 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. Socket() 성공 후 서버에 연결 시도*/
        if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1) 
            break; /* Success 성공 시 반복 중단*/
        if (close(clientfd) < 0) { /* Connect failed, try another, 연결 실패 다른 주소 시도 */ 
            fprintf(stderr, "open_clientfd: close failed: %s\n", strerror(errno));
            return -1; //오류시 -1 반환
        } 
    } 

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

open_listenfd

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;
    }
    
    //socket()과 bind()가 성공할 떄까지 링크드리스트 탐색
    //바인딩할 수 있는 주소를 리스트에서 찾음
    /* 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, 소켓 생성 실패, 다음 주소로 시도 */

        //주소가 이미 사용중입니다 오류 방지 - SO_REUSEADDR => 주소 재사용 가능하게 함
        /* Eliminates "Address already in use" error from bind */
        setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR,    //line:netp:csapp:setsockopt
                   (const void *)&optval , sizeof(int));

        //bind를 활용하여 포트번호를 ip주소에 묶음
        /* 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;
}

echoclient.c


//0번째 인자 : 실행 파일, 1번쨰 인자: host name, 2번째 : 포트 번호
int main(int argc, char **argv) {
    int clientfd; //클라이언트 소켓 파일 디스크립터
    char *host, *port, buf[MAXLINE]; //호스트, 포트, 데이터 버퍼
    rio_t rio; //Robust I/O 구조체

    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 연결
    Rio_readinitb(&rio, clientfd);

    //표준 입력에서 텍스트 줄을 반복적으로 읽음
    //표준 입력 Stdin에서 MAXLINE만큼 바이트를 가져와 buf에 저장
    //fgets가 EOF 표준 입력을 만나면 종료 (사용자가 Ctrl + D를 눌렀거나 파일로 텍스트 줄을 모두 소진)
    while (Fgets(buf, MAXLINE, stdin) != NULL) {
        Rio_writen(clientfd, buf, strlen(buf)); //사용자 입력을 서버에 전송
        Rio_readlineb(&rio, buf, MAXLINE); //서버로부터 응답을 받음
        Fputs(buf, stdout); //받은 응답을 표준 출력으로 출력
    }
    Close(clientfd); //클라이언트 소켓을 닫음
    exit(0); //클라이언트 종료
}
 

echoserver.c

void echo(int connfd);

int main(int argc, char **argv) {
    int listenfd, connfd; //서버의 리스닝 소켓, 연결 소켓 파일 디스크립터

    socklen_t clientlen; //클라이언트 주소의 크기

    struct sockaddr_storage clientaddr; //클라이언트 주소 정보를 저장할 구조체
    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);
}

echo.c


//클라이언트로부터 데이터를 받아 동일한 
void echo(int connfd) {
    size_t n;
    char buf[MAXLINE];
    rio_t rio;

    Rio_readinitb(&rio, connfd);

    //클라이언트로부터 한줄 씩 읽기 - 반환값이 0이면 클라이언트가 연결을 닫았음을 의미
    //loop는 클라이언트 연결이 닫힐 때까지 계속
    while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) { 
        printf("server received %d bytes\n", (int)n); //서버가 받은 데이터의 바이트 수 출력
        Rio_writen(connfd, buf, n); //받은 데이터를 그대로 다시 보내기
    }
}

잘못된 부분이 있다면 댓글로 알려주세요,,,

참고)
https://asidefine.tistory.com/75

profile
📝 It's been waiting for you

0개의 댓글