민혁이의 IT스토리

tiny.c로 배우는 HTTP와 소켓 프로그래밍의 기초 본문

web

tiny.c로 배우는 HTTP와 소켓 프로그래밍의 기초

FE_Minhyuk 2025. 11. 7. 10:24

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"을 물리적으로 분리합니다.
  • '?'가 없으면 인자가 없으므로 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);

하나씩 보면 👇

  1. <html><title>Tiny Error</title>
    → 웹 페이지의 제목을 설정
    → 브라우저 탭에 “Tiny Error”라고 표시됨
  2. <body bgcolor="ffffff">
    → 배경색을 흰색으로 설정
  3. %s%s: %s\r\n
    → 예를 들어 "404: Not found" 같은 문장을 생성
  4. <p>%s: %s\r\n
    → 좀 더 자세한 설명 (예: "Tiny couldn’t find this file: home.html")
  5. <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