Tiny 서버 구현 코드 해석 및 응답 확인하기
echo 서버 구현이 끝났다면, 이제 실제 클라이언트가 요청한 콘텐츠를 처리할 수 있는 서버를 만들어 볼 차례인데요. 바로 Tiny 웹 서버입니다. 이제 우리가 요청한 웹 페이지나 이미지를 요청하면 이를 서버가 제공해줄 수 있고, 심지어 두 개의 숫자를 인자로 넘겨주면 그 합을 받아볼 수도 있습니다.
기존의 echo 서버가 단순히 클라이언트의 메시지를 다시 반환하는 수준에 머물렀다면, 이제는 Tiny 웹 서버를 통해 클라이언트가 HTTP 프로토콜을 이용해 요청한 정적 콘텐츠와 동적 콘텐츠를 실제로 처리하여 응답해주는 과정을 경험하게 됩니다.
이 글에서는 Tiny 서버의 핵심 함수들을 라인별로 하나씩 뜯어보며, 어떤 방식으로 요청을 처리하고 응답을 생성하는지 살펴보고자 합니다. 마지막에는 우리가 만든 서버가 웹 브라우저에서 콘텐츠를 잘 처리하는지 실제로 테스트해보는 방법까지 소개합니다.
구현 코드 해석하기
main()
클라이언트의 요청을 반복적으로 수락하고 처리하는 웹 서버의 메인 루프 함수입니다.
int main(int argc, char **argv)
{
int listenfd, connfd; // listenfd: 클라이언트의 연결 요청을 기다리는 소켓
// connfd: 실제 통신에 사용되는 소켓 (accept 후 사용됨)
char hostname[MAXLINE], port[MAXLINE]; // 클라이언트의 주소 정보(IP, 포트)를 저장할 버퍼
socklen_t clientlen; // 클라이언트 주소 구조체의 크기를 저장할 변수
struct sockaddr_storage clientaddr; // 클라이언트 주소 정보를 담을 구조체
// IPv4, IPv6 모두 호환 가능한 형태
// 입력 인자 개수가 2개가 아닌 경우 (즉, 포트 번호가 제공되지 않으면) 에러 출력
if (argc != 2)
{
fprintf(stderr, "usage: %s <port>\n", argv[0]); // 사용법 안내
exit(1); // 에러로 인한 강제 종료
}
// 지정된 포트 번호로 듣기 소켓 생성 및 바인딩
listenfd = Open_listenfd(argv[1]);
// 서버는 무한 루프를 돌며 클라이언트 요청을 기다림
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()
클라이언트 요청을 분석해 정적 또는 동적 콘텐츠를 제공하는 Tiny 웹 서버의 핵심 처리 함수입니다.
void doit(int fd) {
int is_static; // 요청한 URI가 정적 콘텐츠인지 동적 콘텐츠인지 구분
struct stat sbuf; // 요청한 파일의 상태(존재 여부, 권한 등)를 저장하는 구조체
// 클라이언트 요청 줄과 메서드, URI, HTTP 버전 등을 저장할 버퍼
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
// 정적 콘텐츠일 경우 파일 이름, 동적 콘텐츠일 경우 CGI 인자들이 저장될 버퍼
char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio; // robust I/O를 위한 구조체
// 요청 줄 읽기
Rio_readinitb(&rio, fd); // rio 구조체를 fd로 초기화
Rio_readlineb(&rio, buf, MAXLINE); // 요청 줄 한 줄 읽음
printf("Request headers:\n");
printf("%s", buf); // 요청 줄 출력 (예: GET /index.html HTTP/1.0)
// 요청 줄을 파싱하여 메서드, URI, 버전 추출
sscanf(buf, "%s %s %s", method, uri, version);
// Tiny는 GET 메서드만 지원하므로, 다른 메서드는 에러 응답
if (strcasecmp(method, "GET")) {
clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method");
return;
}
// 요청 헤더는 무시하고 읽기 처리만 진행 (다음 요청 처리를 위한 준비)
read_requesthdrs(&rio);
/* URI를 분석해 파일 이름과 CGI 인자 추출 */
is_static = parse_uri(uri, filename, cgiargs); // 정적이면 1, 동적이면 0 반환
// 요청한 파일이 존재하는지 확인. 실패하면 404 응답
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");
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);
}
}
clienterror()
클라이언트 요청 처리 중 오류가 발생했을 때, HTML 형식의 에러 메시지를 클라이언트에게 전송합니다.
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) {
char buf[MAXLINE], body[MAXBUF]; // 응답 헤더와 바디를 저장할 버퍼
/* HTML 형식의 에러 메시지 바디 생성 */
sprintf(body, "<html><title>Tiny Error</title>"); // HTML 타이틀
sprintf(body, "%s<body bgcolor=\"ffffff\">\r\n", body); // 배경색 흰색으로 설정
sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg); // ex)"404: Not found"
sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause); // ex)"Tiny couldn't find this file: /bad/path"
sprintf(body, "%s<hr><em>The Tiny Web Server</em>\r\n", body); // 서버 서명
/* HTTP 응답 헤더 생성 및 전송 */
// 상태 줄 전송: 예) HTTP/1.0 404 Not found
sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf));
// 콘텐츠 타입: HTML
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));
/* HTML 본문 전송 */
Rio_writen(fd, body, strlen(body)); // 위에서 만든 에러 HTML 바디 전송
}
read_requesthdrs()
Tiny 서버는 요청 헤더를 별도로 처리하지 않기 때문에, 단순히 모든 헤더 줄을 읽고 서버 터미널에 출력한 뒤 무시합니다. 이는 주로 디버깅을 위한 용도로 쓰이지요. (doit 하위 함수)
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; // 헤더는 따로 처리하지 않고 무시
}
parse_uri()
URI를 분석해 요청이 정적 콘텐츠인지 동적 콘텐츠인지 구분하고, 파일 경로와 CGI 인자들을 추출합니다. (doit 하위 함수)
int parse_uri(char *uri, char *filename, char *cgiargs) {
char *ptr; // 동적 콘텐츠 인자 분리에 쓰이는 포인터
/* 정적 콘텐츠: URI에 "cgi-bin"이 포함되지 않은 경우 */
if (!strstr(uri, "cgi-bin")) {
strcpy(cgiargs, ""); // CGI 인자는 없음 (빈 문자열로 설정)
strcpy(filename, "."); // 상대 경로 시작 (서버 루트 기준)
strcat(filename, uri); // URI를 이어 붙여 실제 파일 경로 생성
// URI가 '/'로 끝나면 기본 파일인 "home.html"로 설정
if (uri[strlen(uri) - 1] == '/') {
strcat(filename, "home.html");
}
return 1; // 정적 콘텐츠임을 반환
}
/* 동적 콘텐츠: URI에 "cgi-bin"이 포함된 경우 */
else {
ptr = index(uri, '?'); // '?' 문자를 기준으로 CGI 인자 분리 시도
if (ptr) {
// '?' 뒤의 문자열을 cgiargs에 저장
strcpy(cgiargs, ptr + 1);
*ptr = '\0'; // '?' 자리에 널 문자 삽입 → URI 문자열 자르기
} else {
strcpy(cgiargs, ""); // 인자가 없는 경우
}
strcpy(filename, "."); // 상대 경로 시작
strcat(filename, uri); // 나머지 URI로 파일 이름 구성 (스크립트 경로)
return 0; // 동적 콘텐츠임을 반환
}
}
예시로 이해하기
- 요청: GET /index.html HTTP/1.0
- filename = ./index.html, cgiargs = "", 정적 콘텐츠 → return 1
- 요청: GET /cgi-bin/add?1&2 HTTP/1.0
- filename = ./cgi-bin/add, cgiargs = 1&2, 동적 콘텐츠 → return 0
serve_static()
정적 콘텐츠(예: HTML, 이미지 등)를 클라이언트에게 전송하는 함수입니다.
void serve_static(int fd, char *filename, int filesize) {
int srcfd; // 디스크에서 파일을 열 때 사용할 파일 디스크립터
char *srcp; // 파일을 메모리로 매핑할 포인터
char filetype[MAXLINE], buf[MAXBUF]; // MIME 타입, 응답 헤더를 저장할 버퍼
/* 응답 헤더 생성 및 전송 */
get_filetype(filename, filetype); // 파일 확장자 기반으로 MIME 타입 결정
// 상태 줄, 헤더 정보 구성
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); // 파일 열기 (읽기 전용)
// 파일을 가상 메모리 주소에 매핑 (읽기 전용, private)
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
Close(srcfd); // 매핑 완료 후 파일 디스크립터는 닫음
Rio_writen(fd, srcp, filesize); // 매핑된 파일 내용을 클라이언트로 전송
Munmap(srcp, filesize); // 매핑된 메모리 해제
}
get_filetype()
요청된 파일의 확장자를 보고 MIME 타입을 결정하여 filetype에 저장합니다. (serve_static 하위 함수)
void get_filetype(char *filename, char *filetype) {
// 확장자에 따라 MIME 타입을 결정하여 filetype에 저장
if (strstr(filename, ".html")) {
strcpy(filetype, "text/html"); // HTML 문서
} else if (strstr(filename, ".gif")) {
strcpy(filetype, "image/gif"); // GIF 이미지
} else if (strstr(filename, ".png")) {
strcpy(filetype, "image/png"); // PNG 이미지
} else if (strstr(filename, ".jpg")) {
strcpy(filetype, "image/jpeg"); // JPG 이미지
} else {
strcpy(filetype, "text/plain"); // 기본값: 일반 텍스트
}
}
serve_dynamic()
CGI 프로그램을 실행해 동적 콘텐츠를 생성하고, 결과를 클라이언트에게 전송합니다.
void serve_dynamic(int fd, char *filename, char *cgiargs) {
char buf[MAXLINE]; // 응답 헤더를 담을 버퍼
char *emptylist[] = {NULL}; // CGI 프로그램에 넘길 인자 리스트 (빈 리스트)
/* 클라이언트에게 기본적인 HTTP 응답 헤더 전송 */
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) { // 자식 프로세스
// CGI 환경 변수 설정 (쿼리 문자열 전달)
setenv("QUERY_STRING", cgiargs, 1); // 예: QUERY_STRING="1&2"
// 표준 출력을 클라이언트 소켓으로 변경 (CGI 출력이 클라이언트로 직접 전송됨)
Dup2(fd, STDOUT_FILENO); // STDOUT_FILENO == 1
// CGI 프로그램 실행 (인자 없음, 환경 변수는 부모와 동일)
Execve(filename, emptylist, environ); // 실패 시 return하지 않고 종료됨
}
/* 부모 프로세스는 자식이 끝날 때까지 기다림 */
Wait(NULL); // 좀비 프로세스 방지
}
CGI: adder.c
웹 브라우저에서 전달받은 두 수를 더해 결과를 HTML 형식으로 출력하는 CGI 프로그램입니다. (serve_dynamic에서 실행)
#include "csapp.h"
int main(void)
{
char *buf, *p; // buf: 전체 쿼리 문자열, p: '&'를 찾기 위한 포인터
char arg1[MAXLINE], arg2[MAXLINE]; // 첫 번째/두 번째 인자를 담을 문자열 버퍼
char content[MAXLINE]; // 최종 출력할 HTML 콘텐츠
int n1 = 0, n2 = 0; // 파싱한 두 정수
/* 쿼리 문자열에서 두 인자를 추출 */
if ((buf = getenv("QUERY_STRING")) != NULL)
{
// QUERY_STRING 예: "num1=3&num2=5"
p = strchr(buf, '&'); // '&' 위치 찾기 → "num2=5" 시작 위치
*p = '\0'; // '&'를 널 문자로 바꿔 "num1=3"과 "num2=5"를 분리
strcpy(arg1, buf); // arg1 = "num1=3"
strcpy(arg2, p + 1); // arg2 = "num2=5"
// '=' 다음 문자열을 정수로 변환
n1 = atoi(strchr(arg1, '=') + 1); // n1 = 3
n2 = atoi(strchr(arg2, '=') + 1); // n2 = 5
}
/* 결과 HTML 콘텐츠 구성 */
sprintf(content, "QUERY_STRING=%s\r\n<p>", buf); // 쿼리 문자열 출력
sprintf(content + strlen(content), "Welcome to add.com: ");
sprintf(content + strlen(content), "THE Internet addition portal.\r\n<p>");
sprintf(content + strlen(content), "The answer is: %d + %d = %d\r\n<p>",
n1, n2, n1 + n2); // 덧셈 결과 출력
sprintf(content + strlen(content), "Thanks for visiting!\r\n");
/* HTTP 응답 헤더 및 바디 출력 (stdout → 클라이언트로 전달됨) */
printf("Content-type: text/html\r\n"); // MIME 타입
printf("Content-length: %d\r\n", (int)strlen(content)); // 길이 헤더
printf("\r\n"); // 헤더와 바디 구분
printf("%s", content); // HTML 본문 출력
fflush(stdout); // 출력 즉시 전송
exit(0); // 정상 종료
}
브라우저에서 Tiny 서버의 응답 확인하기
Tiny 서버의 구현이 끝났다면, 이제 잘 동작하는지 확인을 해봐야겠죠. 브라우저에서 URL을 입력해 직접 정적 콘텐츠와 동적 콘텐츠를 받아보면서, 하나의 서버를 완성해 냈다는 보람을 느껴 보셨으면 좋겠습니다.
1. make로 tiny 및 adder 컴파일
각각 tiny 디렉터리와 cgi-bin 디렉터리에서 make
명령어를 통해 Tiny 서버 프로그램과 CGI 프로그램(adder)을 만들어 줍니다.
2. Tiny 서버 실행
터미널에서 아래의 명령어를 실행해 tiny 서버를 실행합니다.
./tiny 8000
3. 정적 콘텐츠 응답 확인하기
브라우저에서 아래의 URL을 입력한 뒤, 서버의 home.html가 실제로 전송되어 브라우저에 표시되는지 확인해 봅니다.
http://localhost:8000/home.html
4. 동적 콘텐츠 응답 확인하기
브라우저에서 아래 URL을 입력 후, 서버가 adder를 통해 계산된 두 수의 합이 전송되어 브라우저에 잘 표시되는지 확인해 봅니다.
http://localhost:8000/cgi-bin/adder?num1=3&num2=4
'크래프톤 정글 > Code 정글(C언어)' 카테고리의 다른 글
[WebProxy] 프록시 서버 제대로 알고 시작하기 (0) | 2025.05.05 |
---|---|
[WebProxy] Tiny 서버 숙제 문제 분석 및 해결하기 (0) | 2025.05.03 |
[WebProxy] echo 서버 구현 실습 및 코드 해석하기 (0) | 2025.05.02 |
[WebProxy] VSCode에서 빨간 줄이 뜨는 현상 해결하기 (0) | 2025.05.02 |
[Malloc] (참고용) Segregated Free List 기반 동적 할당기 구현하기 (0) | 2025.04.28 |