[TIL/크래프톤 정글] DAY 58

배재준·2025년 5월 6일

크래프톤 정글 - TIL

목록 보기
51/93
post-thumbnail

2025.05.06

TIL(TODAY I LEARN)


  • 오늘한 내용 : C - 네트워크 프로그래밍 - echo server, tiny web server 구현

  • WEEK08: BSD소켓, IP, TCP, HTTP, file descriptor, DNS


echo server

컴파일 방법

  1. echo 폴더 안에 echo_client.c, echo_server.c, csapp.h, csapp.c 넣어 놓는다.
  2. 컴파일 한번에 해주기
gcc -o echo_server echo_server.c csapp.c
gcc -o echo_client echo_client.c csapp.c
  1. 터미널 두개 켜서 서버 실행 ./echo_server 9190(포트번호)
  2. 클라이언트 실행./echo_client 127.0.0.1(localhost) 9190(포트번호)
  3. 클라이언트 창에서 테스트

echo_client.c

#include "csapp.h" 

int main(int argc, char **argv)
{
    int clientfd;
    char *host, *port, buf[MAXLINE];
    rio_t 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);
    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);
    exit(0);
}

echo_server.c

#include "csapp.h" 

void echo(int connfd);

int main(int argc, char ** argv)
{
    int listenfd, connfd;
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;
    char client_hostname[MAXLINE], clinet_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,
                    clinet_port, MAXLINE, 0);
        printf("Connected to (%s, %s)\n", client_hostname, clinet_port);
        echo(connfd);
        Close(connfd);
    }
    exit(0);
}

void echo(int connfd)
{
    size_t n;
    char buf[MAXLINE];
    rio_t rio;

    Rio_readinitb(&rio, connfd);
    while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0){
        printf("server received %d bytes\n", (int)n);
        Rio_writen(connfd, buf, n);
    }
}

tiny web server

실행 방법

  1. make 실행
  2. ./tiny 8000(port)
  3. 웹으로 접속 localhost:8000
    1. serve_static() 수행 → godzilla 사진 등장
  4. http://localhost:8080/cgi-bin/adder?x=1&y=2 접속
    1. serve_dynamic 수행 → adder 실행됨

main.c

int main(int argc, char **argv)
{
  int listenfd, connfd;
  char hostname[MAXLINE], port[MAXLINE];
  socklen_t clientlen;
  struct sockaddr_storage clientaddr;

  /* Check command line args */
  if (argc != 2)
  {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(1);
  }

  listenfd = Open_listenfd(argv[1]);
  while (1)
  {
    clientlen = sizeof(clientaddr);
    connfd = Accept(listenfd, (SA *)&clientaddr,
                    &clientlen); // line:netp:tiny:accept
    Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE,
                0);
    printf("Accepted connection from (%s, %s)\n", hostname, port);
    doit(connfd);  // line:netp:tiny:doit
    Close(connfd); // line:netp:tiny:close
  }
}

단일 연결(iterative) 서버 패턴

  1. Accept에서 블로킹되어 한 클라이언트의 연결 요청이 올 때까지 대기
  2. 연결되면 doit(connfd)로 요청을 처리
  3. 클라이언트가 종료되면 Close(connfd)로 소켓 닫고
  4. 다시 루프 맨 위로 올라가 또 Accept에서 대기

doit()

void doit(int fd)
{
  int is_static;
  struct stat sbuf;
  char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
  char filename[MAXLINE], cgiargs[MAXLINE];
  rio_t rio;

  Rio_readinitb(&rio, fd);
  Rio_readlineb(&rio, buf, MAXLINE);
  printf("Request headers:\n");
  printf("%s", buf);
  sscanf(buf, "%s %s %s",method, uri, version);
  if (strcasecmp(method, "GET")){
    clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method");
    return;
  }
  read_requesthdrs(&rio);

  is_static = parse_uri(uri, filename, cgiargs);
  if (stat(filename, &sbuf) < 0){
    clienterror(fd, filename, "404", "Not found","Tiny couldn't find this file");
    return;
  }

  if (is_static){
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
      clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't read the file");
      return;
    }
    serve_static(fd, filename, sbuf.st_size);
  }
  else{
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
      clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program");
      return;
    }
    serve_dynamic(fd, filename, cgiargs);
  }
}
  • 한개의 HTTP 트랜잭션을 처리한다.
    • 한 번의 “요청 → 응답” 과정
상태 코드명칭분류발생 조건
400Bad Request클라이언트 오류 (4xx)잘못된 요청 라인(구문 오류) 혹은 서버가 이해할 수 없는 요청 형식일 때 (추가 구현 시)
403Forbidden클라이언트 오류 (4xx)파일이 존재하지만 읽기·실행 권한이 없을 때– 정적: serve_static 전 권한 검사 실패– 동적: serve_dynamic 전 권한 검사 실패
404Not Found클라이언트 오류 (4xx)요청한 파일이 서버에 존재하지 않을 때 (stat 호출 실패)
500Internal Server Error서버 오류 (5xx)CGI 실행 중 오류 발생 시(예: execve 실패 등 추가 구현 시)
501Not Implemented클라이언트 오류 (4xx)GET 이외의 HTTP 메서드가 들어왔을 때

