[Webserver]miniWebserver+Reverse Proxy-(12)

👉🏻 주요 변경사항은 아래와 같습니다.
The major changes are as follows.

✔️ ❗️ 표시된 부분이 변경사항입니다.
The parts marked with ❗️ are the changes.

✔️ 리버스 프록시 기능을 추가했습니다.
Added a reverse proxy feature.

✔️ 나머지 설명은 코드 주석을 참고하세요
Please refer to the code comments for the rest of the explanation.

👉🏻코드 / Code

✔️ CMakeLists.txt

# 실행 파일 생성 대상 소스 파일 나열 / List source files to be used for creating executable files
add_executable(main
    Main.cpp
    Router.cpp
    Server.cpp
    HttpsServer.cpp 
    Proxy.cpp // 파일추가 / Added file❗️
)

✔️ Proxy.h

— 파일 추가 / Add file

#ifndef PROXY_H
#define PROXY_H

#include <string>

// 리버스 프록시 요청 함수 선언 / Declaration of reverse proxy request function
// backend_ip: 백엔드 서버 IP 주소 / Backend server IP address,
// backend_port: 포트번호 / port number
// client_request: 클라이언트에서 받은 HTTP 요청 전체 문자열 / The entire string of the HTTP request received from the client
// 반환: 백엔드로부터 받은 HTTP 응답 문자열
// Return: HTTP response string received from the backend

std::string forward_request_to_backend(const std::string& backend_ip, int backend_port, const std::string& client_request);

#endif // PROXY_H

✔️ Proxy.cpp

— 파일 추가 / Add file

#include "Proxy.h"
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <chrono>     // timeout
#include <fcntl.h>

// 간단한 timeout 설정 함수
// bool set_nonblocking(int sock) {
//     int flags = fcntl(sock, F_GETFL, 0);
//     return fcntl(sock, F_SETFL, flags | O_NONBLOCK) != -1;
// }

std::string forward_request_to_backend(const std::string& backend_ip,
                                       int backend_port,
                                       const std::string& client_request) {

    // IPV4로 소켓 연결 / Socket connection via IPv4
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        return "HTTP/1.1 500 Internal Server Error\r\n\r\n";
    }

    /*
    10초 connect timeout
    - 10은 초(seconds)를 의미하고, 0은 마이크로초(microseconds)를 의미합니다..
    - setsockopt() 함수는 소켓의 다양한 옵션을 설정합니다.
    - SO_RCVTIMEO: 소켓에서 데이터를 수신할 때 최대 대기 시간을 설정하는 옵션입니다.
                   즉, 10초 이내에 데이터가 오지 않으면 수신 함수(recv 등)가 타임아웃됩니다.
    - SO_SNDTIMEO: 데이터를 전송할 때 최대 대기 시간을 지정합니다.
                   10초 이내에 전송이 되지 않으면 타임아웃 처리합니다.

    10 second connect timeout
    - 10 represents seconds, and 0 represents microseconds.
    - The setsockopt() function sets various options for a socket.
    - SO_RCVTIMEO: This option sets the maximum waiting time when receiving data from a socket.
                   In other words, if data does not arrive within 10 seconds, the receive function (such as recv) times out.
    - SO_SNDTIMEO: Specifies the maximum waiting time when transmitting data.
                   If transmission does not occur within 10 seconds, a timeout is triggered.
   */
    struct timeval tv = {10, 0};
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
    setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));

    sockaddr_in serv_addr{};
    serv_addr.sin_family = AF_INET; //IPV4
    serv_addr.sin_port = htons(backend_port); // Host TO Network Short(big endian)

    // inet_pton : 텍스트형식을 이진 IP주소로변환 / Convert text format to binary IP address
    // backend_ip.c_str() : IP 문자열 / IP string
    // &serv_addr.sin_addr) : 저장될 위치 / Location to be saved
    //
    if (inet_pton(AF_INET, backend_ip.c_str(), &serv_addr.sin_addr) <= 0) {
        close(sock);
        return "HTTP/1.1 500 Internal Server Error\r\n\r\n";
    }

    // 소켓으로 지정된 주소에 접속시도 실패시 오류 반환
    // Returns an error if the attempt to connect to the address specified by the socket fails.
    if (connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) {
        close(sock);
        return "HTTP/1.1 504 Gateway Timeout\r\n\r\n";
    }

    // 전체 데이터 전송 보장 / Full data transfer guaranteed
    size_t total_sent = 0; // 전송된 바이트 누적 / Accumulation of transferred bytes

    // 요청 크기만큼 전송 반복 / Repeat transmission for the requested size
    while (total_sent < client_request.size()) {
        // client_request.data() + total_sent
        // total_sent로 보낸 데이터의 다음 메모리 위치
        // Next memory location of the data sent with total_sent
        //
        // 데이터 전송이 완료된 위치에 커서가 있고 여기서 다시 데이터 크기만큼 전송
        // The cursor is at the location where data transmission is complete,
        // and from here, data of the size is transmitted again.
        ssize_t sent = send(sock, client_request.data() + total_sent,
                           client_request.size() - total_sent, 0);
        if (sent < 0) {
            close(sock);
            return "HTTP/1.1 502 Bad Gateway\r\n\r\n";
        }
        total_sent += sent;
    }

    // 응답 수신 / Receive response
    // html파일 내용 읽어오기 / Read HTML file content
    char buffer[8192];
    std::string response;
    response.reserve(65536);  // 대략적인 초기 용량

    // 반복적으로 한번에 8192만큼 데이터 읽어와서 response변수에 저장하기
    // Repeatedly read 8192 bytes of data at a time and store them in the response variable
    ssize_t received;
    while ((received = recv(sock, buffer, sizeof(buffer), 0)) > 0) {
        response.append(buffer, received);
    }

    close(sock);

    if (received < 0 && response.empty()) {
        return "HTTP/1.1 502 Bad Gateway\r\n\r\n";
    }

    // 응답이 아예 없으면 에러
    // Error handling if there is no response at all
    if (response.empty()) {
        return "HTTP/1.1 502 Bad Gateway\r\n\r\n";
    }

    return response;
}

