EYEN
[빡공팟 4기] 4~5주차:c로 http 서버 만들기 본문
*wsl2에서 설치한 우분투로 하면 실행파일은 생성되지만 웹에서의 접속이 안된다고한다.
그래서 vmware라는 가상머신이랑 우분투를 설치해서 연결시켰다.
#1 준비
1. HTTP(hyper text transfer protocol)
*hyper text:링크를 가지며 링크로 연결된 문서에 대한 모델
=>그 문서들을 나르기 위한 프로토콜(규약)
2. 서버

-socket programming, 통신과 웹서버의 관계 관련 그림
3. http 서버의 작동
server에서 소켓을 생성해 어떤 포트에서 요청을 기다리고 있으면, 클라이언트가 인터넷 브라우저에서 서버의 (ip:port)로 요청을 보낸다. 이때 인터넷 브라우저는 http 프로토콜(=규약)로 구현이 되어있다. 그래서 http request 형식에 맞춰 서버에게 메시지를 보내야 하는 것이다.
4. http 서버 구현 절차
1. socket(), bind(), listen()를 활용해 TCP 소켓을 만든다
2. accept()를 활용해 클라이언트와 연결하는 함수를 만든다.
3. 그리고 그에 따른 에러들에 대해 어떻게 대응할지도 정해준다.
더 자세히
1) socket()
int socket(int domain, int type, int protocol);
=소켓에 대한 정보들
-int domain: 어떤 영역(domain)에서 통신할 것인지(protocol family)
(1)AF_UNIX(프로세스끼리)
(2)AF_INET(IPv4)
(3)AF_INET6(IPv6)가 올 수 있음
IPv6는 ip주소가 부족해질까봐 만든건데 공유기의 등장으로 잘 안씀. 그래서 주로 AF_INET을 씀!
-int type: 어떤 서비스 type의 소켓을 생성할 것인가.
(1) SOCK_STREAM(TCP)
(2) SOCK_DGRAM(UDP)
(3) SOCK_RAW(Raw 방식 TCP나 UDP를 거치지 않고 바로 IP계층)
ㄴ여기서 계층이란, OSI모델을 말한다. 7계층인 http와 가까운 tcp를 거치지 않고 바로 ip로 간다는 뜻.
TCP는 패킷을 보내서 받았으면 받았다고 다시 패킷을 보내는데 UDP는 패킷을 보내면? 끝이다. 그래서 게임서버같이 빠른 속도를 구현할 때 쓴다고 한다.
-int protocol: 소켓이 어떤 프로토콜(규약)을 따를 것인지
(1) IPROTO_TCP:TCP
(2) IPROTO_UDP:UDP
(3) 0:type에서 미리 정해진 경우: type에서 stream이면 0으로 입력해도 IPROTO_TCP하는 것과 같다는 뜻
return:
sockfd: 소켓을 가리키는 소켓 디스크립터 반환
-1: 소켓 생성 실패
0>= : 소켓 디스크립터
2) bind()
: ip,port, protocol로 소켓을 구성(unique한 소켓을 만듦)
-int sockfd
socket의 리턴값으로 받은 걸 넣어주기
-struct sockaddr *myaddr
서버의 IP주소를 넣어준다
-socklen_t addrlen:
주소의 길이를 넣어준다.
return:
성공시 0, 실패시 -1
3) listen()
: 서버가 클라이언트의 연결 요청을 '대기'하는 상태로 만듦
- int sockfd
- int backlog: 연결 대기열의 크기. 몇개가 기다릴 수 있는지.
return:
성공시 0, 실패시 -1
4) accept()
: listening socket을 갖고 클라이언트의 연결을 기다림
여기서 클라이언트와 클라이언트 주소가 나오고 연결이 되는 거임.
서버소켓에 클라이언트를 연결하는 함수
그래서 두번째, 세번째 주소엔 클라이언트 정보가 들어간다.
-int sockfd
-struct sockaddr*addr
클라이언트 주소정보 구조체
-socklen_t *addrlen
클라이언트 주소정보 구조체 길이
#2 TCP socket 만들기
1. TCP(Transimission Control Protocol)
- IP 프로토콜 위에서 연결형 서비스를 지원하는 전송계층 프로토콜
*OSI 모델과 TCP/IP모델
TCP는 전송 계층에 존재하고, IP는 네트워크 계층에 존재하고, http는 응용계층에 존재한다

