앗! 광고가 차단되었어요!
글 내용이 방문자께 도움이 되었다면, 광고 차단 프로그램 해제를 고려해주세요 😀.
이 글에서는 대학교 수업 중 "네트워크 프로그래밍"의 단골 과제인, C와 TCP를 기반으로 HTTP 서버를 작성한다.
(안타깝게도) "C, C++를 이용한 웹서버"라는 키워드로 블로그 유입이 많이 되고 있어 복습할 겸 작성해봤다.
오늘 필자가 개발하고자 하는 HTTP 서버는 리눅스에서 동작할 수 있는 간단한 서버로,
서버 프로그램(a.out)이 존재하는 디렉터리를 기준으로 파일을 접근할 수 있는 서버다.
예를 들어
a.out이 /home/user/c-serv/에 있고, 8000번으로 bind 한다면
브라우저에서 localhost:8000/index.html을 요청하면 /home/user/c-serv/index.html 파일을 반환하고,
브라우저에서 localhost:8000/index.css를 요청하면 /home/user/c-serv/index.css 파일을 반환하는 웹서버다.
HTTP 서버를 작성하기 위해서 다음의 개발 순서를 생각할 수 있다.
- socket(), bind(), listen() 등을 활용하여 TCP 소켓을 준비한다.
- accept() 후에 HTTP 프로토콜로 처리(요청을 읽고 적절히 응답)하는 함수를 준비한다.
- 처리 중 에러가 발생하면 404, 500의 상태 코드로 응답하는 것을 잊지 않는다.
TCP 소켓 준비
먼저 main 함수를 보이겠다. 아래 코드는 HTTP 관련 함수를 제외한 코드이다.
#define BUF_SIZE 1000
#define HEADER_FMT "HTTP/1.1 %d %s\nContent-Length: %ld\nContent-Type: %s\n\n"
#define NOT_FOUND_CONTENT "<h1>404 Not Found</h1>\n"
#define SERVER_ERROR_CONTENT "<h1>500 Internal Server Error</h1>\n"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
/*
@func assign address to the created socket lsock(sd)
@return bind() return value
*/
int bind_lsock(int lsock, int port) {
struct sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = htonl(INADDR_ANY);
sin.sin_port = htons(port);
return bind(lsock, (struct sockaddr *)&sin, sizeof(sin));
}
/* ...skip: codes related to HTTP... */
int main(int argc, char **argv) {
int port, pid;
int lsock, asock;
struct sockaddr_in remote_sin;
socklen_t remote_sin_len;
if (argc < 2) {
printf("Usage: \n");
printf("\t%s {port}: runs mini HTTP server.\n", argv[0]);
exit(0);
}
port = atoi(argv[1]);
printf("[INFO] The server will listen to port: %d.\n", port);
lsock = socket(AF_INET, SOCK_STREAM, 0);
if (lsock < 0) {
perror("[ERR] failed to create lsock.\n");
exit(1);
}
if (bind_lsock(lsock, port) < 0) {
perror("[ERR] failed to bind lsock.\n");
exit(1);
}
if (listen(lsock, 10) < 0) {
perror("[ERR] failed to listen lsock.\n");
exit(1);
}
// to handle zombie process
signal(SIGCHLD, SIG_IGN);
while (1) {
printf("[INFO] waiting...\n");
asock = accept(lsock, (struct sockaddr *)&remote_sin, &remote_sin_len);
if (asock < 0) {
perror("[ERR] failed to accept.\n");
continue;
}
pid = fork();
if (pid == 0) {
close(lsock); http_handler(asock); close(asock);
exit(0);
}
if (pid != 0) { close(asock); }
if (pid < 0) { perror("[ERR] failed to fork.\n"); }
}
}
TCP, 소켓 프로그래밍을 배운 학생이라면 이 코드가 TCP 소켓을 생성한다는 것을 알 것이다.
몇 가지 강조하면:
- 멀티 스레드를 채택하지 않고 멀티 프로세스를 사용했다. 이는 필자의 개인적인 선택이다. 스레드 형식을 사용해도 된다.
- SIGCHLD에 SIG_IGN를 사용했다. 보통 waitpid 하는 signal handler를 사용하곤 하는데, 대신 SIG_IGN 방법을 사용할 수도 있다. 자세한 내용은 아래 링크를 참고하자.
Do I need to do anything with a SIGCHLD handler if I am just using wait() to wait for 1 child to finish at a time?
I've got a program that is forking to a child to do some work, but I am only doing one child at a time at this time. I am using wait() to wait for the child to finish, do I need to do anything with
stackoverflow.com
HTTP 프로토콜 살펴보기
HTTP가 처음이라면 MDN을 참조하자. 아래 링크를 꼭 읽기 바란다.
An overview of HTTP
HTTP is the foundation of any data exchange on the Web and it is a client-server protocol, which means requests are initiated by the recipient, usually the Web browser. A complete document is reconstructed from the different sub-documents fetched, for inst
developer.mozilla.org
HTTP response status codes
HTTP response status codes indicate whether a specific HTTP request has been successfully completed. Responses are grouped in five classes: Informational responses (100–199) Successful responses (200–299) Redirects (300–399) Client errors (400–499)
developer.mozilla.org
정리하면, HTTP 프로토콜은 TCP 소켓을 바탕으로 특정한 형식, 포맷으로 데이터를 주고받는 것이라고 할 수 있다.
(브라우저가 보내는) 요청과 (서버가 보내는) 응답은 다음과 같아야 한다.
GET / HTTP/1.1
Host: developer.mozilla.org
Accept-Language: fr
HTTP/1.1 200 OK
Date: Sat, 09 Oct 2010 14:28:02 GMT
Server: Apache
Last-Modified: Tue, 01 Dec 2009 20:18:22 GMT
ETag: "51142bc1-7449-479b075b2891b"
Accept-Ranges: bytes
Content-Length: 29769
Content-Type: text/html
<!DOCTYPE html... (here comes the 29769 bytes of the requested web page)
요청은 브라우저에서 보내주므로, 우리가 구현할 서버의 책임은 이 요청을 읽고 적절한 응답을 보내주는 것이다.
그러므로:
- 요청에서 Path "/"를 읽어 적절한 리소스를 반환해야 한다. "/index.html"이라면 index.html 파일 내용으로 응답해야 한다.
- 응답에 적절한 상태 코드(200, OK)와 헤더를 적어줘야 한다. 우리가 필수적으로 포함할 헤더는 Content-Length, Content-Type이다. Content-Length에는 리소스 또는 파일의 크기를, Content-Type은 파일 확장자에 따른 MIME Type 값을 적어준다.
- Content-Length, Content-Type은 중요하다. Content-Length는 브라우저가 헤더 다음 몇 바이트만큼 읽어야 하는지 알려주고, Content-Type은 body가 어떤 타입인지, 브라우저에서 어떻게 보여줘야 하는지 알려준다. 예를 들어 HTML 파일을 보내주는데 Content-Type을 text/plain으로 알려주면, 브라우저는 단순히 HTML 코드 그대로를 화면에 보여준다.
HTTP 구현 함수들
필자가 작성한 함수는 총 5개다.
- fill_header(char *, ...): 상태 코드, 헤더 내용 등을 주어진 포인터에 채운다. 처음 보인 코드에 HTTP 헤더 포맷 매크로(HEADER_FMT)를 정의한 것을 확인할 수 있다. 이 함수에서 매크로를 사용한다.
- find_mime(char *, ...): 파일의 확장자를 참조하여 적절한 Content Type 값을 주어진 포인터에 채운다.
- handle_404(int), handle_500(int): 상태 코드 404, 500로 응답하고자 할 때 사용한다.
- http_handler(...): main 함수에서 호출되는 대표 handler 함수다. 요청된 파일을 읽으려고 하며, 파일 접근에 성공하면 상태 코드 200으로 파일의 내용을 정상적으로 보낸다. 도중에 실패하면 handle_404(), handle_500()을 호출한다.
그럼 5개 함수의 구현을 보이겠다.
/*
@func format HTTP header by given params
@return
*/
void fill_header(char *header, int status, long len, char *type) {
char status_text[40];
switch (status) {
case 200:
strcpy(status_text, "OK"); break;
case 404:
strcpy(status_text, "Not Found"); break;
case 500:
default:
strcpy(status_text, "Internal Server Error"); break;
}
sprintf(header, HEADER_FMT, status, status_text, len, type);
}
/*
@func find content type from uri
@return
*/
void find_mime(char *ct_type, char *uri) {
char *ext = strrchr(uri, '.');
if (!strcmp(ext, ".html"))
strcpy(ct_type, "text/html");
else if (!strcmp(ext, ".jpg") || !strcmp(ext, ".jpeg"))
strcpy(ct_type, "image/jpeg");
else if (!strcmp(ext, ".png"))
strcpy(ct_type, "image/png");
else if (!strcmp(ext, ".css"))
strcpy(ct_type, "text/css");
else if (!strcmp(ext, ".js"))
strcpy(ct_type, "text/javascript");
else strcpy(ct_type, "text/plain");
}
/*
@func handler for not found
@return
*/
void handle_404(int asock) {
char header[BUF_SIZE];
fill_header(header, 404, sizeof(NOT_FOUND_CONTENT), "text/html");
write(asock, header, strlen(header));
write(asock, NOT_FOUND_CONTENT, sizeof(NOT_FOUND_CONTENT));
}
/*
@func handler for internal server error
@return
*/
void handle_500(int asock) {
char header[BUF_SIZE];
fill_header(header, 500, sizeof(SERVER_ERROR_CONTENT), "text/html");
write(asock, header, strlen(header));
write(asock, SERVER_ERROR_CONTENT, sizeof(SERVER_ERROR_CONTENT));
}
/*
@func main http handler; try to open and send requested resource, calls error handler on failure
@return
*/
void http_handler(int asock) {
char header[BUF_SIZE];
char buf[BUF_SIZE];
if (read(asock, buf, BUF_SIZE) < 0) {
perror("[ERR] Failed to read request.\n");
handle_500(asock); return;
}
char *method = strtok(buf, " ");
char *uri = strtok(NULL, " ");
if (method == NULL || uri == NULL) {
perror("[ERR] Failed to identify method, URI.\n");
handle_500(asock); return;
}
printf("[INFO] Handling Request: method=%s, URI=%s\n", method, uri);
char safe_uri[BUF_SIZE];
char *local_uri;
struct stat st;
strcpy(safe_uri, uri);
if (!strcmp(safe_uri, "/")) strcpy(safe_uri, "/index.html");
local_uri = safe_uri + 1;
if (stat(local_uri, &st) < 0) {
perror("[WARN] No file found matching URI.\n");
handle_404(asock); return;
}
int fd = open(local_uri, O_RDONLY);
if (fd < 0) {
perror("[ERR] Failed to open file.\n");
handle_500(asock); return;
}
int ct_len = st.st_size;
char ct_type[40];
find_mime(ct_type, local_uri);
fill_header(header, 200, ct_len, ct_type);
write(asock, header, strlen(header));
int cnt;
while ((cnt = read(fd, buf, BUF_SIZE)) > 0)
write(asock, buf, cnt);
}
지금까지 간단하게 서버를 작성해보았다.
하루 정도 투자해서 코드를 작성한 것이라 부족한 점이 있을 수 있다. 결과는 다음과 같다.

