Krafton Jungle/1. 정글 개발일지

[WEEK08] 웹 서버 개발일지

munsik22 2025. 5. 3. 13:39

1️⃣ Echo 서버 

서버/클라이언트/echo 코드는 CSAPP 11.4.9의 코드를 사용했다.

🔹 Makefile

# Echo_server_client
echoserver: echoserveri.o echo.o csapp.o
	$(CC) $(CFLAGS) echoserveri.o echo.o csapp.o -o echoserver $(LDFLAGS)

echoclient: echoclient.o echo.o csapp.o
	$(CC) $(CFLAGS) echoclient.o echo.o csapp.o -o echoclient $(LDFLAGS)

echo.o: echo.c csapp.h
	$(CC) $(CFLAGS) -c echo.c

echoclient.o: echoclient.c csapp.h
	$(CC) $(CFLAGS) -c echoclient.c

echoserveri.o: echoserveri.c csapp.h
	$(CC) $(CFLAGS) -c echoserveri.c
$ make echoserver
$ make echoclient

🔹 echo server

🔹 echo client

서버를 연 터미널은 그대로 두고 새로운 터미널을 열어서 EC2에 접속한 후에 클라이언트 프로그램을 실행해야 한다.


2️⃣ Tiny 서버

서버/adder 코드는 CSAPP 11.5~11.6의 코드를 사용했다.

  • 11.6c ✅
  • 11.7   ✅
  • 11.9   ✅
  • 11.10 ✅
  • 11.11 ✅

🤯 1차 실패

http://<ip주소>:8000/에 접속을 했을 때, 서버 프로그램이 home.html표시하는 데 실패했다.

수정 전 수정 후
  get_filetype(filename, filetype);
  sprintf(buf, "HTTP/1.0 200 OK\r\n");
  sprintf(buf, "Server: Tiny Web Server\r\n");
  sprintf(buf, "Content-length: %d\r\n", filesize);
  sprintf(buf, "Content-type: %s\r\n\r\n", filetype);
  Rio_writen(fd, buf, strlen(buf));
  printf("Response headers:\n");
  printf("%s", buf);



  get_filetype(filename, filetype);
  sprintf(buf, "HTTP/1.0 200 OK\r\n");
  Rio_writen(fd, buf, strlen(buf));
  sprintf(buf, "Server: Tiny Web Server\r\n");
  Rio_writen(fd, buf, strlen(buf));
  sprintf(buf, "Content-length: %d\r\n", filesize);
  Rio_writen(fd, buf, strlen(buf));
  sprintf(buf, "Content-type: %s\r\n\r\n", filetype);
  Rio_writen(fd, buf, strlen(buf));
  printf("Response headers:\n");
  printf("%s", buf);

 

serve_static 함수를 오른쪽 코드와 같이 한 줄마다 Rio_writen으로 입력을 받는 식으로 코드를 수정해서 문제를 해결했다.

이제 home.html이 정상적으로 표시된다!


🔹 숙제 11.6C

Tiny의 출력 결과를 본 결과 사용중인 브라우저의 HTTP 버전이 1.1임을 확인할 수 있었다.


😵 2차 실패 (숙제 11.10)

이거는 되는데
이거는 안 된다

 

adder.c에서 arg1, arg2에 '='가 포함된 경우 '=' 이후의 문자열만 정수로 변환하도록 코드를 수정해서 문제를 해결했다.

char *res1 = strchr(arg1, '=');
char *res2 = strchr(arg2, '=');
if (res1 != NULL) n1 = atoi(res1 + 1);
else n1 = atoi(arg1);
if (res2 != NULL) n2 = atoi(res2 + 1);
else n2 = atoi(arg2);

문제 해결!


🔹 숙제 11.9

수정 전 수정 후
  srcfd = Open(filename, O_RDONLY, 0);
  srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
  Close(srcfd);
  Rio_writen(fd, srcp, filesize);
  Munmap(srcp, filesize);




  srcfd = Open(filename, O_RDONLY, 0);
  srcp = malloc(filesize);
  if (srcp == NULL) {
      Close(srcfd);
      return;
  }
  Rio_readn(srcfd, srcp, filesize);
  Close(srcfd);
  Rio_writen(fd, srcp, filesize);
  free(srcp);

 

🔸 mmap과 malloc의 차이점:

  • mmap: 파일을 직접 가상 메모리 주소 공간에 매핑한다. 실제 파일 내용은 필요할 때(페이지 폴트 발생 시) 물리 메모리로 로드된다. 명시적인 파일 읽기/쓰기 호출 없이 메모리 접근처럼 사용할 수 있어 대용량 파일 처리에 효율적일 수 있다.
  • malloc: 힙 영역에서 순수하게 메모리 공간을 할당한다. 파일 내용을 메모리로 가져오려면 read 등의 함수를 사용하여 파일 내용을 명시적으로 읽어와야 한다. 할당받은 메모리는 프로그램이 직접 관리하고, 사용 후 free로 해제해야 한다.
  • mmap을 malloc으로 대체할 때는 파일 내용을 메모리로 읽어오는 과정(Rio_readn)이 추가되어야 한다.

🔹 숙제 11.10

사용할 수 있는 mpeg 파일이 없어서 mp4 비디오 파일을 처리하는 것으로 대체했다.

