TIL

[TIL][정글] Echo Server 만들기

아람2 2024. 10. 28. 23:17
반응형

정글 7주차, Web Server 를 만들게 되었다 

 

첫 단계로 socket 을 이용하여, 요청을 하는 Client 와 응답을 하는 Server 를 만들었다 

클라이언트는 서비스를 사용하는 주체이고, 서버는 서비스를 제공하는 주체이다

쉽게 생각하면 클라이언트는 커피를 구매하는 손님, 서버는 커피(서비스)를 제공하는 커피숍이라고 비유할 수 있다 

그림에서 Resource는 커피숍에 재료 (ex.얼음) 가 떨어졌을 때 재료를 사 올 수 있는 편의점이라고 보면 된다

 

Client-Server Model 은 클라이언트와 서버 간의 작업을 분리해주는 분산 어플리케이션 구조이자 네트워크 아키텍처이다

이 구조에서는 클라이언트와 서버가 각자 역할에 맞게 구성되어 있다 

 

Socket 은 소통의 종단점 (Endpoint) 으로, 쉽게 말하면 데이터를 주고 받기 위한 창구이다 

Socket 은 Protocol, IP Address, Port Number 로 정의된다 

아래 그림과 같은 흐름으로 소통이 이루어지며, 자세한 내용은 다른 글에 써놨으니 생략하겠다 

https://helloahram.tistory.com/119

 

1. Server 구축하기

먼저 서버를 구축해보자, 서버에는 소켓을 두 개 열어줘야 한다 

1) 클라이언트 연결 요청할 때 사용하는 듣기 소켓
2) 개별 클라이언트와의 통신을 위한 연결 소켓 

 

듣기 소켓은 항상 열려 있어야 하고, 연결 소켓은 클라이언트와 연결할 때마다 생성해야 하므로 둘은 분리되어야 한다 

먼저, 클라이언트의 주소 크기와 주소, 호스트 이름과 포트를 저장할 변수를 설정하고,

그런 다음 서버를 열고, 클라이언트의 요청이 있을 경우 연결 소켓을 통해 클라이언트와 통신한다 

물론, 통신이 종료되면 연결 소켓을 닫아주는 것도 잊지 않아야 한다 

🐣 close() 함수를 이용하여 소켓을 닫지 않으면 자원 누수 + 파일 디스크립터 고갈 이라는 문제가 발생할 수 있다!

echoserveri.c 전체 코드 

#include "csapp.h"
#include "echo.c"

int main(int argc, char **argv)
{
    int listenfd; // 서버 소켓의 파일 디스크립터, 클라이언트 연결 요청 시 사용 -> 듣기 소켓
    int connfd;   // connfd 개별 클라이언트와의 통신을 위한 소켓 디스크립터 -> 연결 소켓
    // 듣기 소켓과 연결 소켓은 분리되어야 한다
    socklen_t clientlen; // 클라이언트 주소 구조체의 크기를 저장할 변수
    struct sockaddr_storage clientaddr;
    /* Enough space for any address 왜냐하면 IPv4, IPv6 모두 처리할 수 있도록 */
    char client_hostname[MAXLINE], client_port[MAXLINE];
    // Python 으로 치면 char test[MAXLINE] = string test
    // C언어에는 string 이 없고 C++ 에서 있음

    // 명령행 인자 체크 - 포트 번호를 인자로 받아야 한다
    // ./echoserveri 12345
    // ./echoserveri 인자 1, 12345 인자 2
    if (argc != 2) // 포트 번호가 제대로 입력되었는지 확인
    {
        fprintf(stderr, "usage: %s <port> \n", argv[0]);
        exit(0);
    }

    // Open_listenfd 함수는 지정된 포트에서 수신 대기할 서버 소켓을 열어 반환한다
    // 쉽게 말하면 서버 소켓 생성 및 특정 포트에서 연결 요청 대기
    // argv[1] 은 포트 번호, 주어진 포트 번호로 리스닝 소켓 생성
    listenfd = Open_listenfd(argv[1]);
    // 무한 루프로 클라이언트의 연결 요청을 처리
    while (1)
    {
        clientlen = sizeof(struct sockaddr_storage); // 구조체의 크기 저장
        /* Accept 함수는 새로운 소켓 디스크립터 connfd 를 반환하며
        connfd 는 해당 클라이언트와 통신할 때 사용한다
        쉽게 말해서 Accept 함수로 클라이언트의 연결 요청을 수락 */
        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);  // 데이터를 송수신을 담당하는 echo 함수 호출, 클라이언트와 통신
        Close(connfd); // 클라이언트와의 통신이 끝나면 연결 소켓 닫기
    }
    exit(0);
}

 

우리가 만드는 Echo Server 는 말 그대로 echo,

Client 가 보낸 메시지를 Server 가 그대로 Client 한테 돌려주는 역할을 한다 

그 역할을 하는 echo.c 는 아래와 같다 

echo.c 전체 코드

#include "csapp.h"
/*
echo 함수는 서버 소켓 프로그래밍에서, 클라이언트로부터 메시지를 받아
다시 클라이언트에게 그대로 돌려주는 역할을 한다
connfd 라는 연결 파일 디스크립터를 매개변수로 받아 클라이언트와의 연결을 처리
*/
void echo(int connfd)
{
    size_t n; // 읽은 바이트 수를 저장할 변수
    char buf[MAXLINE];
    // Robust I/O 구조체 rio 를 선언하여
    // 버퍼링된 I/O 연산을 위한 정보를 저장할 공간을 만든다
    rio_t rio;

    // connfd 로부터 읽기 위한 rio 구조체 초기화
    Rio_readinitb(&rio, connfd);
    // 클라이언트로부터 메시지를 읽어서 echo 한다
    // Rio_readlineb 클라이언트로부터 한 줄을 읽음
    while ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0)
    {
        // 읽은 바이트 수 출력
        printf("server received %d bytes\n", (int)n);
        // 읽은 메시지를 클라이언트에게 다시 전송
        Rio_writen(connfd, buf, n);
    }
}

