jun-wiki

View My GitHub Profile

Posts (Latest 10 updated) :
Read all
Contents:
  1. ♔♕ tiny server
    1. ♗ 흐름
    2. ♘ 주요 함수 역할
      1. ♙ main
      2. ♙ doit
    3. ♗ 보조 함수
      1. ♙ Open_listenfd
      2. ♙ Accept
      3. ♙ Getnameinfo
      4. Close
      5. ♙ Rio 시리즈
      6. ♙ 정적 파일 보조
      7. ♙ 동적 콘텐츠 보조
    4. ♖ 정적 콘텐츠 처리 흐름
    5. ♖ 동적 콘텐츠 처리 흐름

벌써 9월이다

그것도 9월 첫째주 월요일

미국에서는 노동절이라 불리는 휴일이다

우리나라는 그런 거 없으니 바로 시작하겠다

심지어 노동도 아니고 학습이라 쉴거면 학습절에 쉬어야 한다

물론, 난 개인적으로 매주 일요일에 학습적 가지고 있다 ㅇㅇ


♔♕ tiny server

타이니 서버 정리할거다

tiny 서버라는 이름이 있기 보단 그냥 기초중의 기초 기능만 넣은 작은 서버라는 뜻이다

단일 프로세스를 반복하는 간단하고 구린 서버다


♗ 흐름

main -> Accept -> doit -> (serve_static 또는 serve_dynamic) -> Close

기본 흐름은 이렇다고 볼 수 있다


서버 시작 하면 main함수에서 포트 번호 받아 해당 포트에 리스닝 소켓을 연다
(Open_listenfd호출) 이후 서버 시작

main은 무한 루프를 돌며 클라이언트의 연결 요청을 기다리다 Accept 함수를 통해 새 클라이언트 연결 시 (connfd 획득)
클라이언트의 호스트이름과 포트정보를 로그로 출력

연결 이후 maindoit(connfd) 함수를 호출해 HTTP 요청을 처리한다
doit 함수는 요청을 읽고 해석 후, 정적 파일 요청인지 동적 콘텐츠(cgi-bin만 지금은 다룬다) 요청인지 판단 후 각각에 걸맞는 처리 함수를 호출한다

정적 콘텐츠의 경우 serve_static 함수를 통해 요청된 파일을 읽고 클라이언트에 전송한다

동적 콘텐츠의 경우 serve_dynamic 함수를 통해 새로운 프로세스를 fork하여 CGI 프로그램 실행 후 그 출력을 클라이언트에 보낸다

doit이 이렇게 끝나면 main으로 복귀후 연결 소켓을 닫고(Close 호출) 다음 요청을 대기한다


♘ 주요 함수 역할

여러 함수들 쓰이는 거 다 정리해 놓을 생각이다


♙ main

딱 기본만 하는 놈이다


코드:

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

처음에 포트 번호 받아 에러 아니면 리스닝 소켓을 열어(Open_listenfd) HTTP연결 대기한다

이후 무한 루프를 돌며 Accept로 연결 수락하고 Getnameinfo로 클라이언트 주소 정보 출력한다

그리고 doit() 호출해 요청 처리후 close()로 닫아준다

개구린 기초 서버라 한번에 하나씩 처리하고 바로 닫아버린다



♙ doit

HTTP 트랜잭션 해주는 핵심 함수다

