본문 바로가기
C

[Tiny web server] serve static content

by 파피요트 2023. 3. 9.

이론 간단정리 

server와 client는 연결을 통해서 통신한다. 연결은 다음과 같이 두 소켓으로 식별된다. (튜플로 나타내는 소켓 쌍)

(소켓에 들어있는 host ip는 private ip 가 아닌 public ip 임에 유의하자)

(클라이언트 측 포트는  포트 번호가 연결이 수립될 때 정해지는 단기 포트이다)

server는 socket , bind, listen , accept 함수를 호출해서 연결을 수립한다.

client는 socket , connect 함수를 호출해서 연결을 수립한다. 

이 함수들의 인자들은 getaddrinfo 함수를 이용해서 얻는다.

 

open_listenfd는 socket , bind, listen 함수를 감싼 것으로 서버 측에서 호출한다.

이 함수를 호출하면 listening socket 이 오픈된다. 

그리고 accept 함수를 호출해서 연결 요청을 기다린다.

 

open_clientfd는 socket,connect 함수를 감싼 것으로 클라이언트 측에서 호출한다.

이 함수를 호출하면 서버측에 연결을 요청한다.

 

연결 요청을 하면 다음과 같이 연결이 수립된다. 

listenfd는 계속 존재하고( 포트에서 서비스하는 동안 ) connfd는 연결이 수립되는 동안에만 존재한다.

이런 방식을 통해서 동시에 여러 연결을 수립할 수 있다. (tiny web server는 이 방식으로 동작하지만 동시에 연결을 수립할 수는 없음)

연결이 수립되면 UNIX I/O 함수로 데이터를 전송하고 연결이 종료된다.

위 과정은 다음과 같이 요약된다. 


이제 Tiny web server 가 어떻게 응답을 하는지 알아보자. 

aws 포트열기

EC2 > 인스턴스 > 보안> 보안그룹 링크 클릭 > 인바운드 규칙 편집

 

유형 : 사용자지정TCP

소스 : 0.0.0.0/0

포트범위 : 8000

 

Tiny web server란 ? 

Iterative HTTP/1.0 웹 서버 .

GET method 만 가능

정적,동적 컨텐츠 제공

 

Transaction 과정

듣기 소켓 오픈 

ubuntu@ip-172-31-6-22:~/project/webproxy/tiny$ make  (tiny.c파일이 있는 폴더에서 make를 한다.)

ubuntu@ip-172-31-6-22:~/project/webproxy/tiny$ ./tiny 8000 (tiny 를 8000(인자)와 함께 실행한다.)(포트번호를 인자로 전달)

이제 듣기 소켓이 오픈되었다.

 

클라이언트 요청

브라우저 주소 창에 서버 프로세스가 돌아가고 있는 host의 public ip와 포트를 입력한다.(e.g. 3.39.238.173:8000/) (소켓 주소/)

 

연결 수립

서버측에서 연결요청을 수락하고 connfd 를 생성.  

 

트랜잭션 수행(서버측 관점)

rio_readlineb함수로 요청 라인을 읽는다

요청 라인을 method , uri , version 변수에 저장한다.

parse_uri로  uri를 파일이름과 인자로 파싱해서 저장한다.

이 경우 uri는 cgi-bin을 포함하고 있지 않으므로 static content 요청이며 ,

앞에 .을 붙혀 파일이름을 저장한다 만약 ./일 경우 ./home.html로 확장하여 저장한다.

response 헤더를 생성하고 Rio_writen함수로 전송한다.

open함수로 파일의 식별자를 얻고

mmap함수로 파일을 가상메모리에 매핑한다.

가상메모리에 있는 파일을 클라이언트에 전송한다. 

Close함수로 connfd를 닫는다. 

 

코드

#include "csapp.h"

void doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
void serve_static(int fd, char *filename, int filesize);
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs);
void clienterror(int fd, char *cause, char *errnum, char *shortmsg,
                 char *longmsg);

int main(int argc, char **argv) {
  printf("main실행\n");
  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]);
  printf("듣기 소켓 오픈\n");
  while (1) {
    clientlen = sizeof(clientaddr);
    connfd = Accept(listenfd, (SA *)&clientaddr,&clientlen);  
    Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE,0);
    printf("Accepted connection from (%s, %s)\n", hostname, port);
    doit(connfd);   // handles one HTTP transaction
    Close(connfd);  
    printf("connfd 닫음\n");
  }
}
void doit(int fd)
{
  printf("doit함수호출\n");
  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); // Rio_readlineb함수를 사용해서 요청 라인을 읽어들임
  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;
  }

  printf("Request headers:\n");
  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_IXUSR & sbuf.st_mode)) { // 보통파일인지? 실행가능한지? 
      clienterror(fd, filename, "403", "Forbidden","Tiny couldn’t run the CGI program");
      return;
    }
    serve_dynamic(fd, filename, cgiargs);  //동적 컨텐츠 제공
  }
}

