null

FastCGI на Java + конфигурация Apache Httpd

Зачем это нужно

Содержимое данной статьи вряд ли когда-нибудь кому-нибудь пригодится в современной веб-разработке, все-таки FastCGI в экосистеме Java абсолютно бесполезен в виду наличия сервлетов, реакторов и прочих более удобных технологий для разработки серверов на этом языке. Поэтому данная статья носит сугубо иллюстративно-развлекательный характер :)

 

Что такое FastCGI

В бородатых 90-ых были разработаны 2 стандарта, обеспечивающих запуск и исполнение серверных сценариев на различных ЯП: CGI и FastCGI. Оба стандарта просты как две копейки и среди достоинств у обоих в современном вебе можно выделить только возможность написания сценариев на произвольных ЯП (в том числе даже на bash-е, хотя он не является полноценным ЯП). К слову, позднее были разработаны всем известные стандарты WSGI/ASGI, ныне обеспечивающих основу для разработки прикладных серверов в мире Python, как нетрудно догадаться по созвучию аббревиатур смыслу стандартов - WSGI прямой идейный потомок CGI.

Небольшая сводка по тому как вообще устроен CGI. В CGI используется понятие веб-сервера и серверного сценария, написанного на произвольном ЯП. Веб-сервер при обработке запроса запускает CGI процесс серверного сценария и при помощи stdin/stdout общается с прикладной программой. Очевидным недостатком такого подхода являются высокие накладные расходы на запуск отдельного процесса под обработку каждого запроса.

Лекция 6. Общий шлюзовый интерфейс (CGI) | Веб-программирование

FastCGI стал развитием CGI в сторону оптимизации. Так, в FastCGI веб-сервер запускает пулл FastCGI процессов и общается с ними при помощи Unix sockets или TCP/IP sockets. Альтернативным вариантом может быть также внешний по отношении к веб-серверу FastCGI сервер. За счет поддержания постоянно запущенных процессов для обработки запросов снижаются накладные расходы. К тому же, так как появилась возможность использования внешного FastCGI сервера - стало возможным распределять обработку по нескольким узлам, что так же повышает потенциальную пропускную способность подобного решения. Важно отметить, что FastCGI не канул в лету как обычный CGI - используя эту идею ныне продолжают существовать и развиваться прикладные сервера на PHP.

Использование Nginx FastCGI Cache / Хабр

 

FastCGI на Java

С точки зрения наличия библиотек для подобного несуразного эксперимента - все плохо. Есть поддержка FastCGi у Netty, некоторые application-сервера типа Jetty поддерживают обращений к FastCGI-серверам, однако полноценного мощного инструмента для разработки бэкенда на Java + FastCGI автору статьи не удалось. Зато нашлась простенькая библиотека 97 года, предлагающая базовую абстракцию над TCP/IP сокетом и перенаправляющая потоки ввода-вывода сокета на System.in и System.out - https://github.com/FastCGI-Archives/fcgi2