void get_filetype(char *filename, char *filetype) {
  if (strstr(filename, ".html"))
    strcpy(filetype, "text/html");
  else if (strstr(filename, ".gif"))
    strcpy(filetype, "image/gif");
  else if (strstr(filename, ".png"))
    strcpy(filetype, "image/png");
  else if (strstr(filename, ".jpg"))
    strcpy(filetype, "image/jpeg");
  else if (strstr(filename, ".mp4"))
    strcpy(filetype, "video/mp4");
  else if (strstr(filename, ".mpeg"))
    strcpy(filetype, "video/mpeg");
  else
    strcpy(filetype, "text/plain");
}

귀여운 문식이 영상이 잘 재생된다.


🔹 숙제 11.11

void doit(int fd) {
  ...
  if ((strcasecmp(method, "GET")) && (strcasecmp(method, "HEAD"))) {
      clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method");
      return;
    }
    read_requesthdrs(&rio);
  ...
}

void serve_static(int fd, char *filename, int filesize, char *method) {
  ...
  if (strcasecmp(method, "GET") == 0) {
    srcfd = Open(filename, O_RDONLY, 0);
    srcp = malloc(filesize);
    if (srcp == NULL) {
        Close(srcfd);
        return;
    }
    Rio_readn(srcfd, srcp, filesize);
    Close(srcfd);
    Rio_writen(fd, srcp, filesize);
    free(srcp);
  }
}

void serve_dynamic(int fd, char *filename, char *cgiargs, char *method) {
  ...
  if (strcasecmp(method, "GET") == 0) {
    if (Fork() == 0) { // Child 
      // Real server would set all CGI vars here 
      setenv("QUERY_STRING", cgiargs, 1);
      Dup2(fd, STDOUT_FILENO);              // Redirect stdout to client 
      Execve(filename, emptylist, environ); // Run CGI program 
    }
    Wait(NULL); // Parent waits for and reaps child 
  }
}

 

허용 메소드에 HEAD를 추가해주고, GET 요청이 들어왔을 때만 response body를 출력하도록 코드를 수정했다.

 


3️⃣ 프록시 서버

점수 채점을 위해 driver.sh를 실행하려고 하면 권한이 없다며 실행에 실패하게 된다. AWS EC2 기준으로 다음 단계를 거쳐 문제를 해결할 수 있다.

$sudo apt install net-tools	# 드라이버 실행을 위한 프로그램 설치
$sudo apt install dos2unix	# 줄 구분자 변환을 위한 프로그램 설치
$chmod +x driver.sh		# driver.sh의 권한 변경
$chmod +x nop-server.py		# nop-server.py의 권한 변경
$dos2unix driver.sh		# driver.sh의 줄 구분자 변환
./driver.sh

Windows에서는 줄 구분자 "\r\n"을 사용하지만 UNIX 시스템에서는 "\n"을 사용하기 때문에 우분투에서는 제대로 인식이 되지 않았다. 따라서 Windows에서 작성된 텍스트 파일을 UNIX 시스템에서 읽을 수 있도록 형식을 변환하기 위해 dos2unix를 사용하는 것이다. (참고)


🔹 웹 오브젝트 캐싱하기

🔸 캐싱 기본 개념

  • 무엇을 캐싱해야 하는가: (헤더를 포함한) 완전한 HTTP response
  • response를 parse할 필요는 없다. (하지만 실제 프록시는 한다)
  • size(response) > MAX_OBJECT_SIZE → 캐싱 안함

🔸 캐시 교체

  • LRU 정책 : 접근 타임스탬프가 가장 오래된 캐시 엔트리를 버린다.
  • 언제 교체하는가?
    • 남은 공간이 없을 때!
    • sum of size(cache_entries) + size(new_entry) > MAX_CACHE_SIZE

🔸 캐시 동기화

  • 단일 캐시가 모든 프록시 쓰레드들에 의해 공유되기 때문에 캐시로의 접근을 조심스럽게 관리해야 한다.
  • 어떤 동작이 lock되어야 할까?
    • add_cache_entry
    • remove_cache_entry
    • lookup_cache_entry
  • 여러 개의 readers는 아무 문제 없이 공존할 수 있다.
  • 하지만 한 writer가 도착하면, 그것의 쓰레드는 반드시 다른 것들과 접근을 동기화시켜야 한다.

🔸 캐싱 동작 방식

  • 캐시 확인: 요청을 받은 쓰레드는 먼저 자체 캐시에 해당 콘텐츠가 저장되어 있는지 확인한다.
    • 캐시 히트: 캐시에 콘텐츠가 있다면, 쓰레드는 최종 서버에 접근하지 않고 캐시된 데이터를 클라이언트에게 즉시 전송한다.
    • 캐시 미스: 캐시에 콘텐츠가 없다면, 쓰레드는 최종 서버로 요청을 전달하고 응답을 기다린다.
      최종 서버로부터 응답을 받으면, 해당 쓰레드는 받은 데이터를 클라이언트에게 전송함과 동시에 캐시에 저장해둔다.
  • 동시 처리: 이 모든 과정은 여러 쓰레드에 의해 병렬적으로 수행된다.
    • 즉, 한 쓰레드가 최종 서버와 통신하고 있는 동안 다른 쓰레드는 다른 클라이언트의 캐시된 요청을 처리하거나 또 다른 요청을 최종 서버에 전달할 수 있습니다.

이번 프로젝트에서 readers-writers 해결책을 구현하기 위해 사용할 수 있는 동기화 기법은 다음과 같다.

  • 캐시 파티셔닝
  • Pthreads readers-writers locks ✅
  • 세마포어 (단, 하나의 큰 exclusive lock을 사용하는 것은 금지됨)

프록시 driver 결과