OSI모델 TCP/IP 모델
2. socket
- 프로토콜, ip주소, port number로 정의되는 구조체이며, 소켓을 열어서 데이터를 주고 소켓을 통해 데이터를 받을 수 있다.
-bind()
#define BUF_SIZE 1000
#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>
//bind()
int bind_lsock(int lsock, int port) { //socket이랑 socket주소랑 묶기
struct sockaddr_in sin; //sockaddr:socketaddress 구조체
sin.sin_family = AF_INET; //IPv4 쓸거임
sin.sin_addr.s_addr = htonl(INADDR_ANY); //ip, htonl:host to network long(32bit), INADDR_ANY: 아무데나
sin.sin_port = htons(port); //port, htons:host to network short(16bit)
return bind(lsock, (struct sockaddr *)&sin, sizeof(sin));
}
-main()
socket()/bind()/listen()/accept()
int main(int argc, char **argv) { //main 함수의 원형, int argc:메인함수에 전달되는 정보의 개 고정됨
int port, pid; //char* argv:메인함수에 전달되는 실질적인 정보
int lsock, asock;
struct sockaddr_in remote_sin;
socklen_t remote_sin_len;
if (argc < 2) {
exit(0);
}
port = atoi(argv[1]); //atoi:문자열을 int 숫자로 변경
lsock = socket(AF_INET, SOCK_STREAM, 0); //TCP 소켓 생성
//에러났을 때 알려주기
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);
}
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();
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");
}
}
}
signal(SIGCHLD, SIG_IGN)
소스코드 상에서 pid 위의 프로세스들을 exit가 나와도 계속 유지해야 하기 때문에 활용함.
pid=fork()
현재 실행되고 있는 프로세스(부모 프로세스)를 복사해 pid 값만 다르고 같은 자식 프로세스를 만들어줌.
하나의 서비스에 대해서 여러 개의 클라이언트를 받을 수도 있는데 부모 프로세스가 응답하면 서버당 한 클라이언트만 접속할 수 있게됨. 그러므로 부모 프로세스가 자신의 자식프로세스를 fork()해서 전달해야한다.
#3 http 구현하기
-fill_header(): 상태에 따라 헤더 내용을 바꿔줌
void fill_header(char *header, int status, long len, char *type) {
char status_text[40];
switch (status) {
case 200: strcpy(status_text, "OK"); //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(): /html /png 같은 확장자를 보고 적절한 content type을 ct_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 400번대 오류는 요청 오류로, 클라이언트에 오류가 있는 상태이다.
(1) 400: Bad Request 클라이언트가 서버에 잘못된 요청을 했을 때
(2) 401: Unauthorized 서버가 클라이언트의 요청에 대해 http인증 확인을 요구할 때
(3) 403: Forbidden 클라이언트의 요청에 대해 접근을 차단할 때
(4) 404: Not Found 클라이언트가 서버에 요청한 자료가 존재하지 않을 때
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));
}
-handle_500 500번대 오류는 서버가 유효한 요청을 명백하게 수행하지 못한 상태이다.
(1) 500: internal Server Error 서버에 오류가 발생해 요청을 수행할 수 없을 때
(2) 502: Bad Gateway 서버가 게이트웨이나 프록시 역할을 하고 있거나 내부 방의 웹서버로부터 잘못된 응답을 받았을 때
(3) 503:Service Unvailable 서버가 오버로드되거나 유지관리를 위해 다운됐을 때
(4) 504: Gateway Timeout 서버가 게이트웨이나 프록시 역할을 하고 있거나 서버에서 제때 요청을 받지 못했을 때
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));
}
-http_handler() main 함수에서 호출되어, 요청된 파일을 읽어서 접근해보고 성공했는지 실패했는지 알려줌
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;
}
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); //fd:file descripter 서버잘못
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);
}
#4 과제 제출
*404 not found를 위해 wrong.html을 실행한 파일들과 다른 폴더에 저장했다.
400은 어떻게 하는지 모르겠다...
*한번 갔던 포트의 프로세스를 없애줘야 다음에 같은 포트를 이용할 수 있다. 리눅스에서는 net-tool을 get한 뒤에
sudo netstat -lntp
해서 PID밑에 적힌 숫자들을 확인하고
sudo kill 숫자들
이렇게 하면 지워줄 수 있다.
-참고한 내용
많은 곳에서 참고했지만 기억이 안나고... 제일 많이 참고한 곳
[C언어 실시간 강의19] TCP/IP 소켓 프로그래밍 도전(채팅 프로그램 만들기) - YouTube
C, TCP 기반으로 간단한 HTTP 서버 작성하기 (tistory.com)
'대외활동 및 팀플 > 빡공팟(P4C) 시스템 해킹 트랙 4기' 카테고리의 다른 글
[빡공팟 4기] 6주차 과제: 어셈블리어로 구구단 구현하기/c로 더블 링크드 리스트 구현하기 (0) | 2022.05.29 |
---|---|
[빡공팟 4기]6주차 과제: 제공된 파일 한장으로 요약 (0) | 2022.05.25 |
[빡공팟 4기] 3주차: C언어 배우기 코드업 기초 100제(71번~)write-up (0) | 2022.05.08 |
[빡공팟 4기] 2주차: C언어 배우기 코드업 기초 100제(70번 이하)write-up (0) | 2022.05.01 |
[빡공팟 4기] 1주차: C언어 배우기 코드업 기초 100제(20~30번) write-up (0) | 2022.04.24 |