stat()

  • int stat(const char *path, struct stat *buf);
  • 파일 또는 심볼릭 링크가 아닌 “실제 파일”의 메타데이터를 buf에 채워 넣음.
struct stat {
    dev_t     st_dev;     /* 파일이 속한 디바이스 ID */
    ino_t     st_ino;     /* 파일의 i-node 번호 */
    mode_t    st_mode;    /* 파일 종류 및 접근 권한 비트 */
    nlink_t   st_nlink;   /* 하드링크 개수 */
    uid_t     st_uid;     /* 소유자 사용자 ID */
    gid_t     st_gid;     /* 소유 그룹 ID */
    off_t     st_size;    /* 파일 크기 (바이트 단위) */
    /* 그 외 타임스탬프, 블록 수 등 */
};

clineterror()

  • HTTP 프로토콜 상으로는 아래 순서대로 바이트 스트림을 보내야 함:

    • 상태 라인(Status Line)
    • 헤더(Headers)
    • 빈 줄(blank line)
    • 본문(Body)
  • 왜 나눠 쓰느냐

    • 각각의 헤더 필드를 계산해서 포맷팅한 뒤 바로 전송하는 게 구현도 간단하고 실수할 여지가 적음.
    • 한 번에 다 합쳐서 malloc/sprintf로 거대한 버퍼를 만들 필요 없이, 필요한 만큼만 작은 버퍼(buf)에 찍어서 바로 보내는 방식.
  • 빈 줄(\r\n) 은 “헤더의 끝”을 알리는 중요한 구분자.

  • 이 세 번의 Rio_writen 호출이 모여서 완전한 HTTP 응답을 만듦.

HTTP/1.0 404 Not Found\r\n
Content-type: text/html\r\n
Content-length: 123\r\n
\r\n
<html>…error body…</html>

read_requesthds()

  • 요청 메서드(GET, POST 등)에 관계없이 첫 번째 요청 라인 다음에 따라오는 모든 헤더 라인을 읽고 단순히 버리는 역할

parse_uri()

  • parse_uri 함수는 클라이언트가 보낸 URI를 분석해서
    1. 정적 파일 요청(static)인지
    2. 동적 CGI 요청(dynamic)인지 판별
  • 로컬 파일 경로(filename)
  • CGI 인자 문자열(cgiargs) 를 각각 설정해 주는 역할
  • <string.h> 헤더에 선언된 ****C 문자열 함수들
함수원형설명
strstrchar *strstr(const char *hay, const char *ndl);문자열 hay 안에서 처음으로 ndl이 나타나는 위치의 포인터를 반환. 없으면 NULL.
strcpychar *strcpy(char *dest, const char *src);src 문자열 전체(널 종료 포함)를 dest에 복사하고 dest 반환.
strcatchar *strcat(char *dest, const char *src);dest 문자열 끝(널 바이트 위치) 뒤에 src를 붙이고 dest 반환.
strlensize_t strlen(const char *s);문자열 s의 길이(널 종료 문자 제외) 반환.
indexchar *index(const char *s, int c); (BSD)문자열 s에서 문자 c를 처음 만나는 위치의 포인터를 반환. 없으면 NULL. ※ POSIX 표준 함수명은 strchr.

serve_static()

  • 정적 파일을 클라이언트에 전달
  • 헤더 작성: HTTP 상태 라인, 서버 정보, 콘텐츠 길이와 타입 헤더를 sprintf로 차례로 버퍼에 붙인 뒤 Rio_writen으로 전송.
  • 파일 전송:
    • 파일을 openmmap으로 메모리에 맵핑
    • Rio_writen으로 매핑된 영역 전체(파일 내용)를 한 번에 전송
    • munmap으로 매핑 해제

mmap()

void *mmap(void *addr, size_t length,
int prot, int flags,
int fd, off_t offset);
  • 파일 디스크립터(fd)로 열린 파일(또는 장치)의 내용을 프로세스 가상 주소 공간에 “매핑”하여, 마치 메모리 배열인 것처럼 접근할 수 있게 해 줌.

왜 사용하나?

  1. 효율적인 대용량 파일 처리
    • read/write 반복 호출 없이,
    • 한 번의 가상 메모리 매핑으로 파일 전체를 포인터처럼 다룰 수 있어 속도가 빠름.
  2. 간단한 메모리 접근
    • mmapchar *p로 파일 내용을 바로 p[i] 처럼 인덱싱할 수 있어 코드가 깔끔.
  3. 커널과 사용자 공간 간 데이터 복사 비용 절감
    • 전통적인 read/write는 커널 ↔ 유저 버퍼를 매번 복사해야 함
    • mmap은 페이지 테이블만 설정해 주면 부가 복사 없이 바로 동일 페이지를 공유할 수 있음.

serve_dynamic()

  • 클라이언트 요청에 따라 CGI 프로그램을 실행하고, 그 출력을 HTTP 응답으로 돌려주는 역할
  • Fork() 호출로 부모/자식 프로세스를 분기
  • 부모는 뒤에서 자식 종료를 기다리고(Wait(NULL))
  • 자식만 이후 CGI 실행 로직을 수행

0개의 댓글