크래프톤 정글(C언어 WEEK 5 ~ 8)

echo 서버 코드 분석(C언어)

devkty 2025. 5. 5. 02:00
728x90

echo 서버 코드 분석

11장 같은 경우 아는 내용이 대부분이라 따로 정리를 하지 않았습니다. 만약에 프록시 서버까지 구현하고 시간이 남는다면 정리해볼 것 같습니다.

echo는 서버와의 연결을 수립한 이후에 클라이언트는 표준 입력에서 텍스트 줄을 반복해서 읽는 루프에 진입하고, 서버에 텍스트 줄을 전송하고, 서버에서 echo 줄을 읽어서 결과를 표준 출력으로 인쇄합니다.

echoclient.c(클라이언트)

// 클라이언트 파일
#include "csapp.h"     // Robust I/O와 소켓 함수가 정의된 헤더 파일 포함

int main(int argc, char **argv) 
{
    int clientfd;                     // 서버와 연결된 소켓 파일 디스크립터
    char *host, *port, buf[MAXLINE];  // 호스트 이름, 포트 번호, 입출력 버퍼
    rio_t rio;                        // robust I/O를 위한 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);  // 서버에 연결하고 소켓 fd를 받아옴
    Rio_readinitb(&rio, clientfd);         // rio 버퍼를 소켓과 연결하여 초기화

    // 표준 입력에서 한 줄씩 입력을 받아 서버로 전송하고, 서버 응답을 출력
    while (Fgets(buf, MAXLINE, stdin) != NULL) {   // 표준 입력에서 문자열 읽기
    Rio_writen(clientfd, buf, strlen(buf));        // 입력 문자열을 서버로 전송
    Rio_readlineb(&rio, buf, MAXLINE);             // 서버가 echo한 문자열을 수신
    Fputs(buf, stdout);                            // 수신한 문자열을 화면에 출력
    }
    Close(clientfd);  // 연결 종료 (소켓 닫기)  //line:netp:echoclient:close
    exit(0);          // 정상 종료
}
/* $end echoclientmain */

이 코드는 서버와 TCP 연결을 맺고, 사용자가 입력한 문자열을 서버에 보내면, 서버로부터 다시 받은 결과를 출력하는 간단한 echo client입니다.

  • Open_clientfd(): 서버와 연결하는 함수
  • Rio_readinitb()Rio_readlineb(): 이 책에서 제공하는 견고한(robust) 버퍼 입출력 함수
  • FgetsFputs: 표준 입출력 함수
  • Rio_writen: 지정한 크기만큼 정확히 소켓에 데이터를 쓰는 함수

echo.c(반복 출력 유틸)

#include "csapp.h"    // Robust I/O와 소켓 함수가 정의된 헤더 파일 포함

void echo(int connfd) 
{
    size_t n;             // 읽은 바이트 수를 저장할 변수
    char buf[MAXLINE];    // 데이터 버퍼
    rio_t rio;            // Robust I/O를 위한 구조체

    Rio_readinitb(&rio, connfd);   // Robust I/O으로 소켓 디스크럽터 초기화

    // 클라이언트가 보낸 메시지를 한 줄씩 읽어서 다시 클라이언트에 전송
    while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {   // 읽을 내용이 있으면 반복   //line:netp:echo:eof
    printf("server received %d bytes\n", (int)n);           // 서버에서 받은 바이트 수 출력
    Rio_writen(connfd, buf, n);                             // 받은 내용을 그대로 클라이언트에 전송 (에코)
    }
}

echo() 함수는 클라이언트와 연결된 소켓 connfd를 받아, 그 소켓으로부터 한 줄씩 문자열을 읽고, 같은 내용을 다시 보내줍니다.

  • Rio_readinitb(): connfd를 기반으로 robust I/O 구조체를 초기화합니다.
  • Rio_readlineb(): 클라이언트가 보낸 한 줄을 읽습니다.
  • Rio_writen(): 읽은 내용을 다시 클라이언트에게 돌려보냅니다.
  • 클라이언트가 연결을 끊으면 read가 0을 반환하고, 루프가 종료됩니다.

→ 즉, 클라이언트 ↔ 서버 간의 단순 메시지 반사기(에코 서버) 역할을 하는 구조입니다.

