Tiny 서버 C언어 구현
tiny 서버
tiny서버는 소형 웹서버입니다. HTTP 요청을 처리하고 정적 또는 동적 콘텐츠를 클라이언트(주로 웹 브라우저)에 전달하는 기능을 수행합니다.
역할(기능)
주로 다음과 같은 역할을 합니다.
- 클라이언트(브라우저)로부터 HTTP 요청 수신(예: GET 요청)
- 요청 URL에 따라 정적 파일(HTML, 이미지 등) 또는 동적 프로그램(CGI 등)을 처리
- HTTP 응답 생성 및 전송
기본 구조 및 구현 이론
전에 배웠던 socket 연결 과정과 비슷합니다.
- 서버 소켓 생성 및 바인딩
socket()
함수로 소켓 생성bind()
함수로 IP와 포트 생성listen()
함수로 연결 대기
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
bind(listenfd, (SA *)&serveraddr, sizeof(serveraddr));
listen(listenfd, SOMAXCONN);
- 클라이언트 연결 수락
accept()
함수로 클라이언트 요청 수락
int connfd = accept(listenfd, (SA *)&clientaddr, &clientlen);
- HTTP 요청 읽기 및 파싱(분석)
read()
를 통해 요청 줄(GET, URI, HTTP 버전)을 읽음parse_uri()
함수로 요청 경로 분석- 정적/동적 콘텐츠 여부 결정
→ 여기서 URI는 인터넷에서 리소스를 식별하는 고유 문자열입니다. 모든 자원을 식별합니다. 웹 페이지, 파일, DB 등 다양한 자원을 포함할 수 있습니다. 즉, URI는 URL, URN의 상위 개념입니다.
- 정적 콘텐츠 처리
fopen()
으로 파일을 열고fread()
로 내용 읽음- 응답 헤더를 생성하고 클라이언트로 전송
- 동적 콘텐츠 처리 (예: CGI)
fork()
와exec()
를 통해 자식 프로세스를 만들어 CGI 프로그램 실행dup2()
를 이용해 표준 출력을 클라이언트로 리다렉션
- 연결 종료
- 응답 후
close(connfd)
로 연결 종료
흐름 요약
main()
└→ open_listenfd()
└→ accept()
└→ get request
└→ parse URI
└→ serve_static() or serve_dynamic()
└→ close()
코드 구현
그럼 이제 코드를 살펴보겠습니다.
adder.c 종합
두 숫자를 더하는 최소 CGI 프로그램입니다.
#include "csapp.h"
int main(void) {
char *buf, *p; // QUERY_STRING을 저장할 포인터와 분리용 포인터
char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE]; // 파싱된(분석) 인자들과 출력할 content 버퍼
int n1 = 0, n2 = 0; // 숫자형으로 변환된 두 값
// QUERY_STRING 환경 변수 가져오기 (예시로 "100&200")
if((buf = getenv("QUERY_STRING")) != NULL) {
p = strchr(buf, '&'); // '&' 문자 위치 탐색(strchr의 역할)
*p = '\0'; // '&'를 NULL 종료로 바꿔서 문자열을 분리(문자열의 끝 표시)
// buf → "100", p+1 → "200"
// 문자열을 arg로 복사한 후 atoi로 정수형으로 변환
// 문자열을 개별 변수로 복사
srtcpy(arg1, buf); // arg1 ← "100"
strcpy(arg2, p + 1); // arg2 ← "200"
//문자열을 정수로 변환
n1 = atoi(arg1); // n1 = 100
n2 = atoi(arg2); // n2 = 200
}
// content 버퍼에 응답 HTML 본문 작성(응답 몸체)
sprintf(content, "QUERY_STRING = %s", buf); // 원래 쿼리 문자열 출력
sprintf(content, "Welcome to add.com: "); // 인삿말
sprintf(content, "%s THE Internet additin portal, \r\n<p>", content); // 누적해서 작성
sprintf(content, "%s The answer is: %d + %d = %d\r\n<p>", content, n1, n2, n1 + n2); // 덧셈 결과 출력
sprintf(content, "%s Thanks for visiting!\r\n", content); // 마무리 메시지
// HTTP 응답 헤더 출력
printf("Connection: close\r\n"); // 연결 종료
printf("Content-length: %d\r\n", (int)strlen(content)); // 본문 길이
printf("Content-type: text/html\r\n\r\n"); // MIME 타입
printf("%s", content); // 본문 출력
fflush(stdout); // 출력 버퍼 비움
exit(0); // 프로그램 종료
}
항목 | 설명 |
---|---|
getenv("QUERY_STRING") |
웹 서버가 CGI로 전달한 쿼리 문자열을 가져옴 |
strchr(buf, '&') |
쿼리 문자열을 '&'로 분리 (100&200 ) |
atoi() |
문자열을 정수로 변환 |
printf() |
CGI 프로그램은 stdout으로 HTTP 응답을 출력해야 함 |
sprintf() |
여러 번 누적해서 content 문자열을 작성 |
tiny.c
tiny.c의 함수 선언부
앞으로 사용될 함수들에 대해 인자와 함께 미리 선언했습니다.
#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);
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);
tiny.c의 main 함수
main()
은 웹서버의 전체 흐름을 제어하는 관제탑입니다.
- 이 구조는 간단한 HTTP 1.0 웹서버의 기본 루프입니다.
while (1)
루프는 iterative 서버 구조로, 하나의 요청을 처리한 뒤 다음 요청을 기다리는 방식입니다.
(다중 접속을 처리하려면fork
,pthread
등을 써야 함)
과정
- 초기 설정: 포트 설정 및 서버 소켓 열기
- 무한 루프: 클라이언트 요청 수락 → 처리 → 종료
- 요청 처리는
doit()
함수에서 담당
int main(int argc, char **argv)
{
int listenfd, connfd; // 소켓 파일 디스크립터들
char hostname[MAXLINE], port[MAXLINE]; // 클라이언트의 호스트이름과 포트번호 저장용
socklen_t clientlen; // 주소 길이
struct sockaddr_storage clientaddr; // 클라이언트 주소 정보 저장용 구조체
// 포트 번호 인자가 제대로 들어왔는지 확인
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]); // 사용법 출력
exit(1); // 인자 부족 시 종료
}
// 인자로 받은 포트 번호로 듣기 소켓 오픈
listenfd = Open_listenfd(argv[1]);
// while문으로 서버 무한 루프 실행
while (1) {
// clientaddr 구조체 크기 설정
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); // 해당 연결에 대해 HTTP 처리 루틴 실행
Close(connfd); // 연결 종료
}
}
코드 라인 | 설명 |
---|---|
listenfd = Open_listenfd(argv[1]); |
주어진 포트 번호로 서버 소켓을 열고 listen 상태로 둡니다. |
connfd = Accept(...) |
클라이언트가 접속하면 연결 수락하고 새로운 소켓 반환. |
Getnameinfo(...) |
접속한 클라이언트의 주소 정보를 문자열 형태로 변환 (디버깅용). |
doit(connfd); |
핵심 요청 처리 함수로 HTTP 요청 파싱 및 응답을 담당. |
Close(connfd); |
작업 후 클라이언트와의 연결을 닫습니다. |
tiny.c의 doit 함수
클라이언트의 HTTP 요청을 처리하고 적절한 응답을 보냅니다.
클라이언트 요청을 읽고 분석하여, 정적이면 파일 전송, 동적이면 CGI 실행으로 응답을 처리하는 핵심 함수입니다.
과정
요청 파싱 → 파일 확인 → 정적/동적 처리 분기 → 응답 전송
void doit(int fd)
{
int is_static; // 정적 페인지 여부를 저장(1: 정적, 0: 동적)
struct stat sbuf; // 파일 정보 저장용 구조체
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE]; // 요청 라인 저장
char filename[MAXLINE], cgiargs[MAXLINE]; //파일 이름과 CGI 인자 저장
rio_t rio; // Rio I/O 구조체
// 요청 헤더를 읽기 위한 초기화 및 요청 라인 읽기
Rio_readinitb(&rio, fd); // Rio I/O 초기화
Rio_readlineb(&rio, buf, MAXLINE); // 요청 라인 읽기 (ex. "GET /index.html HTTP/1.0")
printf("Request headers:\n"); // 헤더 요청
printf("%s", buf); // 요청 라인 출력(디버깅)
// 요청 라인에서 메서드, URI, 버전 추출
sscanf(buf, "%s %s %s", method, uri, version);
// 지원하지 않는 메서드일 경우 501 에러 전송 후 종료
if (strcasecmp(method, "GET")) { // method가 GET 요청이 아닐때 if문 실행
clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method");
return;
}
read_requesthdrs(&rio); // 요청 헤더들을 모두 읽어 들임 (분석은 X)
is_static = parse_uri(uri, filename, cgiargs); // URI 분석을 통해 정적/동적 구분 및 파일 이름, CGI 인자 추출
// 요청한 파일이 실제 존재하는지 확인
if (stat(filename, &sbuf) < 0) { // 함수를 뜯어보면 알겠지만, stat가 파일이 존재하지 않으면 -1을 내보내므로 0보다 작아 if문이 실행된다
clienterror(fd, filename, "404", "Not found", "Tiny couldn't find this file");
return;
}
// 정적 컨텐츠 처리
if (is_static) {
// 일반 파일이 아니거나 읽기 권한이 없을 경우 403 에러 리턴
// S_ISREG나 S_IRUSR이 아닐경우 True를 내보내는데, !에 따라 false으로 if문을 연산한다.
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);
}
// 동적 컨텐츠 처리(CGI)
else {
// 일반 파일이 아니거나 실행 권한이 없을 경우 403 에러 리턴
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program");
return;
}
// 정상이면 CGI 프로그램 실행 및 결과 전송
serve_dynamic(fd, filename, cgiargs);
}
}
tiny.c의 read_requesthdrs 함수
read_requesthdrs
함수는 클라이언트의 HTTP 요청 헤더를 한 줄씩 읽어들이는 함수입니다.
요청 헤더는 \r\n
(빈 줄)로 끝나므로, 이 줄이 나올 때까지 반복해서 읽습니다.
헤더 내용을 출력은 하지만 학습 목적용 간단한 서버이므로 특별한 파싱은 하지 않습니다.
과정
- 첫 번째 요청 헤더 줄을 읽음
- 줄이
\r\n
이 아닌 동안 계속 읽으며 출력 \r\n
을 만나면 헤더 끝으로 인식하고 종료
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;
}
예를 들어
요청 헤더가 다음과 같으면, read_requesthdrs
는 위의 4줄을 출력한 뒤 빈 줄을 만나서 종료합니다.
GET /index.html HTTP/1.1
Host: localhost:8000
User-Agent: curl/7.81.0
Accept: **/**
<빈 줄>
tiny.c의 parse_uri 함수
parse_uri
함수는 클라이언트가 요청한 URI를 분석해서 정적 콘텐츠(static) 요청인지, 동적 콘텐츠(CGI) 요청인지 구분하고 해당하는 파일 경로와 CGI 인자를 분리하여 저장합니다.
과정
uri
에"cgi-bin"
이 없으면: 정적 콘텐츠 요청으로 판단- URI를
filename
에 복사하고,/
로 끝나면"home.html"
을 붙임 cgiargs
는 빈 문자열- →
return 1
(정적)
- URI를
uri
에"cgi-bin"
이 있으면: 동적 콘텐츠 요청으로 판단?
가 있으면cgiargs
에 인자 저장,?
이후 제거- URI를
filename
에 저장 - →
return 0
(동적)
int parse_uri(char *uri, char *filename, char *cgiargs)
{
char *ptr;
// "cgi-bin"이 포함되어 있지 않으면 정적 콘텐츠 요청으로 판단
if (!strstr(uri, "cgi-bin")) {
strcpy(cgiargs, ""); // CGI 인자는 없음
strcpy(filename, "."); // 상대 경로 시작
strcat(filename, uri); // uri를 파일명으로 추가
if (uri[strlen(uri)-1] == '/') // URI가 '/'로 끝나면
strcat(filename, "home.html"); // 기본 파일로 home.html 지정
return 1; // 정적 콘텐추임을 의미한다
}
else {
// 동적 콘텐츠 요청 (cgi-bin 포함)
ptr = index(uri, '?'); // '?' 위치 탐색 (인자 시작점)
if (ptr) {
strcpy(cgiargs, ptr+1); // '?' 이후 문자열을 cgiargs에 저장
*ptr = '\0'; // '?'를 null로 바꿔 URI를 자른다
}
else {
strcpy(cgiargs, ""); // 인자가 없는 경우
}
strcpy(filename, "."); // 상대 경로 사적
strcat(filename, uri); // uri를 파일명으로 추가
return 0; // 동적 콘텐츠임을 의미함.
}
}
예시 입출력
입력 URI: /index.html
- 정적 콘텐츠
filename
→./index.html
cgiargs
→""
- 리턴값:
1
입력 URI: /cgi-bin/adder?123&456
- 동적 콘텐츠
filename
→./cgi-bin/adder
cgiargs
→"123&456"
- 리턴값:
0
tiny.c의 serve_static 함수
serve_static
함수는 정적 컨텐츠(static content)를 클라이언트에게 전달하는 역할을 합니다. HTML, 이미지, 텍스트 등 서버의 파일 시스템에 존재하는 정적 파일을 읽어 HTTP 응답 형식으로 클라이언트에 전송합니다.
과정
- 파일의 MIME 타입을 결정
- HTTP 응답 헤더를 생성
- 파일을 메모리로 매핑
- 컨텐츠 내용을 클라이언트 소켓으로 전송
void serve_static(int fd, char *filename, int filesize)
{
int srcfd; // 파일 디스크립터
char *srcp, filetype[MAXLINE], buf[MAXBUF];
get_filetype(filename, filetype); // 확장자에 따라 MIME 타입이 결정(.html -> text/html 등)
// HTTP 응답 헤더 작성
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); // MIME 타입(html, jpg 등)
Rio_writen(fd, buf, strlen(buf)); // 클라이언트에게 응답 헤더 전송
printf("Response headers:\n"); // 응답헤더 관련 출력
printf("%s", buf); // 서버 측에 헤더 출력(디버깅)
// 파일을 열어서 메모리 맵핑 후 응답 본문으로 전송
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); // 메모리 매핑 해제
}
tiny.c의 get_filetype 함수
get_filetype
함수는 전달된 파일 이름(filename
)을 보고 확장자를 분석하여 HTTP 응답 헤더에 들어갈 Content-type
필드에 적절한 MIME 타입 문자열을 filetype
변수에 저장합니다.
과정
- 파일 이름에
.html
,.gif
,.png
,.jpg
중 하나가 포함되어 있는지 확인하고, - 해당되는 MIME 타입 문자열을
filetype
에 저장합니다. - 해당 확장자가 없을 경우 기본값
"text/plain"
을 사용합니다.
void get_filetype(char *filename, char *filetype)
{
if (strstr(filename, ".html")) // 파일 이름에 ".html"이 포함되어 있으면
strcpy(filetype, "text/html"); // MIME 타입: HTML 문서
else if (strstr(filename, ".gif")) // ".gif" 확장자 → 이미지(gif)
strcpy(filetype, "image/gif");
else if (strstr(filename, ".png")) // ".png" 확장자 → 이미지(png)
strcpy(filetype, "image/png");
else if (strstr(filename, ".jpg")) // ".jpg" 확장자 → 이미지(jpeg)
strcpy(filetype, "image/jpeg");
else // 위에 해당되지 않으면 일반 텍스트로 처리
strcpy(filetype, "text/plain");
}
tiny.c의 serve_dynamic 함수
동적 컨텐츠를 실행하고 그 결과를 클라이언트에게 반환하는 함수입니다.
Tiny 서버는 CGI 프로그램을 fork-exec으로 실행하고, 그 결과를 HTTP 응답으로 전송합니다.
과정
- 클라이언트에게 HTTP 응답 헤더 전송
- 자식 프로세스를 fork하여, 환경변수
QUERY_STRING
을 설정 - stdout을 클라이언트 소켓에 연결하여 CGI 프로그램 실행 (
execve
) - 부모 프로세스는 자식이 끝날 때까지
wait
더알아보기
- CGI 실행은 별도의 프로세스에서 수행됩니다.
- stdout이 fd(클라이언트 소켓)로 연결되어 있어 CGI 결과가 클라이언트로 직접 전송됩니다.
- CGI 스크립트는
QUERY_STRING
환경변수를 통해 GET 파라미터를 전달받습니다.
void serve_dynamic(int fd, char *filename, char *cgiargs)
{
char buf[MAXLINE], *emptylist[] = { NULL }; // execve 인자 없이 실행할 배열 (argv)
// 클라이언트에게 응답 헤더 전송 (동적 컨텐츠는 내용을 CGI가 출력)
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) {
setenv("QUERY_STRING", cgiargs, 1); // 환경변수 QUERY_STRING 설정 (ex: "100&200")
Dup2(fd, STDOUT_FILENO); //stdout을 클라이언트 연결(fd)로 리다이렉트
Execve(filename, emptylist, environ); // CGI 프로그램 실행 (argv 없음)
}
Wait(NULL); // 부모 프로세스는 자식이 끝날 때까지 대기
}
tiny.c의 clienterror 함수
클라이언트에게 에러가 발생했음을 알리는 HTTP 응답 메시지(헤더 + HTML 본문)를 작성하고 전송하는 함수입니다. (ex: 파일이 없을 때(404), 지원하지 않는 메소드일 때(501), 접근 권한이 없을 때(403) 등)
과정
- HTML 형식의 오류 메시지 본문(body)을 작성
- HTTP 응답 헤더(buf)를 작성
- 클라이언트 소켓(fd)에 헤더와 본문을 차례로 전송
더알아보기
sprintf
를 사용해 문자열을 계속 누적해서 작성- 에러 메시지 형식을 갖춘 HTML 페이지로 만들어 사용자가 보기 좋게 표시
Rio_writen
은 신뢰성 있게 지정된 크기만큼 데이터를 소켓에 사용
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); // 상세 설명 (원인 포함)
sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body); // 서버 이름 추가
// HTTP 응답 헤더 작성 및 전송
sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg); // 상태 줄
Rio_writen(fd, buf, strlen(buf)); // 헤더 전송
sprintf(buf, "Content-type: text/html\r\n"); // MIME 타입
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body)); // 본문 길이
Rio_writen(fd, buf, strlen(buf));
// 본문(body) 전송
Rio_writen(fd, body, strlen(body));
}
예시
만약 ‘404 Not Found’가 발생하면 다음과 같이 출력됩니다.
HTTP/1.0 404 Not Found
Content-type: text/html
Content-length: 137
<html><title>Tiny Error</title>
<body bgcolor="ffffff">
404: Not Found
<p>Tiny couldn't find this file: missing.html
<hr><em>The Tiny Web server</em>
tiny.c 종합
모든 함수를 종합하면 tiny.c의 코드는 다음과 같습니다.
// 들어가기 앞서 if문에서 0은 false이고 1은 true를 의미한다.
// 해당 코드에서 함수 앞에 !을 적어 true를 false를 시키고 그걸 또 if문으로 비교하는 경우가 많으니 유념해서 해석하자.
#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);
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; // 소켓 파일 디스크립터들
char hostname[MAXLINE], port[MAXLINE]; // 클라이언트의 호스트이름과 포트번호 저장용
socklen_t clientlen; // 주소 길이
struct sockaddr_storage clientaddr; // 클라이언트 주소 정보 저장용 구조체
// 포트 번호 인자가 제대로 들어왔는지 확인
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]); // 사용법 출력
exit(1); // 인자 부족 시 종료
}
// 인자로 받은 포트 번호로 듣기 소켓 오픈
listenfd = Open_listenfd(argv[1]);
// while문으로 서버 무한 루프 실행
while (1) {
// clientaddr 구조체 크기 설정
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); // 해당 연결에 대해 HTTP 처리 루틴 실행
Close(connfd); // 연결 종료
}
}
/////////////////////////////////////////////////////////
void doit(int fd)
{
int is_static; // 정적 페인지 여부를 저장(1: 정적, 0: 동적)
struct stat sbuf; // 파일 정보 저장용 구조체
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE]; // 요청 라인 저장
char filename[MAXLINE], cgiargs[MAXLINE]; //파일 이름과 CGI 인자 저장
rio_t rio; // Rio I/O 구조체
// 요청 헤더를 읽기 위한 초기화 및 요청 라인 읽기
Rio_readinitb(&rio, fd); // Rio I/O 초기화
Rio_readlineb(&rio, buf, MAXLINE); // 요청 라인 읽기 (ex. "GET /index.html HTTP/1.0")
printf("Request headers:\n"); // 헤더 요청
printf("%s", buf); // 요청 라인 출력(디버깅)
// 요청 라인에서 메서드, URI, 버전 추출
sscanf(buf, "%s %s %s", method, uri, version);
// 지원하지 않는 메서드일 경우 501 에러 전송 후 종료
if (strcasecmp(method, "GET")) { // method가 GET 요청이 아닐때 if문 실행
clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method");
return;
}
read_requesthdrs(&rio); // 요청 헤더들을 모두 읽어 들임 (분석은 X)
is_static = parse_uri(uri, filename, cgiargs); // URI 분석을 통해 정적/동적 구분 및 파일 이름, CGI 인자 추출
// 요청한 파일이 실제 존재하는지 확인
if (stat(filename, &sbuf) < 0) { // 함수를 뜯어보면 알겠지만, stat가 파일이 존재하지 않으면 -1을 내보내므로 0보다 작아 if문이 실행된다
clienterror(fd, filename, "404", "Not found", "Tiny couldn't find this file");
return;
}
// 정적 컨텐츠 처리
if (is_static) {
// 일반 파일이 아니거나 읽기 권한이 없을 경우 403 에러 리턴
// S_ISREG나 S_IRUSR이 아닐경우 True를 내보내는데, !에 따라 false으로 if문을 연산한다.
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);
}
// 동적 컨텐츠 처리(CGI)
else {
// 일반 파일이 아니거나 실행 권한이 없을 경우 403 에러 리턴
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program");
return;
}
// 정상이면 CGI 프로그램 실행 및 결과 전송
serve_dynamic(fd, filename, cgiargs);
}
}
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;
}
int parse_uri(char *uri, char *filename, char *cgiargs)
{
char *ptr;
// "cgi-bin"이 포함되어 있지 않으면 정적 콘텐츠 요청으로 판단
if (!strstr(uri, "cgi-bin")) {
strcpy(cgiargs, ""); // CGI 인자는 없음
strcpy(filename, "."); // 상대 경로 시작
strcat(filename, uri); // uri를 파일명으로 추가
if (uri[strlen(uri)-1] == '/') // URI가 '/'로 끝나면
strcat(filename, "home.html"); // 기본 파일로 home.html 지정
return 1; // 정적 콘텐추임을 의미한다
}
else {
// 동적 콘텐츠 요청 (cgi-bin 포함)
ptr = index(uri, '?'); // '?' 위치 탐색 (인자 시작점)
if (ptr) {
strcpy(cgiargs, ptr+1); // '?' 이후 문자열을 cgiargs에 저장
*ptr = '\0'; // '?'를 null로 바꿔 URI를 자른다
}
else {
strcpy(cgiargs, ""); // 인자가 없는 경우
}
strcpy(filename, "."); // 상대 경로 사적
strcat(filename, uri); // uri를 파일명으로 추가
return 0; // 동적 콘텐츠임을 의미함.
}
}
void serve_static(int fd, char *filename, int filesize)
{
int srcfd; // 파일 디스크립터
char *srcp, filetype[MAXLINE], buf[MAXBUF];
get_filetype(filename, filetype); // 확장자에 따라 MIME 타입이 결정(.html -> text/html 등)
// HTTP 응답 헤더 작성
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); // MIME 타입(html, jpg 등)
Rio_writen(fd, buf, strlen(buf)); // 클라이언트에게 응답 헤더 전송
printf("Response headers:\n"); // 응답헤더 관련 출력
printf("%s", buf); // 서버 측에 헤더 출력(디버깅)
// 파일을 열어서 메모리 맵핑 후 응답 본문으로 전송
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); // 메모리 매핑 해제
}
void get_filetype(char *filename, char *filetype)
{
if (strstr(filename, ".html")) // 파일 이름에 ".html"이 포함되어 있으면
strcpy(filetype, "text/html"); // MIME 타입: HTML 문서
else if (strstr(filename, ".gif")) // ".gif" 확장자 → 이미지(gif)
strcpy(filetype, "image/gif");
else if (strstr(filename, ".png")) // ".png" 확장자 → 이미지(png)
strcpy(filetype, "image/png");
else if (strstr(filename, ".jpg")) // ".jpg" 확장자 → 이미지(jpeg)
strcpy(filetype, "image/jpeg");
else // 위에 해당되지 않으면 일반 텍스트로 처리
strcpy(filetype, "text/plain");
}
void serve_dynamic(int fd, char *filename, char *cgiargs)
{
char buf[MAXLINE], *emptylist[] = { NULL }; // execve 인자 없이 실행할 배열 (argv)
// 클라이언트에게 응답 헤더 전송 (동적 컨텐츠는 내용을 CGI가 출력)
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) {
setenv("QUERY_STRING", cgiargs, 1); // 환경변수 QUERY_STRING 설정 (ex: "100&200")
Dup2(fd, STDOUT_FILENO); //stdout을 클라이언트 연결(fd)로 리다이렉트
Execve(filename, emptylist, environ); // CGI 프로그램 실행 (argv 없음)
}
Wait(NULL); // 부모 프로세스는 자식이 끝날 때까지 대기
}
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); // 상세 설명 (원인 포함)
sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body); // 서버 이름 추가
// HTTP 응답 헤더 작성 및 전송
sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg); // 상태 줄
Rio_writen(fd, buf, strlen(buf)); // 헤더 전송
sprintf(buf, "Content-type: text/html\r\n"); // MIME 타입
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body)); // 본문 길이
Rio_writen(fd, buf, strlen(buf));
// 본문(body) 전송
Rio_writen(fd, body, strlen(body));
}
결과 화면
해당되는 주소와 포트로 들어가면 다음과 같이 출력됩니다.
GET과 POST 인자값과 결과를 확인하는 postman이라는 프로그램을 통해 gif값을 확인할 수 있습니다.
비주얼스튜디오 코드로 보면 다음과 같이 출력됩니다. 항상똑같지 않습니다. 왜냐하면, 요청하는 형식에 따라 헤더가 달라지기 때문입니다.
tiny 서버 구동시 트러블 슈팅
연결하는데 문제가 있어서 해결했습니다.
왜냐하면, 저같은 경우는 시놀로지에 있는 도커를 통해 해당 서버를 구현했습니다. 그래서 도커 자체 우분투에서도 포트포워딩을 하고 공유기도 포트포워딩을 해줘야했습니다. 공유기 같은 경우 남는 포트번호 20000을 써서 포트포워딩을 해두었고, 도커 같은 경우 SSH 접속을 위해 22번 포트만 해뒀었습니다.
근데 도커의 경우 이미지 파일이라서 실행상태에서는 포트포워딩이 안되고 dockerfile을 편집해서 포트포워딩 문구를 추가하고, 재빌딩을 해야되는 걸로 알고 있었습니다. 새로 도커를 만들어 되나 싶어서 기존의 이미지 파일로 새로 만들려했다. 이 과정이 생각보다 오래걸려서(환경 세팅) 다른방법이 없나 고민을 많이 했습니다.
그 과정에서 시놀로지는 도커를 끄면 자체 시스템을 통해 포트포워딩을 할 수 있음을 알게되었습니다. 그래서 20000번 포트로 포트포워딩을 하고 도커를 다시 실행하여 tiny서버를 실행시키니 맥북의 브라우저에 ‘집 공유기 외부 ip주소:20000’의 형태로 결과물을 확인할 수 있었습니다. 뿌듯함과 성취감을 말로 표현할 수 없었습니다.
마지막으로 postman이라는 post와 get 요청인자를 수동으로 입력해서 리턴되는 값을 볼 수 있는 프로그램으로 조회해보니 정상적으로 작동하는 것 또한 확인할 수 있었습니다.
지금까지의 네트워크 지식을 동원한 트러블 슈팅을 해결해서 뜻 깊은 시간이었습니다.