Pull to refresh

Web-приложение на C/C++ с помощью FastCGI — это просто

Reading time20 min
Views114K
Добрый день.
В этой статье я бы хотел рассказать про протокол FastCGI и способы работы с ним. Не смотря на то, что сам протокол и его реализация появились ещё в 1996 году, подробных руководств по этому протоколу просто нет — разработчики так и не написали справки к собственной библиотеке. Зато года два назад, когда я только начал пользоваться этим протоколом, часто встречались фразы типа «я не совсем понимаю, как пользоваться этой библиотекой». Именно этот недостаток я и хочу исправить — написать подробное руководство по использованию данного протокола в многопоточной программе и рекомендации по выбору различных параметров, которым могли бы воспользоваться все желающие.

Хорошая новость — способ кодирования данных в FastCGI и в CGI одинаковый, меняется только способ их передачи: если CGI-программа использует интерфейс стандартного ввода-вывода, то FastCGI-программа — сокеты. Другими словами, нужно всего лишь разобраться с несколькими функциями библиотеки для работы с FastCGI, а дальше просто воспользоваться опытом написания CGI-программ, примеров которых, к счастью, очень много.

Итак, в этой статье мы рассмотрим:
— Что такое FastCGI и чем отличается от протокола CGI
— Зачем мне нужен FastCGI, когда уже есть много языков для разработки под веб
— Какие реализации протокола FastCGI существуют
— Что такое сокеты
— Описание функций библиотеки FastCGI
— Простой пример многопоточной FastCGI-программы
— Простой пример конфигурации Nginx
К сожалению, очень сложно написать статью одинаково понятной новичкам и интересной опытным старожилам, поэтому я буду стараться осветить все моменты как можно подробнее, а Вы можете просто пропустить неинтересные Вам разделы.

Что такое FastCGI?


Про FastCGI можно прочитать в Википедии. Если в двух словах, это CGI-программа, запущенная в цикле. Если обычная CGI-программа заново запускается для каждого нового запроса, то в FastCGI-программе используется очередь запросов, которые обрабатываются последовательно. А теперь представьте: на Ваш 4-8-ядерный сервер поступило 300-500 одновременных запросов. Обычная CGI-программа будет запущена на выполнение эти самые 300-500 раз. Очевидно, такого количества процессов слишком много — Ваш сервер физически не сможет отработать их все сразу. Значит, у Вас получится очередь процессов, ожидающих свой квант процессорного времени. Обычно планировщик будет распределять процессорное равномерно (так так в данном случае приоритеты всех процессов одинаковые), а значит у Вас будет 300-500 «почти готовых» ответов на запросы. Звучит как-то не очень оптимистично, не правда ли? В FastCGI-программе все эти проблемы решаются простой очередью запросов (то есть применяется мультиплексирование запросов).

Зачем мне FastCGI, когда уже есть PHP, Ruby, Python, Perl и т.п.?


