11.6절 Putting It Together: The Tiny Web Server
이 절에서는 지금까지 배운 내용을 종합하여 작동 가능한 소형 웹 서버 Tiny를 구현한다. 이 서버는 다음을 처리할 수 있다:
- 정적 콘텐츠 (HTML, 이미지 등)
- 동적 콘텐츠 (CGI 프로그램 실행 결과)
Tiny는 단 250줄 내외의 코드로 구성되어 있지만, 프로세스 제어, 소켓 인터페이스, Unix I/O, HTTP 프로토콜의 핵심 개념을 모두 포함하고 있다.
1. main() 함수 (그림 11.29)
전체 코드 확인하기
/* $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, int is_head);
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; // listenfd: 서버가 클라이언트 연결을 기다리는 리스닝 소켓
// connfd: 수락된 클라이언트와 통신하는 전용 연결 소켓
char hostname[MAXLINE], port[MAXLINE]; // hostname, port: 클라이언트 주소 정보 출력을 위한 문자열 버퍼
socklen_t clientlen; // clientlen: accept() 에서 주소 크기 전달 및 갱신용
struct sockaddr_storage clientaddr; // clientaddr: 클라이언트의 주소 정보 저장용 구조체
/* Check command line args */
if (argc != 2) // 포트 번호를 명령줄 인자로 하나 받지 않으면 메시지 출력 후 종료
{
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
/*
Open_listenfd()는 CSAPP에서 제공하는 에러 처리를 포함한 레퍼 함수
주어진 포트 번호로 소켓 생성 + 바인딩 + 리스닝을 한 번에 수행
반환된 listenfd는 클라이언트 연결 요청을 수락할 준비가 된 소켓
*/
listenfd = Open_listenfd(argv[1]);
while (1)
{
clientlen = sizeof(clientaddr); // accept()에 넘길 주소 버퍼의 크기를 설정
connfd = Accept(listenfd, (SA *)&clientaddr, // 클라이언트 연결 요청을 수락하고 전용 소켓(connfd)을 생성
&clientlen); // line:netp:tiny:accept
/*
clientaddr에 저장된 바이너리 주소를 문자열 형태로 변환
hostname: 클라이언트 IP 또는 호스트명
port: 클라이언트가 사용한 포트
로그 및 디버깅용
*/
Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port); // 클라이언트가 연결한 주소와 포트를 출력
doit(connfd); // 핵심 처리 함수 호출, 클라이언트가 보낸 HTTP 요청을 읽고 분석하여 적절한 응답을 생성
Close(connfd); // connfd는 이 클라이언트 요청 하나만을 위해 열려 있던 소켓, 처리 끝나면 반드시 닫아야 함
}
}
Tiny 웹 서버 동작 흐름 설명
먼저 선언한 변수이다.
int listenfd, connfd; // listenfd: 서버가 클라이언트 연결을 기다리는 리스닝 소켓
// connfd: 수락된 클라이언트와 통신하는 전용 연결 소켓
char hostname[MAXLINE], port[MAXLINE]; // hostname, port: 클라이언트 주소 정보 출력을 위한 문자열 버퍼
socklen_t clientlen; // clientlen: accept() 에서 주소 크기 전달 및 갱신용
struct sockaddr_storage clientaddr; // clientaddr: 클라이언트의 주소 정보 저장용 구조체
1. 명령줄 인자 확인
사용자가 포트 번호를 인자로 주지 않으면 사용법을 출력하고 종료한다.
int main(int argc, char **argv){
...
/* Check command line args */
if (argc != 2) // 포트 번호를 명령줄 인자로 하나 받지 않으면 메시지 출력 후 종료
{
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
...
}
- argc: argument count의 약자. 인자의 개수를 의미함.
- argv: argument vector. 각 인자를 문자열로 저장한 배열.
- argv[0]: 실행된 프로그램의 경로
- argv[1] ~ argv[argc-1]: 사용자가 입력한 실제 인자
./tiny 8080
항목 | 값 |
argc | 2 |
argv[0] | "./tiny" |
argv[1] | "8080" |
2. 리스닝 소켓 생성
listenfd = Open_listenfd(argv[1]);
Open_listenfd(argv[1])로 리스닝 소켓을 생성한다. 이 함수는 내부적으로 socket(), bind(), listen()을 수행한다. Open_listenfd() 와 책에 있는 open_listenfd() 와의 차이는 이미 프로젝트 상에 csapp.c 라는 파일이 들어있고 그 안에 open_listenfd의 에러 처리 기능을 추가한 매핑 함수가 Open_listenfd() 이다.
3. 무한 루프 - 클라이언트 요청 수락
while (1){
...
}
while (1) 루프를 통해 서버는 계속해서 클라이언트 연결 요청을 수락하고 처리한다.
4. Accept()로 연결 수락
clientlen = sizeof(clientaddr); // accept()에 넘길 주소 버퍼의 크기를 설정
// 클라이언트 연결 요청을 수락하고 전용 소켓(connfd)을 생성
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
클라이언트가 연결을 요청하면 connfd라는 전용 소켓이 생성된다. 이 소켓은 해당 클라이언트 요청을 처리하는 데 사용된다.
5. 클라이언트 정보 출력
Getnameinfo()를 이용해 접속한 클라이언트의 IP 주소와 포트를 문자열로 변환하고 출력한다.
/*
clientaddr에 저장된 바이너리 주소를 문자열 형태로 변환
hostname: 클라이언트 IP 또는 호스트명
port: 클라이언트가 사용한 포트
로그 및 디버깅용
*/
Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
// 클라이언트가 연결한 주소와 포트를 출력
printf("Accepted connection from (%s, %s)\n", hostname, port);
6. 핵심 함수 호출 - doit(connfd)
연결된 소켓을 통해 클라이언트가 보낸 HTTP 요청을 처리하고 응답을 보내는 주 함수이다.
doit(connfd); // 핵심 처리 함수 호출, 클라이언트가 보낸 HTTP 요청을 읽고 분석하여 적절한 응답을 생성
7. 소켓 닫기
해당 요청 처리가 끝나면 Close(connfd)로 연결을 종료한다.
Close(connfd); // connfd는 이 클라이언트 요청 하나만을 위해 열려 있던 소켓, 처리 끝나면 반드시 닫아야 함
2. doit() 함수 전체 코드 요약 및 설명
전체코드 확인하기
💡 주의 내 코드는 숙제 문제 코드도 있어 책의 코드와 다른 부분이 있을 수 있다.
void doit(int fd){
int is_static; // 요청이 정적 콘텐츠인지 동적 콘텐츠인지 구분
int is_head = 0;
struct stat sbuf; //파일의 메타데이터를 저장하기 위한 구조체 (특정 파일에 대한 다양한 정보)
/*
buf: 한 줄씩 읽는 버퍼
method, uri, version: 요청 라인의 구성 요소 ex) GET /index.html HTTP/1.1
*/
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiargs[MAXLINE]; // 실제 서버가 처리할 파일 경로와 CGI 인자
rio_t rio; //robust I/O 를 구현하기 위한 버퍼 기반의 I/O 상태를 담는 구조체
Rio_readinitb(&rio, fd); // 클라이언트 소켓 fd를 rio 구조체와 연결
Rio_readlineb(&rio, buf, MAXLINE); // 클라이언트가 보낸 첫 줄 요청 라인 (예: GET /index.html HTTP/1.1) 읽기
printf("Request headers:\n"); // 디버깅 출력용
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //요청 라인을 method, uri, version으로 분리
if (strcasecmp(method, "GET") != 0 && strcasecmp(method, "HEAD") != 0) {
// GET외 다른 HTTP 메소드는 지원하지 않는다. 대소문자 구분 없이 비교 strcasecmp
clienterror(fd, method, "501", "NOT implemented", "Tiny does not implement this method");
return;
}
read_requesthdrs(&rio); // 요청 헤더는 사용하지 않고 그냥 읽기만 한다.
if (strcasecmp(method, "HEAD") == 0) {
is_head = 1;
}
is_static = parse_uri(uri, filename, cgiargs); // URI 가 정적이면 filename만 동적이면 cgiargs까지 추출
if (stat(filename, &sbuf) < 0){ // 실제로 해당 파일이 존재하는지 확인 실패하면 404 Not Fount 반환
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");
return;
}
serve_static(fd, filename, sbuf.st_size, is_head); // 문제 없으면 serve_static() 호출
}
else{ // 동적 콘텐츠 처리
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)){ // 실행 권한이 있는 정규 파일이어야 CGI 프로그램 실행 가능
clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs); // serve_dynamic() 호출
}
}
doit(int fd)는 다음과 같은 순서로 작동한다:
- 요청 라인을 파싱 한다.
- 지원하지 않는 메서드(GET/HEAD 외)는 거부한다.
- 요청 헤더를 읽는다.
- URI를 분석해 요청이 정적인지 동적인지 판별한다.
- 파일의 존재 및 권한을 확인한다.
- 적절한 핸들러(serve_static or serve_dynamic)를 호출해 응답을 생성한다.
1. 변수 선언 및 초기화
int is_static, is_head = 0;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio;
- is_static: 정적/동적 콘텐츠 구분용 플래그
- is_head: HEAD 요청 구분 플래그
- sbuf: 파일 메타정보용 구조체 (stat() 함수 결과 저장)
- buf: 한 줄씩 읽기 위한 버퍼
- filename, cgiargs: 실제 파일 경로, CGI 인자 저장용
- rio: robust I/O 상태 저장용 구조체
💡 부록 : Robust I/O 란?
갑자기 rio_t 등과 같이 rio가 붙은 함수들이 무엇인지 궁금하다면 더 보기를 클릭하여 확인하면 좋을 것이다.
왜 Robust I/O가 필요한가?
Unix의 저수준 I/O 함수들(read, write)은 다음과 같은 문제를 가진다:
- read는 요청한 바이트 수보다 적은 수의 바이트를 반환할 수 있다 (예: 네트워크, 터미널 등).
- 반복 호출하여 전체 데이터를 받아야 하므로 번거롭고 오류 가능성이 있음.
👉 이런 문제를 피하고, 입출력 오류, 부분 전송, 버퍼 처리 등을 자동으로 처리하기 위해 csapp.c와 csapp.h에서 rio_ 접두어가 붙은 함수들을 제공한다.
Robust I/O 인터페이스 구성
CSAPP에서 제공하는 robust I/O는 두 가지 방식으로 나뉜다:
1. Unbuffered I/O (무버퍼) 함수
- 내부 버퍼를 사용하지 않으며, 정확히 지정한 바이트 수를 처리
함수 | 이름 설명 |
rio_readn | 지정한 바이트 수만큼 읽기, 모두 읽을 때까지 반복 호출 |
rio_writen | 지정한 바이트 수만큼 쓰기, 모두 쓸 때까지 반복 호출 |
2. Buffered I/O (버퍼드) 함수
- 내부 버퍼를 사용하여 효율적으로 줄 단위 또는 특정 크기만큼 읽기
필요한 구조체
typedef struct {
int rio_fd; // 읽기 대상 디스크립터
int rio_cnt; // 남은 바이트 수
char *rio_bufptr; // 버퍼 내 현재 위치
char rio_buf[RIO_BUFSIZE]; // 내부 버퍼
} rio_t;
함수 이름 | 설명 |
rio_readinitb() | rio_t 구조체를 파일 디스크립터에 연결하고 초기화 |
rio_readlineb() | 한 줄(개행 문자 기준)을 robust하게 읽기 |
rio_readnb() | 지정한 바이트 수를 robust하게 읽기 |
예시: 소켓에서 줄 단위 읽기
rio_t rio;
Rio_readinitb(&rio, connfd);
while (Rio_readlineb(&rio, buf, MAXLINE) != 0) {
printf("%s", buf);
}
- 클라이언트가 보낸 줄 단위 데이터를 중단 없이 안정적으로 읽어 들임
- read를 직접 쓰면 위 코드와 같은 안정성을 보장하기 어렵다.
웹서버와 같이 정확한 내용을 읽고 쓰기를 하기 위해 안정성 보당 도구인 Robust I/O를 사용한다.
Robust I/O(Rio 패키지)에 대한 설명은 CSAPP 10.5절 “Robust Reading and Writing with the Rio Package”에서 다뤄진다.
2. 요청 라인 읽기 및 파싱
Rio_readinitb(&rio, fd);
Rio_readlineb(&rio, buf, MAXLINE);
sscanf(buf, "%s %s %s", method, uri, version);
- HTTP 요청의 첫 줄을 읽고 "GET /index.html HTTP/1.1" 같은 형식을 파싱
- method, uri, version으로 분리
3. 메서드 유효성 검사
if (strcasecmp(method, "GET") != 0 && strcasecmp(method, "HEAD") != 0) {
// GET외 다른 HTTP 메소드는 지원하지 않는다. 대소문자 구분 없이 비교 strcasecmp
clienterror(fd, method, "501", "NOT implemented", "Tiny does not implement this method");
return;
}
- GET과 HEAD만 지원
- 다른 메서드(POST, PUT 등)는 501 오류 반환
4. 헤더 읽기
read_requesthdrs(&rio);
- 헤더 내용은 실제로 사용되진 않지만, 프로토콜 상 읽어야 함
- rio_readlineb()로 줄 단위 읽기
5. HEAD 요청 처리 구분
if (strcasecmp(method, "HEAD") == 0) {
is_head = 1;
}
- HEAD는 본문 없이 응답 헤더만 보내는 요청
- 이후 serve_static() 호출 시 이 플래그로 본문 전송 여부 제어
6. URI 분석
is_static = parse_uri(uri, filename, cgiargs);
- /cgi-bin/ 포함 여부로 정적/동적 콘텐츠 판단
- 정적이면 filename만 설정
- 동적이면 filename + cgiargs 설정
7. 파일 존재 확인
if (stat(filename, &sbuf) < 0){ // 실제로 해당 파일이 존재하는지 확인 실패하면 404 Not Fount 반환
clienterror(fd, filename, "404", "Not found", "Tiny couldn't find this file");
return;
}
- 실제 디스크에 파일이 있는지 검사
- 실패 시 404 오류 응답
8. 정적 콘텐츠 처리
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");
return;
}
serve_static(fd, filename, sbuf.st_size, is_head); // 문제 없으면 serve_static() 호출
}
- 정규 파일이고 사용자 읽기 권한이 있는 경우에만 응답 가능
- 문제가 없으면 serve_static() 호출하여 파일 내용을 응답
9. 동적 콘텐츠 처리
else{ // 동적 콘텐츠 처리
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)){ // 실행 권한이 있는 정규 파일이어야 CGI 프로그램 실행 가능
clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs); // serve_dynamic() 호출
}
- 정규 파일이고 사용자 실행 권한이 있는 경우에만 CGI 실행 가능
- serve_dynamic()을 통해 CGI 프로그램 실행 및 응답 전송
다음 포스팅에 이어서 나머지 함수들도 설명하겠다.
'크래프톤 정글 (컴퓨터 시스템: CSAPP) > 11장 네트워크 프로그래밍' 카테고리의 다른 글
컴퓨터 시스템 : CSAPP 11장 정리 - 11.6 종합설계 :소형 웹 서버 Part.2 (2) | 2025.05.03 |
---|---|
컴퓨터 시스템 : CSAPP 11장 정리 - 11.5 웹 서버 이론편 (0) | 2025.05.03 |
컴퓨터 시스템 : CSAPP 11장 정리 - 11.4 소켓 인터페이스 (0) | 2025.05.02 |
컴퓨터 시스템 : CSAPP 11장 정리 - 11.3 글로벌 IP 인터넷 (0) | 2025.05.02 |
컴퓨터 시스템 : CSAPP 11장 정리 - 11.1 ~ 11.2 (1) | 2025.05.02 |