С использованием данной библиотеки, сервер на Java выглядит следующим образом:

    public static void main(String[] args) throws IOException {
        var fcgiInterface = new FCGIInterface();
        while (fcgiInterface.FCGIaccept() >= 0) {
            var method = FCGIInterface.request.params.getProperty("REQUEST_METHOD");
            if (method == null) {
                System.out.println(errorResult("Unsupported HTTP method: null"));
                continue;
            }

            if (method.equals("GET")) {
                var queryString = FCGIInterface.request.params.getProperty("QUERY_STRING");
                if (queryString != null && queryString.equals("debug=1")) {
                    var paramsDump = FCGIInterface.request
                            .params
                            .entrySet()
                            .stream()
                            .map((entry) -> "%s: %s".formatted(entry.getKey().toString(), entry.getValue().toString()))
                            .reduce("", (acc, el) -> acc + "\n" + el);
                    System.out.println(echoPage(paramsDump));
                } else {
                    System.out.println(getHelloPage());
                }
                continue;
            }

            if (method.equals("POST")) {
                var contentType = FCGIInterface.request.params.getProperty("CONTENT_TYPE");
                if (contentType == null) {
                    System.out.println(errorResult("Content-Type is null"));
                    continue;
                }

                if (!contentType.equals("application/x-www-form-urlencoded")) {
                    System.out.println(errorResult("Content-Type is not supported"));
                    continue;
                }

                var requestBody = simpleFormUrlEncodedParsing(readRequestBody());
                var xStr = requestBody.get("x");
                var yStr = requestBody.get("y");
                if (xStr == null || yStr == null) {
                    System.out.println(errorResult("X and Y must be provided as x-www-form-urlencoded params"));
                    continue;
                }

                int x, y;
                try {
                    x = Integer.parseInt(xStr.toString());
                } catch (NumberFormatException e) {
                    System.out.println(errorResult("X must be an integer"));
                    continue;
                }
                try {
                    y = Integer.parseInt(yStr.toString());
                } catch (NumberFormatException e) {
                    System.out.println(errorResult("Y must be an integer"));
                    continue;
                }

                System.out.println(sumPage(x, y, x + y));
                continue;
            }

            System.out.println(errorResult("Unsupported HTTP method: " + method));
        }
    }

В этом примере сервер отвечает на GET запрос простой html-страничкой, а на POST запрос отдает html-страничку с вычисленной суммой x + y. Как можно видеть, основная работа с TCP/IP скрыта за вызовом FCGIAccept(). Также важно отметить, что библиотека работает поверх статических полей и методов класса FCGIInterface, например FCGIInterface.request.params содержит параметры, передаеваемые веб-сервером FastCGI-серверу. Перед чтением тела запроса с System.in следует вызвать FCGIInterface.request.inStream.fill() для заполнения внутреннего стрима-буфера, используемого библиотекой. В противном случае данные тела запроса останутся непрочитанными из сокета и, следовательно, недоступными приложению.

 

Конфигурация Apache Httpd

Чтобы браузер мог обращаться к FastCGI-серверу нужен промежуточный веб-сервер, пусть для примера это будет Apache Httpd. Для его настройки потребуется скомпилировать и установить модуль mod_fastcgi - https://github.com/FastCGI-Archives/mod_fastcgi.gi. В качестве примера приведу Dockerfile для сборки кастомного образа Httpd.

FROM httpd:2.4.62-bookworm

RUN apt update && apt install -y git libapr1 libapr1-dev apache2-dev

RUN git clone https://github.com/FastCGI-Archives/mod_fastcgi.git /mod_fastcgi && \
    cd /mod_fastcgi/ && \
    apxs -i -a -n fastcgi -o mod_fastcgi.so -c *.c

RUN mkdir /usr/local/apache2/fcgi-bin

COPY httpd.conf /usr/local/apache2/conf/httpd.conf

Также потребуется в конфигурации веб-сервера слегка поиграть с бубном:

LoadModule fastcgi_module modules/mod_fastcgi.so

FastCgiExternalServer "/usr/local/apache2/fcgi-bin/hello-world.jar" -host app:9000 -nph
Alias /fcgi-bin/ "/usr/local/apache2/fcgi-bin/"
<Directory "/usr/local/apache2/fcgi-bin">
     AllowOverride None
     Options None
     Require all granted
</Directory>

Здесь при помощи директивы LoadModule подгружается дополнительный модуль в рантайн Httpd. Директива FastCgiExternalService нужна для того, чтобы определить имя файла, по запросу которого будет выполняться обращение к внешнему FastCGI-серверу. Кроме того, чтобы веб-сервер мог искать виртуальные файлы, нацеленные на FastCGI сервер, потребуется сконфигурировать директорию, где якобы они хранятся при помощи директивы Directory (обратим внимание, что настраиваемая директория должна существовать в фс веб-сервера, иначе ничего не получится).

 

Как-то так :)

Вперед