Пожалуй, главная причина — компилируемая программа будет работать быстрее интерпретируемой. Для PHP, например, существует целая линейка акселераторов, среди которых — APC, eAccelerator, XCache, которые уменьшают время интерпретации кода. Но для C/C++ всё это просто не нужно.
Второе, о чём Вы должны помнить — динамическая типизация и сборщик мусора занимают много ресурсов. Иногда — очень много. Например, массивы целых чисел в PHP занимают примерно в 18 раз больше памяти (до 35 раз в зависимости от различных параметров компиляции PHP), чем в C/C++ для того же объема данных, поэтому задумайтесь о накладных расходах для сравнительно больших структур данных.
Третье — FastCGI-программа может хранить общие для разных запросов данные. Например, если PHP каждый раз начинает обработку запроса с «чистого листа», то FastCGI-программа может сделать ряд подготовительных действий ещё до поступления первого запроса, например выделить память, загрузить часто используемые данные и т.п. — очевидно, всё это может повысить общую производительность системы.
Четвёртое — масштабируемость. Если mod_php предполагает, что веб-сервер Apache и PHP находятся на одной и той же машине, то FastCGI-приложение может использовать TCP-сокеты. Другими словами, у Вас может быть целый кластер из нескольких машин, связь с которыми осуществляется по сети. При этом FastCGI также поддерживает Unix domain sockets, что позволяет при необходимости эффективно запускать FastCGI-приложение и веб-сервер на одной и той же машине.
Пятое — безопасность. Вы не поверите, но с настройками по умолчанию Apache позволяет выполнять всё на свете. Например, если злоумышленник загрузит на сайт вредоносный скрипт exploit.php.jpg под видом «невинной картинки» и потом откроет её в браузере, Apache «честно» выполнит вредоносный php-код. Пожалуй, единственное достаточно надежное решение — удалять или изменять все потенциально опасные расширения из имен загружаемых файлов, в данном случае — php, php4, php5, phtml и т.п. Такой приём используется, например, в Drupal — ко всем «дополнительным» расширениям добавляется символ подчеркивания и получается exploit.php_.jpg. Правда следует отметить, что системный администратор может добавить любое дополнительное расширение файла в качестве обработчика php, так что какое-нибудь .html может вдруг превратиться в ужасную дыру в безопасности только из-за того, что .php выглядело некрасиво, было плохо для SEO или не нравилось заказчику. Итак, что же нам даёт в плане безопасности FastCGI? Во первых, если использовать вместо Apache веб-сервер Nginx, то он будет просто отдавать статические файлы. Точка. Другими словами, файл exploit.php.jpg будет отдан «как есть», без какой-либо обработки на стороне сервера, так что запустить вредоносный скрипт просто не получится. Во вторых, FastCGI-программа и веб-сервер могут работать из под разных пользователей, а значит и права на файлы и папки у них будут разные. Например, веб-сервер может только читать загруженные файлы — для отдачи статических данных этого достаточно, а FastCGI-программа может только читать и изменять содержимое папки с загружаемыми файлами — этого достаточно для загрузки новых и удаления старых файлов, но доступа непосредственно к самим загруженным файлам иметь не будет, а значит выполнить вредоносный код тоже не сможет. В третьих, FastCGI-программа может работать в chroot'е, отличном от chroot'а веб-сервера. Сам по себе chroot (смена корневой директории) позволяет сильно ограничить права программы, то есть повысить общую безопасность системы, потому что программа просто не сможет получить доступ к файлам за пределами указанного каталога.

Какой веб-сервер с поддержкой FastCGI лучше выбрать?


Если коротко — я пользуюсь Nginx. Вообще, серверов с поддержкой FastCGI довольно много, в том числе коммерческих, так что позвольте рассмотреть несколько альтернатив.
Apache — пожалуй, это первое, что приходит в голову, правда он потребляет гораздо больше ресурсов, чем Nginx. Например, на 10 000 неактивных HTTP keep-alive соединений Nginx расходует около 2,5M памяти, что вполне реально даже для сравнительно слабой машины, а Apache вынужден создавать новый поток для каждого нового соединения, так что 10 000 потоков — просто фантастика.
Lighttpd — главный недостаток этого веб-сервера в том, что он обрабатывает все запросы в одном потоке. Это значит, что могут быть проблемы с маштабируемостью — Вы просто не сможете задействовать все 4-8 ядер современных процессоров. И второе — если по какой-то причине подвиснет поток веб-сервера (например из-за длительного ожидания ответа от жесткого диска), у вас «зависнет» весь сервер. Другими словами, все остальные клиенты перестанут получать ответы из-за одного медленного запроса.
Еще один кандидат — Cherokee. По заявлениям разработчиков, в ряде случаев работает быстрее Nginx и Lighttpd.

Какие есть реализации протокола FastCGI?


На данный момент есть две реализации протокола FastCGI — библиотека libfcgi.lib от создателей протокола FastCGI, и Fastcgi++ — библиотека классов на С++. Libfcgi разрабатывалась с 1996 года и, по заявлениям Open Market, является очень стабильной, к тому же более распространена, поэтому пользоваться в этой статье будем ей. Хочется отметить, что библиотека написана на C, встроенную «обертку» C++ нельзя назвать высокоуровневой, поэтому будем использовать C-интерфейс.
Думаю, на установке самой библиотеки останавливаться смысла нет — в ней есть makefile, так что проблем быть не должно. Кроме того, в популярных дистрибутивах эта библиотека доступна из пакетов.

Что такое сокеты?


