Chapter 24 - HTTP 서버 제작하기
Chapter 24 - HTTP 서버 제작하기
24-1 : HTTP(Hypertext Transfer Protocol)의 개요
웹(Web) 서버 : HTTP 프로토콜을 기반으로 웹 페이지에 해당하는 파일을 클라이언트에게 전송하는 역할의 서버
HTTP : Hypertext(이동이 가능한 텍스트) Transfer Protocol - Hypertext의 전송을 목적으로 설계된 어플리케이션 레벨 프로토콜, TCP/IP를 기반으로 구현됨
Stateless
프로토콜 : 클라이언트의 요청에 응답 후 바로 연결 종료, 서버가 클라이언트의 상태정보를 유지하지 않음- 이를 보완하고자 쿠키(Cookies)와 세션(Session)이라는 기술이 사용됨
- 클라이언트와 웹 서버 사이의 요청방식은 표준화되어있음
- 요청 라인 : 요청방식(목적)에 대한 정보,
GET
(주로 데이터 요청)과POST
(데이터 전송) 방식 등이 있음 - 메시지 헤더 : 요청에 사용된 브라우저 정보, 사용자 인증정보 등 HTTP 메시지에 대한 부가적인 정보
- 헤더와 몸체 사이에는 공백 라인이 한 줄 삽입되어있어 둘을 구분함
- 메시지 몸체 : 클라이언트가 웹서버에 전송할 데이터,
POST
방식으로의 요청 필요
- 요청 라인 : 요청방식(목적)에 대한 정보,
- 웹서버가 클라이언트에 전달하는 응답 메시지 또한 표준화되어있음
- 상태 라인 : 클라이언트의 요청에 대한 결과정보를 상태코드로 전달
- 200 - OK / 404 - Not Found / 400 - Bad Request
- 메시지 헤더 : 전송되는 데이터의 타입 및 길이정보
- 헤더와 몸체 사이에는 공백 라인이 한 줄 삽입되어있어 둘을 구분함
- 메시지 몸체 : 클라이언트가 요청한 파일의 데이터
- 상태 라인 : 클라이언트의 요청에 대한 결과정보를 상태코드로 전달
23-2 : IOCP의 단계적 구현
웹 서버는 HTTP 프로토콜을 기반으로 하기 때문에 IOCP 및 epoll
모델을 적용함으로써 엄청난 이점을 얻을 수는 없음
윈도우 멀티쓰레드 기반 웹 서버 예시
// webserv_win.cpp
#include <iostream>
#include <process.h>
#include <winsock2.h>
using namespace std;
#define BUF_SIZE 2048
#define BUF_SMALL 100
unsigned WINAPI RequestHandler(void *arg);
char* ContentType(char *file);
void SendData(SOCKET sock, char *ct, char *filename);
void SendErrorMSG(SOCKET sock);
void ErrorHandling(char *message);
int main(int argc, char *argv[])
{
WSADATA wsaData;
SOCKET hServSock, hClntSock;
SOCKADDR_IN servAdr, clntAdr;
HANDLE hThread;
DWORD dwThreadID;
int clntAdrSize;
if(argc != 2)
{
cout << "Usage : " << argv[0] << " <port>\n";
exit(1);
}
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
ErrorHandling("WSAStartup() error");
hServSock = socket(PF_INET, SOCK_STREAM, 0);
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family = AF_INET;
servAdr.sin_addr.s_addr = htonl(INADDR_ANY);
servAdr.sin_port = htons(atoi(argv[1]));
if (::bind(hServSock, (SOCKADDR*)&servAdr, sizeof(servAdr)) == SOCKET_ERROR)
ErrorHandling("bind() error");
if (::listen(hServSock, 5) == SOCKET_ERROR)
ErrorHandling("listen() error");
// 요청 및 응답
while (1)
{
clntAdrSize = sizeof(clntAdr);
hClntSock = accept(hServSock, (SOCKADDR*)&clntAdr, &clntAdrSize);
cout << "Connection Request : " << inet_ntoa(clntAdr.sin_addr) << " : " << ntohs(clntAdr.sin_port) << endl;
hThread = (HANDLE)_beginthreadex(NULL, 0, RequestHandler, (void *)hClntSock, 0, (unsigned*)&dwThreadID);
}
closesocket(hServSock);
WSACleanup();
return 0;
}
unsigned WINAPI RequestHandler(void *arg)
{
SOCKET hClntSock = (SOCKET)arg;
char buf[BUF_SIZE];
char method[BUF_SMALL];
char ct[BUF_SMALL];
char fileName[BUF_SMALL];
recv(hCLntSock, buf, BUF_SIZE, 0);
if (strstr(buf, "HTTP/") == NULL) // HTTP에 의한 요청인지 확인
{
SendErrorMSG(hClntSock);
closesocket(hClntSock);
return 1;
}
strcpy(method, strtok(buf, " /"));
if (strcmp(method, "GET")) // Get 방식 요청인지 확인
SendErrorMSG(hClntSock);
strcpy(fileName, strtok(NULL, " /")); // 요청 파일이름 확인
strcpy(ct, ContentType(fileName)); // Content-type 확인
SendData(hClntSock, ct, fileName); // 응답
return 0;
}
void SendData(SOCKET sock, char *ct, char *fileName)
{
char protocol[] = "HTTP/1.0 200 OK\r\n";
char servName[] = "Server : siple web server\r\n";
char cntLen[] = "Content-length : 2048\r\n";
char cntType[BUF_SMALL];
char buf[BUF_SIZE];
FILE *sendFile;
sprintf(cntType, "Content-type : %s\r\n\r\n", ct);
if ((sendFile = fopen(fileName, "r")) == NULL)
{
SendErrorMSG(sock);
return ;
}
// 헤더 정보 전송
send(sock, protocol, strlen(protocol), 0);
send(sock, servName, strlen(servName), 0);
send(sock, cntLen, strlen(cntLen), 0);
send(sock, cntType, strlen(cntType), 0);
// 요청 데이터 전송
while (fgets(buf, BUF_SIZE, sendFile) != NULL)
send(sock, buf, strlen(buf), 0);
closesocket(sock); // HTTP 프로토콜에 의해 응답 후 종료
}
void SendErrorMSG(SOCEKT sock) // 오류 발생시 메시지 전달
{
char protocol[] = "HTTP/1.0 400 Bad Request\r\n";
char servName[] = "Server : siple web server\r\n";
char cntLen[] = "Content-length : 2048\r\n";
char cntType[] = "Content-type : text/html\r\n\r\n";
char content[] = "<html><head><title>NETWORK</title></head>"
"<body><font size = +5><br>오류 발생! 요청 파일명 및 요청 방식 확인!"
"</font></body></html>";
send(sock, protocol, strlen(protocol), 0);
send(sock, servName, strlen(servName), 0);
send(sock, cntLen, strlen(cntLen), 0);
send(sock, cntType, strlen(cntType), 0);
send(sock, content, strlen(content), 0);
closesocket(sock);
}
char* ContentType(char *file) // Content-Type 구분
{
char extension[BUF_SMALL];
char fileName[BUF_SMALL];
strcpy(fileName, file);
strtok(fileName, ".");
strcpy(extension, strtok(NULL, "."));
if (!strcmp(extension, "html") || !strcmp(extension, "htm"))
return "text/html";
else
return "text/plain";
}
void ErrorHandling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
리눅스 멀티쓰레드 기반 웹 서버 예시
// webserv_linux.cpp
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <sys/socket.h>
using namespace std;
#define BUF_SIZE 2048
#define BUF_SMALL 100
void* request_handler(void *arg);
char* content_type(char *file);
void send_data(FILE *fp, char *ct, char *filename);
void send_error(FILE *fp);
void serror_handling(char *message);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t clnt_adr_size;
char buf[BUF_SIZE];
pthread_t t_id;
if(argc != 2)
{
cout << "Usage : " << argv[0] << " <port>\n";
exit(1);
}
serv_sock = socket(PF_INET, SOCK_STREAM, 0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family = AF_INET;
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
serv_adr.sin_port = htons(atoi(argv[1]));
if (::bind(serv_sock, (sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
serror_handling("bind() error");
if (::listen(serv_sock, 20) == -1)
serror_handling("listen() error");
// 요청 및 응답
while (1)
{
clnt_adr_size = sizeof(clnt_adr);
clnt_sock = accept(serv_sock, (sockaddr*)&clnt_adr, &clnt_adr_size);
cout << "Connection Request : " << inet_ntoa(clnt_adr.sin_addr) << " : " << ntohs(clnt_adr.sin_port) << endl;
pthread_create(&t_id, NULL, request_handler, &clnt_sock);
pthread_detach(t_id);
}
close(serv_sock);
return 0;
}
void* request_handler(void *arg)
{
int clnt_sock = *((int*)arg);
char req_line[BUF_SMALL];
FILE *clnt_read;
FILE *clnt_write;
char method[BUF_SMALL];
char ct[BUF_SMALL];
char file_name[BUF_SMALL];
clnt_read = fdopen(clnt_sock, "r");
clnt_write = fdopen(dup(clnt_sock), "w");
fgets(req_line, BUF_SMALL, clnt_read);
if (strstr(req_line, "HTTP/") == NULL)
{
send_error(clnt_write);
fclose(clnt_read);
fclose(clnt_write);
return NULL;
}
strcpy(method, strtok(req_line, " /"));
strcpy(file_name, strtok(NULL, " /")); // 요청 파일이름 확인
strcpy(ct, content_type(file_name)); // Content-type 확인
if (strcmp(method, "GET")) // Get 방식 요청인지 확인
{
send_error(clnt_write);
fclose(clnt_read);
fclose(clnt_write);
return NULL;
}
fclose(clnt_read);
send_data(clnt_write, ct, file_name); // 응답
}
void send_data(FILE *fp, char *ct, char *file_name)
{
char protocol[] = "HTTP/1.0 200 OK\r\n";
char server[] = "Server : simple web server\r\n";
char cnt_len[] = "Content-length : 2048\r\n";
char cnt_type[BUF_SMALL];
char buf[BUF_SIZE];
FILE *send_file;
sprintf(cnt_type, "Content-type : %s\r\n\r\n", ct);
if ((send_file = fopen(file_name, "r")) == NULL)
{
send_error(fp);
return ;
}
// 헤더 정보 전송
fputs(protocol, fp);
fputs(server, fp);
fputs(cnt_len, fp);
fputs(cnt_type, fp);
// 요청 데이터 전송
while (fgets(buf, BUF_SIZE, send_file) != NULL)
{
fputs(buf, fp);
fflush(fp);
}
fflush(fp);
fclose(fp);
}
void send_error(FILE *fp) // 오류 발생시 메시지 전달
{
char protocol[] = "HTTP/1.0 400 Bad Request\r\n";
char server[] = "Server : siple web server\r\n";
char cnt_len[] = "Content-length : 2048\r\n";
char cnt_type[] = "Content-type : text/html\r\n\r\n";
char content[] = "<html><head><title>NETWORK</title></head>"
"<body><font size = +5><br>오류 발생! 요청 파일명 및 요청 방식 확인!"
"</font></body></html>";
fputs(protocol, fp);
fputs(server, fp);
fputs(cnt_len, fp);
fputs(cnt_type, fp);
fflush(fp);
}
char* content_type(char *file) // Content-Type 구분
{
char extension[BUF_SMALL];
char file_name[BUF_SMALL];
strcpy(file_name, file);
strtok(file_name, ".");
strcpy(extension, strtok(NULL, "."));
if (!strcmp(extension, "html") || !strcmp(extension, "htm"))
return "text/html";
else
return "text/plain";
}
void serror_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
내용 확인문제
- 웹 서버와 웹 서버의 접속에 사용되는 웹 브라우저에 대한 설명으로 옳지 않은 것을 모두 고르면?
- 웹 브라우저는 소켓을 생성하고 이 소켓으로 서버에 접속하는 클라이언트 프로그램으로 보기 어렵다. (X)
- 웹 서버는 TCP 소켓을 생성해서 서비스한다. 그 이유는 클라이언트와 연결을 일정시간 이상 유지한 상태에서 각종 정보를 주고받기 때문이다. (X)
- Hypertext와 일반 text의 가장 큰 차이점은 이동성의 존재유무이다. (O)
- 웹 서버는 웹 브라우저가 요청하는 파일을 전송해주는 일종의 파일전송 서버로 볼 수 있다. (O)
- 웹 서버에는 웹 브라우저가 아니면 접속이 불가능하다. (X)
- HTTP 프로토콜과 관련된 설명으로 옳지 않은 것을 모두 고르면?
- HTTP 프로토콜은 상태를 유지하지 않는 Stateless 프로토콜이다. 따라서 TCP가 아닌 UDP를 기반으로 구현해도 문제되지 않는다. (X)
- HTTP 프로토콜을 가리켜 상태를 유지하지 않는 Stateless 프로토콜이라 하는 이유는, 한번의 요청과 응답의 과정을 마치고 나면 연결을 끊어버리기 때문이다. 따라서 동일한 웹 서버와 동일한 웹 브라우저가 총 세번의 요청 및 응답의 과정을 거치는 경우에는 총 세번의 소켓생성 과정을 거치게된다. (O)
- 서버가 클라이언트에게 전송하는 상태코드에는 클라이언트의 요청에 대한 결과정보가 담겨있다. (O)
- HTTP 프로토콜은 인터넷을 기반으로 하는 프로토콜이다. 따라서 인터넷 기반에서 많은 클라이언트에게 서비스를 제공할 수 있도록 HTTP를 Stateless 프로토콜로 설계한 것이다. (O)
-
IOCP와
epoll
은 우수한 성능을 보장하는 대표적인 서버 모델들이다. 그런데 HTTP 프로토콜을 기반으로 하는 웹 서버에 이 모델을 적용하는 경우에는, 다른 모델에 비해 우수한 성능을 보장한다고 말할 수 없다. 그렇다면 그 이유는 무엇인가?웹 서버에서는 소켓의 연결 요청 및 응답 과정 후 바로 연결을 종료하기 때문에 둘 이상의 소켓을 계속해서 관리할 필요가 없기 때문