✔️main.cpp

        // 리버스 프록시 적용 / Apply reverse proxy❗️
        // 간단히 /api/ 경로를 리버스 프록시로 판단
        // Simply treat the /api/ path as a reverse proxy
        if ((method == "GET" || method == "POST") &&
            (path == "/api" || path.find("/api/") == 0)) {

            std::string backend_ip = "127.0.0.1";
            int backend_port = 8080;

            std::string modified_req = req;

            // /api → / 로 변경 (백엔드가 루트 경로로 받도록)
            if (path == "/api") {
                size_t pos = modified_req.find(" /api ");
                if (pos != std::string::npos) {
                    modified_req.replace(pos, 5, " /");
                }
            }
            else if (path.find("/api/") == 0) {
                size_t pos = modified_req.find("/api/");
                if (pos != std::string::npos) {
                    modified_req.replace(pos, 5, "/");
                }
            }

            // Host 헤더 수정 / Modify Host header
            size_t host_pos = modified_req.find("\r\nHost:");
            if (host_pos == std::string::npos) {
                host_pos = modified_req.find("\r\nhost:");
            }
            if (host_pos != std::string::npos) {
                size_t line_end = modified_req.find("\r\n", host_pos + 1);
                if (line_end != std::string::npos) {
                    modified_req.replace(host_pos, line_end - host_pos, "\r\nHost: 127.0.0.1:8080");
                }
            }

            // Connection: close
            size_t conn_pos = modified_req.find("Connection:");
            if (conn_pos != std::string::npos) {
                size_t line_end = modified_req.find("\r\n", conn_pos);
                if (line_end != std::string::npos) {
                    modified_req.replace(conn_pos, line_end - conn_pos, "Connection: close");
                }
            }

            return forward_request_to_backend(backend_ip, backend_port, modified_req);
        }

👉🏻 빌드 / Build

cd build
cmake ..
make

👉🏻 실행 / Run

./main

👉🏻 브라우저 접속 / Access Browser

— 아래의 주소로 접속하면 http://localhost:8080/api 로 리버스 프록시되는 것을 확인 할 수 있습니다.
If you access the address below, you can verify that it is reverse proxyed to http://localhost:8080/api.

https://localhost:6443/api
http://localhost:5080/api

Leave a Reply