Общее понятие о сокетах можно получить в Википедии. Если в двух словах, сокеты — это способ межпроцессного взаимодействия.
Как мы помним, во всех современных операционных системах каждый процесс использует собственное адресное пространство. За непосредственный доступ к оперативной памяти отвечает ядро операционной системы, и если программа обратиться по несуществующему (в контексте данной программы) адресу памяти, ядро вернет segmentation fault (ошибка сегментирования) и закроет программу. Это замечательно — теперь ошибки в одной программе просто не могут повредить другим — они находятся как бы в других измерениях. Но раз у программ разное адресное пространство, от общих данных или обмена данными тоже быть не может. А если очень нужно передать данные из одной программы в другую, как тогда? Собственно, для решения этой проблемы и разрабатывались сокеты — два или более процесса (читайте: программы) подключаются к одному и тому же сокету и начинают обмен данными. Получается этакое «окно» в другой мир — через него можно получать и отправлять данные в другие потоки.
В зависимости от типа использования соединения сокеты бывают разные. Например, есть TCP-сокеты — они используют обычную сеть для обмена данными, то есть программы могут работать на разных компьютерах. Второй наиболее распространенный вариант — доменные сокеты Unix (Unix domain socket) — пригодны для обмена данными только в рамках одной машины и выглядят как обычный путь в файловой системе, но реально жесткий диск не используется — весь обмен данными происходит в оперативной памяти. Из-за того, что не нужно использовать сетевой стек, работают несколько быстрее (примерно на 10%), чем TCP-сокеты. Для ОС Windows данный вариант сокетов называется named pipe (именованный канал).
Примеры использования сокетов для ОС GNU/Linux можно найти в этой статье. Если Вы еще не работали с сокетами, я бы рекомендовал с ней ознакомиться — это не является обязательным, но улучшит понимание изложенных здесь вещей.

Как пользоваться библиотекой Libfcgi?


Итак, мы хотим создать многопоточное FastCGI-приложение, поэтому разрешите описать ряд наиболее важных функций.
Прежде всего, библиотеку нужно инициализировать:
int FCGX_Init(void);

Внимание! Эту функцию нужно вызывать перед любыми другими функциями этой библиотеки и только один раз (всего один раз, для любого количества потоков).

Далее нам нужно открыть слушающий сокет:
int FCGX_OpenSocket(const char *path, int backlog);

Переменная path содержит строку подключения к сокету. Поддерживаются как доменные сокеты Unix, так и TCP-сокеты, всю необходимую работу по подготовке параметров и вызова функции библиотека сделает сама.
Примеры строк подключения для доменных сокетов Unix:
"/tmp/fastcgi/mysocket"
"/tmp/fcgi_example.bare.sock"

Думаю, тут всё понятно: нужно просто передать уникальный путь в виде строки, при этом все взаимодействующие с сокетом процессы должны иметь к нему доступ. Еще раз повторюсь: этот способ работает только в рамках одного компьютера, но несколько быстрее, чем TCP-сокеты.
Примеры строк подключения для TCP-сокетов:
":5000"
":9000"

В этом случае открывается TCP-сокет на указанном порту (в данном случае — 5000 или 9000 соответственно), при этом запросы будут приниматься с любого IP-адреса. Внимание! Данный способ потенциально небезопасен — если Ваш сервер подключен к сети Internet, то Ваша FastCGI-программа будет принимать запросы от любого другого компьютера. Значит, любой злоумышленник сможет отправить Вашей FastCGI-программе «пакет смерти». Разумеется, ничего хорошего в этом нет — в лучшем случае Ваша программа может просто «упасть» и получится отказ в обслуживании (DoS-атака, если хотите), в худшем — удаленное выполнение кода (это если совсем уж не повезёт), поэтому всегда ограничивайте доступ к таким портам при помощи файервола (межсетевого экрана), при этом доступ нужно предоставлять только тем IP-адресам, которые реально используются при штатной работе FastCGI-программы (принцип «запрещено все, что явно не разрешено»).
Следующий пример строк подключения:
"*:5000"
"*:9000"

Способ полностью аналогичен предыдущему: открывается TCP-сокет с приёмом соединений от любого IP-адреса, поэтому в этом случае так же необходимо со всей тщательностью настраивать файервол. Единственный плюс от такой строки подключения сугубо административный — любой читающий конфигурационные файлы программист или системный администратор поймёт, что Ваша программа принимает соединения с любого IP-адреса, поэтому при прочих равных условиях лучше предпочесть данных вариант предыдущему.
Более безопасный вариант — явно указать IP-адрес в строке подключения:
"5.5.5.5:5000"
"127.0.0.1:9000"

