이전 포스팅에서는 GET method의 정적 컨텐츠를 위주로 살펴보았다.
이번에는 동적 컨텐츠의 처리와 POST method를 다뤄보자.
쿼리 스트링으로 인자를 전달하고 CGI 프로그램의 결과를 반환받는다.
결과물은 다음과 같다.

이렇게 ? 이후의 쿼리 스트링을 파싱해서 서버 내부의 CGI 프로그램을 수행한다.
좀 더 상세하게 과정을 따라가보자.
.
.
http://localhost:12345/cgi-bin/adder?num1=1&num2=2
위와 같은 URL로 Tiny 서버에 GET 요청이 수신된다면 num1=1&num2=2의 내용을 인자로 CGI 프로그램을 수행한다.
우리 예제의 CGI프로그램은 두 수를 더하는 간단한 adder 프로그램이다.
이 프로그램은 cgi-bin 디렉터리 내, adder.c로 구성하면 된다.
/*
* adder.c - a minimal CGI program that adds two numbers together
*/
/* $begin adder */
#include "csapp.h"
int main(void)
{
char *buf, *p;
char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE];
int n1 = 0, n2 = 0;
/* Extract the two arguments */
if ((buf = getenv("QUERY_STRING")) != NULL)
{
p = strchr(buf, '&');
*p = '\0';
strcpy(arg1, buf);
strcpy(arg2, p + 1);
n1 = atoi(strchr(arg1, '=') + 1);
n2 = atoi(strchr(arg2, '=') + 1);
}
/* Make the response body */
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");
/* Generate the HTTP response */
printf("Content-type: text/html\r\n");
printf("Content-length: %d\r\n", (int)strlen(content));
printf("\r\n");
printf("%s", content);
fflush(stdout);
exit(0);
}
/* $end adder */
CGI 프로그램은 printf()를 통해 표준 출력(stdout)으로 HTML을 출력하며, 이 stdout은 dup2(fd, STDOUT_FILENO)를 통해 클라이언트 소켓에 연결되어 있어 곧바로 브라우저로 전달된다.
브라우저의 개발자 도구 탭으로 네트워크 헤더를 보면 아래와 같이 정상적으로 요청/응답하고있음을 확인할 수 있다.
[ Request ]
Request URL: http://localhost:12345/cgi-bin/adder?num1=1&num2=2
Request Method: GET
Status Code: 200 OK
[ Response ]
Content-Length: 129
Content-Type: text/html
Server: Tiny Web Server

하나 더,
우리는 네이버에서 검색을 하는 등, 웹페이지에서 어떠한 동작을 수행할 때 직접 url을 변경하는 경우는 거의 없다.
그니까, 57이라는 숫자와 13이라는 숫자를 더한 결과를 보기 위해
?num1=57&num2=13
이런 url을 직접 타이핑하지 않는다는 말이다.
어떤 UI가 있으면 그 입력창에 입력하고 전송, 혹은 확인 버튼만 누르면 결과 화면이 뜨는 게 보통이다.

이런 화면을 만들어서 계산하기 버튼을 누르면 결과 화면으로 이동할 수 있도록 재구성 해보자.
간단하게, 기존에 있던 home.html 말고 하나의 html을 더 생성해주면 된다.
나는 adder.html을 하나 생성하여 아래와 같이 작성하였다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>add</title>
</head>
<body>
<h1>더하고 싶은 두 숫자를 입력하세요</h1>
<form action="/cgi-bin/adder" method="GET">
<input type="text" name="num1">
+
<input type="text" name="num2">
<input type="submit" value="계산하기">
</form>
</body>
</html>
여기에서 중요한 점!
form 태그를 사용해야 한다.
form 태그가 가지는 특성이 있다.
<form> 태그 내의 <input type="submit"> 버튼을 클릭하면 form 전체의 데이터를 서버로 전송한다.
GET 요청이라면, 이렇게 매핑된 정보를 uri 쿼리 스트링에 담아 보내고,
POST 요청이라면, body에 담아서 보낸다.
따라서, adder.c 서브 루틴에서 필요한 두 인자 num1, num2 를 input 태그에 name 속성으로 지정해서 GET 요청을 보내면 된다.
이 때, 서브 루틴에서 필요로 하는 인자를 정확하게 일치시켜야 한다는 점을 명심!