(이미지는 lena라고 불리는 이미지로, 비전 분야에서 가끔 사용된다. Hello, World 같은)
요즘 C를 기반으로 HTTP 서버를 작성하는 소스는 널리고 널려서, 다음과 같은 심화 질문을 던져보는 것도 좋겠다.
- 현재는 fork로 멀티 프로세스를 채택했다. 멀티 스레드 형식으로 바꿔보자.
- http://localhost:8000//root 로 접속하면 Internal Server Error가 발생한다. 보안을 위해 접근가능한 디렉터리를 한정시켜보자. 또는 요청할 수 있는 확장자를 제한해보자. 디렉터리를 요청한 경우 어떻게 처리할 것인지 고민해보자.
- handle_404, handle_500에서 응답의 body로 짧은 text을 넘겨주고 있다. 이 대신 에러 페이지 HTML을 작성해보고, handle_*에서 이를 넘겨주도록 만들어보자. 필요하면 코드의 간결성도 고려하자.
- 현재는 method를 고려하지 않고 있다. POST 메서드 및 요청의 form data를 처리하려면 어떻게 코드를 수정해야 하는지 고민해보자. 그리고 form data에 따라 응답 HTML 내용을 바꿀 수 있을까?
마지막 질문은 구현을 할 필요는 없다. C에서 이를 처리하려면 복잡해질 것이고,
Python, Ruby 계열의 서버 프레임워크에서 이미 이 기능을 쉽게 제공하고 있다.
'공돌이' 카테고리의 다른 글
문서화를 위한 drf-yasg 적용하기 (2) | 2020.10.26 |
---|---|
Python: Decorator (0) | 2020.10.26 |
Python: Context Manager (0) | 2020.10.26 |
Python: Generator (0) | 2020.10.26 |
Coursera 재정지원 (Financial Aid) 요청하기 (0) | 2020.07.26 |