В этом случае запросы будут приниматься только от указанного IP-адреса (в данном случае — 5.5.5.5 или 127.0.0.1 соответственно), для всех остальных IP-адресов данный порт (в данном случае — 5000 или 9000 соответственно) будет закрыт. Это повышает общую безопасность системы, поэтому по возможности всегда используйте этот формат строки подключения к TCP-сокетам — а вдруг системный администратор «просто забудет» настроить файервол? Прошу обратить внимание на второй пример — там указан адрес той же машины (localhost). Это позволяет создать TCP-сокет на одной и той же машине, если по каким-то причинам Вы не можете использовать доменные сокеты Unix (например, потому что chroot веб-сервера и chroot FastCGI-программы находятся в разных папках и не имеют общих файловых путей). К сожалению, Вы не можете указать два или более разных IP-адреса, поэтому если Вам действительно нужно принимать запросы от нескольких веб-серверов, расположенных на разных компьютерах, то придётся или полностью открыть порт (см. предыдущий способ) и положиться на настройки Вашего файервола, или использовать несколько сокетов на разных портах. Так же библиотека libfcgi не поддерживает IPv6-адреса — в далёком 1996 году этот стандарт только-только появился на свет, так что придётся ограничить свои аппетиты обычными IPv4-адресами. Правда, если Вам действительно необходима поддержка IPv6, её сравнительно просто добавить, пропатчив функцию FCGX_OpenSocket — лицензия библиотеки это позволяет.
Внимание! Использование функции указания IP-адреса при создании сокета не является достаточной защитой — возможны атаки IP-спуфинга (подмены IP-адреса отправителя пакета), поэтому настройка файервола всё равно обязательна. Обычно в качестве защиты от IP-спуфинга файервол проверяет соответствие между IP-адресом пакета и MAC-адресом сетевой карты для всех хостов нашей локальной сети (точнее — для широковещательного домена с нашим хостом), и отбрасывает все поступающие из Интернета пакеты, обратный адрес которых находится в зоне частных IP-адресов или локального хоста (маски 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7, 127.0.0.0/8 и ::1/128). Тем не менее, всё же лучше использовать данную возможность библиотеки — в случае неверно настроенного файервола отправить «пакет смерти» с подделанного IP-адреса гораздо сложнее, чем с любого, так как TCP-протокол имеет встроенную защиту от IP-спуфинга.
Последний вид строки подключения — использовать доменное имя хоста:
"example.com:5000"
"localhost:9000"

В этом случае IP-адрес будет получен автоматически на основе доменного имени указанного Вами хоста. Ограничения всё те же — хосту должен соответствовать один IPv4-адрес, иначе возникнет ошибка. Правда, учитывая что сокет создается один раз в самом начале работы с FastCGI, вряд ли этот способ будет очень полезен — динамически менять IP-адрес всё равно не получится (точнее, после каждой смены IP-адреса придётся перезапускать Вашу FastCGI-программу). С другой стороны, возможно это будет полезно для сравнительно большой сети — запомнить доменное имя всё же легче, чем IP-адрес.

