들어가기 전에
- 이번 주차에 학습한, 개념론적인 부분들은 아래 게시글에 정리 하였다.
[[TIL] Jungle TIL : 네트워크 관련 Keyword 정리.]
네트워크는 클라이언트가 요청을 전송하면 서버가 이에 대한 응답을 전송하는, 위와 같은 클라이언트-서버 모델
을 기본으로 한다.
이번 주차에는 c언어를 이용해 웹 서버를 구현해보며 이러한 내용에 대해 학습 한다.
echo()
동작은 서버와 클라이언트의 동작을 보여줄 수 있는, 가장 간단한 형태의 동작이다.echo()
를 호출해 이를 클라이언트로 되돌려준다.이와 같은 간단한 형태의 프로그램을 작성해봄으로써 클라이언트-서버 모델
이 실제로 어떤 식으로 구현되는지 확인할 수 있다.
echo 서버-클라이언트를 제작하면서, 아래와 같은 소켓 인터페이스(Socket Interface)에 대해 이해할 수 있다.
#include "csapp.h"
void echo (int connfd);
int main (int argc, char ** argv)
{
int listenfd, connfd;
socklen_t clientlen;
struct sockaddr_storage clientaddr; /* Enough space for any address */
char client_hostname[MAXLINE], client_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, client_port, MAXLINE, 0);
printf("Connected to (%s, %s)\n", client_hostname, client_port);
echo(connfd);
Close(connfd);
}
exit(0);
}
- 사용자가 포트 번호를 붙여 해당 서버 프로세스를 실행할 것이다.
ex)127.0.0.1 8080
- 프로세스를 실행하면,
Open_listenfd()
를 호출 해 서버 소켓을 연다.
while
반복문 안에서 클라이언트 연결을 기다리고, 연결이 수락되면 클라이언트 주소 정보를 출력한다.
이후echo()
함수를 통해 연결된 클라이언트와 데이터를 주고받고,Close()
로 연결을 닫는다.
위의 내용을 이해하는데 있어 다음과 같이 서버와 클라이언트가 소켓 인터페이스를 통해 연결하는 과정에 대한 이해가 필요하다.
아래 그림에서 서버의 동작을 살펴보자.
우선, socket()
함수를 호출 해 fd
(파일 디스크립터: 연결 식별자, 여기서는 소켓)를 만든다.
Server는 bind()
함수를 호출 해 서버 소켓을 특정 IP 주소와 포트 번호에 연결한다.
Server는 listen()
을 호출 해 listenfd에 클라이언트 연결 요청이 들어오기를 기다린다.
Client에서 connection 요청이 들어오면, Server는 accept()
를 호출 해 연결하고, connfd
(연결 식별자)를 반환한다.
Open_listenfd()
함수는 위에서 서버가 소켓을 열고 listen()
하는 과정(소켓을 리스닝 소켓으로 만드는 과정)까지를 하나의 함수로 래핑 해 listenfd를 반환해 준다.
/*
* open_listenfd - Open and return a listening socket on port. This
* function is reentrant and protocol-independent.
*
* On error, returns:
* -2 for getaddrinfo error
* -1 with errno set for other errors.
*/
/* $begin open_listenfd */
int open_listenfd(char *port)
{
struct addrinfo hints, *listp, *p;
int listenfd, rc, optval=1;
/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Accept connections */
hints.ai_flags = AI_PASSIVE | AI_ADDRCONFIG; /* ... on any IP address */
hints.ai_flags |= AI_NUMERICSERV; /* ... using port number */
if ((rc = getaddrinfo(NULL, port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (port %s): %s\n", port, gai_strerror(rc));
return -2;
}
/* Walk the list for one that we can bind to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor */
if ((listenfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */
/* Eliminates "Address already in use" error from bind */
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, //line:netp:csapp:setsockopt
(const void *)&optval , sizeof(int));
/* Bind the descriptor to the address */
if (bind(listenfd, p->ai_addr, p->ai_addrlen) == 0)
break; /* Success */
if (close(listenfd) < 0) { /* Bind failed, try the next */
fprintf(stderr, "open_listenfd close failed: %s\n", strerror(errno));
return -1;
}
}
/* Clean up */
freeaddrinfo(listp);
if (!p) /* No address worked */
return -1;
/* Make it a listening socket ready to accept connection requests */
if (listen(listenfd, LISTENQ) < 0) {
close(listenfd);
return -1;
}
return listenfd;
}
/* $end open_listenfd */
Open_listenfd() 함수에 대한 간단한 설명
getaddrinfo
함수를 이용 해 서버 주소 정보 목록을 가져와 주소 목록을 순회하면서 각 항목에 대해 소켓을 생성socket()
하려고 시도한다.
(실패하면 다음 항목으로 이동하며, 서버 주소 목록은 연결 리스트 형태로 구현되어 있다.)
- 소켓이 생성되면, 소켓에
SO_REUSEADDR
옵션을 설정(사용한 포트를 재사용하는 옵션)하고, 소켓을 특정 주소와 바인딩bind()
한다.
(성공 시 루프를 빠져나오고, 실패 시close(listenfd)
로 소켓을 닫고 다음 항목으로 이동한다.)
freeaddinfo(listp)
로 할당된 메모리를 해제한다. 바인딩이 실패하면if (!p)
-1을 반환한다.return -1;
listen()
함수를 호출 해 소켓을 '리스닝 소켓'으로 만든다.
- 모든 과정이 성공하면 리스닝 소켓의 파일 디스크립터
listenfd
를 반환한다. (실패하면 -1을 반환)
#include "csapp.h"
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);
}
}
echo()
함수는 연결 디스크립터를 매개변수로 받고, 해당 디스크립터에서 Rio_readlineb()
를 통해 내용을 읽은 후 읽어들인 바이트를 출력하고, Rio_writen()
을 통해 내용을 클라이언트로 전송한다.#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);
}
클라이언트는 서버 주소와 포트 번호를 매개변수로 받아 실행한다.
Open_clientfd()
함수를 호출해 클라이언트 소켓을 만들고, 서버에 connect 연결을 요청한다.
서버와 소켓 연결이 설정되면, Fgets
를 통해 사용자의 입력을 버퍼 buf
에 읽어온다.
Rio_writen
함수는 clientfd
를 통해 서버로 buf
의 내용을 전송한다.
Rio_readlineb
는 서버의 응답을 buf
로 읽어들이고, 이를 표준 출력 stdout
으로 출력한다.
이번엔 아래 그림에서 클라이언트의 동작에 대해 알아보자.
클라이언트도 socket()
함수를 이용해 클라이언트 소켓을 생성한다.
이후 connect()
함수를 호출 해 서버에게 연결 요청을 한다.
서버에서 해당 연결 요청을 받아들이면, 연결이 완료 된다.
Open_clientfd()
함수는 위와 같은 클라이언트 동작을 래핑 해준다.
/*
* open_clientfd - Open connection to server at <hostname, port> and
* return a socket descriptor ready for reading and writing. This
* function is reentrant and protocol-independent.
*
* On error, returns:
* -2 for getaddrinfo error
* -1 with errno set for other errors.
*/
/* $begin open_clientfd */
int open_clientfd(char *hostname, char *port) {
int clientfd, rc;
struct addrinfo hints, *listp, *p;
/* Get a list of potential server addresses */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_socktype = SOCK_STREAM; /* Open a connection */
hints.ai_flags = AI_NUMERICSERV; /* ... using a numeric port arg. */
hints.ai_flags |= AI_ADDRCONFIG; /* Recommended for connections */
if ((rc = getaddrinfo(hostname, port, &hints, &listp)) != 0) {
fprintf(stderr, "getaddrinfo failed (%s:%s): %s\n", hostname, port, gai_strerror(rc));
return -2;
}
/* Walk the list for one that we can successfully connect to */
for (p = listp; p; p = p->ai_next) {
/* Create a socket descriptor */
if ((clientfd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0)
continue; /* Socket failed, try the next */
/* Connect to the server */
if (connect(clientfd, p->ai_addr, p->ai_addrlen) != -1)
break; /* Success */
if (close(clientfd) < 0) { /* Connect failed, try another */ //line:netp:openclientfd:closefd
fprintf(stderr, "open_clientfd: close failed: %s\n", strerror(errno));
return -1;
}
}
/* Clean up */
freeaddrinfo(listp);
if (!p) /* All connects failed */
return -1;
else /* The last connect succeeded */
return clientfd;
}
/* $end open_clientfd */
0-3. 실행 결과
- 왼쪽이 echo-server, 오른쪽이 echo-client의 동작이다.
- telnet을 통해서도 위와 같이 동작 수행을 확인할 수 있었다.
echo 서버와 클라이언트
의 구현을 통해, 간단하게 클라이언트와 서버가 연결되는 것을 눈으로 확인했다면, 이번엔 작은 서버를 만들어 웹 서버의 기능을 실제로 구현 해 볼 차례다.
서버의 구현은 결국 web
을 통해 데이터를 컴퓨터 A에서 컴퓨터 B로 보내기 위함이다.
그리고 이는 대개 html
이라는 문서의 형태로 브라우저에서 화면에 뿌려주는 식으로 이루어지고 있다.
tiny 서버
의 구현은 이러한 웹 서버의 구현을 가벼운 형태로 구현하면서 우리 눈으로 확인할 수 있도록 해준다.
우선, 전체 코드는 아래와 같다. 이를 하나 하나 뜯어보면서 차근차근 tiny 서버 동작에 대해 이해해보도록 하자.
아래 코드는 CS:APP 11장의 연습 문제를 해결하면서 GET
, HEAD
요청을 수행할 수 있도록 작성된 코드이다.
사실, main()
의 동작은 echo
서버 구현에서 본 내용과 크게 다를 바 없다. 위에서 그림과 함께 클라이언트와 서버가 소켓 인터페이스 상에서 어떤 식으로 연결되는지 이해했다면, 어렵지 않게 작성할 수 있다.
tiny서버
에서는 이에 추가적으로 doit()
함수를 작성 해 서버 안의 자원을 핸들링 한다.
doit()
함수 내에서 주요 동작은 정적 컨텐츠, 동적 컨텐츠를 핸들링 할 수 있는 serve_static()
, serve_dynamic()
함수를 통해 이루어진다.
/* $begin tinymain */
/*
* tiny.c - A simple, iterative HTTP/1.0 Web server that uses the
* GET method to serve static and dynamic content.
*/
#include "csapp.h"
void echo(int connfd);
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 serve_static_head(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)
{
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);
// echo(connfd);
doit(connfd); //line:netp:tiny:doit
Close(connfd); //line:netp:tiny:close
}
}
/* $end tinymain */
/*
* doit - handle one HTTP request/response transaction
*/
/* $begin 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;
size_t n;
/* Read request line and headers */
Rio_readinitb(&rio, fd);
Rio_readlineb(&rio, buf, MAXLINE);
printf("Request headers:\n");
printf("%s",buf);
sscanf(buf, "%s %s %s", method, uri, version); //line:netp:doit:parserequest
if (strcasecmp(method, "GET") != 0 && strcasecmp(method, "HEAD")!=0) { //line:netp:doit:beginrequesterr
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
} //line:netp:doit:endrequesterr
read_requesthdrs(&rio); //line:netp:doit:readrequesthdrs
/* Parse URI from GET request */
is_static = parse_uri(uri, filename, cgiargs); //line:netp:doit:staticcheck
if (stat(filename, &sbuf) < 0) { //line:netp:doit:beginnotfound
clienterror(fd, filename, "404", "Not found",
"Tiny couldn't find this file");
return;
} //line:netp:doit:endnotfound
if (is_static) { /* Serve static content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { //line:netp:doit:readable
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't read the file");
return;
}
if (strcasecmp(method, "GET") == 0){
serve_static(fd, filename, sbuf.st_size); //line:netp:doit:servestatic
}
else{
serve_static_head(fd,filename, sbuf.st_size);
}
}
else { /* Serve dynamic content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) { //line:netp:doit:executable
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs); //line:netp:doit:servedynamic
}
}
/* $end doit */
/*
* read_requesthdrs - read HTTP request headers
*/
/* $begin read_requesthdrs */
void read_requesthdrs(rio_t *rp)
{
char buf[MAXLINE];
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
while(strcmp(buf, "\r\n")) { //line:netp:readhdrs:checkterm
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
}
return;
}
/* $end read_requesthdrs */
/*
* parse_uri - parse URI into filename and CGI args
* return 0 if dynamic content, 1 if static
*/
/* $begin parse_uri */
int parse_uri(char *uri, char *filename, char *cgiargs)
{
char *ptr;
if (!strstr(uri, "cgi-bin")) { /* Static content */ //line:netp:parseuri:isstatic
strcpy(cgiargs, ""); //line:netp:parseuri:clearcgi
strcpy(filename, "."); //line:netp:parseuri:beginconvert1
strcat(filename, uri); //line:netp:parseuri:endconvert1
if (uri[strlen(uri)-1] == '/') //line:netp:parseuri:slashcheck
strcat(filename, "home.html"); //line:netp:parseuri:appenddefault
return 1;
}
else { /* Dynamic content */ //line:netp:parseuri:isdynamic
ptr = index(uri, '?'); //line:netp:parseuri:beginextract
if (ptr) {
strcpy(cgiargs, ptr+1);
*ptr = '\0';
}
else
strcpy(cgiargs, ""); //line:netp:parseuri:endextract
strcpy(filename, "."); //line:netp:parseuri:beginconvert2
strcat(filename, uri); //line:netp:parseuri:endconvert2
return 0;
}
}
/* $end parse_uri */
/*
* serve_static - copy a file back to the client
*/
/* $begin serve_static */
void serve_static(int fd, char *filename, int filesize)
{
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
/* Send response headers to client */
get_filetype(filename, filetype); //line:netp:servestatic:getfiletype
sprintf(buf, "HTTP/1.0 200 OK\r\n"); //line:netp:servestatic:beginserve
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)); //line:netp:servestatic:endserve
printf("Response headers:\n");
printf("%s", buf);
/* Send response body to client */
srcfd = Open(filename, O_RDONLY, 0); //line:netp:servestatic:open
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);//line:netp:servestatic:mmap
Close(srcfd); //line:netp:servestatic:close
Rio_writen(fd, srcp, filesize); //line:netp:servestatic:write
Munmap(srcp, filesize); //line:netp:servestatic:munmap
}
void serve_static_head(int fd, char *filename, int filesize)
{
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
/* Send response headers to client */
get_filetype(filename, filetype); //line:netp:servestatic:getfiletype
sprintf(buf, "HTTP/1.0 200 OK\r\n"); //line:netp:servestatic:beginserve
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)); //line:netp:servestatic:endserve
printf("Response headers:\n");
printf("%s", buf);
}
/*
* get_filetype - derive file type from file name
*/
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
strcpy(filetype, "text/plain");
}
/* $end serve_static */
/*
* serve_dynamic - run a CGI program on behalf of the client
*/
/* $begin serve_dynamic */
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) { /* Child */ //line:netp:servedynamic:fork
/* Real server would set all CGI vars here */
setenv("QUERY_STRING", cgiargs, 1); //line:netp:servedynamic:setenv
Dup2(fd, STDOUT_FILENO); /* Redirect stdout to client */ //line:netp:servedynamic:dup2
Execve(filename, emptylist, environ); /* Run CGI program */ //line:netp:servedynamic:execve
}
Wait(NULL); /* Parent waits for and reaps child */ //line:netp:servedynamic:wait
}
/* $end serve_dynamic */
/*
* clienterror - returns an error message to the client
*/
/* $begin clienterror */
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));
}
/* $end clienterror */
doit()
함수를 살펴보자./*
* doit - handle one HTTP request/response transaction
*/
/* $begin 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;
size_t n;
/* Read request line and headers */
Rio_readinitb(&rio, fd);
Rio_readlineb(&rio, buf, MAXLINE);
printf("Request headers:\n");
printf("%s",buf);
sscanf(buf, "%s %s %s", method, uri, version); //line:netp:doit:parserequest
if (strcasecmp(method, "GET") != 0 && strcasecmp(method, "HEAD")!=0) { //line:netp:doit:beginrequesterr
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
} //line:netp:doit:endrequesterr
read_requesthdrs(&rio); //line:netp:doit:readrequesthdrs
/* Parse URI from GET request */
is_static = parse_uri(uri, filename, cgiargs); //line:netp:doit:staticcheck
if (stat(filename, &sbuf) < 0) { //line:netp:doit:beginnotfound
clienterror(fd, filename, "404", "Not found",
"Tiny couldn't find this file");
return;
} //line:netp:doit:endnotfound
if (is_static) { /* Serve static content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) { //line:netp:doit:readable
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't read the file");
return;
}
if (strcasecmp(method, "GET") == 0){
serve_static(fd, filename, sbuf.st_size); //line:netp:doit:servestatic
}
else{
serve_static_head(fd,filename, sbuf.st_size);
}
}
else { /* Serve dynamic content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) { //line:netp:doit:executable
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs); //line:netp:doit:servedynamic
}
}
/* $end doit */
우선, Rio_readinitb(&rio, fd);
를 통해 파일 디스크립터 fd
(서버의 연결 소켓)과 rio
(버퍼링 된 읽기 구조체)를 연결하고, Rio_readlineb(&rio, buf, MAXLINE)
을 통해 요청 라인을 읽어 buf
에 저장한다.
밑의 printf()
동작들은 이 요청 라인에 대한 내용들을 출력한다.
sscanf(buf, "%s %s %s", method, uri, version)
으로 buf
에서 HTTP 메서드, URI, HTTP 버전을 각각 추출한다.
strcasecmp(method, "GET") != 0 && strcasecmp(method, "HEAD") != 0
: HTTP 메서드가 GET
이나 HEAD
가 아닌 경우 501 에러를 반환한다.
read_requesthdrs(&rio)
를 통해 나머지 요청 헤더를 읽는다.
is_static = parse_uri(uri, filename, chiargs)
: URI를 파싱.
stat(filename, &sbuf)
에서 요청된 파일 상태 정보를 sbuf
에 저장하고, 만약 파일이 없다면 404 오류를 반환.is_static
을 통해 정적 콘텐츠라면 serve_static()
호출.HEAD
요청이었다면 serve_static_head()
를 호출하도록 작성하였다.is_static
을 통해 동적 콘텐츠라면 serve_dynamic()
를 호출한다.void serve_static(int fd, char *filename, int filesize)
{
...
/* Send response body to client */
srcfd = Open(filename, O_RDONLY, 0); //line:netp:servestatic:open
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);//line:netp:servestatic:mmap
Close(srcfd); //line:netp:servestatic:close
Rio_writen(fd, srcp, filesize); //line:netp:servestatic:write
Munmap(srcp, filesize); //line:netp:servestatic:munmap
}
Open(filename, O_RDONLY, 0)
: 요청된 파일을 읽기 전용으로 열고, 파일 디스크립터를 srcfd
에 저장한다.
Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0)
: 파일의 내용을 메모리에 매핑하여 srcp에 포인터를 저장한다.
Rio_writen(fd, srcp, filesize)
: 메모리에 매핑된 내용을 클라이언트에게 전송한다.
serve_dynamic 함수 中
void serve_dynamic(int fd, char *filename, char *cgiargs)
{
...
if (Fork() == 0) { /* Child */ //line:netp:servedynamic:fork
/* Real server would set all CGI vars here */
setenv("QUERY_STRING", cgiargs, 1); //line:netp:servedynamic:setenv
Dup2(fd, STDOUT_FILENO); /* Redirect stdout to client */ //line:netp:servedynamic:dup2
Execve(filename, emptylist, environ); /* Run CGI program */ //line:netp:servedynamic:execve
}
Wait(NULL); /* Parent waits for and reaps child */ //line:netp:servedynamic:wait
}
Fork()
: 프로세스를 복제해 자식 프로세스를 생성,
자식 프로세스에서 CGI 프로그램을 실행한다.
setenv("QUERY_STRING", cgiargs, 1)
: CGI 환경 변수 QUERY_STRING
을 설정 → CGI 프로그램이 URL에서 전달된 쿼리 매개변수들을 참조할 수 있도록 한다.
Dup2(fd, STDOUT_FILENO)
: 표준 출력을 클라이언트의 fd
로 리디렉션. → CGI 프로그램에서 출력하는 내용이 클라이언트에게 전송된다.
Execve(filename, emptylist, environ)
: 지정된 CGI 프로그램을 실행한다.
filename
은 CGI 프로그램의 경로이고, emptylist
는 인자 목록이다. environ
은 현재 환경 변수를 나타낸다.
Wait(NULL)
: 부모 프로세스는 자식 프로세스가 종료될 때까지 대기한다. → 부모가 자식 프로세스를 회수하도록 해서, 자식 프로세스가 좀비 프로세스가 되는 것을 방지한다.
에러 처리
- 501 Not Implemented: 지원하지 않는 HTTP 메서드를 요청한 경우.
- 404 Not Found: 요청된 파일이 서버에 없는 경우.
- 403 Forbidden: 요청된 파일에 대해 읽기/실행 권한이 없을 경우.
실행 결과
- tiny서버 구현의 경우
cgi/bin
폴더 안에home.html
문서가 있다. 브라우저를 통해 접속하는 경우 해당 문서가 브라우저에 뿌려지는 것을 확인할 수 있었다.
- 아래는 서버 측의 출력이다.
<html>
<head><title>test</title></head>
<body>
<img align="middle" src="godzilla.gif">
<p>Dave O'Hallaron</p>
<!-- Local Video Embed -->
<video align="middle" width="560" height="315" controls>
<source src="">
Your browser does not support the video tag.
</video>
</body>
</html>
<video>
태그를 이용하면, 아래와 같이 영상을 브라우저에서 화면에 함께 뿌려줄 수 있는 것이다. 원하는 영상을 /cgi/bin
폴더에 올려놓고 경로 지정을 해주면 영상이 돌아갈 것이다.실행 결과
/cgi-bin/
폴더 안의 여러 자원들을 위처럼 html 문서에 올려 사용할 수 있다. adder.c
의 예시이다. 이 프로그램은 /cgi/bin
폴더 안에 있는 adder.html
을 오픈한다./*
* adder.c - a minimal CGI program that adds two numbers together
*/
/* $begin adder */
#include "csapp.h"
int main(void) {
FILE *html_file;
char content[MAXLINE];
html_file = fopen("cgi-bin/adder.html","r");
if (html_file == NULL)
{
sprintf(content, "Content-type: text/html\r\n\r\n");
sprintf(content + strlen(content), "<html><body><p>Error: adder.html not found</p></body></html>");
printf("%s",content);
fflush(stdout);
exit(1);
}
printf("Connection: closer\r\n");
printf("Connect-type: text/html\r\n\r\n");
while (fgets(content,MAXLINE,html_file) != NULL)
{
printf("%s", content);
}
fflush(stdout);
fclose(html_file);
exit(0);
}
/* $end adder */
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Adder test</title>
</head>
<body>
<h2>Adder Test</h2>
<form action="/cgi-bin/adder" method="get">
<label for="n1">First number:</label>
<input type="text" id="n1" name="n1">
<br>
<br>
<label for="n2">First number:</label>
<input type="text" id="n2" name="n2">
<button type="submit">Add</button>
</form>
</body>
</html>
실행 결과
서버 주소:포트번호/cgi-bin/adder
와 같이 입력해 접속하니, 위와 같이adder.html
문서가 브라우저에 뿌려지는 것을 확인할 수 있었다.- 각
textarea
에 각각 3, 5값을 입력하고Add
버튼을 클릭하니, 주소창에URI
로n1=3&n2=5
가 추가 되는 것을 확인할 수 있었다.
Proxy 서버와 관련된 내용은 다음 글에서 다뤄보자.