크래프톤 정글/TIL

[webproxy-lab] 웹 소켓 통신 / Echo Server,Client 구현 (C언어)

양선규 2024. 5. 8. 19:51
728x90
반응형

클라이언트와 서버는 소켓 기반으로 연결된다.

클라이언트의 요청을 서버가 받아서 그대로 되돌려주는 형태의 Echo Server와 Client를 구현해볼 것이다.

주석이 상세히 달려 있으니 추가적인 코멘트는 필요 없을 거라고 생각한다.

 

 

Echo Client

#include "csapp.h"

// 인자 3개를 받는다.
int main(int argc, char **argv) {

    // 클라 소켓의 fd를 저장할 변수
    int clientfd;
    // 서버호스트명, 서버포트번호, 버퍼 저장할 변수
    char *host, *port, buf[MAXLINE];
    // rio 구조체 선언
    rio_t rio;

    // 이 프로그램 실행 시, 인자가 3개가 아니면 사용법을 출력한다
    // 인자 1 : ./echoclient, 인자 2 : 호스트, 인자 3 : 포트번호
    if(argc != 3) {
        fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
        exit(0);
    }

    // 인자로 받은 host, port 변수에 저장
    host = argv[1];
    port = argv[2];

    // 지정한 서버 호스트, 포트에 연결된 클라이언트 소켓 열기
    clientfd = Open_clientfd(host, port);

    // rio 구조체에 클라이언트 소켓 할당하기
    Rio_readinitb(&rio, clientfd);

    // 사용자가 입력한 데이터를 서버로 전송하고, 응답을 출력하는 무한 루프 가동
    // 아무것도 입력하지 않고 엔터를 누르면 \n이 전송되고(1byte) 연결이 끊기지 않는다.
    // 즉 클라이언트가 Ctrl + C를 누르거나 Ctrl + D 를 누를 경우에만 종료된다.
    // 사용자 입력값은 buf에 저장된다
    while(Fgets(buf, MAXLINE, stdin) != NULL) {

        // 사용자로부터 입력받은 데이터를 소켓을 통해 서버로 전송한다
        Rio_writen(clientfd, buf, strlen(buf));
        // 소켓으로 서버로부터 받은 응답을 buf에 저장한다
        Rio_readlineb(&rio, buf, MAXLINE);
        // buf에 저장된 값을 표준 출력한다
        Fputs(buf, stdout);
    }
    // exit 하면서 커널이 자동으로, 열렸던 식별자들을 닫아주지만 명시적으로 닫아 주는 게 좋은 습관
    Close(clientfd);

    exit(0);
}

 

 

 

 

Echo Server

#include "csapp.h"

// 연결 식별자 fd를 받아서 클라에게 메시지 에코해준다.
void echo(int connfd);

// 인자 2개를 받는다.
int main(int argc, char **argv) {

    // 리스닝 소켓, 연결된 소켓 fd 저장할 변수
    int listenfd, connfd;
    // 클라이언트 주소 구조체의 크기를 저장할 변수
    socklen_t clientlen;
    // 클라 주소 정보 저장하는 구조체를 저장할 변수
    // sockaddr_storage는 모든 소켓 주소 유형을 저장할 수 있는 범용적인 구조체다
    struct sockaddr_storage clientaddr;
    // 클라 호스트이름, 클라 포트번호
    char client_hostname[MAXLINE], client_port[MAXLINE];

    // 이 프로그램 실행 시, 인자가 2개가 아니면 사용법을 출력한다
    // 인자 1 : ./echoserveri , 인자 2 : 포트번호
    if(argc != 2) {
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(0);
    }

    // 인자로 받은 포트에서 클라 연결을 수신하기 위한 listen 소켓 열기
    listenfd = Open_listenfd(argv[1]);

    // 무한루프 시작, 클라이언트 측 연결 요청 계속 받기
    while(1) {
        clientlen = sizeof(struct sockaddr_storage);

        // Accept 함수로 클라이언트 측 연결 수락하고, 연결식별자 fd 반환
        connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);

        // Getnameinfo 함수로 클라이언트 호스트이름, 포트번호 가져오기
        Getnameinfo((SA *) &clientaddr, clientlen, client_hostname, MAXLINE, client_port, MAXLINE, 0);

        // 최초 연결 시 클라이언트 호스트이름, 포트번호 출력하기
        printf("Connected to (%s, %s)\n", client_hostname, client_port);

        // 클라가 연결 종료하거나 EOF 보낼 때까지 echo 함수 실행
        // 클라가 보낸 메시지를 똑같이 echo 한다
        echo(connfd);

        // echo가 끝났다면 클라와 연결되었던 소켓을 닫아준다
        Close(connfd);
    }

    exit(0);
}

 

 

 

 

Echo 함수

- 이 함수는 받은 데이터의 크기를 출력 후, 요청을 그대로 돌려주는 서버의 메인 로직을 구현한 것이다.

 

#include "csapp.h"

// 클라이언트와의 연결 소켓 fd를 입력받는다
void echo(int connfd) {

    // 수신한 데이터의 크기를 저장할 변수 n
    size_t n;
    // 수신한 데이터를 저장할 버퍼 선언
    // MAXLINE은 버퍼 최대 크기를 나타내는 상수
    char buf[MAXLINE];
    // 리오 구조체. 버퍼링된 입출력을 지원하는 라이브러리
    // 버퍼링된 입출력 : 데이터를 버퍼에 저장한 뒤 한 번에 입출력하여 효율적인 입/출력을 가능케 함, bufferedreader 같은친구
    rio_t rio;

    // rio를 connfd와 연결한다. rio 구조체는 소켓의 입/출력을 다룬다.
    Rio_readinitb(&rio, connfd);

    // 무한루프 시작. 데이터가 buf에 저장, n에 읽은 바이트 수 저장
    // n == 0 ( 데이터를 읽지 못하면, 즉 클라가 연결을 끊었을 때 ) 루프 종료
    // Rio_readlineb 함수는 클라가 연결을 종료하거나, EOF를 보냈을 경우에 0을 리턴한다!
    while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0) {

        // 수신된 데이터 크기 출력
        printf("server received %d bytes\n", (int)n);

        // connfd 소켓에 있는 buf 데이터를 n만큼 연결된 곳으로(클라) 전송한다
        Rio_writen(connfd, buf, n);
    }
}

 

 

=============================

 

실행 결과

요청을 그대로 응답한다

 

위 : 클라이언트

아래 : 서버

 

클라이언트가 메시지를 보내면, 서버는 자신의 터미널에 수신한 데이터 크기를 출력한 후 그대로 클라이언트에게 돌려주는 모습이다.

728x90
반응형