| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 그래프
- 프론트엔드
- CSS
- 개발자
- 그리디
- 알고리즘
- 코딩
- 프론트앤드
- c언어
- 크래프톤 정글
- HTML기초
- Python
- 정렬
- 혼자 공부해서 개발까지
- Mini-React
- frontend
- react
- 코딩테스트
- html
- 프로그래머스
- 해시
- 정글
- js
- BFS
- Git
- 백준
- DFS
- javascript
- 팀프로젝트
- 알고리즘 기초
- Today
- Total
민혁이의 IT스토리
tiny.c로 배우는 HTTP와 소켓 프로그래밍의 기초 본문
Tiny 서버란 무엇인가
Tiny는 200줄로 동작하는 실제 웹 서버입니다. 단순하지만,
HTTP 프로토콜과 소켓 프로그래밍의 핵심 개념을 모두 담고 있습니다.
Main 함수 - 서버의 시작점
💡 포인트
- “Tiny는 멀티스레드가 아니라 반복적(Iterative) 서버다.”
- “한 번에 한 클라이언트 요청만 처리한다.”
1. 변수 선언부
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;
- listenfd: 서버가 클라이언트의 연결 요청을 "듣는(listen)" 소켓의 파일 디스크립터
- connfd: 실제로 연결이 성립된 후, 클라이언트와 데이터 송수신에 사용하는 “연결 소켓”
- hostname, port: 접속한 클라이언트의 IP 주소, 포트 번호 문자열 저장용
- clientlen: Accept() 호출 시 클라이언트 주소 구조체 크기 저장
- clientaddr: 클라이언트의 주소 정보(IP, 포트 등)를 담는 구조체 (sockaddr_storage는 모든 주소 체계를 포괄)
한 번 연결을 수락할 때마다, “대기용”(listenfd)과 “통신용”(connfd)을 분리해야 서버가 다음 연결을 계속 대기할 수 있어요.
2. 명령행 인자
if (argc != 2)
{
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
프로그램 실행 시 포트 번호를 반드시 하나 입력해야 함.
설명:
./tiny 8000
- 인자 개수가 다르면 사용법을 출력하고 종료.
- argv[1] → 사용자가 입력한 포트 번호 문자열
그 다음은 프로그램 실행 시 전달받은 인자(argc, argv)를 검사한다.
Tiny 서버는 실행할 때 반드시 포트 번호 하나를 명령행 인자로 받아야 한다.
예를 들어, 터미널에서 ./tiny 8000처럼 실행해야 서버가 8000번 포트에서 요청을 받을 준비를 한다.
만약 인자가 주어지지 않으면 usage: tiny <port>라는 메시지를 출력하고 프로그램을 종료한다.
이 단계는 사용자가 서버를 잘못 실행하지 않도록 하기 위한 기본적인 입력 검증이다.
3. 서버 소켓 생성 (리스닝 소켓 열기)
listenfd = Open_listenfd(argv[1]);
Open_listenfd(argv[1]) 함수는 Tiny 서버의 핵심적인 준비 과정이다.
이 한 줄로 내부적으로 socket(), bind(), listen() 과정을 모두 수행한다.
즉, 지정된 포트 번호에 대해 소켓을 만들고, 그 포트를 현재 서버 프로세스에 할당한 뒤, 클라이언트의 연결 요청을 받을 수 있도록 리스닝 상태로 전환한다.
이 과정이 성공적으로 끝나면 listenfd는 “대기 중인 전화기”처럼 클라이언트의 접속을 기다리는 역할을 하게 된다.
이 시점에서 Tiny 서버는 네트워크 상에서 지정한 포트를 통해 들어오는 HTTP 요청을 받을 준비가 완전히 끝난 상태다.
4. 무한 루프 — 연결 반복 처리
while (1)
{
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
...
doit(connfd);
Close(connfd);
}
Tiny 서버는 한 번 실행되면 종료되지 않고 계속 동작해야 한다. 그래서 while (1)이라는 무한 루프를 사용한다.
이 루프는 클라이언트가 요청을 보낼 때마다 그것을 처리하고, 연결이 끝나면 다시 대기 상태로 돌아간다.
즉, 서버는 요청을 한 건씩 순차적으로 처리하는 구조를 가지고 있다.
Tiny는 ‘Iterative Server’, 즉 한 번에 한 명의 클라이언트만 처리하는 단일 스레드 방식이다.
그래서 동시에 여러 요청이 들어오면, 앞선 요청이 끝난 다음에야 다음 요청을 처리할 수 있다
- 서버는 끊임없이 요청을 기다리는 구조입니다.
- while (1) → 서버가 꺼지지 않는 이상 계속 실행.
- 각 반복은 “클라이언트 한 명의 요청”을 처리합니다.
💡Tiny는 단일 스레드(Iterative) 서버이기 때문에,
한 번에 한 클라이언트만 처리하고 나서 다음 요청으로 넘어갑니다.
5. 클라이언트 연결 수락
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
- 클라이언트가 접속을 시도할 때까지 블록(block) 상태로 대기.
- 연결이 들어오면 새로운 소켓(connfd)을 만들어 반환.
- 이후 이 connfd로 클라이언트와 데이터를 주고받습니다.
루프 안에서 Accept() 함수가 호출되면, 서버는 실제로 클라이언트의 접속을 기다린다.
이 함수는 클라이언트가 연결 요청을 보내기 전까지 블록(block) 상태로 멈춰 있게 된다.
요청이 들어오면 커널이 TCP의 3-way handshake 과정을 자동으로 처리하고, 그 결과로 새로운 연결 소켓을 생성해 반환한다.
이때 반환되는 connfd는 바로 그 클라이언트와의 통신에 사용할 수 있는 소켓이다.
이제부터 Tiny는 connfd를 통해 클라이언트와 HTTP 데이터를 주고받을 수 있게 된다.
6. 접속한 클라이언트 정보 출력
Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
- Getnameinfo()는 IP 주소 → 도메인명, 포트번호 문자열로 변환.
- 클라이언트가 어디서 접속했는지 콘솔에 출력.
Accepted connection from (127.0.0.1, 8000)
7. 요청 처리 함수 호출
doit(connfd);
클라이언트와의 실제 HTTP 통신은 doit() 함수에서 처리.
- 요청 헤더 읽기 (read_requesthdrs)
- URI 파싱 (parse_uri)
- 정적/동적 분기 (serve_static, serve_dynamic)
8.연결 종료
Close(connfd);
- 한 클라이언트의 요청을 처리했으면 소켓 닫기.
- 리소스 해제 후 다음 요청을 받을 준비.
🔁 과정 요약
1. socket() → 소켓 생성
2. bind() → 포트 번호 연결
3. listen() → 연결 요청 대기
4. accept() → 연결 요청 수락, 새 소켓(connfd) 생성
5. read/write() → connfd로 실제 데이터 송수신
6. close(connfd) → 연결 종료
요약 다이어그램
[main 함수 흐름]
┌───────────────┐
│ Open_listenfd │ ← 포트 열기 (서버 대기 준비)
└──────┬────────┘
│
┌────▼─────┐
│ Accept() │ ← 클라이언트 연결 수락
└────┬─────┘
│
┌────▼─────┐
│ doit() │ ← HTTP 요청 처리
└────┬─────┘
│
┌────▼─────┐
│ Close() │ ← 연결 종료
└────┴─────┘
│
반복 (while)
doit 함수 - 요청 한 건 처리의 핵심
1. 지역변수
int is_static;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio;
이 변수들은 요청 처리에 필요한 모든 데이터를 담는 저장공간이에요.
- buf : 요청 라인이나 헤더를 임시로 읽어 저장하는 버퍼
- method, uri, version : 요청 라인에서 각각 분리해 저장 (예: "GET", "/home.html", "HTTP/1.0")
- filename : 서버 내부의 실제 파일 경로
- cgiargs : CGI 프로그램 실행 시 전달할 인자 문자열
- rio : robust I/O를 위한 버퍼 구조체
- is_static : 정적 콘텐츠인지(1), 동적 콘텐츠인지(0) 구분
2. RIO 버퍼 초기화
Rio_readinitb(&rio, fd);
역할 :
rio_t 구조체를 파일 디스크립터 fd(= 클라이언트 연결 소켓)에 연결합니다.
이제부터 Rio_readlineb() 등을 이용해 클라이언트로부터 안전하게 데이터를 읽을 수 있어요.
이유 :
네트워크에서는 한 번의 read() 호출로 요청이 다 안 들어올 수도 있어서,
버퍼링된 robust I/O 패키지를 사용하는 거예요.
3. 요청 라인 읽기
Rio_readlineb(&rio, buf, MAXLINE);
sscanf(buf, "%s %s %s", method, uri, version);
역할:
클라이언트가 보낸 첫 줄(요청 라인)을 읽어서 method, uri, version으로 분리합니다.
GET /home.html HTTP/1.0
method = "GET"
uri = "/home.html"
version = "HTTP/1.0"
- 어떤 요청을 보냈는지(메서드), 어떤 리소스를 원했는지(URI), 어떤 버전인지가 여기서 결정됩니다.
4.요청 메서드 검사
if (strcasecmp(method, "GET")) {
clienterror(fd, method, "501", "Not Implemented", "Tiny does not implement this method");
return;
}
Tiny 서버는 단순화를 위해 GET 요청만 지원합니다.
(즉, POST, PUT 등은 “501 Not Implemented” 오류로 응답)
동작:
strcasecmp()은 대소문자를 구분하지 않는 문자열 비교 함수입니다.
→ “get”, “GET”, “Get” 모두 인정하지만, 다른 단어면 조건이 참이 됩니다.
5.요청 헤더 읽기
read_requesthdrs(&rio);
클라이언트가 보낸 HTTP 헤더를 모두 읽습니다.
단, Tiny는 헤더 내용을 실제로 사용하지 않아요.
6. URI 파싱
is_static = parse_uri(uri, filename, cgiargs);
요청한 경로 uri를 분석해서,
- 정적 콘텐츠인지 (is_static = 1)
- 동적 콘텐츠인지 (is_static = 0)
판단하고,
각각 필요한 정보를 filename과 cgiargs에 저장합니다.
7. 파일 상태 확인
if (stat(filename, &sbuf) < 0) {
clienterror(fd, filename, "404", "Not found", "Tiny couldn’t find this file");
return;
}
stat() 시스템 호출을 통해 해당 파일이 존재하는지,
그리고 어떤 종류의 파일인지(보통 일반 파일) 확인합니다.
클라이언트가 요청한 파일이 서버에 없으면,
“404 Not Found” 오류를 보내야 하기 때문이에요.
8. 정적/동적 콘텐츠 처리 분기
if (is_static)
serve_static(fd, filename, sbuf.st_size);
else
serve_dynamic(fd, filename, cgiargs);
- is_static == 1 → 정적 콘텐츠
→ serve_static() 함수 호출
(HTML, 이미지 파일 등 직접 읽어서 브라우저에 보냄) - is_static == 0 → 동적 콘텐츠
→ serve_dynamic() 함수 호출
이유
정적 파일은 그대로 보내면 되지만,
CGI는 프로그램을 실행시켜야 하므로 완전히 다른 처리 방식이 필요합니다.
요약 다이어그램
클라이언트 요청
↓
[1] 요청 라인 읽기 (GET /home.html HTTP/1.0)
↓
[2] 메서드 검사 (GET만 허용)
↓
[3] 요청 헤더 읽기 (무시)
↓
[4] URI 분석 → 파일명/인자 분리
↓
[5] 파일 존재 확인
↓
[6] 정적이면 serve_static / 동적이면 serve_dynamic 호출
↓
[7] 응답 전송 후 doit 종료
read_requesthdrs 함수 — HTTP 요청 헤더 읽기
이 함수는 Tiny Web Server에서 HTTP 요청 헤더(Request Header)를 읽고 버리는 역할을 합니다.
전체 코드는 다음과 같습니다 👇
1. 첫 번째 헤더 줄 읽기
Rio_readlineb(rp, buf, MAXLINE);
rio_t 구조체(rp)를 이용해 한 줄 단위로 데이터를 읽습니다.
3. 헤더의 끝인지 확인
while (strcmp(buf, "\r\n")) {...}
HTTP 요청 헤더는 빈 줄(\r\n)로 끝납니다.
이 조건문은 “현재 줄이 빈 줄이 아닐 때만 반복한다”는 뜻입니다.
4. 다음 줄 읽기
Rio_readlineb(rp, buf, MAXLINE);
헤더가 끝날 때까지 한 줄씩 반복해서 읽습니다.다음 줄을 계속 buf에 저장해 나갑니다.
요청부분 헤더 예시
GET /index.html HTTP/1.1
Host: localhost:8000
User-Agent: Mozilla/5.0
Accept: text/html
Connection: close
read_requesthdrs()에서 한줄씩 읽고 버린다. 실제로 리던값이 없다.
parse_uri 함수 — URI 분석
1.정적(static) 콘텐츠 판별
if (!strstr(uri, "cgi-bin")) { /* Static content */
strcpy(cgiargs, "");
strcpy(filename, ".");
strcat(filename, uri);
if (uri[strlen(uri)-1] == '/')
strcat(filename, "home.html");
return 1;
}
역할
- "cgi-bin"이 없는 경우 → 정적(static) 콘텐츠로 처리합니다.
- 즉, 단순히 파일(html, jpg 등)을 반환하는 요청입니다.
예: /home.html, /img/pic.jpg, /
2. 정적 콘텐츠 파일 경로 구성
strcpy(cgiargs, "");
strcpy(filename, ".");
strcat(filename, uri);
if (uri[strlen(uri)-1] == '/')
strcat(filename, "home.html");
return 1;
- 정적 요청이면 cgiargs는 필요 없으므로 빈 문자열로 초기화합니다.
- filename은 현재 디렉터리(.)를 기준으로 파일 경로를 만듭니다.
→ "/home.html" → "./home.html" - 만약 URI가 /로 끝난다면,
기본 페이지 home.html을 자동으로 붙입니다.
→ "/" → "./home.html"
3.동적(dynamic) 콘텐츠 판별
else { /* Dynamic content */
ptr = index(uri, '?');
...
}
- "cgi-bin"이 포함된 경우, 동적 콘텐츠로 분기됩니다.
- index(uri, '?')는 '?' 문자의 위치를 찾습니다.
'?'는 **프로그램 인자(Query String)**의 시작을 의미합니다.
예: /cgi-bin/adder?15000&213
→ '?' 앞: 프로그램 경로, '?' 뒤: 인자 값
4.프로그램 인자(Query String) 분리
if (ptr) {
strcpy(cgiargs, ptr+1);
*ptr = '\0';
}
else
strcpy(cgiargs, "");
- '?'가 있으면 인자가 존재합니다.
- strcpy(cgiargs, ptr+1) → '?' 뒤의 문자열을 복사합니다.
예: /cgi-bin/adder?15000&213 → cgiargs = "15000&213" - *ptr = '\0' → '?'을 문자열 종료문자로 바꿔
"cgi-bin/adder"와 "15000&213"을 물리적으로 분리합니다.
- strcpy(cgiargs, ptr+1) → '?' 뒤의 문자열을 복사합니다.
- '?'가 없으면 인자가 없으므로 cgiargs를 빈 문자열로 둡니다.
serve_static 함수 — 정적 파일 전송
1. 파일 열기 전 준비
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
- srcfd: 서버가 열 파일(= 정적 콘텐츠)의 파일 디스크립터.
- srcp: 메모리에 매핑된 파일의 시작 주소를 저장할 포인터.
- filetype: MIME 타입 (예: "text/html", "image/gif")을 저장.
- buf: HTTP 응답 헤더를 임시로 저장할 버퍼.
2. 파일의 MIME 타입 구하기
get_filetype(filename, filetype);
get_filetype() 함수는 파일 확장자를 분석해서
브라우저가 이해할 수 있는 MIME 타입을 결정합니다.
| 파일 이름 | filetype 결과 |
| home.html | text/html |
| godzilla.gif | image |
| cat.jpg | image/jpeg |
->이 값은 나중에 HTTP 헤더의 "Content-type" 부분에 들어갑니다.
3. HTTP 응답 헤더 작성
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));
- sprintf() : 문자열을 이어 붙여 HTTP 응답 헤더를 만듭니다.
- Rio_writen() : 완성된 헤더를 클라이언트(브라우저)로 전송합니다.
브라우저야, 이건 200 OK 상태고, 이만큼의 크기, 이 타입의 파일을 보낼 거야” 라는 정보를 먼저 보내주는 단계예요.
예시)
HTTP/1.0 200 OK
Server: Tiny Web Server
Content-length: 1204
Content-type: text/html
4. 파일 열기(티스크에서 읽기 준비)
srcfd = Open(filename, O_RDONLY, 0);
- filename으로 지정된 파일을 읽기 전용(O_RDONLY) 으로 엽니다.
- 성공 시 파일 디스크립터를 반환 (srcfd).
- 실패 시 Open() 내부에서 에러 메시지 출력 후 종료합니다.
파일 열기를 시스템 콜(open()) 대신 Tiny의 안전 래퍼(Open())로 감싼 이유:
에러 발생 시 프로그램이 안전하게 종료되도록 하기 위해서예요.
5. 파일 내용을 메모리에 매핑
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
Close(srcfd);
- Mmap()은 메모리 매핑(memory mapping) 을 수행합니다.
→ 즉, 파일 내용을 한 번에 메모리 주소 공간으로 불러옵니다.
→ 이렇게 하면 read()로 반복해서 읽을 필요가 없습니다. - Close(srcfd) : 파일을 메모리에 올렸으니, 이제 파일 디스크립터는 닫습니다.
6. 파일 테이터(본문) 전송
Rio_writen(fd, srcp, filesize);
- 이제 진짜 파일 내용을 클라이언트에게 전송합니다.
- srcp: 메모리에 매핑된 파일의 시작 주소
- filesize: 파일 크기
이 부분에서 브라우저는 HTML, 이미지, CSS 같은파일 본문(body)을 받게 됩니다.
7. 메모리 해제
Munmap(srcp, filesize);
- 파일을 전송한 후에는, 메모리에 매핑된 파일 영역(srcp)을 해제해서 메모리 누수를 방지합니다.
get_filetype 함수 — MIME 타입 판별
void get_filetype(char *filename, char *filetype)
{
1. if (strstr(filename, ".html"))
2. strcpy(filetype, "text/html");
3. else if (strstr(filename, ".gif"))
4. strcpy(filetype, "image/gif");
5. else if (strstr(filename, ".jpg"))
6. strcpy(filetype, "image/jpeg");
7. else
8. strcpy(filetype, "text/plain");
}
이 함수는 파일 이름(filename)의 확장자를 확인해서
브라우저에 전송할 MIME 타입(filetype) 문자열을 결정합니다.
즉,
“이 파일은 어떤 종류의 데이터인가요?”
를 브라우저에게 알려주는 역할을 합니다.
serve_dynamic 함수 — 동적 콘텐츠 실행
1. 변수 선언
char buf[MAXLINE], *emptylist[] = { NULL };
- buf: HTTP 응답 헤더를 임시 저장할 버퍼
- emptylist: CGI 프로그램 실행 시 인자로 전달할 빈 문자열 배열
→ Execve()는 인자 리스트가 필요하지만,
Tiny는 추가 인자를 사용하지 않으므로 NULL로 초기화합니다.
2. HTTP 응답 상태 헤더 작성
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
- 클라이언트(브라우저)에게 HTTP 상태 코드 200 OK를 보냅니다.
- "이 요청은 정상적으로 처리되었어요" 라는 의미입니다.
- Rio_writen()으로 실제 전송까지 수행합니다.
3. 서버 정보 헤더 작성
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));
- 브라우저에게 서버 정보를 전달합니다.
- 이 부분은 단순히 "Server: Tiny Web Server" 문자열을 헤더에 추가하는 역할입니다.
- (실제 웹 서버들도 "Server: Apache" 같은 헤더를 보냅니다.)
4. 자식 프로세스 생성(Fork)
if (Fork() == 0) { /* child */
...
}
- Fork()를 호출해 자식 프로세스를 생성합니다.
- Fork()는 두 번 반환됩니다:
- 부모 프로세스 → 자식의 PID 반환
- 자식 프로세스 → 0 반환
- 따라서 if (Fork() == 0) 조건문은 자식 프로세스만 실행합니다.
👉 CGI 프로그램 실행은 자식 프로세스에서 이루어집니다.
부모는 기다리기만 합니다.
5.환경 변수 성정
setenv("QUERY_STRING", cgiargs, 1);
- CGI 프로그램은 인자(query string)를
환경 변수 QUERY_STRING으로 받습니다. - 예를 들어, 브라우저 요청이
/cgi-bin/adder?15000&213
라면 cgiargs는 "15000&213" 이고, 이 값이 환경 변수로 설정됩니다.
6. 표준 출력(stdout)을 클라이언트 소켓으로 리다이렉트
Dup2(fd, STDOUT_FILENO);
- Dup2()는 파일 디스크립터를 복제하는 함수입니다.
- 원래 CGI 프로그램은 printf() 하면 터미널(stdout) 로 출력되지만,
Dup2(fd, STDOUT_FILENO)를 통해 소켓(fd) 으로 방향을 바꿉니다.
7. CGI 프로그램 실행
Execve(filename, emptylist, environ);
- Execve()는 실행 중인 프로세스를 다른 프로그램으로 교체합니다.
- 여기서 filename은 실행할 CGI 프로그램 경로 ("./cgi-bin/adder")입니다.
- emptylist는 인자 리스트 (비어 있음)
- environ은 환경 변수 테이블 (QUERY_STRING 포함)
이 줄이 실행되면 자식 프로세스는 CGI 프로그램으로 완전히 바뀌고,
그 프로그램의 printf() 출력이 그대로 브라우저로 전송됩니다.
예시 결과:
Welcome to add.com
The answer is: 15000 + 213 = 15213
실행 흐름 다이어그램
┌──────────────────────────────┐
│ 클라이언트(브라우저) │
│ 요청: /cgi-bin/adder?10&20 │
└──────────────┬───────────────┘
│
▼
┌───────────────────────┐
│ Tiny Web Server │
│ (serve_dynamic 호출) │
└───────────────────────┘
│
│
[1] HTTP 응답 헤더 전송
├─> "HTTP/1.0 200 OK\r\n"
└─> "Server: Tiny Web Server\r\n"
│
▼
[2] 자식 프로세스 생성 (Fork)
├─ 부모 → 계속 실행
└─ 자식 → CGI 실행 준비
│
▼
┌───────────────────────────────────────────────┐
│ 자식 프로세스 (CGI 실행 준비) │
├───────────────────────────────────────────────┤
│ ① 환경 변수 등록: │
│ setenv("QUERY_STRING", "10&20", 1); │
│ │
│ ② 출력 방향 변경: │
│ Dup2(fd, STDOUT_FILENO); │
│ → printf() 결과가 브라우저로 전송됨 │
│ │
│ ③ CGI 프로그램 실행: │
│ Execve("./cgi-bin/adder", NULL, environ); │
│ → 자식 프로세스가 adder 프로그램으로 교체 │
└───────────────────────────────────────────────┘
│
▼
[3] CGI 프로그램 동작 (adder.c)
├─ QUERY_STRING 환경 변수 읽기
├─ 계산 수행: 10 + 20 = 30
├─ 결과 HTML 출력
│ printf("The answer is 10 + 20 = 30");
└─ 출력이 클라이언트 소켓으로 전송됨
│
▼
┌──────────────────────────────┐
│ 부모 프로세스 (Tiny 서버) │
│ Wait(NULL) → 자식 종료 대기 │
└──────────────────────────────┘
│
▼
┌──────────────────────────────┐
│ 클라이언트(브라우저) │
│ 수신: CGI 출력 결과 HTML │
│ │
│ "The answer is 10 + 20 = 30"│
└──────────────────────────────┘
clienterror 함수 — 에러 처리
1. 함수 헤더 (인자 설명)
void clienterror(int fd, char *cause, char *errnum,char *shortmsg, char *longmsg)
이 함수는 브라우저로 에러 메시지를 보낼 때 필요한 모든 정보를 매개변수로 받습니다.
예시)
clienterror(fd, filename, "404", "Not found", "Tiny couldn’t find this file");
| 매개변수 | 역할 |
| fd | |
| cause | 에러 |
| errnum | HTTP 상태 코드 |
| shortmsg | 상태 코드의 간단한 설명 |
| longmsg | 더 구체적인 에러 설명 문장 |
2. 버퍼 선언부
char buf[MAXLINE], body[MAXBUF];
- buf는 HTTP 응답의 헤더(header) 를 만들 때 사용합니다.
- body는 HTTP 응답의 본문(body) (HTML 내용)을 만들 때 사용합니다.
즉, buf와 body는 각각 “봉투(헤더)”와 “편지 내용(본문)” 역할이에요.
3. HTML본문 생성
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);
하나씩 보면 👇
- <html><title>Tiny Error</title>
→ 웹 페이지의 제목을 설정
→ 브라우저 탭에 “Tiny Error”라고 표시됨 - <body bgcolor="ffffff">
→ 배경색을 흰색으로 설정 - %s%s: %s\r\n
→ 예를 들어 "404: Not found" 같은 문장을 생성 - <p>%s: %s\r\n
→ 좀 더 자세한 설명 (예: "Tiny couldn’t find this file: home.html") - <hr><em>The Tiny Web server</em>
→ 구분선과 서버 이름 표시
이렇게 하면 body 안에는 다음과 같은 HTML 문자열이 완성돼요 👇
<html><title>Tiny Error</title>
<body bgcolor="ffffff">
404: Not found
<p>Tiny couldn’t find this file: home.html
<hr><em>The Tiny Web server</em>
브라우저가 이걸 받아서 예쁜 에러 페이지로 보여줍니다.
4. 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");
Rio_writen(fd, buf, strlen(buf));
브라우저가 HTML 문서임을 인식하고 에러 페이지를 렌더링합니다.
'web' 카테고리의 다른 글
| [WebRTC] - SFU 알아보기 (0) | 2025.12.22 |
|---|---|
| 웹서버 인증서 (0) | 2025.11.12 |
| HTTP (0) | 2025.11.12 |
| FD와 FDT 완전 정리 (1) | 2025.11.01 |
| DNS 추상화: 브라우저가 도메인을 IP로 바꾸는 여정 (0) | 2025.10.31 |