그렇게하면 계산하기 버튼을 누름과 동시에 HTML 내에서는 submit이 수행되며, adder.c 서브 루틴으로 동적 컨텐츠가 수행된다.
사실 여기까지 했다면 POST method는 크게 다를 바 없다.
앞서 말했듯, 인자를 서버로 넘기는 방식의 차이가 있을 뿐 복잡하지 않다.
레츠꼬~
다시 한 번 정리해보자면,
POST method는 데이터를 요청 본문(body)에 포함한다.
GET 요청에서는 URL의 길이에 따른 데이터 제한이 불가피하지만 POST 요청에서는 이 점을 극복할 수 있다.
하지만 그렇기 때문에, 이 데이터의 길이(서버가 얼마나 읽어야 하는지)의 정보가 추가로 요구된다.
그래서 우리는 아래 두 가지를 추가로 정의해주며, POST method를 아주 간단한 수준으로 구현하고자 한다.
content-length 정보를 추가로 기억한다.content-length 정보를 바탕으로 body에 포함된 데이터를 파싱한다`매우 간단하다. method만 바꿔준다.
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>add</title>
</head>
<body>
<h1>더하고 싶은 두 숫자를 입력하세요</h1>
<form action="/cgi-bin/adder" method="POST">
<input type="text" name="num1">
+
<input type="text" name="num2">
<input type="submit" value="계산하기">
</form>
</body>
</html>
Tiny 서버의 read_requesthdrs() 함수에서 Content-Length를 파싱하도록 구현을 수정한다.
요청 헤더에는 우리가 필요한 본문의 컨텐츠 길이 정보가 포함된다.
따라서, 전역변수 하나를 정의하여 요청 헤더의 Content-Length 정보를 전역적으로 저장해준다.
이 값은 CGI 프로그램의 인자로 데이터를 넘기기 위해 필수적이다!
int content_length = 0;
void read_requesthdrs(rio_t *rp)
{
char buf[MAXLINE];
while (Rio_readlineb(rp, buf, MAXLINE) != 0 && strcmp(buf, "\r\n"))
{
printf("%s", buf);
if (strncasecmp(buf, "Content-Length:", 15) == 0)
{
content_length = atoi(buf + 15);
}
}
}
가장 중요한 부분이다!!!!!!!!
Tiny 서버의 serve_dynamic 함수를 수정한다.
rp를 추가해준다.method를 추가해준다.
void serve_dynamic(int fd, char *filename, char *cgiargs, char *method, rio_t *rp)
{
char buf[MAXLINE], *emptylist[] = {NULL};
/* Return first part of HTTP response */
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));
if (Fork() == 0)
{
if (strcasecmp(method, "POST") == 0)
{
char *bodybuf = malloc(content_length + 1);
Rio_readnb(rp, bodybuf, content_length);
bodybuf[content_length] = '\0';
setenv("QUERY_STRING", bodybuf, 1);
free(bodybuf);
}
else // GET 요청
{
setenv("QUERY_STRING", cgiargs, 1);
}
Dup2(fd, STDOUT_FILENO);
Execve(filename, emptylist, environ);
}
Wait(NULL); /* Parent waits for and reaps child */
}
이렇게 수정하면 끝이다!
다시 같은 adder.html 페이지로 이동해서 계산하기 버튼을 눌러 submit 해보자.

결과는 다음과 같다.

GET method를 사용했을 때와 결과는 동일하지만, 내부적인 처리 로직은 다르다.
개발자 도구로 네트워크탭을 다시 확인해보자.

[ Payload ]
num1: 57
num2: 13

[ Request ]
Request URL: http://localhost:12345/cgi-bin/adder
Request Method: POST
Status Code: 200 OK
[ Response ]
Content-Length: 133
Content-Type: text/html
Server: Tiny Web Server
페이로드의 Form Data를 통해, 요청 본문에 데이터가 잘 들어간 모습을 확인할 수 있다.