
2025.05.07
오늘한 내용 : C - 네트워크 프로그래밍 - tiny web server 숙제문제 구현
WEEK08: BSD소켓, IP, TCP, HTTP, file descriptor, DNS
요청라인 요청 헤더 echo
void read_requesthdrs(rio_t *rp)
{
char buf[MAXLINE];
Rio_readlineb(rp, buf, MAXLINE);
while(strcmp(buf, "\r\n")){
printf("Header: %s", buf); // 버리기 전에 echo!
Rio_readlineb(rp, buf, MAXLINE);
}
return;
}
정적 컨텐츠 요청
./tiny 8080 > tiny.log // 좌측 명령어로 로그를 파일로
HTTP의 버전 결정
Request headers:
GET /godzilla.gif **HTTP/1.1**
헤더가 갖는 의미를 결정
| 헤더 이름 | 의미 |
|---|---|
| Host: localhost:8080 | 요청 대상의 호스트 이름과 포트 번호를 지정합니다. HTTP/1.1부터 필수 헤더입니다. |
| Connection: keep-alive | 커넥션을 닫지 않고 계속 유지하려는 요청입니다. 여러 요청을 한 커넥션으로 처리하기 위함입니다. |
| Cache-Control: max-age=0 | 캐시된 응답을 허용하지 않고 항상 최신 정보를 원한다는 의미입니다. |
| sec-ch-ua | 브라우저의 사용자 에이전트 브랜드 정보를 나타냅니다. 보안상의 이유로 User-Agent를 보완하려는 목적입니다. |
| sec-ch-ua-mobile: ?0 | 모바일 브라우저인지 아닌지를 나타냅니다 (?0은 데스크탑). |
| sec-ch-ua-platform: "Windows" | 브라우저가 실행 중인 플랫폼(OS)을 나타냅니다. |
| Upgrade-Insecure-Requests: 1 | 가능한 경우 HTTP → HTTPS로 업그레이드 요청을 의미합니다. |
| User-Agent | 브라우저 이름, 버전, 플랫폼 정보 등 클라이언트 식별 정보입니다. |
| Accept | 클라이언트가 받을 수 있는 MIME 타입들입니다. 서버가 응답 형식을 결정할 때 참고합니다. |
| Sec-Fetch-Site | 요청 출처가 같은 사이트인지, 다른 도메인인지 나타냅니다 (none: 직접 입력). |
| Sec-Fetch-Mode | 요청 방식: navigate, cors, no-cors 등. 보안 요청 구분용입니다. |
| Sec-Fetch-User: ?1 | 사용자 상호작용에 의해 발생한 요청인지 여부 (?1은 yes). |
| Sec-Fetch-Dest: document | 이 요청의 목적지가 어떤 리소스인지 (document: HTML 페이지). |
| Accept-Encoding | 클라이언트가 지원하는 압축 방식입니다 (gzip, br 등). |
| Accept-Language | 선호하는 언어 설정입니다 (ko-KR, en-US 등). 콘텐츠 언어 최적화용입니다. |
//serve_static 내부 get_filetpye에 추가
else if (strstr(filename, ".mpg"))
strcpy(filetype, "video/mpeg");
//main 함수 내부
// 11.8 자식 죽었을 때 핸들러 등록
// 핸들러는 항상 시그널이 발생하기 전에 미리 등록
Signal(SIGCHLD, sigchild_handler);
void sigchild_handler(int sig)
{
/*
waitpid(-1, ...): 모든 자식 프로세스를 대상으로
WNOHANG: 종료된 자식만 회수, 없으면 즉시 리턴 (비블로킹)
> 0: 성공적으로 자식 하나를 회수하면 루프 계속
*/
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
//Wait(NULL) 지워주기
main()
├── Signal(SIGCHLD, sigchild_handler); ← 자식 죽으면 알림 받도록 등록
├── Open_listenfd(...) ← 클라이언트 대기 준비
└── while(1)
└── Accept() → doit() → serve_dynamic()
├── Fork() → 자식이 execve() 후 exit
└── 부모는 대기하지 않고 다음 요청 처리
sigchild_handler()
└── waitpid(..., WNOHANG) → 종료된 자식들을 비동기로 회수
srcfd = Open(filename, O_RDONLY, 0);
// 11.9
// srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
srcp = (char *)malloc(sizeof(filesize));
Rio_readn(srcfd,srcp,filesize);
Close(srcfd);
Rio_writen(fd, srcp, filesize);
// Munmap(srcp, filesize);
free(srcp);
}
| 방식 | 원리 | 메모리 | 복사 여부 |
|---|---|---|---|
mmap | 파일을 가상 메모리에 직접 매핑 | OS가 페이지 단위로 관리 | 복사 없이 바로 사용 (제로카피처럼) |
malloc + rio_readn | 사용자 메모리에 직접 복사 | 힙에 메모리 할당 | 파일 내용을 명시적으로 복사 |
<html>
<head><title>Adder Form</title></head>
<body>
<h2>CGI Adder Test</h2>
<form action="/cgi-bin/adder" method="GET">
<p>x = <input type="text" name="x"></p>
<p>y = <input type="text" name="y"></p>
<input type="submit" value="Add">
</form>
</body>
</html>
| 항목 | 설명 |
|---|---|
| 헤더 | HTTP 응답의 메타데이터 (타입, 길이 등) |
| 본문 | 실제 리소스 내용 (HTML, 이미지, 동영상 등) |
| HEAD 요청 시 | 서버가 헤더는 보내고, 본문은 생략 |
| GET 요청 시 | 서버가 헤더 + 본문 모두 전송 |
doit()에서 method 검사 확장
int is_head = 0;
if (strcasecmp(method, "GET") && strcasecmp(method, "HEAD")) {
clienterror(fd, method, "501", "Not implemented",
"Tiny does not implement this method");
return;
}
if (!strcasecmp(method, "HEAD"))
is_head = 1;
serve_static()에서 본문 전송 조건 분기 설정
void serve_static(int fd, char *filename, int filesize, int is_head)
{
int srcfd;
char *srcp;
char filetype[MAXLINE];
char buf[MAXBUF];
get_filetype(filename, filetype);
sprintf(buf, "HTTP/1.0 200 OK\r\n");
sprintf(buf, "%sServer: Tiny Web Server\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));
if (is_head) return; // 본문 생략!
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);
}
serve_static()에 int is_head 인자 추가
serve_static(fd, filename, sbuf.st_size, is_head);
doit()에서 POST method 인식is_post = !strcasecmp(method, "POST");
read_requesthdrs()에서 Content-Length 추출void read_requesthdrs(rio_t *rp, int *content_length)
{
char buf[MAXLINE];
Rio_readlineb(rp, buf, MAXLINE);
while (strcmp(buf, "\r\n")) {
if (strncasecmp(buf, "Content-Length:", 15) == 0)
*content_length = atoi(buf + 15);
printf("Header: %s", buf);
Rio_readlineb(rp, buf, MAXLINE);
}
}
Content-Length: 헤더에서 본문 길이를 읽는다.cgiargs에 저장//doit() 에 추가
if (is_post) {
Rio_readnb(&rio, cgiargs, content_length);
cgiargs[content_length] = '\0'; // null-terminate
}
클라이언트가 연결을 끊었는데,
Tiny 서버가 그걸 모르고 계속
write()를 시도하면 생기는 오류
이때 발생하는 두 가지 현상:
| 현상 | 설명 |
|---|---|
write() 호출 → -1 리턴, errno = EPIPE | 프로세스는 살아있지만 에러 상태 |
| SIGPIPE 시그널 발생 | 기본 동작은 프로세스 비정상 종료 |
//main()에 추가
Signal(SIGPIPE, SIG_IGN); // 11.13: Broken pipe 무시