echo() 함수는 보통 accept()로 새로 생성된 연결에 대해 호출되며, 서버의 main()이나 worker thread 내부에서 사용됩니다.

echoserveri.c(서버)

#include "csapp.h"           // 소켓 프로그래밍과 Robust I/O 함수들이 정의된 헤더 포함

void echo(int connfd);       // 클라이언트와 데이터를 주고받는 echo 함수 선언

int main(int argc, char **argv)  
{
    int listenfd, connfd;                 // listenfd: 서버 리슨 소켓, connfd: 연결 소켓
    socklen_t clientlen;                  // 클라이언트 주소 구조체의 길이
    struct sockaddr_storage clientaddr;   // 어떤 주소든 저장할 수 있는 구조체 (IPv4, IPv6 모두 지원) //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);  // 클라이언트 연결 수락 accept

        // 연결된 클라이언트의 호스트 이름과 포트 번호를 문자열로 얻어옴
        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 서버는 한 번에 하나의 클라이언트 연결만 처리하며, 클라이언트가 연결을 끊으면 다음 클라이언트를 기다립니다.

  • Open_listenfd() : 서버가 연결을 기다릴 수 있도록 소켓을 열고 바인딩하여 리슨 상태로 만듦
  • Accept() : 클라이언트의 연결 요청을 수락하여 새로운 연결 소켓 생성
  • echo() : 클라이언트로부터 받은 데이터를 그대로 다시 돌려주는 함수
  • Close() : 연결 종료

echo 서버는 구현은 생각보다 쓸 내용이 많지만, 이론적으로 간단한 서버라고 생각됩니다.

다음에는 tiny 서버에 대해 분석해보겠습니다.

++ 바이트 수 관련 정보 추가

해당 파일을 telnet을 통해 작동시키면 hi를 쳤을때 바이트수가 4로 찍힘을 알 수 있다. 왜그럴지 알아보겠다.

#include "csapp.h"    // Robust I/O와 소켓 함수가 정의된 헤더 파일 포함

void echo(int connfd) 
{
    size_t n;             // 읽은 바이트 수를 저장할 변수
    char buf[MAXLINE];    // 데이터 버퍼
    rio_t rio;            // Robust I/O를 위한 구조체

    Rio_readinitb(&rio, connfd);   // Robust I/O으로 소켓 디스크럽터 초기화

    // 클라이언트가 보낸 메시지를 한 줄씩 읽어서 다시 클라이언트에 전송
    while((n = Rio_readlineb(&rio, buf, MAXLINE)) > 0) {   // 읽을 내용이 있으면 반복   //line:netp:echo:eof
        // for (size_t i = 0; i < n; i++) {                                                     // hi를 치면 4바이트가 찍힌다. 왜 그럴까? 나는 해당 echo 서버를 telnet으로 돌렸기 때문이다.
        //     printf("buf[%zu] = 0x%x (%c)\n", i, buf[i], isprint(buf[i]) ? buf[i] : '.');     // Carriage Return과 Line Feed가 들어간 hi\r\n 식으로 출력된다. 해당 코드로 바이트수가 2많음을 확인할 수 있다.
        // }
    printf("server received %d bytes\n", (int)n);           // 서버에서 받은 바이트 수 출력
    Rio_writen(connfd, buf, n);                             // 받은 내용을 그대로 클라이언트에 전송 (에코)
    }
}

원래의 문자열보다 2바이트 크게 출력된다. 그 이유는 Windows, PuTTY, Telnet과 같은 환경에서는 Carriage Return이라는 문자가 추가된다.
hi\r\n (Carriage Return + Line Feed)

반면, Linux나 Unix 기반 터미널, netcat 등의 대부분의 CLI 도구는 Carriage Return이 없다.
hi\n (Line Feed만)

Carriage Return은 사용자가 엔터를 입력했을때, 줄바꿈을 수행하고 첫번째 칸으로 돌아가게 해준다. 그러나 리눅스나 유닉스는 해당 문자를 합쳐서 Line Feed에 구현했다. 그렇기 때문에 둘은 같은 의미를 가지지만 환경에 따라서 다른 바이트 수를 출력하게 된다.

해당 코드에서 주석처리된 for을 통해 직접 확인가능하다.

728x90