Второй параметр функции backlog определяет длину очереди запросов сокета. Специальное значение 0 (нуль) означает длину очереди по умолчанию для данной операционной системы.
Каждый раз, когда приходит запрос от веб-сервера, новое соединение ставится в эту очередь в ожидании обработки нашей FastCGI-программой. Если очередь полностью заполнится, все последующие запросы на соединение будут заканчиваться неудачей — веб-сервер получит ответ Connection refused (в подключении отказано). В принципе, ничего плохого в этом нет — у веб-сервера Nginx есть своя очередь запросов, и если свободных ресурсов нет, то новые запросы будут ожидать своей очереди на обработку уже в очереди веб-сервера (по крайней мере до тех пор, пока не истечёт время ожидания). Кроме того, если у Вас несколько серверов с работающей FastCGI-программой, Nginx может передать такой запрос на менее загруженный сервер.
Итак, давайте попробуем разобраться, какая длина очереди будет оптимальной. Вообще, этот параметр лучше настраивать индивидуально исходя из данных нагрузочного тестирования, но мы попробуем оценить наиболее подходящий диапазон для этой величины. Первое, что нужно знать — максимальная длина очереди ограничена (определяется настройками ядра операционной системы, обычно — не более 1024 подключений). Второе — очередь потребляет ресурсы, копеечные, но всё же ресурсы, поэтому необоснованно длинной её делать не стоит. Далее, допустим у нашей FastCGI-программы есть 8 рабочих потоков (вполне реально для современных 4-8-ядерных процессоров), и каждому потоку нужно собственное подключение — задачи обрабатываются параллельно. Значит, в идеале, у нас уже должно быть 8 запросов от веб-сервера, чтобы сразу же, без ненужных задержек, обеспечить работой все потоки. Другими словами, минимальный размер очереди запросов — это количество рабочих потоков FastCGI-программы. Можно попробовать увеличить эту величину на 50%-100%, чтобы обеспечить некоторый запас по загрузке, так как время передачи данных по сети конечно.
Теперь давайте определимся с верхней границей этой величины. Тут нужно знать, сколько запросов мы реально можем обработать и ограничить очередь запросов этой величиной. Представьте, что Вы сделали эту очередь слишком большой — настолько, что Вашим клиентам просто надоедает ждать своей очереди и они просто уходят с Вашего сайта так и не дождавшись ответа. Очевидно, ничего хорошего в этом нет — веб-сервер должен был отправить запрос на открытие соединения, что само по себе дорого, а потом ещё и закрыть это соединение только лишь по тому, что FastCGI-программе не хватило времени на обработку этого запроса. Одним словом, мы только тратим процессорное время впустую, а ведь его нам как раз и не хватает! Но это еще не самое страшное — хуже, когда клиент отказался от получения информации с Вашего сайта уже поле начала обработки запроса. Получается, что мы должны будем полностью обработать в сущности уже никому не нужный запрос, что, согласитесь, только ухудшит ситуацию. Теоретически может возникнуть ситуация, когда большая часть клиентов так и не дождется ответа при 100% загрузке Вашего процессора. Нехорошо.
Итак, допустим один запрос мы можем обработать за 300 миллисекунд (то есть 0,3 секунды). Далее нам известно, что в среднем 50% посетителей покидают ресурс, если веб-страница грузится более 30 секунд. Очевидно, что 50% недовольных — это слишком много, поэтому ограничим максимальное время загрузки страницы в 5 секунд. При этом имеется ввиду уже полностью готовая веб-страница — после применения каскадных таблиц стилей и выполнения JavaScript'ов — этот этап на среднестатистическом сайте может занимать 70% от общего времени загрузки веб-страницы. Итак, на загрузку данных по сети осталось не больше 5*0,3 = 1,5 секунд. Дальше следует вспомнить, что html-код, таблицы стилей, скрипты и графика передаются в разных файлах, причём сначала — html-код, а потом уже всё остальное. Правда, после получения html-кода браузер начинает запрашивать оставшиеся ресурсы параллельно, так что можно оценить время загрузки html-кода как 50% от общего времени получения данных. Итак, в нашем распоряжении осталось не более 1,5*0,5 = 0,75 секунды на обработку одного запроса. Если в среднем один поток обрабатывает запрос за 0,3 секекунды, то в очереди должно быть 0,75/0,3 = 2,5 запроса на поток. Так как у нас 8 рабочих потоков, то результирующий размер очереди должен составлять 2,5*8 = 20 запросов. Хочется отметить условность приведенных расчетов — при наличии конкретного сайта используемые в расчете величины можно определить гораздо точнее, но всё же он дает отправную точку для более оптимальной настройки производительности.

Итак, мы получили дескриптор сокета, после этого необходимо выделить память под структуру запроса. Описание этой структуры следующее:
typedef struct FCGX_Request { 
    int requestId;
    int role; 
    FCGX_Stream *in; 
    FCGX_Stream *out; 
    FCGX_Stream *err; 
    char **envp; 
   struct Params *paramsPtr; 
    int ipcFd;
    int isBeginProcessed;
    int keepConnection;
    int appStatus; 
    int nWriters;
    int flags; 
    int listen_sock; 
    int detached; 
} FCGX_Request;