ㄹㅇ 핵심이라 하는 것도 많다

  1. 요청 라인 읽기:

    Rio_readinitb로 내부 버퍼를 초기화하고, Rio_readlineb를 사용해
    요청 라인의 한 줄(메서드, URI, 버전)을 읽는다
    이후 읽은 거 printf로 출력 후 sscanf로 각 필드(HTTP 메서드, URI, 버전)로 분해한다

  2. 매서드 검증:

    현재 서버는 구려서 GET외의 메서드일시 오류 실행한다 clienterror

  3. 요청 헤더 읽기/무시:

    지원안하는 메서드일 경우 read_requesthdrs 함수를 호출해 나머지 요청 헤더들을 읽어 버퍼를 비운다

  4. URI해석

    parse_uri 함수를 호출해 요청 URI를 파일 경로CGI 인자 문자열로 분리한다
    이후 정적 콘텐츠인지 동적인지 판별한다

  5. 파일 상태 검사

    stat으로 filename 정보 가져온다
    filename의 파일 상태 정보를 확인해 존재 안할시 clieterror호출

  6. 정적/동적 분기 처리

    • 정적 콘텐츠: sbuf1로 가져온 파일 정보에서 일반인지 읽기 권한 있는지 확인 (S_ISREG 및 읽기 비트)
      만족 여부에 따라 오류 혹은, 정적 파일을 클라이언트에 전송
      (serve_static(fd, filename, sbuf.st_size)호출)

    • 동적 콘텐츠: 일반 파일이며 실행 권한이 있는지 (S_ISREG 및 실행 비트) 확인
      만족 여부에 따라 오류 혹은, 동적 콘텐츠 실행해 결과를 클라이언트에 전송
      (serve_dynamic(fd, filename, cgiargs) 호출)

  7. 함수 종료

    콘텐츠 제공 완료시 doit 반환하고 main 루프로 돌아가 다음 연결 처리한다


코드:

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 this file");
      return;
    }
    serve_static(fd, filename, sbuf.st_size);
  }
  else {
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
      clienterror(fd, filename,"403", "Forbidden",
               "Tiny couldn't run the CGI program");
      return;
    }
    serve_dynamic(fd, filename, cgiargs);
  }
}



♗ 보조 함수

♙ Open_listenfd

포트 번호 문자열 받으면 그거 갖다가 리스닝 소켓 열고 파일 디스크립터 반환해주는 헬퍼 함수다

  • open_listenfd(port)
    • getaddrinfo
    • → 소켓
    • setsockopt(SO_REUSEADDR)
    • bind
    • listen
    • -> listenfd

♙ Accept

accept 부른다

소켓하고 주소 정보 버퍼 포인터(선택 사항), 버퍼 크기 포인터(선택 사항) 넣으면 연결 전용 소켓 만들어 준다

Accept(listenfd, 클라이언트 주소, 클라이언트 길이) 형태로 사용하면 연결 전용 소켓 뱉어줘서

이를 connfd로 저장해 사용한다


♙ Getnameinfo

소켓 주소와 크기를 주고 host와 server의 버퍼랑 길이 주면 이름 채워서 돌려준다

Getnameinfo


Close

Open하고 반대된다

닫는다는 뜻이다 ㅇㅇ

그냥 닫을 연결 소켓 넣어주면 끝이다


♙ Rio 시리즈

Robust I/O 패키지 애들로 찾아보면 더 자세히 나온다

여기에서 사용된건 3가지다

  • Rio_readinitb:

    • Robust I/O용 내부 버퍼를 초기화하는 함수

    • 파일디스크립터랑 구조체 연결하고 초기화해 준비한다

  • Rio_readlineb

    • 파일 디스크립터, 버퍼, 최대 길이 입력 하면 최대 최대 길이-1 만큼 한줄 읽어 버퍼에 저장

    • 버퍼 끝엔 항상 NULL 문자(\0) 추가해준다

    • 최대길이-1 만큼 읽거나 EOF(파일 끝)에 도달하거나 개행문자 \n을 읽으면 종료한다

  • Rio_readnb

    • 파일 디스크립터, 버퍼, 길이 입력하면 최대 길이 만큼 읽어 버퍼에 저장

    • 길이만큼 읽거나 EOF에 도달시 종료


♙ 정적 파일 보조

정적 파일 전송 과정에 사용되는 보조 함수들이다

Open / Mmap / Munmap


♢ Open

