W4-W5: C언어로 HTTP 서버 구현
참고 자료: C, TCP 기반으로 간단한 HTTP 서버 작성하기
# 1. 구현 내용
- 서버 프로그램이 존재하는 디렉터리를 기준으로 파일을 접근할 수 있는 서버
- 개발 순서
- socket(), bind(), listen() 등을 활용하여 TCP 소켓을 만듬
- accept() 후에 HTTP 프로토콜로 처리하는 함수를 만듬
- 처리 중에 에러가 발생하면 404, 500 상태 코드로 응답
- 과제 제출 예시 영상
# 2. TCP 소켓 생성하기
1) TCP란?
- Transmission Control Protocol
- 서버와 클라이언트 간에 데이터를 신뢰성 있게 전달하기 위해 만들어진 프로토콜
- 데이터를 전송하기 전에 데이터 전송을 위한 연결을 만드는 연결지향 프로토콜
- 데이터는 네트워크선로를 통해 전달되는 과정에서 손실되거나 순서가 뒤바뀌어서 전달될 수 있는데, TCP는 손실을 검색해내서, 이를 교정하고 순서를 재조립할 수 있도록 해줌
2) TCP 특징
- 신뢰성
- 흐름 제어
- 다중화
- 연결형 서비스
- TCP 연결은 데이터를 양방향으로 운반할 수 있음
- TCP 연결은 3way handshake 절차를 사용하여 열림
3) Socket 이란?
- 소켓은 프로세스가 드넓은 네트워크 세계로 데이터를 내보내거나 혹은 그 세계로부터 데이터를 받기 위한 실제적인 창구 역할을 함
- 프로세스가 데이터를 보내거나 받기 위해서는 소켓을 열어서 소켓에 데이터를 써보내거나 소켓으로부터 데이터를 읽어들여야 함
- 소켓은 프로토콜, IP 주소, 포트 넘버로 정의됨
- 소켓은 떨어져 있는 두 호스트를 연결해주는 도구로써 인터페이스의 역할을 하는데, 데이터를 주고 받을 수 있는 구조체로 소켓을 통해 데이터 통로가 만들어짐
- 소켓은 역할에 따라 서버 소켓, 클라이언트 소켓으로 구분됨
4) 소켓통신의 흐름
- 서버(Server): 클라이언트 소켓의 연결 요청을 대기하고, 연결 요청이 오면 클라이언트 소켓을 생성하여 통신이 가능하게 함
- socket() 함수를 이용하여 소켓을 생성
- bind() 함수로 ip와 port 번호를 설정하게 됨
- listen() 함수로 클라이언트와 접근 요청에 수신 대기열을 만들어 몇 개의 클라이언트를 대기 시킬지 결정
- accept() 함수를 사용하여 클라이언트와의 연결을 기다림
- 클라이언트(Client): 실제로 데이터 송수신이 일어나는 것은 클라이언트 소켓임
- socket() 함수로 가장먼저 소켓을 엶
- connect() 함수를 이용하여 통신 할 서버의 설정된 ip와 port 번호에 통신을 시도
- 통신을 시도 시, 서버가 accept() 함수를 이용하여 클라이언트의 socket descriptor를 반환
- 이를 통해 클라이언트와 서버가 서로 read(), write()를 하며 통신 (이 과정이 반복됨)
5) TCP 소켓 준비
- bind() 함수
/*
생성된 소켓 lsock(sd)에 주소 할당
return bind() 값
*/
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));
}
생성된 소켓 lsock(sd)에 주소 할당 및 bind된 결과를 리턴
- main() 함수
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: %s {port}\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);
}
printf("bind() success\n"); // 바인드 성공
if (listen(lsock, 10) < 0) {
perror("[ERR] failed to listen lsock.\n");
exit(1);
}
printf("socket() success\n"); // 소켓 성공
signal(SIGCHLD, SIG_IGN);
while (1) {
asock = accept(lsock, (struct sockaddr *)&remote_sin, &remote_sin_len);
if (asock < 0) {
perror("[ERR] failed to accept.\n");
continue;
}
pid = fork(); // 멀티프로세스 생성 -> 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 소켓 생성
이때 멀티 스레드 방식과 멀티 프로세스 방식을 사용할 수 있는데 해당 코드는 멀티 프로세스를 사용함
# 3. HTTP 구현
1) HTTP 프로토콜
- 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("/")를 읽어 적절한 리소스를 반환해야 함
- 응답에 적절한 상태 코드와 헤더를 적어주어야 함
- Content-Length: 브라우저가 헤더 다음 몇 바이트만큼 읽어야 하는지 알려줌
- Content-Type: body가 어떤 타입인지, 브라우저에서 어떻게 보여주어야 하는지 알려줌
2) HTTP 구현 함수
- fill_header(): 상태 코드, 헤더 내용 등을 주어진 포인터에 채움
/*
주어진 매개 변수를 기준으로 HTTP 헤더 형식 지정
*/
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);
}
- find_mime(): 파일의 확장자를 참조하여 적절한 Content Type 값을 주어진 포인터에 채움
/*
uri로부터 content type 찾기
*/
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");
}
- handle_404(), handle_500(): 상태 코드 400, 500로 응답할 때 사용
/*
handler for not found(404)
*/
void handle_404(int asock) {
char header[BUF_SIZE];
fill_header(header, 404, sizeof(NOT_FOUND_CONTENT), "text/html");
write(asock, header, strlen(header));
}
/*
handler for internal server error(500)
*/
void handle_500(int asock) {
char header[BUF_SIZE];
fill_header(header, 500, sizeof(SERVER_ERROR_CONTENT), "text/html");
write(asock, header, strlen(header));
}
- http_handler(): main() 함수에서 호출되는 대표 handler로, 요청된 파일을 읽으려고하며, 파일 접근에 성공하면 상태 코드 200으로 파일의 내용을 정상적으로 보냄. 도중에 실패하면 위의 handle_404() 혹은 handle_500()을 호출
/*
main http handler
요청된 리소스를 열고 전송
failure에 대한 에러 호출
*/
void http_handler(int asock) {
char header[BUF_SIZE];
char buf[BUF_SIZE];
char safe_uri[BUF_SIZE];
char *local_uri;
struct stat st;
if (read(asock, buf, BUF_SIZE) < 0) {
perror("[ERR] Failed to read request.\n");
handle_500(asock); return;
}
printf("%s",buf); // 버퍼에 읽어들인 내용 모두 출력
char *method = strtok(buf, " ");
char *uri = strtok(NULL, " ");
strcpy(safe_uri, uri);
if (!strcmp(safe_uri, "/")) strcpy(safe_uri, "/index.html"); // '/'라면 자동으로 index.html을 match
local_uri = safe_uri + 1;
if (stat(local_uri, &st) < 0) {
handle_404(asock); return;
}
int fd = open(local_uri, O_RDONLY);
if (fd < 0) {
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);
}
# 4.산출물
- 컴파일: gcc -o http_server http_server.c (MacOS/Linux)
- 사용 파일: index.html, index.css, index.js
- index.html
<!doctype html>
<html>
<head>
<title>Testing</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<p>
<strong>Hello, nonsong!</strong>
</p>
<img src="img/nonsong.png">
<script src="index.js"></script>
</body>
</html>
- index.css
* {
background-color: skyblue;
color: gray;
}
img {
border-radius: 10px;
}
- index.js
console.log("Hello!");
console.log("Thank you.");
- 결과 영상
* 실행 후 터미널에서 해당 port에 대한 프로세스를 kill 해주어야 다음번에도 실행이 가능함 (다른 포트를 사용한다면 안해줘도 됨)
sudo lsof -i :8080
kill [위의 결과로 나온 프로세스 번호]
'etc... > 빡공팟(P4C) 4기' 카테고리의 다른 글
[P4C] W7: 올드 스쿨 취약점과 올드 스쿨 공격 기법 공부하기 (0) | 2022.06.05 |
---|---|
[P4C] W6: Double Linked List CRUD 구현, 어셈블리로 구구단 구현, Stack 개념 공부하기 (0) | 2022.05.29 |
[P4C] W3: 코드업 기초 100제 70번 대 이후 문제들 중 가장 어려웠던 10문제 write-up 작성하기 (0) | 2022.05.08 |
[P4C] W2: 코드업 기초 100제 70번 이하 문제들 중 가장 어려웠던 5문제 write-up 작성하기 (0) | 2022.04.29 |
[P4C] W1: 코드업 기초 100제 20번~30번대 문제들 중 가장 어려웠던 5문제 write-up 작성하기 (0) | 2022.04.23 |