말 그대로 uri를 나누는 함수이다.
uri를 도메인, path, cgiargs으로 나눈다.
cgiargs란 동적 컨텐츠의 실행파일에 들어갈 인자다.
tiny서버의 모든 동적 컨텐츠를 위한 실행파일은 cgi-bin이라는 디렉토리에 넣는 고전적인 방식으로 정적 컨텐츠와 분리를 시킬것이다.
따라서 uri에 cgi-bin이라는 경로가 있는지 확인을 해 정적 컨텐츠를 보내야 하는지 동적 컨텐츠를 보내야하는지 판단할 수 있다.
int parse_uri(char *uri, char *filename, char *cgiargs)
{
char *ptr;
if (!strstr(uri, "cgi-bin")) { //static content
strcpy(cgiargs, ""); //uri에 cgi-bin과 일치하는 문자열이 없다면 cgiargs에는 빈 문자열을 저장
strcpy(filename, "."); //아래 줄과 더불어 상대 리눅스 경로이름으로 변환(./index.html과 같은 )
strcat(filename, uri);
if (uri[strlen(uri)-1] == '/') //uri가 / 문자로 끝난다면 기본 파일 이름 추가 -> /로 안끝나는 경우가 있나?
strcat(filename, "home.html");
return 1;
}
else { //dynamic content
ptr = index(uri, '?');
if (ptr) { //cgi 인자 추출
strcpy(cgiargs, ptr+1);
*ptr = '\0';
}
else {
strcpy(cgiargs, "");
}
strcpy(filename, ".");
strcpy(filename, uri);
return 0;
}
}
서버의 디스크 파일을 정적 컨텐츠라고 하며, 디스크 파일을 가져와 클라이언트에게 전달하는 작업을 정적 컨텐츠를 처리한다고 말한다.
serve_static 함수의 실행 과정은 아래와 같다.
1. 컨텐츠를 보내기 이전에 어떤 컨텐츠를 보낼지, 어느 정도 크기의 컨텐츠를 보낼지를 포함한 response header를 보낸다.
2. 요청 한 파일을 읽기 전용으로 열고 파일의 내용을 가상메모리 영역에 저장한다.
3. 가상메모리에 저장된 내용을 클라이언트와 연결된 연결식별자에 작성해 컨텐츠를 클라이언트에게 보낸다.
void serve_static(int fd, char *filename, int filesize)
{
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
//client에게 response header 보내기
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);
//O_RDONLY -> 파일을 읽기 전용으로 열기 <-> O_WRONLY, 둘 합치면 O_RDWR
srcfd = Open(filename, O_RDONLY, 0);
//mmap는 요청한 파일을 가상메모리 영역으로 매핑함
//Mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset)
//fd로 지정된 파일에서 offset을 시작으로 length바이트 만큼 start주소로 대응시키도록 한다.
//start주소는 단지 그 주소를 사용했으면 좋겠다는 정도로 보통 0을 지정한다.
//mmap는 지정된 영역이 대응된 실제 시작위치를 반환한다.
//prot 인자는 원하는 메모리 보호모드(:12)를 설정한다
//-> PROT_EXEC - 실행가능, PROT_READ - 읽기 가능, NONE - 접근 불가, WRITE - 쓰기 가능
//flags는 대응된 객체의 타입, 대응 옵션, 대응된 페이지 복사본에 대한 수정이 그 프로세스에서만 보일 것인지, 다른 참조하는 프로세스와 공유할 것인지 설정
//MAP_FIXED - 지정한 주소만 사용, 사용 못한다면 실패 / MAP_SHARED - 대응된 객체를 다른 모든 프로세스와 공유 / MAP_PRIVATE - 다른 프로세스와 대응 영역 공유하지 않음
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); //srcfd의 첫 번째 filesize 바이트를 주소 srcp에서 시작하는 사적 읽기-허용 가상메모리 영역으로 매핑함
Close(srcfd);
Rio_writen(fd, srcp, filesize); //파일을 클라이언트에게 전송 -> 주소 srcp에서 시작하는 filesize 바이트를 클라이언트의 연결 식별자로 복사함.
Munmap(srcp, filesize);//매핑된 가상메모리 주소를 반환
}
response header에 들어갈 내용인 클라이언트가 요청한 파일의 타입을 확인한다.
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, "image/png"))
strcpy(filetype, "image/png");
else if (strstr(filename, "image/jpg"))
strcpy(filetype, "image/jpeg");
else
strcpy(filetype, "text/plain");
}
마지막으로 동적 컨텐츠를 처리하는 함수이다.
처음 보는 함수들이 많은데 아래 주석을 참고하면 이해할 수 있을 것이다.
실행 과정은 아래와 같다.
1. 부모의 메모리를 물려받는 자식 프로세스를 만들고 새로운 프로세스를 실행할 준비를 한다.
2. exec는 fork와 달리 메모리를 물려받지 않기 때문에 전달하고 싶은 변수는 환경변수로 저장 해야한다. 따라서 cgiargs를 QUERY_STRING이라는 환경변수에 저장한다.
3. 클라이언트와 연결된 connfd를 표준 출력으로 재설정한다. 이를 통해 CGI프로그램이 표준 출력에 쓰는 모든 것은 직접 클라이언트 프로세스로 전달된다.
4. 실행파일(CGI프로그램)을 새로운 프로세스로 실행한다. 이때 마지막 인자로 environ을 넣어주면 프로그램 내에서 getenv 함수를 통해 기존에 설정했던 QUERY_STRING 변수를 사용할 수 있다.
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));
// fork 함수를 호출하는 프로세스는 부모 프로세스가 되고, 새롭게 생성되는 프로세스는 자식 프로세스가 됨
// fork 함수에 의해 생성된 자식 프로세스는 부모 프로세스의 메모리를 그대로 복사하여 갖게 됨
// fork 함수 호출 이후 코드부터 각자의 메모리를 사용해 실행
// fork()의 반환 값 = 부모는 자식프로세스의 PID(프로세스 아이디)값, 자식프로세스는 0
// fork()의 값을 어디 변수에 저장해 놓는다면 조건문으로 부모와 자식 프로세스에서 원하는 것 따로 실행할 수 있음
// pid_t pid = fork() / if (pid > 0) (=부모){~~} else if (pid == 0) (=자식){~~}
if (Fork() == 0) { //자식프로세스
//setenv(const char* name, const char* value, int overwrite)
//환경변수 "name"을 현재의 환경 리스트에 삽입 또는 재설정함
//overwrite가 0이면 재설정 되지 않음, 그 외의 경우 주어진 값에 재설정됨
//exec 호출하면 명령줄 인수, 환경변수만 전달받음
//exec 호출하면 코드 영역에 있는 내용을 지우고, 새로우 코드로 바꿈. 또한 데이터 영역이 새로운 변수로 채워지고, 스택 영역이 리셋됨.
setenv("QUERY_STRING", cgiargs, 1);
//dup2(fd, fd2) = fd의 값을 fd2로 지정함 -> connfd를 STDOUT_FILENO로 바꿈 (연결을 바꿈)
Dup2(fd, STDOUT_FILENO); //redirect stdout to client -> 프로세스가 로드되기 전에 표준 출력을 클라이언트와 연관된 연결식별자로 재지정함
//경로 또는 파일 이름으로 지정한 실행 파일을 실행하여 프로세스 생성
//부모와 자식 다른 작업 하면서 둘 다 살아있게 하기 위해 사용
//execve(실행 파일 or 명령어, argv(맨 뒤를 NULL로 넣어줘야 함(argc를 전달할 수 없기 때문에)), 전달할 환경변수 - environ넣으면 기존에 설정한 환경변수 사용)
Execve(filename, emptylist, environ); // Run CGI program
}
Wait(NULL); // 자식프로세스가 종료되어 정리되는 것을 기다림
}
파고 들어가자니 끝도 없고 이정도만 알고 넘어가자니 찝찝했던 웹서버 만들기 주차가 끝났다.
그래도 소프트웨어 엔지니어가 접할 수 있는 가장 밑단계라고 하니 찝찝함을 거두고 OS프로젝트에 열심히 임해야겠다.
피드백 환영합니다.
-끝-
고마워요 표표피!