Внимание! После получения нового запроса все предыдущие данные будут утеряны, поэтому при необходимости длительного хранения данных применяйте глубокое копирование (копируйте сами данные, а не указатели на данные).
Вы должны знать об этой структуре следующее:
— переменные in, out и err играют роль соответственно потоков ввода, вывода и ошибок. Поток ввода содержит данные POST-запроса, в поток вывода нужно отправить ответ FastCGI-программы (например, http-заголовки и html-код веб-страницы), а поток ошибок просто добавит запить в лог ошибок веб-сервера. При этом потоком ошибок можно вообще не пользоваться — если Вам действительно нужно логгировать ошибки, то, пожалуй, для этого лучше использовать отдельный файл — передача данных по сети и их последующая обработка веб-сервером потребляет дополнительные ресурсы.
— переменная envp содержит значения переменных окружения, устанавливаемых веб-сервером, и http-заголовки, например: SERVER_PROTOCOL, REQUEST_METHOD, REQUEST_URI, QUERY_STRING, CONTENT_LENGTH, HTTP_USER_AGENT, HTTP_COOKIE, HTTP_REFERER и так далее. Эти заголовки определяются соответственно стандартами CGI и HTTP протоколов, примеры их использования можно найти в любой CGI-программе. Сами данные хранятся в массиве строк, при этом последний элемент массива содержит нулевой указатель (NULL) в качестве обозначения конца массива. Каждая строка (каждый элемент массива строк) содержит одно значение переменной в формате НАЗВАНИЕ_ПЕРЕМЕННОЙ=ЗНАЧЕНИЕ, например: CONTENT_LENGTH=0 (в данном случае означает, что у данного запроса нет POST-данных, так как их длина равна нулю). Если в массиве строк envp нет нужного Вам заголовка, значит он не был передан. Если Вы хотите получить все переданные FastCGI-программе значения переменных, просто прочитайте в цикле все строки массива envp до тех пор пока не встретите указатель на NULL.
Собственно, на этом с описанием этой структуры мы закончили — всё остальные переменные Вам не понадобятся.

Память выделили, теперь нужно выполнить инициализацию структуры запроса:
int FCGX_InitRequest(FCGX_Request *request, int sock, int flags);

Параметры функции следующие:
request — указатель на структуру данных, которую нужно инициализировать
sock — дескриптор сокета, который мы получили после вызова функции FCGX_OpenSocket. Хочется отметить, что вместо уже готового дескриптора можно передать 0 (нуль) и получить сокет с настройками по умолчанию, но для нас данный способ совершенно не интересен — сокет будет открыт на случайном свободном порте, а, значит, мы не сможем правильно настроить наш веб-сервер — нам неизвестно заранее, куда именно нужно отправлять данные.
flags — флаги. Собственно, в эту функцию можно передать только один флаг — FCGI_FAIL_ACCEPT_ON_INTR — не вызывать FCGX_Accept_r при разрыве.

После этого нужно получить новый запрос:
int FCGX_Accept_r(FCGX_Request *request);

В неё нужно передать уже инициализированную на прошлом этапе структуру request. Внимание! В многопоточной программе необходимо использовать синхронизацию при вызове данной функции.
Собственно, эта функция выполняет всю работу по работе с сокетами: сначала она отправляет ответ веб-серверу на предыдущий запрос (если таковой был), закрывает предыдущий канал передачи данных и освобождает все связанные с ним ресурсы (в том числе — переменные структуры request), потом получает новый запрос, открывает новый канал передачи данных и подготавливает новые данные в структуре request для их последующей обработки. В случае ошибки получения нового запроса функция возвращает код ошибки, меньший нуля.

Далее Вам наверняка потребуется получить переменные окружения, для этого можно или самостоятельно обработать массив request->envp, или воспользоваться функцией
char *FCGX_GetParam(const char *name, FCGX_ParamArray envp);

где name — строка, содержащая название переменной окружения или http-заголовка, значение которого Вы хотите получить,
envp — массив переменных окружения, которые содержатся в переменной request->envp
Функция возвращает значение нужной нам переменной окружения в виде строки. Пусть внимательного читателя не пугает несоответствие типов между char ** и FCGX_ParamArray — эти типы объявлены синонимами (typedef char **FCGX_ParamArray).
Кроме того, Вам наверняка понадобится отправить ответ веб-серверу. Для этого нужно воспользоваться потоком вывода request->out и функцией
int FCGX_PutStr(const char *str, int n, FCGX_Stream *stream);

где str — буффер, содержащий данные для вывода, без завершающего нуля (то есть буффер может содержать двоичные данные),
n — длинна буффера в байтах,
stream — поток, в который мы хотим вывести данные (request->out или request->err).