void clienterror(int fd, char *cause, char *errnum,char *shortmsg, char *longmsg)
{
  char buf[MAXLINE] , body[MAXBUF];

  /* Build the HTTP response body */
  sprintf(body, "<html><title>Tiny Error</title>");
  sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
  sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
  sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
  sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body);

  /* Print the HTTP response */
  sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
  Rio_writen(fd, buf, strlen(buf));

  sprintf(buf, "Content-type: text/html\r\n");
  Rio_writen(fd, buf, strlen(buf));

  sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body));
  Rio_writen(fd, buf, strlen(buf));

  Rio_writen(fd, body, strlen(body));

}

void read_requesthdrs(rio_t *rp)
{
  printf("read_requesthdrs함수호출\n");
  char buf[MAXLINE];

  Rio_readlineb(rp, buf, MAXLINE);
  while(strcmp(buf, "\r\n")) {   //buf 와 \r\n이 같으면 0.
    Rio_readlineb(rp, buf, MAXLINE);
    printf("%s", buf);
  }
  return;
}

int parse_uri(char *uri, char *filename, char *cgiargs)
{
  printf("parse_uri함수호출\n");
  char *ptr;

  if (!strstr(uri, "cgi-bin")) { //uri가 cgi-bin을 포함하고 있지 않으면 static content
    strcpy(cgiargs, "");
    strcpy(filename, ".");
    strcat(filename, uri);
    if (uri[strlen(uri)-1] == '/')
      strcat(filename, "home.html");
    return 1;
  }
  else {  /* Dynamic content */
    ptr = index(uri,'?');
    if (ptr) {
      strcpy(cgiargs, ptr+1); 
      *ptr = '\0'; //이거 왜 함 ? 
    }
    else
      strcpy(cgiargs,"");
    strcpy(filename,".");
    strcat(filename,uri);
    return 0;
  }
}

void serve_static(int fd, char *filename, int filesize)
{
  printf("serve_static함수호출\n");
  int srcfd;
  char *srcp, filetype[MAXLINE], buf[MAXBUF];

  /* Send response headers to client */
  get_filetype(filename, filetype);
  sprintf(buf, "HTTP/1.0 200 OK\r\n");
  sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
  sprintf(buf, "%sConnection: close\r\n", buf);
  sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
  sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);
  Rio_writen(fd, buf, strlen(buf));

  printf("Response headers:\n");
  printf("%s", buf);

  /* Send response body to client */
  srcfd = Open(filename, O_RDONLY, 0); // 요청한 파일의 식별자를 얻음
  srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); //요청한 파일을 가상메모리에 매핑
  Close(srcfd);  //매핑했으니 이제 식별자 필요없음  
  Rio_writen(fd, srcp, filesize); // 주소 srcp에서 시작하는 filesize byte를 클라이언트의 연결식별자로 복사. (파일 전송)
  Munmap(srcp, filesize); //매핑된 가상메모리 영역을 free 
}

void get_filetype(char *filename, char *filetype)
{
  printf("get_filetype함수호출\n");
  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
    strcpy(filetype, "text/plain");
}

void serve_dynamic(int fd, char *filename, char *cgiargs)
{
  char buf[MAXLINE], *emptylist[] = { NULL };

  /* Return first part of HTTP response */
  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));

  if (Fork() == 0) { //자식 프로세스 fork (자식 프로세스의 context에서 cgi 프로그램을 돌린다)
    setenv("QUERY_STRING", cgiargs, 1);  // QUERY_STRING 환경변수를 받은 인자로 초기화
    Dup2(fd, STDOUT_FILENO); // 자식의 표준 출력을 연결 파일 식별자로 redirect.
    Execve(filename, emptylist, environ); // cgi 프로그램을 로드하고 실행. 
    //everything that the CGI program writes to standard output goes directly to the client process
  }
  Wait(NULL); 
}

확인 

브라우저에 http://3.39.238.173:8000/ 를 입력하면 

서버는 GET / HTTP/1.1 요청라인을 읽고 home.html파일을 전송하고 연결을 끊는다.

home.html은 다음과 같다.

<html>
<head><title>test</title></head>
<body>
<img align="middle" src="godzilla.gif">
Dave O'Hallaron
</body>
</html>

브라우저는 받은 html파일에 서버에 요청해야 할 godzilla.gif가 있음을 발견하고 요청을 한다.

서버는 GET /godzilla.gif HTTP/1.1 요청라인을 읽고 godzilla.gif파일을 전송하고 연결을 끊는다.

 

결과적으로 브라우저에 렌더링 되는 화면은 다음과 같다. 

 

 

 

 

 

 

'C' 카테고리의 다른 글

[Tiny web server] serve dynamic content  (0) 2023.03.11
csapp 연습문제 11.2  (0) 2023.03.01