Open(filename, O_RDONLY, 0)으로 filename 경로의 파일 열어 줄 수 있다

실제 open함수의 래퍼 함수다

// 원형
int open(const char *pathname, int flags, mode_t mode);

O_RDONLYflags인자에 들어가는 열기 모드(open flags)다

  • O_RDONLY → 읽기 전용으로 열기

  • O_WRONLY → 쓰기 전용으로 열기

  • O_RDWR → 읽기/쓰기 모드


세 번째 인자 mode는 파일 생성 시 권한과 관련 있는데

flagsO_CREAT 없으면 무시해서 걍 0으로 둔다


♢ Mmap

Mmap(addr, len, prot, flags, fd, offset)은 메모리에 매핑하기 위한 함수다

fd(열린 파일 디스크립터)의 내용을 프로세스 메모리에 붙이고
그 시작 주소를 포인터로 반환한다

교재에서는 srcfd(파일 디스크립터) 넣으면 srcp(시작 주소 포인터)로 받는 식으로 구현되어있다

Tiny 서버 예제에는 인자들 이렇게 들어간다

  • addr = 0 : 커널이 알아서 매핑 위치 정함

  • len = filesize : 파일 크기만큼 매핑

  • prot = PROT_READ : 읽기 전용 권한

  • flags = MAP_PRIVATE : 사적 매핑 (쓰기 시 원본 파일에 반영 X)

  • fd = srcfd : 매핑할 파일 디스크립터

  • offset = 0 : 파일 처음부터 매핑


♢ Mummap

Mmap으로 매핑한 가상 메모리 영역 해제한다

Munmap(srcp, filesize)srcp 주소로부터 filesize 만큼 영역 해제 한다


♙ 동적 콘텐츠 보조

동적 콘텐츠 처리에 사용되는 프로세스 제어 관련 함수들이다

Fork / Execve / Dup2 / Wait

익숙한 놈들이 여럿 보인다

♦ Fork

fork다 현재 프로세스 복제해서

자식 프로세스 생성 후 에러 시 메시지 출력하는 거 한다

실패시 -1 반환하고

자식 프로세스에는 0, 부모 프로세스에는 자식의 PID2 반환한다

♦ Execve

execve

현재 프로세스 코드/데이터/스택을 통째로 교체하여 filename 프로그램을 실행한다

이를 통해 자식 프로세스를 CGI프로그램으로 바꿀 수 있다

실패할 경우에만 값(-1)을 반환한다

♦ Dup2

int dup2(int oldfd, int newfd);

oldfd에 복사하고 싶은 거 넣고 newfd에 당할 거 넣으면

newfd로 하여금 oldfd와 같은 파일/소켓을 가리키도록 복제한다

실패시 -1반환하고 성공시 newfd 반환한다

♦ Wait

pid_t wait(int *status); // 자식 프로세스 종료 상태 저장용 int

부모 프로세스가 호출하면, 자식 프로세스가 종료될 때까지 대기

그리고 종료된 자식의 PID를 반환하고 status에 종료 상태 기록한다

  • 성공 시: 종료한 자식 프로세스의 PID
  • 실패 시: -1


♖ 정적 콘텐츠 처리 흐름

