socket으로 메아리 구현하기

설현아·2025년 5월 3일

echo 함수

socket 통신을 하는 간단한 서버, 클라이언트를 구현하기 위한 실습이다.

클라이언트의 어떤 요청이 있을 때, 서버가 어떻게 응답할 지를 정의한다.
echo는 메아리다. 클라이언트의 메시지를 동일하게 돌려준다.

미리 결과를 보자면 다음과 같다.

이렇게 동작하는 프로그램을 만들 것이다.

echo.c

서버가 클라이언트와 연결된 소켓을 통해 입력받은 데이터를 그대로 다시 돌려주는 echo(connfd) 함수이다.
클라이언트의 요청에 따른 서버의 동작을 정의해둔 함수라고 보면 된다.

/*
 * echo - read and echo text lines until client closes connection
 */
/* $begin echo */
#include "csapp.h"

void echo(int connfd) // 클라이언트와 연결 완료된 소켓 디스크립터
{
  size_t n;
  char buf[MAXLINE];
  rio_t rio; // Robust I/O를 위한 구조체(커널 버퍼 + 사용자 버퍼 사이 관리)

  Rio_readinitb(&rio, connfd);                         // connfd 소켓을 rio와 연결하여 입출력 구조체를 사용할 수 있도록 한다.
  while ((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) // 클라이언트로부터 받은 데이터를 한 줄씩 읽는다.
  {
    printf("server received %d bytes\n", (int)n); // 서버가 받은 데이터의 bytes 단위 사이즈
    Rio_writen(connfd, buf, n);                   // 클라이언트로 받은 버퍼와 동일한 버퍼를 response로 전송한다.
  }
}
/* $end echo */

echoserveri.c

echo를 수행할 server 프로그램이다.

  1. 포트 번호를 인자(argv[1])로 받는다.
  2. 해당 포트 번호를 기반으로 서버 소켓을 생성한다.
  3. 컴퓨터에서 가능한 IP 중 하나에 port 주소를 바인딩한다.
  4. 생성된 소켓 디스크립터를 클라이언트 연결 요청을 대기하는 상태로 둔다.
  5. 클라이언트에서 연결 요청(connet)이 수신되면 연결 요청을 승낙(accept)하여 연결된 소켓(connfd)을 생성한다.
  6. 연결 소켓을 통해 echo 처리를 수행한다.
/*
 * echoserveri.c - An iterative echo server
 */
/* $begin echoserverimain */
#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 */
  char client_hostname[MAXLINE], client_port[MAXLINE]; // 연결될 클라이언트의 host(IP 혹은 도메인), port를 담는다.

  if (argc != 2) // 인자료 포트 번호를 요구한다.
  {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(0);
  }

  // getaddrinfo(), socket(), bind(), listen() 과정이 포함된다.
  listenfd = Open_listenfd(argv[1]); // 서버의 소켓 디스크립터를 만들고 + 주소 바인딩 + 클라이언트의 요청을 기다린다.
  while (1)
  {
    clientlen = sizeof(struct sockaddr_storage);
    connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);           // 클라이언트의 연결 요청을 수락한다.(connfd→클라이언트와의 통신용 소켓)
    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);
}
/* $end echoserverimain */

+) 더하여, Open_listenfd 함수가 어떻게 생겨 먹었는 지도 훑어보자.

서버는 이 함수를 호출해서 연결 요청을 받을 준비가 된 듣기 식별자(bind + listen)를 생성한다.

  1. getaddrinfo() → 내 컴퓨터에 있는 IP 주소와 port 목록을 보면서 bind()에 쓸 수 있는 주소들을 전부 연결 리스트로 만들어준다.
  2. socket() → 주소 목록을 순회하며 소켓 생성을 시도한다. 실패하면 다음 후보로 넘아간다.
  3. bind() → 소켓 생성에 성공하면 바인딩을 시도한다. 실패하면 다음 후보로 넘어간다.
  4. listen() → 연결 대기 상태로 전환한다.

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;
    }

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

echclient.c

echo를 수행할 TCP client 프로그램을 정의한다.

  1. 연결할 서버의 호스트 주소(argv[1]), 포트 번호(argv[2])를 인자로 받는다.
  2. 이를 통해 클라이언트 소켓을 생성하고 서버에 연결한다.
  3. Robust I/O 방식을 사용하여 서버로 문자열을 전송/수신한다.
/*
 * echoclient.c - An echo client
 */
/* $begin echoclientmain */
#include "csapp.h"

// 프로그램은 호스트 주소와 포트 번호를 인자로 받는다.
int main(int argc, char **argv)
{
  int clientfd;                    // 서버와 연결될 소캣 식별자가 담긴다.
  char *host, *port, buf[MAXLINE]; // 연결할 서버의 host, port, 입출력 버퍼가 담긴다.
  rio_t rio;                       // Robust I/O를 위한 구조체(커널 버퍼 + 사용자 버퍼 사이 관리)

  if (argc != 3) // 요구하는 인자를 다 받지 못했을 경우 에러 처리
  {
    fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
    exit(0);
  }
  host = argv[1]; // 첫 번째 인자는 연결하는 서버의 host IP 주소(혹은 도메인)를 의미한다.
  port = argv[2]; // 두 번째 인자는 연결하는 서버의 포트를 의미한다.

  // socket(), getaddrinfo(), connect() 과정이 포함된다.
  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 */

+) 더하여 open_clientfd() 함수도 살펴보자.

이 함수는 연결하고자 하는 서버의 host, port를 인자로 받아, 클라이언트 소켓을 생성하고 연결된 소켓을 반환한다.

  1. getaddrinfo() → 내 컴퓨터에 있는 IP 주소와 port 목록을 보면서 bind()에 쓸 수 있는 주소들을 전부 연결 리스트로 만들어준다.
  2. socket() → 주소 목록을 순회하며 소켓 생성을 시도한다. 실패하면 다음 후보로 넘아간다.
  3. connect() → 인자로 받은 서버 주소로 연결을 시도한다. 실패하면 다음 후보로 넘어간다.
  4. 연결된 주소가 있다면 소켓 디스크립터를 반환, 없다면 -1을 반환한다.
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 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;
    }

    /* 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 */
        if (close(clientfd) < 0)
        { /* Connect failed, try another */ // line:netp:openclientfd:closefd
            fprintf(stderr, "open_clientfd: close failed: %s\n", strerror(errno));
            return -1;
        }
    }

    /* Clean up */
    freeaddrinfo(listp);
    if (!p) /* All connects failed */
        return -1;
    else /* The last connect succeeded */
        return clientfd;
}
profile
어서오세요! ☺️ 후회 없는 내일을 위해 오늘을 열심히 살아가는 개발자입니다.

0개의 댓글