Если Вы пользуетесь стандартами C-строками с завершающим нулём, удобнее будет использовать функцию
int FCGX_PutS(const char *str, FCGX_Stream *stream);

которая просто определит длину строки функцией strlen(str) и вызовет предыдущую функцию. Поэтому, если Вам заранее известна длина строки (например, Вы пользуетесь C++-строками std::string), лучше используйте предыдущую функцию по соображениям эффективности.
Хочется отметить, что эти функции прекрасно работают с UTF-8-строками, так что с многоязычными веб-прилодениями проблем быть не должно.
Вы так же можете вызывать эти функции несколько раз во время обработки одного и того же запроса, в ряде случаев это может повысить производительность. Например, Вам нужно отправить какой-то большой файл. Вместо того, чтобы загружать весь этот файл с жёсткого диска, а потом уже отправить его «одним куском», Вы можете сразу же начать отправлять данные. В результате клиент вместо белого экрана браузера начнёт получать интересующие его данные, что чисто психологически заставит его ещё немного подождать. Другими словами, Вы как бы выигрываете немного времени для загрузки страницы. Так же хочется отметить, что большинство ресурсов (каскадные таблицы стилей, JavaScript'ы и т.п.) указываются в начале веб-страницы, то есть браузер сможет проанализировать часть html-кода и начать загрузку этих ресурсов раньше — ещё один повод выводить данные по частям.

Следующее, что Вам может понадобиться — это обработать POST-запрос. Для того, что бы получить его значение, нужно прочитать данные из потока request->in при помощи функции
int FCGX_GetStr(char * str, int n, FCGX_Stream *stream);

где str — указатель на буффер,
n — размер буффера в байтах,
stream — поток, из которого мы читаем данные.
Размер передаваемых данных в POST-запросе (в байтах) можно определить с помощью переменной окружения CONTENT_LENGTH, значение которой, как мы помним, можно получить с помощью функции FCGX_GetParam. Внимание! Создавать буффер str на основании значения переменной CONTENT_LENGTH без каких-либо ограничений очень плохая идея: любой злоумышленник может отправить любой, сколь угодно большой POST-запрос, и у Вашего сервера может просто закончиться свободная оперативная память (получится DoS-атака, если хотите). Вместо этого лучше ограничить размер буффера какой-то разумной величиной (от нескольких килобайт до нескольких мегабайт) и вызывать функцию FCGX_GetStr несколько раз.

Последняя важная функция флеширует потоки вывода и ошибок (отправляет клиенту всё ещё не отправленные данные, которые мы успели поместить в потоки вывода и ошибок) и закрывает соединение:
void FCGX_Finish_r(FCGX_Request *request);

Хочется особо отметить, что эта функция не является обязательной: функция FCGX_Accept_r так же отправляет клиенту данные и закрывает текущее соединение перед получением нового запроса. Спрашивается: тогда зачем же она нужна? Представьте, что Вы уже отправили клиенту все необходимые данные, и сейчас Вам нужно выполнить какие-то завершающие операции: записать статистику в базу данных, ошибки в лог-файл и т.п. Очевидно, что соединение с клиентом уже больше не нужно, но клиент (в смысле, браузер) всё ещё ждёт от нас информацию: а вдруг мы отправим что-нибудь еще? При этом очевидно, что мы не можем вызвать FCGX_Accept_r раньше времени — после этого нужно будет начать обрабатывать следующий запрос. Как раз в этом случае Вам понадобится функция FCGX_Finish_r — она позволит закрыть текущее соединение до получения нового запроса. Да, мы сможем обработать такое же число запросов в единицу времени, как и без использования этой функции, но клиент получит ответ раньше — ему уже не придется ждать конца выполнения наших завершающих операций, а ведь именно из-за большей скорости обработки запросов мы и используем FastCGI.
На этом, собственно, заканчивается описание функций библиотеки и начинается обработка полученных данных.

Простой пример многопоточной FastCGI-программы


Думаю, в примере всё будет понятно. Единственное, печать отладочных сообщений и «засыпание» рабочего потока сделаны исключительно в демонстрационных целях. При компилировании программы не забудьте подключить библиотеки libfcgi и libpthread (параметры компилятора gcc: -lfcgi и -lpthread).

#include <pthread.h> 
#include <sys/types.h> 
#include <stdio.h> 

#include "fcgi_config.h" 
#include "fcgiapp.h" 


#define THREAD_COUNT 8 
#define SOCKET_PATH "127.0.0.1:9000" 

//хранит дескриптор открытого сокета 
static int socketId; 

static void *doit(void *a) 
{ 
    int rc, i; 
    FCGX_Request request; 
    char *server_name; 

    if(FCGX_InitRequest(&request, socketId, 0) != 0) 
    { 
	    //ошибка при инициализации структуры запроса 
        printf("Can not init request\n"); 
	    return NULL; 
    } 
    printf("Request is inited\n"); 
 
    for(;;) 
    { 
        static pthread_mutex_t accept_mutex = PTHREAD_MUTEX_INITIALIZER; 

        //попробовать получить новый запрос 
        printf("Try to accept new request\n"); 
        pthread_mutex_lock(&accept_mutex); 
        rc = FCGX_Accept_r(&request); 
        pthread_mutex_unlock(&accept_mutex); 

        if(rc < 0) 
        { 
	        //ошибка при получении запроса 
            printf("Can not accept new request\n"); 
            break; 
        } 
        printf("request is accepted\n"); 

        //получить значение переменной 
        server_name = FCGX_GetParam("SERVER_NAME", request.envp); 

        //вывести все HTTP-заголовки (каждый заголовок с новой строки) 
        FCGX_PutS("Content-type: text/html\r\n", request.out); 
        //между заголовками и телом ответа нужно вывести пустую строку 
        FCGX_PutS("\r\n", request.out); 
        //вывести тело ответа (например - html-код веб-страницы) 
        FCGX_PutS("<html>\r\n", request.out); 
        FCGX_PutS("<head>\r\n", request.out); 
        FCGX_PutS("<title>FastCGI Hello! (multi-threaded C, fcgiapp library)</title>\r\n", request.out); 
        FCGX_PutS("</head>\r\n", request.out); 
        FCGX_PutS("<body>\r\n", request.out); 
        FCGX_PutS("<h1>FastCGI Hello! (multi-threaded C, fcgiapp library)</h1>\r\n", request.out); 
        FCGX_PutS("<p>Request accepted from host <i>", request.out); 
        FCGX_PutS(server_name ? server_name : "?", request.out); 
        FCGX_PutS("</i></p>\r\n", request.out); 
        FCGX_PutS("</body>\r\n", request.out); 
        FCGX_PutS("</html>\r\n", request.out); 

        //"заснуть" - имитация многопоточной среды 
        sleep(2); 

        //закрыть текущее соединение 
        FCGX_Finish_r(&request); 
        
        //завершающие действия - запись статистики, логгирование ошибок и т.п. 
    } 
 
    return NULL; 
} 

int main(void) 
{ 
    int i; 
    pthread_t id[THREAD_COUNT]; 

    //инициализация библилиотеки 
    FCGX_Init(); 
    printf("Lib is inited\n"); 
    
    //открываем новый сокет 
    socketId = FCGX_OpenSocket(SOCKET_PATH, 20); 
    if(socketId < 0) 
    { 
	    //ошибка при открытии сокета 
	    return 1; 
    } 
    printf("Socket is opened\n"); 

    //создаём рабочие потоки 
    for(i = 0; i < THREAD_COUNT; i++) 
    { 
        pthread_create(&id[i], NULL, doit, NULL); 
    } 
    
    //ждем завершения рабочих потоков 
    for(i = 0; i < THREAD_COUNT; i++) 
    { 
        pthread_join(id[i], NULL); 
    } 

    return 0; 
}


Простой пример конфигурации Nginx


Собственно, простейший пример конфига выглядит так:

server { 
	server_name localhost; 

	location / { 
		fastcgi_pass 127.0.0.1:9000; 
		#fastcgi_pass  unix:/tmp/fastcgi/mysocket; 
		#fastcgi_pass localhost:9000; 
		 
		include fastcgi_params; 
	} 
}


В данном случае этого конфига достаточно для корректной работы нашей FastCGI-программы. Закоментированные строчки — это пример работы с соответственно доменными сокетами Unix и заданием доменного имени хоста вместо IP-адреса.
После компиляции и запуска программы, и настройки Nginx у меня по адресу localhost появилась гордая надпись:
FastCGI Hello! (multi-threaded C, fcgiapp library)

Спасибо всем, кто дочитал до конца.
Tags:
Hubs:
Total votes 102: ↑97 and ↓5+92
Comments104

Articles