Зачем это нужно
Содержимое данной статьи вряд ли когда-нибудь кому-нибудь пригодится в современной веб-разработке, все-таки FastCGI в экосистеме Java абсолютно бесполезен в виду наличия сервлетов, реакторов и прочих более удобных технологий для разработки серверов на этом языке. Поэтому данная статья носит сугубо иллюстративно-развлекательный характер :)
Что такое FastCGI
В бородатых 90-ых были разработаны 2 стандарта, обеспечивающих запуск и исполнение серверных сценариев на различных ЯП: CGI и FastCGI. Оба стандарта просты как две копейки и среди достоинств у обоих в современном вебе можно выделить только возможность написания сценариев на произвольных ЯП (в том числе даже на bash-е, хотя он не является полноценным ЯП). К слову, позднее были разработаны всем известные стандарты WSGI/ASGI, ныне обеспечивающих основу для разработки прикладных серверов в мире Python, как нетрудно догадаться по созвучию аббревиатур смыслу стандартов - WSGI прямой идейный потомок CGI.
Небольшая сводка по тому как вообще устроен CGI. В CGI используется понятие веб-сервера и серверного сценария, написанного на произвольном ЯП. Веб-сервер при обработке запроса запускает CGI процесс серверного сценария и при помощи stdin/stdout общается с прикладной программой. Очевидным недостатком такого подхода являются высокие накладные расходы на запуск отдельного процесса под обработку каждого запроса.
FastCGI стал развитием CGI в сторону оптимизации. Так, в FastCGI веб-сервер запускает пулл FastCGI процессов и общается с ними при помощи Unix sockets или TCP/IP sockets. Альтернативным вариантом может быть также внешний по отношении к веб-серверу FastCGI сервер. За счет поддержания постоянно запущенных процессов для обработки запросов снижаются накладные расходы. К тому же, так как появилась возможность использования внешного FastCGI сервера - стало возможным распределять обработку по нескольким узлам, что так же повышает потенциальную пропускную способность подобного решения. Важно отметить, что FastCGI не канул в лету как обычный CGI - используя эту идею ныне продолжают существовать и развиваться прикладные сервера на PHP.
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
(обратим внимание, что настраиваемая директория должна существовать в фс веб-сервера, иначе ничего не получится).
Как-то так :)