정적 콘텐츠는 파일 시스템에 저장된 파일(예: HTML, 이미지 파일 등)을 그대로 클라이언트에게 전송하는 것이다

  1. 요청 확인:

    doit에서 parse_uri호출로 is_static == 1이면
    정적 파일이란 뜻
    filename 변수에는 ./<uri 경로> 형태의 실제 파일 경로가 세팅된다

    • (예: URI /foo/bar.html -> 파일 경로 ./foo/bar.html)


  1. 파일 존재 및 권한 확인:

    stat(filename, &sbuf) 호출로 파일의 상태 확인
    존재하지 않을 시 404오류 반환후 종료
    존재할 시 S_ISREG(sbuf.st_mode) 매크로로 정규 파일인지, 그리고 S_IRUSR & sbuf.st_mode로 읽기 권한이 있는지 검사
    파일이 디렉토리거나 권한 없으면 403오류 후 종료


  1. HTTP 응답 헤더 생성:

    드디어 serve_static함수로 진입한다
    get_filetype(filename, filetype)을 호출해 파일 확장자에 따른 MIME 타입 문자열을 얻고 (예: .htmltext/html)
    이후 sprintfbuf에 연이어 작성한다
    헤더 작성된 건 Rio_writen으로 클라이언트에 전송된다

    보내지는 내용:
    HTTP/1.0 200 OK 상태, Server: Tiny Web Server, Connection: close, Content-length: [파일크기], Content-type: [파일 MIME]


  1. 파일 내용 전송:

    파일 전송을 위해 Open(filename, O_RDONLY, 0)로 파일 열고
    filesize만큼 메모리에 Mmap로 매핑한다
    그 뒤 close(srcfd)로 닫고 매핑된 주소는 Rio_writen로 소켓에 써서 전송한다
    전송완료후 Munmap으로 매핑 해제한다


  1. 로그 및 종료:

    응답 헤더를 printf로 출력해 로그 남긴다
    파일 전송 완료시 server_static이 리턴되어 다시 doit으로 돌아간다
    정적 요청 처리 완료



♖ 동적 콘텐츠 처리 흐름

  1. 요청 확인:

    doit에서 parse_uri호출로 is_static == 0이면
    동적 파일이란 뜻
    filename에는 실행할 CGI 프로그램 경로가 세팅된다

    • (예: ./cgi-bin/adder)

    cgiargs에는 URI의 '?' 뒷부분, 즉 프로그램에 전달할 인자 문자열이 설정된다

    • (예: URI가 /cgi-bin/adder?15000&213이면 cgiargs="15000&213")


  1. 파일 존재 및 권한 확인:

    stat으로 파일 정보를 가져와 존재하지 않으면 404 오류 반환
    존재한다면 S_ISREG로 일반 파일 여부 확인하고, S_IXUSR & sbuf.st_mode로 실행 권한이 있는지 검사
    실행 파일 아니거나 권한 없으면 403 오류 응답 후 종료


  1. 초기 응답 헤더 전송:

    serve_dynamic 드디어 호출
    상태코드의 응답줄과 Server 헤더를 간단히 생성해 클라이언트로 보낸다


  1. CGI 프로그램 실행 준비:

    Fork()를 호출해 새 프로세스를 실행한다
    자식 프로세스에서 수행할 작업과 부모 프로세스의 처리가 분리

    • 자식 프로세스:

      Fork의 반환값이 0인 경로로 들어와 CGI실행
      먼저 환경변수 QUERY_STRING을 설정하여, 프로그램이 입력 인자를 환경변수를 통해 사용할 수 있게 한다
      그 다음 Dup2(fd, STDOUT_FILENO)를 호출해 자식 프로세스의 표준 출력(STDOUT_FILENO)을 클라이언트와의 소켓 fd로 연결

    • 부모 프로세스:

      Fork의 반환값이 자식 PID2로 부모는 곧바로 Wait(NULL)을 호출하여 자식이 종료될 때까지 기다린다
      그동안 블록된 상태로 대기하다가 자식이 종료시 Wait에서 돌아와 serve_dynamic 함수를 마무리하고 리턴


  1. CGI 출력 전송 및 완료:

    자식 프로세스에서 실행된 CGI 프로그램의 출력결과는 클라이언트로 바로 전송
    이를 통해 CGI 프로그램의 응답을 클라이언트는 받을 수 있음
    이후, Tiny 서버는 연결 닫고 다음 요청 처리 준비한다





  1. struct stat 구조체, stat() 시스템 콜로 채워진 파일의 메타데이터(종류, 권한, 크기 등) 

  2. 프로세스 식별자(Process ID)  2