tiny web server의 코드를 작성하려면 네트워크 프로그래밍과 시스템 수준의 입출력에 대해서 이해하고 있어야 한다. 우선 책에 있는 코드를 기준으로 순차적으로 관련 내용들을 살펴보자.
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
}
}
tiny.c 의 메인 코드이다. 우선 실행 명령어의 인자(argc)가 2개가 아니면 사용법을 출력해서 제대로 사용하라고 꾸짖는다. 제대로 된 명령어가 들어왔다면, listenfd를 생성한다. 여기서 Open_listenfd 함수를 살펴보자.
int Open_clientfd(char *hostname, char *port) {
int rc;
if ((rc = open_clientfd(hostname, port)) < 0)
unix_error("Open_clientfd error");
return rc;
}
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;
}
함수의 첫 번째 글자가 대문자인 것은 에러핸들링을 해서 함수로 한 번 감쌌다고 생각하면 된다. 실제 실행되는 소문자 함수를 보면 우선 이거저거 설정해주고 getaddrinfo함수를 실행한다. 이후 listp부터 시작해서 다음 주소가 있을 동안 socket함수와 bind함수가 성공하는지 판단한다. 이후 유효한 listenfd가 생성됐다면 그 값을 리턴하고 아니면 에러코드를 리턴한다.
이후 while문에서 연결요청에 대해 Accept함수로 connfd를 생성하고 연결됐다는 문구와 함께 doit함수를 실행한 다음 완료되면 close한다.
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_IXUSR & sbuf.st_mode)){
clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program"):
return;
}
serve_dynamic(fd, filename, cgiargs);
}
}
doit함수에 Robust I/O 관련 함수(Rio_...)가 나오는데 이 내용은 CSAPP 10장을 참고하고 여기서는 read_requesthdrs함수를 실행하기 위한 작업 정도로 생각하자. 이 코드에서는 GET 이외의 요청은 처리하지 못 한다는 에러를 설정한다.
이후에는 parse_uri함수로 uri를 분석하고, 파일을 찾을 수 없으면 404 error를 반환하고 해당 파일이 정적 파일인지 동적 파일인지에 따라 작업이 이루어진다. 권한이 있는지 판단한 후 각각 serve_static함수, serve_dynamic함수를 실행한다.
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg){
char buf[MAXLINE], body[MAXBUF];
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);
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));
}
clineterror; 에러 화면을 컨트롤한다.
void read_requesthdrs(rio_t *rp){
char buf[MAXLINE];
Rio_readlineb(rp, buf, MAXLINE);
while(strcmp(buf, "\r\n")){
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
}
return;
}
read_requesthdrs; rio_t 구조체에 담았던 내용을 읽는 작업을 한다.
int parse_uri(char *uri, char *filename, char *cgiargs){
char *ptr;
if(!strstr(uri, "cgi-bin")){
strcpy(cgiargs, "");
strcpy(filename, ".");
strcat(filename, uri);
if(uri[strlen(uri)-1] == '/')
strcat(filename, "home.html");
return 1;
}else{
ptr = index(uri, '?');
if(ptr){
strcpy(cgiargs, ptr+1);
*ptr = '\0';
}else{
strcpy(cgiargs, "");
}
strcpy(filename, ".");
strcpy(filename, uri);
return 0;
}
}
parse_uri함수에서 인자로 받은 uri에 "cgi-bin"이 포함되었는지 여부로 정적 파일과 동적 파일을 구분한다. 각 파일 형식에 맞는 filename으로 변경해준 뒤 정적 파일은 1, 동적 파일은 0 값을 리턴한다.
void serve_static(int fd, char *filename, int filesize){
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
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);
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);
}
//filename의 확장자로 filetype 가져오는 함수
void get_filetype(char *filename, char *filetype){
if(strstr(filename, ".html"))
strcpy(filetype, "text/html");
else if(strtstr(filename, ".gif"))
strcpy(filetype, "image/gif");
else if(strtstr(filename, ".png"))
strcpy(filetype, "image/png");
else if(strtstr(filename, ".jpg"))
strcpy(filetype, "image/jpeg");
else
strcpy(filetype, "text/plain");
}
Response headers를 출력하고, 해당 파일을 읽기전용으로 열어서 srcfd에 식별자를 할당한다. srcp에 메모리매핑을 한 뒤에 열었던 파일을 다시 닫고 메모리에 매핑된 것으로 쓰기작업을 한 뒤 메모리도 다시 반환해준다.
void serve_dynamic(int fd, char *filename, char *cgiargs){
char buf[MAXLINE], *emptylist[] = { NULL };
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){
setenv("QUERY_STRING", cgiargs, 1);
Dup2(fd, STDOUT_FILENO);
Execve(filename, emptylist, environ);
}
Wait(NULL);
}
동적 파일인 경우에는 프로세스를 fork하고 표준출력 식별자를 fd 식별자값으로 재지정해주고 자식 프로세스에서 해당 파일을 실행한다.