2. Client 만들기 

서버를 구축했으니 이제 클라이언트를 만들어보자, 

클라이언트는 먼저 프로그램의 이름, 호스트 이름, 포트 번호 등 명령행 인자들을 저장하는 argv 배열을 가져온다 

이 때, host 와 port 는 포인터를 이용하여 별도의 메모리 할당 없이 사용하도록 할 수 있다 

서버에 연결하기 위해서는 [프로그램 실행 경로] + [호스트 이름] + [포트 번호] 3개 인자를 전달하여 연결을 시도한다 

서버와 연결이 성공하면 Fgets 로 서버에 데이터를 보낼 수 있고, 서버의 응답을 읽어와서 해당 응답을 출력해준다 

연결을 종료할 때는 서버와 동일하게 Close(clientfd); 함수로 소켓을 안전하게 종료해야 한다 

 

추가로, while (Fgets(buf, MAXLINE, stdin) != NULL) 구문에서 한 줄을 입력하면 연결이 종료될 것으로 예상했지만

실제로는 연결을 계속 유지하고 있어서 찾아봤더니, while 루프가 계속해서 stdin 에서 입력을 기다리고 있어서 그렇다고 한다 

MAXLINE 은 입력이 NULL (ex. EOF 입력으로 종료) 일 때 서버와의 연결이 닫히고 프로그램이 종료되도록 한다 

정리하면, MAXLINE 은 입력 버퍼의 한 줄 최대 길이를 의미하며, 한 번의 읽기에서 MAXLINE 만큼 읽고, 

줄바꿈이나 EOF 가 없는 경우 다음 줄에서 남은 입력을 이어서 처리한다

EOF 는 표준 입력의 끝을 의미하며, CTRL+D (Unix, Linux, Mac) or CTRL+Z (Windows) 로 표준 입력에 EOF 를 보내면

Fgets 가 NULL 을 반환하고, 그때서야 while 루프가 종료되어 프로그램이 끝난다 

 

echoclient.c 전체 코드

#include "csapp.h"

int main(int argc, char **argv)
{                 // argv argument vector, c 는 count
    int clientfd; // 서버와의 연결을 나타내는 소켓 파일 디스크립터
    /* argv 배열은 프로그램 실행 시 제공되는 명령행 인자들을 저장하며
    host 와 port 는 포인터를 이용하여 별도 메모리 할당 없이 사용할 수 있다 */
    char *host, *port, buf[MAXLINE];
    rio_t rio; // 읽기/ 입력을 위한 RIO (Buffered I/O) 구조체

    if (argc != 3)
    { // argc != 3 인 경우, 사용법 메시지 출력하고 종료
        /*
        * argc 는 프로그램이 실행될 때 전달된 인수의 수
        argv[0] 실행할 프로그램의 이름 (자동으로 포함됨)
        argv[1] 연결할 서버의 호스트 이름 (첫번째 사용자 인수)
        argv[2] 연결할 서버의 포트 번호 (두번째 사용자 인수)
        */
        fprintf(stderr, "usage: %s <host> <port> \n", argv[0]);
        exit(0);
    }

    host = argv[1];
    port = argv[2];

    /* Open_clientfd(host, port) 로 지정된 호스트와 포트에 대해
     서버와 연결을 설정하고, 연결된 소켓 FD clientfd 를 반환한다 */
    clientfd = Open_clientfd(host, port);
    /* Rio 버퍼에 소켓 FD 를 초기화하여 읽기 작업 준비 */
    Rio_readinitb(&rio, clientfd);

    // 표준 입력 stdin 으로 입력을 받아 buf 에 저장
    while (Fgets(buf, MAXLINE, stdin) != NULL)
    {
        // 입력이 있을 때마다 서버에 데이터를 보낸다
        Rio_writen(clientfd, buf, strlen(buf));
        // 서버의 응답을 읽는다
        Rio_readlineb(&rio, buf, MAXLINE);
        // 표준 출력 stdout 에 출력한다
        Fputs(buf, stdout);
    }

    // 서버와의 연결을 닫고 프로그램 종료
    Close(clientfd);
    exit(0);
}

Echo Server 연결 화면

클라이언트가 접속하면, 서버에 클라이언트의 접속 정보 (호스트명과 포트) 가 출력된다 

그리고 클라이언트에서 서버에 메시지를 보내면, 서버에서 읽은 바이트 수 출력되면서

받은 메시지를 다시 클라이언트로 전송하여 클라이언트에 메시지가 출력됨을 확인할 수 있다

+ CTRL+C 로 연결 종료도 가능하다 

 

+ Tiny Server 는 책에 설명이 잘 나와있으니 결과만!

 

1) 클라이언트가 접속할 때 

2) adder 에 접속할 때 

반응형

'TIL' 카테고리의 다른 글

[TIL] CORS 오류  (1) 2025.01.17
Solid Principle  (0) 2024.12.20
[TIL] Demand-Zero Memory  (1) 2024.10.22
[TIL] mmap()  (0) 2024.10.22
[TIL] 힙 정렬 Heap Sort  (0) 2024.10.22