
//터미널에 ./tiny 4000 을 입력했을 때, argc = 2, argv[0] = tiny, argv[1] = 4000
int main(int argc, char **argv) {
//listen socket 과 connection socket 선언
int listenfd, connfd;
//connection을 위한 hostname 과 port 선언
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);
}
//listen socket open, 인자로 포트 번호를 넘겨줌
//Open_listenfd는 요청받은 준비가 된 듣기 식별자를 리턴(listenfd)
listenfd = Open_listenfd(argv[1]);
//무한루프 서버
while (1) {
//accept 함수 인자에 넣기 위한 주소 길이 계산
clientlen = sizeof(clientaddr);
//반복적으로 연결 요청을 접수
//accept함수 (듣기 식별자, 소켓주소구조체의 주소, 주소의 길이)
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
//getnameinfo함수: 소켓주소구조체를 호스트주소, 포트 번호로 변환
Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
doit(connfd); // transaction 실행
Close(connfd); // 연결 닫기
}
}
getaddrinfo 함수는 호스트 이름: 호스트 주소, 서비스 이름: 포트 번호의 스트링 표시를 소켓 주소 구조체로 변환하는 함수.
getnameinfo 함수는 반대로 소켓 주소 구조체를 호스트의 주소와 포트 번호의 스트링으로 변환하는 함수
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_readlineb를 위해 rio_t 타입 구조체의 읽기 버퍼를 선언
rio_t rio;
//req line, headers 읽기
//&rio 주소를 가지는 읽기 버퍼와 식별자 connfd 연결
Rio_readinitb(&rio, fd);
//버퍼에서 읽은 것이 담겨있음
Rio_readlineb(&rio, buf, MAXLINE);
printf("Request headers:\n");
//"GET / HTTP/1.1"
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //버퍼에서 자료형을 읽음
//GET이 아닌 메소드를 req했다면 error를 return
if ((strcasecmp(method, "GET")) && (strcasecmp(method, "HEAD"))) { //숙제
clienterror(fd, method, "501", "Not implementd", "Tiny does not implement this method.");
return;
}
read_requesthdrs(&rio); //req headers 읽기
//GET req로부터 URI parse
//URI를 parse해서 파일 이름, 비어있을수 있는 CGI인자 스트링으로 분석
//정적/동적 컨텐츠인지 판단
is_static = parse_uri(uri, filename, cgiargs);
printf("uri: %s, filename: %s, cgiargs: %s \n", uri, filename, cgiargs);
//file이 disk에 없다면 error를 return
//stat은 파일 정보를 불러오고 sbuf에 내용을 적어줌. ok -> 0, error -> -1
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.");
//못 읽으면 error return
return;
}
//static content 제공
serve_static(fd, filename, sbuf.st_size, method);
} else { //동적 콘텐츠 제공
//(읽을 수 있는) 일반 파일인지, CGI 프로그램을 실행할 수 없으면 error return
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program.");
return;
}
//dynamic content 제공
serve_dynamic(fd, filename, cgiargs, method);
}
}
참고한 글들
https://it-serial.tistory.com/entry/Linux-%EB%A6%AC%EB%88%85%EC%8A%A4-%ED%8C%8C%EC%9D%BC-%EC%A2%85%EB%A5%98%EC%99%80-%EB%94%94%EB%A0%89%ED%86%A0%EB%A6%AC-%EA%B5%AC%EC%A1%B0-%EB%A6%AC%EB%88%85%EC%8A%A4-%EC%82%AC%EC%9A%A9-%EA%B5%AC%EC%A1%B0
https://coding-factory.tistory.com/499
https://suhwanc.tistory.com/131
리눅스에서 파일은 연속된 m개의 바이트이다.
네트워크, 디스크, 터미널 같은 모든 I/O 디바이스들은 파일로 모델링 된다. 모든 입력과 출력은 해당 파일을 읽거나 쓰는 형식으로 수행된다.
읽기 연산은 현재 파일 위치 k에서 시작해서 n(n > 0)바이트를 파일에서 메모리로 복사하고 k를 n만큼 증가시킨다.
k > m(파일의 최대 길이) 의 읽기 연산을 수행하면 End-Of-File(EOF)라고 알려진 조건이 발생하며, 이는 응용 프로그램에서 감지할 수 있다. 하지만 실제 파일 끝에서 "EOF문자"가 명시적으로 존재하는 것은 아니다.
쓰기 연산은 현재 파일 k에서 시작해서 n(n > 0)바이트를 메모리에서 파일로 복사하고 k를 갱신시킨다.
우선 robust라는 단어는 건장한, 견고한 정도의 뜻을 가진 단어고 비단 Computer Science분야에서만 쓰이는 단어는 아니다.
Rio 패키지는 short count를 자동으로 처리한다. (Unix I/O 와 차이).
short count 가 발생할 수 있는 네트워크 프로그램 같은 어플리케이션에서 편리하고 안정적이고 효율적인 I/O 패키지이다.
일부의 경우에 Unix I/O의 Read, Write 함수는 어플리케이션이 요청하는 것보다 더 적은 바이트를 전송한다. 이를 short count라고 명시한다. (Short Count는 에러를 나타내는 것은 아니다.)
보통 short counts는 다음과 같은 상황일 때 발생한다.
반면 절대 발생하지 않는 경우도 있는데 아래와 같은 상황일 때이다.
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) {
char buf[MAXLINE], body[MAXBUF];
//브라우저 사용자에게 에러를 설명하는 res 본체에 HTML도 함께 보낸다.
//HTML res는 본체에서 컨텐츠의 크기와 타입을 나타내야 하기 때문에,
//HTML 컨텐츠를 한 개의 스트링으로 만들고 그 길이도 잼.
//이는 sprintf를 통해 body는 인자에 스택되어 하나의 긴 스트링으로 저장된다.
//HTTP res 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);
//HTTP res 출력하기
sprintf(buf, "HTTP/1.1 %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));
}
#include <stdio.h>
int sprintf(char* str, const char* format, ...);

int fprintf(FILE* stream, const char* format, ...)

//tiny 웹서버는 req header를 읽고 무시함.
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;
}
int parse_uri(char *uri, char *filename, char *cgiargs) {
char *ptr;
//정적 콘텐츠: URI에 /cgi-bin을 포함하는 경로가 없다.
if (!strstr(uri, "cgi-bin")) {
strcpy(cgiargs, "");
strcpy(filename, ".");
strcat(filename, uri);
//uri 문자열 끝이 / 인 경우 home.html을 filename에 붙여줌
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;
}
}
char* strstr(char* str1, const char* str2);
str1에서 str2와 일치하는 문자열이 있는지 확인하는 함수.
일치하는 문자열이 있다면 해당 위치의 포인터(char*)를 return한다.
일치하는 문자열을 찾지 못했다면 NULL 포인터를 return 하기 때문에 NULL check를 해야한다.
char* strcpy(char* dest, const char* origin);
origin의 문자열 전체를 dest로 복사하는 함수이다.
char* strncpy(char* dest, const char* origin, size_t n);
n은 number를 의미한다.
origin의 문자열을 dest로 n번만큼 복사하는 함수이다.
주의할 점으로는 strcpy는 문자열 끝(다시말해 '\0'까지)까지 복사를 하기 때문에 문자열의 길이를 계산할때도, 문자열을 다른 곳으로 복사할 때에도 이를 기억해야 한다.
char* strcat(char* dest, const char* origin);
origin에 있는 문자열을 dest 뒤에 이어붙이는 함수이다.
dest 뒤에 있던 \0은 사라지고 그 위치에 origin의 문자열이 붙는다.
char* strncat(char*dest, const char* origin, size_t n);
origin에 있는 문자열 n개를 dest뒤에 이어붙이는 함수이다.
dest 뒤에 있던 \0은 사라지고 그 위치에 origin의 문자열 중 앞에서부터 n개까지가 붙는다.
char* index(const char *s, int c);
문자열 s에서 검색할 문자 c를 찾아 문자가 있는 위치를 return 하는 함수이다.
찾는 문자가 없다면 NULL을 return 한다.
void serve_static(int fd, char *filename, int filesize, char* method) {
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
//client에게 res headers 보내기
//접미어를 통해 filetype 결정
get_filetype(filename, filetype);
//client 에게 req line, req headers 보내기
//데이터를 보내기 전에 버퍼로 임시로 가지고 있다.
sprintf(buf, "HTTP/1.1 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);
if (strcasecmp(method, "HEAD") != 0) { //숙제: method 가 HEAD일 때 body를 보내지 않기 위함
//client에게 res body 보내기
//읽을 수 있는 파일로 열기(open read only)
//숙제: malloc으로 바꾸기
srcfd = Open(filename, O_RDONLY, 0);
//malloc으로 가상 메모리 할당
srcp = (char *)malloc(filesize);
Rio_readn(srcfd, srcp, filesize);
Close(srcfd);
Rio_writen(fd, srcp, filesize);
free(srcp);
// srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
// Close(srcfd);
// Rio_writen(fd, srcp, filesize);
// Munmap(srcp, filesize);
}
}
HTTP method HEAD는 GET과 기본적으로 유사한 res를 요청한다.
하지만 GET과는 달리 HTTP res의 header만 요청하고 body는 요청하지 않는다.
요청하는 데이터 양이 줄어들기 때문에 빠르게 서버의 상태를 조회할 수 있다.
res header의 content-length는 GET과 똑같기 때문에 resource 양에 대한 조회만 할때는 유용하게 사용할 수 있다.
#include <strings.h>
int strcasecmp(const char* string1, const char* string2);
strcasecmp 함수는 string1과 string2를 대소문자를 구분하지 않고 비교한다. 문자열 내부의 모든 영문자는 비교전에 소문자로 변환된다.
두 문자열이 같다면 0을 return, string1이 더 작다면 음수, 더 크다면 양수를 return 한다.
//filename 으로부터 file 의 형식을 알아내는 함수
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 if (strstr(filename, ".mp4")) { //숙제
strcpy(filetype, "video/mp4");
} else {
strcpy(filetype, "text/plain");
}
}
void serve_dynamic(int fd, char *filename, char *cgiargs, char *method) {
char buf[MAXLINE], *emptylist[] = {NULL};
//HTTP res의 first part return
sprintf(buf, "HTTP/1.1 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 (strcasecmp(method, "HEAD") != 0) { //숙제: method 가 HEAD일 때 body를 보내지 않기 위함
if (Fork() == 0) { //동적 콘텐츠를 실행하고 결과를 return할 자식 프로세스를 포크
setenv("QUERY_STRING", cgiargs, 1);
Dup2(fd, STDOUT_FILENO); //redirect stdout to client
Execve(filename, emptylist, environ); //CGI program 실행
}
Wait(NULL); //자식 프로세스가 실행되고 결과를 출력하고 종료될 때까지 기다림
}
/* $begin tinymain */
/*
* tiny.c - A simple, iterative HTTP/1.0 Web server that uses the
* GET method to serve static and dynamic content.
*
* Updated 11/2019 droh
* - Fixed sprintf() aliasing issue in serve_static(), and clienterror().
*/
#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, char* method);
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs, char* method);
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg);
//터미널에 ./tiny 4000 을 입력했을 때, argc = 2, argv[0] = tiny, argv[1] = 4000
int main(int argc, char **argv) {
//listen socket 과 connection socket 선언
int listenfd, connfd;
//connection을 위한 hostname 과 port 선언
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);
}
//listen socket open, 인자로 포트 번호를 넘겨줌
//Open_listenfd는 요청받은 준비가 된 듣기 식별자를 리턴(listenfd)
listenfd = Open_listenfd(argv[1]);
//무한루프 서버
while (1) {
//accept 함수 인자에 넣기 위한 주소 길이 계산
clientlen = sizeof(clientaddr);
//반복적으로 연결 요청을 접수
//accept함수 (듣기 식별자, 소켓주소구조체의 주소, 주소의 길이)
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
//getnameinfo함수: 소켓주소구조체를 호스트주소, 포트 번호로 변환
Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
doit(connfd); // transaction 실행
Close(connfd); // 연결 닫기
}
}
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_readlineb를 위해 rio_t 타입 구조체의 읽기 버퍼를 선언
rio_t rio;
//req line, headers 읽기
//&rio 주소를 가지는 읽기 버퍼와 식별자 connfd 연결
Rio_readinitb(&rio, fd);
//버퍼에서 읽은 것이 담겨있음
Rio_readlineb(&rio, buf, MAXLINE);
printf("Request headers:\n");
//"GET / HTTP/1.1"
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //버퍼에서 자료형을 읽음
//GET이 아닌 메소드를 req했다면 error를 return
if ((strcasecmp(method, "GET")) && (strcasecmp(method, "HEAD"))) { //숙제
clienterror(fd, method, "501", "Not implementd", "Tiny does not implement this method.");
return;
}
read_requesthdrs(&rio); //req headers 읽기
//GET req로부터 URI parse
//URI를 parse해서 파일 이름, 비어있을수 있는 CGI인자 스트링으로 분석
//정적/동적 컨텐츠인지 판단
is_static = parse_uri(uri, filename, cgiargs);
printf("uri: %s, filename: %s, cgiargs: %s \n", uri, filename, cgiargs);
//file이 disk에 없다면 error를 return
//stat은 파일 정보를 불러오고 sbuf에 내용을 적어줌. ok -> 0, error -> -1
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.");
//못 읽으면 error return
return;
}
//static content 제공
serve_static(fd, filename, sbuf.st_size, method);
} else { //동적 콘텐츠 제공
//(읽을 수 있는) 일반 파일인지, CGI 프로그램을 실행할 수 없으면 error return
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program.");
return;
}
//dynamic content 제공
serve_dynamic(fd, filename, cgiargs, method);
}
}
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) {
char buf[MAXLINE], body[MAXBUF];
//브라우저 사용자에게 에러를 설명하는 res 본체에 HTML도 함께 보낸다.
//HTML res는 본체에서 컨텐츠의 크기와 타입을 나타내야 하기 때문에,
//HTML 컨텐츠를 한 개의 스트링으로 만들고 그 길이도 잼.
//이는 sprintf를 통해 body는 인자에 스택되어 하나의 긴 스트링으로 저장된다.
//HTTP res 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);
//HTTP res 출력하기
sprintf(buf, "HTTP/1.1 %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 웹서버는 req header를 읽고 무시함.
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;
}
int parse_uri(char *uri, char *filename, char *cgiargs) {
char *ptr;
//정적 콘텐츠: URI에 /cgi-bin을 포함하는 경로가 없다.
if (!strstr(uri, "cgi-bin")) {
strcpy(cgiargs, "");
strcpy(filename, ".");
strcat(filename, uri);
//uri 문자열 끝이 / 인 경우 home.html을 filename에 붙여줌
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;
}
}
void serve_static(int fd, char *filename, int filesize, char* method) {
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
//client에게 res headers 보내기
//접미어를 통해 filetype 결정
get_filetype(filename, filetype);
//client 에게 req line, req headers 보내기
//데이터를 보내기 전에 버퍼로 임시로 가지고 있다.
sprintf(buf, "HTTP/1.1 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);
if (strcasecmp(method, "HEAD") != 0) { //숙제: method 가 HEAD일 때 body를 보내지 않기 위함
//client에게 res body 보내기
//읽을 수 있는 파일로 열기(open read only)
//숙제: malloc으로 바꾸기
srcfd = Open(filename, O_RDONLY, 0);
//malloc으로 가상 메모리 할당
srcp = (char *)malloc(filesize);
Rio_readn(srcfd, srcp, filesize);
Close(srcfd);
Rio_writen(fd, srcp, filesize);
free(srcp);
// srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
// Close(srcfd);
// Rio_writen(fd, srcp, filesize);
// Munmap(srcp, filesize);
}
}
//filename 으로부터 file 의 형식을 알아내는 함수
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 if (strstr(filename, ".mp4")) { //숙제
strcpy(filetype, "video/mp4");
} else {
strcpy(filetype, "text/plain");
}
}
void serve_dynamic(int fd, char *filename, char *cgiargs, char *method) {
char buf[MAXLINE], *emptylist[] = {NULL};
//HTTP res의 first part return
sprintf(buf, "HTTP/1.1 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 (strcasecmp(method, "HEAD") != 0) { //숙제: method 가 HEAD일 때 body를 보내지 않기 위함
if (Fork() == 0) { //동적 콘텐츠를 실행하고 결과를 return할 자식 프로세스를 포크
setenv("QUERY_STRING", cgiargs, 1);
Dup2(fd, STDOUT_FILENO); //redirect stdout to client
Execve(filename, emptylist, environ); //CGI program 실행
}
Wait(NULL); //자식 프로세스가 실행되고 결과를 출력하고 종료될 때까지 기다림
}
}
다음 시리즈 언제나와요 ?