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);
}

내용 확인문제

  1. 웹 서버와 웹 서버의 접속에 사용되는 웹 브라우저에 대한 설명으로 옳지 않은 것을 모두 고르면?
    • 웹 브라우저는 소켓을 생성하고 이 소켓으로 서버에 접속하는 클라이언트 프로그램으로 보기 어렵다. (X)
    • 웹 서버는 TCP 소켓을 생성해서 서비스한다. 그 이유는 클라이언트와 연결을 일정시간 이상 유지한 상태에서 각종 정보를 주고받기 때문이다. (X)
    • Hypertext와 일반 text의 가장 큰 차이점은 이동성의 존재유무이다. (O)
    • 웹 서버는 웹 브라우저가 요청하는 파일을 전송해주는 일종의 파일전송 서버로 볼 수 있다. (O)
    • 웹 서버에는 웹 브라우저가 아니면 접속이 불가능하다. (X)
  2. HTTP 프로토콜과 관련된 설명으로 옳지 않은 것을 모두 고르면?
    • HTTP 프로토콜은 상태를 유지하지 않는 Stateless 프로토콜이다. 따라서 TCP가 아닌 UDP를 기반으로 구현해도 문제되지 않는다. (X)
    • HTTP 프로토콜을 가리켜 상태를 유지하지 않는 Stateless 프로토콜이라 하는 이유는, 한번의 요청과 응답의 과정을 마치고 나면 연결을 끊어버리기 때문이다. 따라서 동일한 웹 서버와 동일한 웹 브라우저가 총 세번의 요청 및 응답의 과정을 거치는 경우에는 총 세번의 소켓생성 과정을 거치게된다. (O)
    • 서버가 클라이언트에게 전송하는 상태코드에는 클라이언트의 요청에 대한 결과정보가 담겨있다. (O)
    • HTTP 프로토콜은 인터넷을 기반으로 하는 프로토콜이다. 따라서 인터넷 기반에서 많은 클라이언트에게 서비스를 제공할 수 있도록 HTTP를 Stateless 프로토콜로 설계한 것이다. (O)
  3. IOCP와 epoll은 우수한 성능을 보장하는 대표적인 서버 모델들이다. 그런데 HTTP 프로토콜을 기반으로 하는 웹 서버에 이 모델을 적용하는 경우에는, 다른 모델에 비해 우수한 성능을 보장한다고 말할 수 없다. 그렇다면 그 이유는 무엇인가?

    웹 서버에서는 소켓의 연결 요청 및 응답 과정 후 바로 연결을 종료하기 때문에 둘 이상의 소켓을 계속해서 관리할 필요가 없기 때문