port 번호를 인자로 받아 클라이언트의 요청이 올 때마다 새로 연결 소켓을 만들어 doit() 함수를 호출한다.
#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);
/* port 번호를 인자로 받는다. */
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]);
/* 클라이언트의 요청이 올 때마다 새로 연결 소켓을 만들어 doit() 호출*/
while (1) {
/* 클라이언트에게서 받은 연결 요청을 accept한다. connfd = 서버 연결 식별자 */
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); // line:netp:tiny:accept
/* 연결이 성공했다는 메세지를 위해. Getnameinfo를 호출하면서 hostname과 port가 채워진다. */
Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\\n", hostname, port);
/* doit 함수를 실행! */
doit(connfd); // line:netp:tiny:doit
/* 서버 연결 식별자를 닫아준다. */
Close(connfd); // line:netp:tiny:close
}
}
클라이언트의 요청 라인을 확인해 정적, 동적 컨텐츠인지를 구분하고 각각의 서버에 보낸다.
void doit(int fd)
{
int is_static;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE]; // 클라이언트에게서 받은 요청(rio)으로 채워진다.
char filename[MAXLINE], cgiargs[MAXLINE]; // parse_uri를 통해 채워진다.
rio_t rio;
/* Read request line and headers */
/* 클라이언트가 rio로 보낸 request 라인과 헤더를 읽고 분석한다. */
Rio_readinitb(&rio, fd); // rio 버퍼와 fd, 여기서는 서버의 connfd를 연결시켜준다.
Rio_readlineb(&rio, buf, MAXLINE); // 그리고 rio(==connfd)에 있는 string 한 줄(응답 라인)을 모두 buf로 옮긴다.
printf("Request headers:\\n");
printf("%s", buf); // 요청 라인 buf = "GET /godzilla.gif HTTP/1.1\\0"을 표준 출력만 해줌!
sscanf(buf, "%s %s %s", method, uri, version); // buf에서 문자열 3개를 읽어와 method, uri, version이라는 문자열에 저장한다.
// 요청 method가 GET이 아니면 종료. main으로 가서 연결 닫고 다음 요청 기다림.
if (strcasecmp(method, "GET")) { // method 스트링이 GET이 아니면 0이 아닌 값이 나옴
clienterror(fd, method, "501", "Not implemented",
"Tiny does not implement this method");
return;
}
// 요청 라인을 뺀 나머지 요청 헤더들을 무시(그냥 프린트)한다.
read_requesthdrs(&rio);
/* Parse URI from GET request */
/* parse_uri : 클라이언트 요청 라인에서 받아온 uri를 이용해 정적/동적 컨텐츠를 구분한다. */
is_static = parse_uri(uri, filename, cgiargs); // 정적이면 1
/* stat(file, *buffer) : file의 상태를 buffer에 넘긴다. */
/* 여기서 filename : 클라이언트가 요청한 서버의 컨텐츠 디렉토리 및 파일 이름? */
if (stat(filename, &sbuf) < 0) { // 못 넘기면 fail. 파일이 없다. 404.
clienterror(fd, filename, "404", "Not found",
"Tiny couldn't find this file");
return;
}
/* 컨텐츠의 유형(정적, 동적)을 파악한 후 각각의 서버에 보낸다. */
if (is_static) { /* Serve static content */
// !(일반 파일이다) or !(읽기 권한이 있다)
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't read the file");
return;
}
// 정적 서버에 파일의 사이즈를 같이 보낸다. -> Response header에 Content-length 위해!
serve_static(fd, filename, sbuf.st_size);
}
else { /* Serve dynamic content */
// !(일반 파일이다) or !(실행 권한이 있다)
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));
}
클라이언트가 버퍼 rp에 보낸 나머지 요청 헤더들을 무시한다(그냥 프린트한다).
void read_requesthdrs(rio_t *rp)
{
char buf[MAXLINE];
Rio_readlineb(rp, buf, MAXLINE);
/* 버퍼 rp의 마지막 끝을 만날 때까지("Content-length: %d\\r\\n\\r\\n에서 마지막 \\r\\n) */
/* 계속 출력해줘서 없앤다. */
while(strcmp(buf, "\\r\\n")) {
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
}
return;
}
uri를 받아 요청받은 파일의 이름(filename)과 요청 인자(cgiarg)를 채워준다.
int parse_uri(char *uri, char *filename, char *cgiargs)
{
char *ptr;
/* uri에 cgi-bin이 없다면, 즉 정적 컨텐츠를 요청한다면 1을 리턴한다.*/
// 예시 : GET /godzilla.jpg HTTP/1.1 -> uri에 cgi-bin이 없다
if (!strstr(uri, "cgi-bin")) { /* Static content, uri안에 "cgi-bin"과 일치하는 문자열이 있는지. */
strcpy(cgiargs, ""); // 정적이니까 cgiargs는 아무것도 없다.
strcpy(filename, "."); // 현재경로에서부터 시작 ./path ~~
strcat(filename, uri); // filename 스트링에 uri 스트링을 이어붙인다.
// 만약 uri뒤에 '/'이 있다면 그 뒤에 home.html을 붙인다.
// 내가 브라우저에 http://localhost:8000만 입력하면 바로 뒤에 '/'이 생기는데,
// '/' 뒤에 home.html을 붙여 해당 위치 해당 이름의 정적 컨텐츠가 출력된다.
if (uri[strlen(uri)-1] == '/')
strcat(filename, "home.html");
/* 예시
uri : /godzilla.jpg
->
cgiargs :
filename : ./godzilla.jpg
*/
// 정적 컨텐츠면 1 리턴
return 1;
}
else { /* Dynamic content */
ptr = index(uri, '?');
// '?'가 있으면 cgiargs를 '?' 뒤 인자들과 값으로 채워주고 ?를 NULL로 만든다.
if (ptr) {
strcpy(cgiargs, ptr+1);
*ptr = '\\0';
}
else // '?'가 없으면 그냥 아무것도 안 넣어준다.
strcpy(cgiargs, "");
strcpy(filename, "."); // 현재 디렉토리에서 시작
strcat(filename, uri); // uri 넣어준다.
/* 예시
uri : /cgi-bin/adder?123&123
->
cgiargs : 123&123
filename : ./cgi-bin/adder
*/
return 0;
}
}