본문 바로가기
개인공부/윤성우의 열혈 TCP&IP 소켓 프로그래밍

Chapter 24 - HTTP 서버 제작하기

by 하고싶은건많은놈 2023. 2. 24.

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

댓글