11.6 종합 설계: 소형 웹 서버
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/*
* 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 doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
void serve_sattic(int fd, char *filename, char *cgiargs);
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);
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
prot, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
doit(connfd);
Close(connfd);
}
}
TINY main
반복실행 서버로 명령줄에서 넘겨받은 포트로의 연결 요청 듣는다.
open_listenfd
함수를 호출해서 듣기 소켓을 연다.- TINY는 전형적인 무한 서버 루프 실행
- 반복적으로 연결 요청을 접수(line 32), 트랜잭션 수행(line 36), 자신 쪽의 연결 끝을 닫음(line 37)
doit
함수 : 한개의 HTTP 트랜잭션을 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
void doit(int fd)
{
int is_static;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filenmae[MAXLINE], cgiargs[MAXLINE];
rio_t rio;
/* 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);
if (strcasecmp(method, "GET")) {
clienterror(fd, method, "501", "Not implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio);
/* Parse URI from GET request */
is_static = parse_uri(uri, filename, cgiargs);
if (stat(filename, &sbuf) < 0) {
clienterror(fd, filenname, "404", "Not found",
"Tiny coudn't find this file");
return;
}
if (is_static) { /* Serve static content */
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 { /* Serve dynamic content */
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);
}
}
- 한 개의 HTTP 트랜잭션 처리.
- 요청 라인을 읽고 분석(line 11~ 14) <- 위 경우
rio_readlineb
함수를 사용해서 읽음 - TINY는 GET 메소드만 지원
- 따라서 클라이언트가 다른 메소드(ex. POST)를 요청하면 에러 메시지를 보내고 main 루틴으로 돌아옴(line 15~19)
- 그 후에 연결을 닫고 다음 연결 요청을 기다림
- 아닐때는 다른 요청 헤더들을 무시(line 20)
- 그 후 URI를 파일 이름과 비어 있을 수도 있는 CGI 인자 스트링으로 분석하고, 요청이 정적인지 동적인지 플래그 설정(line 23)
- 이 파일이 디스크 상에 있지 않으면, 에러 메시지를 클라이언트에 보내고 리턴
- 요청이 정적 컨텐츠라면, 이 파일이 보통 파일인지, 읽기 권한을 가지고 있는지 검증(line 31)
- 맞다면 클라이언트에게 정적 컨텐츠 제공(line 36)
- 요청이 동적 컨텐츠라면, 이 파일이 실행 가능한지 검증(line 39)
- 맞다면 동적 컨텐츠 제공(line 44)
Clienterror
함수 : 에러 메시지를 클라이언트에게 보낸다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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>");
sprinf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
sprinf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
sprinf(body, "%s<p>%s : %s\r\n", body, longmsg, cause);
sprinf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body);
/* Print the HTTP response */
sprinf(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));
}
HTTP 응답을 응답 라인에 적절한 상태 코드와 상태 메시지와 함꼐 클라이언트에 보냄
브라우저 사용자에게 에러를 설명하는 응답 본체에 HTML 파일도 함께 보냄
- HTML 응답은 본체에서 컨텐츠의 크기와 타입을 나타낸다.
- HTML 컨텐츠를 한 개의 스트링으로 만들었고, 그 크기를 쉽게 결정할 수 있음.
read_requesthdrs
함수 : 요청 헤더를 읽고 무시한다.
1
2
3
4
5
6
7
8
9
10
11
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;
}
TINY는 요청 헤더 내의 어떤 정보도 사용하지 않음
요청 헤더를 종료하는 빈 텍스트 줄이 line 6에서 체크하고 있는 carriage return과 line feed 쌍으로 구성되어 있음.
TINY parse_uri
: HTTP URI를 분석한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int parse_uri(char *uri, char *filename, char *cgiargs)
{
char *ptr;
if (!strstr(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;
}
}
-
TINY : static content를 위한 홈 디렉토리가 자신의 현재 디렉토리고, 실행파일의 홈 디렉토리는 /cgi-bin 이라고 가정.
- 스트링 cgi-bin을 포함하는 모든 URI는 dynamic content를 요청하는 것이라고 가정. 기본 파일 이름은 ./home.html
- URI를 파일 이름과 옵션으로 CGI 인자 스트링을 분석
- 요청이 정적 컨텐츠를 위한것이면(line 5)
- CGI 인자 스트링을 지운다(line 6)
- URI를 ./index.html 같은 상대 리눅스 경로이름(line 7~8)으로 변환
- 만일 URI가 ‘/’문자로 끝난다면(line 9), 기본 파일 이름을 추가한다(line 10).
- 요청이 동적 컨텐츠를 위한것이면(line 13)
- 모든 CGI 인자들을 추출(line 14~20)
- 나머지 URI 부분을 상대 리눅스 파일 이름으로 변환(line 21~22)
serve_static
함수 : 정적 컨텐츠를 클라이언트에게 서비스한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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);
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);
Munmap(srcp, filesize);
}
/*
* get_filetype - Derive file type from filename
*/
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, ".webp"))
strcpy(filetype, "image/png");
else if (strstr(filename, ".webp"))
strcpy(filetype, "image/jpeg");
else
strcpy(filetype, "text/plain");
}
TINY가 지원하는 5개의 정적 컨텐츠 : HTML 파일, 무형식 텍스트 파일, GIF, PNG, JPEG으로 인코딩된 영상
serve_static
함수는 지역 파일의 내용을 포함하고 있는 본체를 갖는 HTTP 응답을 보낸다.
- 파일 이름의 접미어(suffix)부분을 검사해 파일 타입을 결정한다.(line 7)
- 클라이언트에 응답 줄과 응답 헤더를 보낸다(line 8~13) <= 빈 줄 1개가 헤더를 종료하고 있다.
- 요청한 파일의 내용을 연결 식별자 fd로 복사해서 응답 본체를 보낸다.
- 읽기 위해서 filename을 오픈하고 식별자를 얻어오는 과정(line 18)
- 리눅스 mmap함수가 요청한 파일을 가상메모리 영역으로 mapping(line 19)
- 파일을 메모리로 mapping한 후 이 식별자는 필요 없으니 파일을 닫음(line 20) => 없으면 메모리 누수 발생
- 실제로 파일을 클라이언트에게 전송(line 21)
rio_writen
함수는 주소 srcp에서 시작하는 filesize 바이트(요청한 파일에 mapping 되어있음)를 클라이언트의 연결 식별자로 복사한다.- 매핑된 가상메모리 주소를 반환(line 22)
serve_dynamic
함수 : 동적 컨텐츠를 클라이언트에게 서비스한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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, strle(buf));
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));
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 */
}
TINY는 자식 프로세스를 fork하고 그 후에 CGI 프로그램을 자식의 컨텍스트에서 실행하며 모든 종류의 동적 컨텐츠를 제공한다.
serve_dynamic
함수
- 클라이언트에 성공을 알려주는 응답 라인을 보내는 것으로 시작
- CGI 프로그램은 응답의 나머지 부분을 보내야 함
- CGI 프로그램이 에러를 만날 수 있다는 가능성을 배제하였기에 우리 기대만큼 견고하지 않음
- 응답의 첫 번째 부분을 보낸 후, 새로운 자식 프로세스를 fork(line 11)
- 자식은 QUERY_STRING 환경변수를 요청 URI의 CGI 인자들로 초기화(line 13)
- 실제 서버는 여기서 다른 CGI 환경변수들도 설정하지만 여기선 생략됨
- 자식은 자식의 표준 출력을 연결 파일 식별자로 재지정(line 14)
- CGI 프로그램을 로드하고 실행(line 15)
- CGI 프로그램이 자식 컨텍스트에서 실행되기 때문에
execve
함수를 호출하기 전에 존재하던 열린 파일들과 환경변수들에도 동일하게 접근 가능 - 따라서 CGI 프로그램이 표준 출력에 쓰는 모든 것은 직접 클라이언트 프로세스로 부모 프로세스의 어떤 간섭도 없이 전달
- CGI 프로그램이 자식 컨텍스트에서 실행되기 때문에
- 부모는 자식이 종료되어 정리되는 것을 기다리기 위해
wait
함수에 블록됌(line 17)
11.7 요약
- 모든 network application 은 client-server model에 기초하고 있다.
- 이 모델을 사용하면 application은 1 개의 server와 1개 이상의 clients 로 구성된다.
- server는 resouce를 관리하고, client에게 service를 제공한다.
- 클라이언트-서버 모델에서의 기본 연산은 client-server transaction이다.
- 이 트랜잭션은 클라이언트로부터의 request와 이에 대한 서버의 response로 이루어진다.
- 클라이언트와 서버는 Internet이라고 하는 글로벌 네트워크를 통해서 통신한다.
- 프로그래머의 관점에서 인터넷은 전 세계적인 규모의 호스트 집단인데 다음 특성을 지녔다.
- 각 Internet host는 IP adress라고 하는 고유한 32비트 이름을 가진다.
- IP 주소 집합은 Internet domain 이름 집합과 대응된다.
- 서로 다른 인터넷 호스트에서의 process들은 connection을 통해서 서로 통신한다.
- 클라이언트와 서버는 socket interface를 사용해서 연결을 수립한다.
- 소켓은 연결의 end point이며 application에게는 file descriptor(식별자)의 형태로 제공된다.
- 소켓 인터페이스는 소켓 식별자를 열고 닫기 위한 함수들을 제공한다.
- 이 식별자들을 서로 읽고 써서 클라리언트와 서버는 통신한다.
- 웹 서버와 이들의 클라이언트들(like 브라우저)은 HTTP protocol을 사용해서 서로 통신한다.
- 브라우저는 서버로부터 static or dynamic contents를 요청한다.
- 정적 컨텐츠를 위한 요청은 서버의 디스크에서 파일을 가져와서 이것을 클라이언트에 돌려주는 방식으로 처리한다.
- 동적 컨텐츠에 대한 요청은 서버에서 child process의 컨텍스트에서 프로그램을 돌리고, 그 출력을 클라이언트로 리턴해서 처리한다.