Tiny라는
작지만 동작하는 웹서버를 개발해보자.
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);
#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(intfd, 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;
// 인자는 2개만 받겠다.
if (argc !=2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
// 듣기 소켓용 준비해놨다.
listenfd = Open_listenfd(argv[1]);
//연결 시킨후 doit에 넘기겠다.
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);
Close(connfd);
}
}
쭉 적었지만 사실 서버 기본 골자는 똑같고
그냥 doit에 넘겨버렸다.
정적과 동적 컨텐츠를 어떻게 처리하는지 보자.
한 개의 HTTP 트랜잭션을 처리한다.
요청라인 분석
읽기 시작하고,
sscanf로 (형식에 따라 데이터 나눠주는 함수) method, uri, version에 넣어준다.
strcasecmp 함수로 method가 GET인지 확인한다.
만약 아니라면 반환시킨다.
read_requesthdrs로 그외 요청헤더는 무시한다.
URI로 정적, 동적인지 구분.
파일이 존재하지 않으면 리턴.
정적이면 읽기 권한 확인 후 serve_static으로 전달.
동적이면 실행 권한 확인 후 serve_dynamic으로 전달.
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);
// URI 분석하여 정적/동적인지 구분.
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_TRUSR & 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);
}
일부 명백한 오류에 대해서만 체크하고,
이를 클라이언트에게 보고한다.
HTTP 응답을
응답 라인에 적절한 상태코드, 상태 메시지와 함께
클라이언트에게 보내고,
응답 본체에도 HTML 파일을 같이 보낸다.
HTML 응답은 컨텐츠 크기와 타입을 나타내야해서,
HTML 컨텐츠를 한개의 스트링으로 해서 보낸다.
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));
}
Tiny는 요청 헤더 내 어떤 정보도 사용하지 않는다.
그래서 이들을 읽고 무시한다.
void read_requesthdrs(rio_t *rp)
{
char buf[MAXLINE];
//첫번째 줄은 요청 라인이라 먼저 읽음.
Rio_readlineb(rp, buf, MAXLINE);
//요청 헤더를, \r\n이 나올때까지 계속 읽음.
while(strcmp(buf, "\r\n")){
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
}
return;
}
printf로 디버깅이나 요청 내용을 확인할 순 있을 것이다.
Tiny는
정적 컨텐츠를 위한 홈 디렉토리 = 자신의 현재 디렉토리
실행 파일의 홈 디렉토리 = /cgi-bin
이라 가정한다.
스트링 cgi-bin을 포함하는 모든 URI를
동적 컨텐츠를 요청한다고 가정한다.
기본 파일 이름은 ./home.html이다.
요청이 정적 컨텐츠일때(cgi-bin을 포함하지않을때)
CGI 인자 줄을 지우고(strcpy로 빈칸으로 만들어버림)
URI를 ./index.html 같은 상대 리눅스 경로이름으로 변환.
만약 URI가 /로 끝난다면 기본 파일이름을 추가.
요청이 동적 컨텐츠일때
모든 CGI 인자를 추출해서
(? 부분을 찾아서 그 뒤를 싹 다 CGI 인자로 저장)
(그리고 인자 부분이 잘리도록 \0으로 ? 부분 변환.)
나머지 URI 부분을 상대 리눅스 파일이름으로 변환
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,".");
strcat(filename, uri);
return 0;
}
}
Tiny는 서로 다른 다섯 개의 정적 컨텐츠를 지원
: HTML 파일, 무형식 텍스트 파일, GIF, PNG, JPEG로 인코딩된 이미지.
지역 파일의 내용을 포함하고 있는 본체를 가진,
HTTP 응답을 보낸다.
void serve_static(int fd, char *filename, int filesize){
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
// 파일 타입 결정.
get_filetype(filename, filetype);
//응답 라인, 헤더 송신.(HTTP 줄이 라인.)
//(서버에도 같은내용 출력.)
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);
Mummap(srcp, filesize);
}
read가 아니라 mmap을 쓰는 이유를
지피티에게 물어봤는데
파일을 메모리에 매핑하면
메모리에서 가져오는 거라 훨씬 빠르다고 하는군.
그치만 어차피 메모리에 매핑하려면
디스크에서 가져와야하기때문에 다른 점이 뭔지 모르겠다고 했더니
가상 메모리는 필요한 부분만 그때그때 가져오니 그렇다네.
이건 안 읽어도 알거같다
하지만 읽는다
...를 하려고했는데 설명도 안 하네
웃기는 책이다
strstr 함수 : 문자열에서 특정 문자열을 찾는 함수
strcpy 함수 : 문자열을 복사하는 함수
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");
}
자식 프로세스를 fork 후,
CGI 프로그램을 자식 컨텐스트에서 실행하여
모든 종류의 동적 컨텐츠를 제공한다.
이 함수는
CGI 프로그램이 에러를 만날 수 있는 가능성에 대한
처리를 해두지는 않았다.
CGI 프로그램이 자식 컨텍스트에서 실행되어
execve 호출 전
존재하던 열린 파일, 환경 변수에
동일하게 접근할 수 있다.
그래서 CGI는 표준출력에만 써도
부모 프로세스의 간섭없이 클라이언트 프로세스로 전달된다.
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));
//자식 프로세스에서 CGI 프로그램 실행.
if(Fork() == 0){
setenv("QUERY_STRING", cgiargs, 1); //환경변수 설정.
Dup2(fd, STDOUT_FILENO); // 자식 프로세스의 표준 출력을 클라이언트에게 보낼 fd로 재지정.
Execve(filename, emptylist, environ); // 자식 프로세스를 새로운 프로그램으로 교체.
// CGI 파일 경로, 명령 라인 인수 배열, 환경 변수 리스트.
}
Wait(NULL);
}
자식 프로세스의 표준 출력을 클라이언트 프로세스로 ->
그냥 자식 프로세스에서 printf로 해도
writen으로 보낸 것 같은 효과가 나타난다.
int dup2(int oldfd, int newfd);
해서 복제할 파일 디스크립터/복제할 대상 파일 디스크립터...
setenv는
int setenv(const char *name, const char *value,
int overwrite);
, 환경변수 이름/환경 변수 값을 나타내는 문자열/0이면 이미 존재하는 환경변수를 덮어쓰지 않음, 1이면 덮어씀.
setenv는 프로그램 실행 되는 동안 동작 제어나(디버깅 모드등)
프로그램 간 getenv, setenv로 통신하거나,
설정 파일등을 지정할 수 있다네.
execve는
int execve(const char *filename, char *const argv[],
char *const envp[]);
실행할 프로그램의 경로,
해당 프로그램에 전달될 명령 라인 인수(Command Line Arguments)의 배열,
프로그램에 전달되는 환경 변수의 배열.
명령 라인 인수는
프로그램에 전달되는 인자... 같은거같고
환경변수는 아까 그 QUERY_STRING 같은 거겠지..
environ이 한번밖에 선언 안되어서
뻑나지 않느냐고 물었떠니
전역 변수로 정의된 환경 변수 배열이라고...
11.6c
Tiny 출력을 조사하여
우리가 사용하는 브라우저의 HTTP 버전을 결정하기.
11.7
Tiny를 확장해서 MPG 비디오 파일을 처리하기.
11.9
Tiny를 수정해서 정적 컨텐츠를 처리할 때 요청한 파일을
mmap과 rio_readn 대신에
malloc, rio_readn, rio_writen을 사용하여 연결 식별자에게 복사하기.
11.10
A. 그림 11.27 CGI adder함수에 대한 HTML 형식을 작성하라.
사용자가 함께 더할 두개의 숫자로 채우는
두개의 텍스트 상자를 포함하라.
GET으로 이 컨텐츠를 요청해야한다.
B 실제 브라우저로 Tiny에게 이 형식을 요청해서,
채운 형식을 Tiny에 보내서,
adder가 생성한 동적 컨텐츠를 표시하는 방식으로 작업을 체크하기.
11.11
Tiny를 확장해서 HTTP HEAD 메소드를 지원하게 하기.
TELNET을 웹 클라이언트로 사용해서 작업 결과 체크하기.
이 중 세 문제 이상 풀어야한다...