Общий шлюзовый интерфейс (CGI). Программирование с использованием CGI

10.07.2019

Введение.

В этой статье я хочу рассказать о CGI интерфейсе вообще, его реализации для windows и использовании при написании CGI-программ языка ассемблер в частности. В рамки этой статьи не входит полное описание CGI, так-как в Интернете материала по этому вопросу просто море и пересказывать все это здесь я просто не вижу смысла.

Теория CGI .

CGI – (Common Gateway Interface) – Общий Шлюзовый Интерфейс. Как не трудно догадаться интерфейс этот служит шлюзом между сервером (здесь я подразумеваю программу - сервер) и какой-либо внешней программой написанной для ОС на которой этот самый сервер запущен. Таким образом CGI отвечает за то, каким именно образом данные будут переданы от программы-сервера к CGI-программе и обратно. Интерфейс не накладывает никаких ограничений на то, на чем должна быть написана CGI-программа, это может быть как обычный исполнимый файл, так и любой другой файл – главное, чтобы сервер смог его запустить (в среде windows это например может быть файл с расширением, привязанным к какой-либо программе).

С момента когда Вы вызвали (например нажали кнопку формы, к которой привязан вызов CGI-программы) CGI-программу до получения вами результата в окно браузера происходит следующее:

Вэб-клиент (например браузер) создает подключение к серверу, указанному в URL;

Вэб-клиент посылает запрос серверу, запрос этот обычно делается с помощью двух методов GET или POST;

Данные из запроса клиента (например значения полей формы) передаются сервером, используя CGI-интерфейс, CGI-программе, указанной в URL;

CGI-программа обрабатывает данные клиента, полученные от сервера и генерирует на основе этой обработки ответ клиенту, который она передает по все тому же CGI-интерфейсу серверу, а он в свою очередь передает его уже непосредственно клиенту;

Сервер разрывает соединение с клиентом.

В стандартной спецификации CGI принято, что сервер может обмениваться с программой следующими способами:

Переменные окружения – они могут быть установлены сервером при запуске программы;

Стандартный поток ввода (STDIN) – с его помощью сервер может передать данные программе;

Стандартный поток вывода (STDOUT) – программа может писать в него свой вывод, передающийся серверу;

Командная строка – в ней сервер может передать некоторые параметры программе.

Стандартные потоки ввода/вывода весьма удобны и широко используются на UNIX-системах, чего не скажешь о windows, поэтому существует спецификация CGI, разработанная специально для windows-систем так и называемая «Windows CGI». Но, естественно, и стандартные потоки ввода/вывода так же можно использовать в windows CGI программировании. Здесь я не буду затрагивать стандарт «Windows CGI», и на это существует по крайней мере две причины – первая, и самая главная – на данный момент не все http-сервера под windows поддерживают эту спецификацию (в частности мой любимый Apache 1.3.19). Вторую причину вы можете наблюдать набрав в любой поисковой системе строчку «Windows CGI». Отмечу относительно этого интерфейса лишь общие детали – все данные от сервера к клиенту передаются посредством обычного для windows *.ini файла, имя которого передается программе в командной строке. При этом все данные в файле уже заботливо разбиты по секциям сервером и вам лишь остается используя функции «GetPrivateProfile*» извлечь их оттуда. Ответ серверу передается опять же посредством файла, имя которого указано в соответствующей записи ini-файла.

Какие же данные могут быть переданы клиентом CGI-программе? – практически любые. В общем случае программе передаются значения полей формы, которые заполняет клиент, но это также могут быть и какие-либо двоичные данные, например файл с картинкой или музыкой. Данные могут быть переданы на сервер двумя различными методами – это метод GET и метод POST. Когда мы создаем форму для заполнения на нашей страничке мы явно указываем каким из приведенных методов мы хотим отправить введенные пользователем данные, делается это в основном тэге формы примерно так:

get action="/cgi-bin/name_script">

При отправке данных методом GET данные браузером считываются из формы и помещаются следом за URL скрипта, за знаком вопроса, если значимых полей в форме несколько, то они передаются все через значёк «&», имя поля и его значение пишутся в URL через знак «=». Например запрос, сгенерированный браузером из формы при нажатии на кнопку, к которой привязан скрипт «/cgi-bin/test.exe», при учете что первое поле формы называется «your_name», второе – «your_age», может выглядеть так:

GET /cgi-bin/test.exe?your_name=Pupkin&your_age=90 HTTP/1.0

Использование метода GET имеет сразу несколько слабых сторон – первое и самое главное – т.к. данные передаются в URL то он имеет ограничение на количество этих самых передаваемых данных. Вторая слабость опять же вытекает из URL – это конфиденциальность, при такой передаче данные остаются абсолютно открытыми. Итак, хорошо если у нас в форме 2-3 небольших поля… встает вопрос что же делать если данных больше? Ответ – использовать метод POST!

При использовании метода POST данные передаются серверу как блок данных, а не в URL, что несколько развязывает нам руки для увеличения объема передаваемой информации, для вышеприведенного примера формы POST блок, посылаемый серверу будет примерно такой:

POST /cgi-bin/test.exe HTTP/1.0

Accept: text/plain

Accept: text/html

Accept: */*

Content-type: application/x-www-form-urlencoded

Content-length: 36

your_name=Pupkin&your_age=90

Как уже говорилось выше, после получения данных сервер должен преобразовать их и передать CGI программе. В стандартной спецификации CGI введенные клиентом данные при запросе GET помещаются сервером в переменную среды программы «QUERY_STRING». При запросе POST данные помещаются в стандартный поток ввода приложения, откуда могут быть им считаны. Кроме того, при таком запросе сервером устанавливаются еще две переменные среды - CONTENT_LENGTH и CONTENT_TYPE, по которым можно судить о длине запроса в байтах и о его содержании.

Помимо самих данных сервером устанавливаются и другие переменные окружения вызываемой программы, приведу некоторые из них:

REQUEST_METHOD

Описывает каким именно методом получены данные

Пример :REQUEST_METHOD=GET

QUERY_STRING

Строка запроса, если использовался метод GET

Пример :QUERY_STRING= your_name=Pupkin&your_age=90&hobby=asm

CONTENT_LENGTH

Длина в байтах тела запроса

Пример:CONTENT_LENGTH=31

CONTENT_TYPE

Тип тела запроса

GATEWAY_INTERFACE

Версия протокола CGI

Пример: GATEWAY _ INTERFACE = CGI /1.1

REMOTE_ADDR

IP-Адрес удаленного хоста, то бишь клиента, нажавшего кнопочку в форме

Пример:REMOTE_ADDR=10.21.23.10

REMOTE_HOST

Имя удаленного хоста, это может быть его доменное имя или например имя компьютера в среде Windows, если таковые получены быть не могут, то поле содержит его IP

Пример :REMOTE_HOST=wasm.ru

SCRIPT_NAME

Имя скрипта, использованное в запросе.

Пример :SCRIPT_NAME=/cgi-bin/gols.pl

SCRIPT_FILENAME

Имя файла скрипта на сервере.

Пример :SCRIPT_FILENAME=c:/page/cgi-bin/gols.pl

SERVER _ SOFTWARE

Программное обеспечение сервера

Пример:Apache/1.3.19 (WIN 32)

В общем-то это вкратце все, для получения более подробной информации об Общем Шлюзовом Интерфейсе смотрите специализированную документацию, это описание я сделал для того, чтобы напомнить вам, а если не знали то ввести в курс дела. Давайте попробуем что-нибудь сделать на практике.

Практическая часть.

Для практики нам понадобятся как минимум 3 вещи – какой-нибудь http-сервер для Windows, все примеры я пробовал на Apache 1.3.19 для Windows, сервер бесплатный, скачать его можно с http://httpd.apache.org/download.cgi . Да, и сервер нам понадобится не абы – какой, а настроенный для запуска cgi-скриптов! Как это делается для сервера используемого вами смотрите документацию. Вторая вещь, которая нам понадобится это, естественно, ассемблер, так же необходимо, чтобы компилятор поддерживал создание консольных WIN32 приложений, я использую Tasm, но прекрасно подойдут и Fasm и Masm и множество других *asm’ов. Ну и наконец самое главное, что потребуется это желание.

Итак, я допускаю, что сервер был вами благополучно поставлен и настроен, так, что в корневой директории документов сервера лежит файлик index.html, который замечательно показывается в браузере, когда вы набираете адрес http://127.0.0.1 . Так же я учту, что где-то в дебрях папок сервера существует папочка «cgi-bin», в которой разрешен запуск скриптов.

Давайте проверим настройку сервера, а заодно и напишем небольшой скрипт. Скрипт наш будет обычным *.bat файлом. Предвижу вопросы – как? неужели? Да, это обычный командный файл, как уже говорилось выше спецификация CGI не делает различий между типами файлов, главное, чтобы сервер мог его запустить, а он в свою очередь, имел доступ к stdin/stdout и переменным окружения, bat-файл, пусть и не в полной мере, но для примера нас вполне устроит. Создадим файл примерно такого содержания:

@echo off rem Заголовок апроса echo Content-type: text/html echo. rem Тело запроса echo "Привет!
echo "С запросом GET пришли данные: %QUERY_STRING%

Файл назовем test.bat и поместим его в директорию для запуска скриптов, скорее всего это будет директория «cgi-bin». Следующее, что нам нужно будет сделать, это каким либо образом вызвать этот скрипт, в принципе, сделать это можно напрямую набрав в окошке адреса браузера примерно следующее «http://127.0.0.1/cgi-bin/test.bat», но давайте сделаем его вызов с нашей главной странички, заодно проверим работу метода GET. Создадим в корне сервера файл index.html со следующим содержанием:

Введите данные для передачи серверу:
Данные:

Теперь при входе на сервер (http://127.0.0.1 в строке адреса браузера) должна появиться форма, наберите в ней что-нибудь и нажмите кнопку «послать», если все было сделано правильно, Вы увидите в окне браузера ответ нашего bat-скрипта. Теперь давайте посмотрим что же мы там намутили.

Как можно догадаться команда «echo» осуществляет вывод в stdout, первым делом мы передаем серверу заголовок нашего ответа – «echo Content-type: text/html». Это есть стандартный заголовок спецификации CGI, говорящий о том, что передавать мы хотим текст или документ html, существуют и другие заголовки. Очень важный момент – заголовок должен отделяться от тела ответа пустой строкой, что мы и делаем следующей командой «echo.». Дальше передается тело самого ответа – это обычный html-документ, в теле документа я для наглядности отображаю одну из переменных среды, переданной нам сервером – «QUERY_STRING», как уже говорилось при методе GET (а это именно наш случай) в этой переменной передаются все введенные пользователем данные, что мы и можем наблюдать в ответе скрипта. Вы могли заметить «кавычки не к месту» в последних 2-х строках файла, сразу после «echo», стоят они там из-за специфичности bat-файлов, как можно заметить тэги html обрамляются символами «<» и «>», в тоже время эти символы служат перенаправлением ввода/вывода в bat-файлах, а посему мы не можем их здесь свободно использовать.

Рекомендую немного побаловаться с подобными bat-скриптами, это бывает очень полезно, попробуйте посмотреть другие переменные окружения. Немного скажу, отступив от темы, на UNIX-системах языки командных интерпретаторов очень сильно развиты и грань между программированием на языке командного интерпретатора и программированием на «реальном» языке программирования весьма и весьма размыта в некоторых случаях, поэтому на UNIX-системах частенько простенькие скрипты пишутся именно на языках командных интерпретаторов, но windows-интерпретатор cmd.exe или, ранее, command.com явно слабоваты для этих целей.

Теперь перейдем к самой главной задаче этой статьи, к собственно написанию CGI-программы на ассемблере. В принципе, если учесть все вышесказанное о CGI мы можем сделать вывод о том, что требует CGI-интерфейс от нашей программы:

2. Программа должна уметь писать в стандартный поток вывода (stdout), чтобы передать результат своей работы серверу;

3. Из первых двух пунктов следует, то, что для того, чтобы сервер мог передать нашей программе что-либо в stdin, а она могла ему что-либо ответить в stdout CGI-программа должна быть консольным приложением;

Этого вполне достаточно для создания полноценного CGI-приложения.

Начнем с последнего пункта. Для получения доступа к переменным окружения Windows-приложения используется функция API «GetEnvironmentStrings», функция не имеет аргументов и возвращает указатель на массив переменных окружения (ИМЯ=ЗНАЧЕНИЕ) разделенных между собой нулем, массив закрывается двойным нулем, при запуске программы сервером в окружение программы помимо стандартных переменных добавляются специфические CGI-переменные, описанные выше, при запуске программы из командной строки вы их не увидите, естественно.

Для того, что бы писать что-то в stdout или читать из stdin сначала мы должны получить хэндлы этих потоков, делается это с помощью функции API «GetStdHandle», в качестве параметра функции передается одно из следующих значений:

STD_INPUT_HANDLE - для stdin (стандартный ввод);

STD_OUTPUT_HANDLE - для stdout (стандартный вывод);

STD_ERROR_HANDLE - для stderr.

Функция возвратит необходимый нам для операций чтения/записи хэндл. Следующее что нам необходимо делать это писать/читать эти потоки. Делается это обычными операциями чтения/записи файлов, т.е. ReadFile и WriteFile. Тут есть одна тонкость, можно подумать, что для этих целей можно использовать WriteConsole/ReadConsole, да это действительно справедливо для консоли и будет прекрасно работать, результаты, так же как и с WriteFile будут выводиться на консоль, но продолжаться это будет пока мы не запустим нашу программу как скрипт на сервере. Происходит это потому что, когда нашу программу запускает сервер хэндлы, возвращаемые функцией «GetStdHandle» уже не будут хэндлами консоли как таковыми, они будут хэндлами pipe, что необходимо для связи двух приложений.

Вот небольшой пример того, как должна выглядеть CGI-программа на ассемблере, думаю разобраться в ней не составит большого труда:>

386 .model flat,stdcall includelib import32.lib .const PAGE_READWRITE = 4h MEM_COMMIT = 1000h MEM_RESERVE = 2000h STD_INPUT_HANDLE = -10 STD_OUTPUT_HANDLE = -11 .data hStdout dd ? hStdin dd ? hMem dd ? header: db "Content-Type: text/html",13,10,13,10,0 start_html: db "Окружение CGI-программы выглядит \ так:
",13,10,0 for_stdin: db "STDIN программы содержит:
",13,10,0 end_html: db "",13,10,0 nwritten dd ? toscr db 10 dup (32) db " - Тип файла",0 .code _start: xor ebx,ebx call GetStdHandle,STD_OUTPUT_HANDLE mov hStdout,eax call GetStdHandle,STD_INPUT_HANDLE mov hStdin,eax call write_stdout, offset header call write_stdout, offset start_html call VirtualAlloc,ebx,1000,MEM_COMMIT+MEM_RESERVE,PAGE_READWRITE mov hMem,eax mov edi,eax call GetEnvironmentStringsA mov esi,eax next_symbol: mov al, or al,al jz end_string mov ,al next_string: cmpsb jmp short next_symbol end_string: mov ,">rb<" add edi,3 cmp byte ptr ,0 jnz next_string inc edi stosb call write_stdout, hMem call write_stdout, offset for_stdin call GetFileSize,,ebx mov edi,hMem call ReadFile,,edi, eax,offset nwritten, ebx add edi, mov byte ptr ,0 call write_stdout, hMem call write_stdout, offset end_html call VirtualFree,hMem call ExitProcess,-1 write_stdout proc bufOffs:dword call lstrlen,bufOffs call WriteFile,,bufOffs,eax,offset nwritten,0 ret write_stdout endp extrn GetEnvironmentStringsA:near extrn GetStdHandle:near extrn ReadFile:near extrn WriteFile:near extrn GetFileSize:near extrn VirtualAlloc:near extrn VirtualFree:near extrn ExitProcess:near extrn lstrlen:near ends end _start

Исполняемый файл строится командами:

tasm32.exe /ml test.asm

tlink32.exe /Tpe /ap /o test.obj

Не забудьте, что программа должна быть консольной.

Архив с программой .

Вызывать эту программу можно используя вышеописанную html-форму, нужно только поменять имя test.bat в форме на test.exe и скопировать его в /cgi-bin/ соответственно, при том можно выставить в методе запроса POST, программа его обрабатывает.

Еще хочу отметить, что можно вызывать программу и по-другому, можно создать в каталоге cgi-bin файл например test.cgi с одной единственной строчкой «#!c:/_путь_/test.exe» и вызывать в запросах его, а сервер в свою очередь будет читать первую его строчку и запускать exe-файл, для этого необходимо, чтобы в настройках http-сервера было прописано расширение *.cgi как расширение для скриптов. При таком подходе сервер запустит нашу программу с командной строкой «test.exe путь_к_test.exe» это имеет несколько плюсов – первое, это то, что человек, запускающий наш скрипт не будет даже догадываться на чем скрипт написан, второе – так-как нам передается имя файла с нашей строчкой мы можем например дописать в этот файл какие-либо настройки для нашего скрипта, что упрощает отладку, кстати именно так работают все интерпретаторы – вы успели заметить, что во всех perl/php/итд программах, присутствует подобная строка – указывающая на сам командный интерпретатор. Так вот сервер при запуске cgi-программы, если расширение программы прописано у него как скрипт в настройках читает первую строку файла, и если она оказывается описанного выше формата, то запускает указанную в строчке программу с именем этого файла ч/з пробел, допустим что в строчке указан интерпретатор перла, он получив такой подарок начинает его выполнение, т.к. комментарий в перле это символ «#», то первую строчку он пропускает и идет дальнейшее выполнение скрипта, в общем штука удобная.

Вот в общем и все о чем я хотел написать, не знаю насколько это все окажется Вам полезным, но скажу что у меня работает сервер интрасети используя скрипты на ассемблере. Каюсь, больших оснований делать это не было, но все же я сделал это сначала просто из эстетических соображений и некоторой не охоты учить перл/php или что-то еще. НО я никоим образом не отговариваю Вас учить перл, а наоборот скажу что сделать это нужно, и даже очень нужно, это я понял позже, но все же считаю, что на сильно загруженных серверах, где скорость выполнения, загрузки и объем памяти занимаемый приложением играет решающую роль cgi-скрипты, написанные на ассемблере займут свое достойное место.

Common Gateway Interface (CGI, рус. Общий шлюзовый интерфейс) - это стандартный метод динамического управления содержимым веб-страниц с помощью специальных программ, выполняющихся на стороне веб-сервера. Эти программы называются CGI-обработчики или шлюзы, но чаще - CGI-скрипты, т.к. обычно они пишутся на скриптовых языках, в основном на Perl.

Поскольку гипертекст статичен по своей природе, веб-страница не может непосредственно взаимодействовать с пользователем. До появления JavaScript , не было иной возможности отреагировать на действия пользователя, кроме как передать введенные им данные на веб-сервер для дальнейшей обработки. В случае CGI эта обработка осуществляется с помощью внешних программ и скриптов, обращение к которым выполняется через стандартизованный (см. RFC 3875: CGI Version 1.1) интерфейс - общий шлюз. Упрощенная модель, иллюстрирующая работу CGI, приведена на рис. 1.

Как работает CGI?

Обобщенный алгоритм работы через CGI можно представить в следующем виде:

  1. Клиент запрашивает CGI-приложение по его URI .
  2. Веб-сервер принимает запрос и устанавливает переменные окружения , через них приложению передаются данные и служебная информация.
  3. Веб-сервер перенаправляет запросы через стандартный поток ввода (stdin) на вход вызываемой программы.
  4. CGI-приложение выполняет все необходимые операции и формирует результаты в виде HTML.
  5. Сформированный гипертекст возвращается веб-серверу через стандартный поток вывода (stdout). Сообщения об ошибках передаются через stderr.
  6. Веб-сервер передает результаты запроса клиенту.

Области применения CGI

Наиболее частая задача, для решения которой применяется CGI - создание интерактивных страниц, содержание которых зависит от действий пользователя. Типичными примерами таких веб-страниц являются форма регистрации на сайте или форма для отправки комментария. Другая область применения CGI, остающаяся за кулисами взаимодействия с пользователем, связана со сбором и обработкой информации о клиенте: установка и чтение «печенюшек»-cookies ; получение данных о браузере и операционной системе; подсчет количества посещений веб-страницы; мониторинг веб-трафика и т.п.

Эти возможности обеспечиваются тем, что CGI-скрипт может быть подключен к базе данных или обращаться к файловой системе сервера. Таким образом CGI-скрипт может сохранять информацию в таблицах БД или файлах и получать ее оттуда по запросу, чего нельзя сделать средствами HTML.

ОБРАТИТЕ ВНИМАНИЕ: CGI - это не язык программирования! Это простой протокол, позволяющий веб-серверу передавать данные через stdin и читать их из stdout. Поэтому, в качестве CGI-обработчика может ипользоваться любая серверная программа, способная работать со стандарными потоками ввода-вывода.

Hello, world!

Пример простого CGI-скрипта на языке Perl приведен в листинге 1. Если этот код сохранить в файле с именем hello (имя может быть любым, расширение - тоже), поместить файл в серверный каталог cgi-bin (точнее, в тот каталог веб-сервера, который предназначен для CGI-приложений и указан в настройках веб-сервера) и установить для этого файла права на исполнение (chmod uo+x hello), то он станет доступен по адресу вида http://servername/cgi-bin/hello.

Листинг 1. Пример CGI-скрипта (Perl)

#!/usr/bin/perl print "Content-type: text/html\n\n"; print < CGI say Hello

Hello, world!

HTML exit;

В этом коде строка #!/usr/bin/perl указывает полный путь к интерпретатору Perl. Строка Content-type: text/html\n\n - http-заголовок, задающий тип содержимого (mime-type). Удвоенный символ разрыва строки (\n\n) - обязателен, он отделяет заголовки от тела сообщения.

Переменные окружения

Все CGI-приложения имеют доступ к переменным окружения, устанавливаемым веб-сервером. Эти переменные играют важную роль при написании CGI-программ. В таблице перечислены некоторые из переменных, доступных CGI.

Переменная окружения Описание
CONTENT_TYPE Тип данных, передаваемых на сервер. Используется, когда клиент отправляет данные, например, загружает файл.
CONTENT_LENGTH Размер содержимого запроса. Эта переменная определена для POST-запросов.
HTTP_COOKIE Возвращает набор «куков» в виде пар «ключ значение».
HTTP_USER_AGENT Информация об агенте пользователя (браузере)
PATH_INFO Путь к каталогу CGI
QUERY_STRING Строка запроса (URL-encoded), передаваемая методом GET.
REMOTE_ADDR IP-адрес клиента, выполняющего запрос.
REMOTE_HOST Полное имя (FQDN) клиента. (Если доступно)
REQUEST_METHOD Метод, которым выполняется запрос. Чаще всего GET или POST.
SCRIPT_FILENAME Полный путь к запрашиваемому скрипту (в файловой системе сервера).
SCRIPT_NAME Имя скрипта
SERVER_NAME Имя сервера
SERVER_ADDR IP-адрес сервера
SERVER_SOFTWARE Информация о серверном ПО

В листинге 2 приведен код небольшой программы на Perl, выводящей список переменных окружения. Результат ее работы приведен на рис. 2.

Листинг 2. Переменные окружения

#!/usr/bin/perl print "Content-type: text/html\n\n"; print "\n\n

Environment

\n"; foreach (sort keys %ENV) { print "$_: $ENV{$_}
\n"; } print "\n"; exit;

Передача данных: метод GET

Метод GET используется для передачи urlencoded -данных через строку запроса. Адрес запрашиваемого ресурса (CGI-скрипта) и передаваемые ему данные отделяются знаком «?». Пример такого адреса:

http://example.com/cgi-bin/script.cgi?key1=value1&key2=value2

Метод GET используется по умолчанию для данных, введенных в адресную строку браузера. Такая же строка может быть сформирована при отправке данных из веб-формы (тег

), если метод передачи для формы не указан. Вся информация, отправляемая методом GET, передается в открытом виде, поэтому никогда не следует использовать его для отправки на сервер паролей или другой подобной информации. Метод GET имеет ограничение по размеру: строка запроса должна быть не длинее 1024 символов.

Информация, отправляемая методом GET передается в заголовке QUERY_STRING в виде строки, состоящей из пар вида ключ=значение , CGI-скрипт может получить ее через одноименную переменную окружения.

Листинг 3. Отправка данных из веб-формы методом GET

A simple CGI scripting: Sending data using GET-method You name: name="user" >
Where are you from?: name="from" >

После ввода данных в форму из листинга 3 и нажатия кнопки "Submit" будет сформирована строка запроса вида:

http://example.com/cgi-bin/sayhello ?user =sometext &from =anothertext

где: sayhello - имя CGI-скрипта; user и from - имена параметров; sometext и anothertext - введенные пользователем значения соответствующих параметров.

В листинге 4 приведен пример скрипта, который может обработать данные формы из листинга 3.

Листинг 4. Отправка данных из веб-формы методом GET

#!/usr/bin/perl local ($buffer, @pairs, $pair, $name, $value, %FORM); # Анализируем окружение $ENV{"REQUEST_METHOD"} =~ tr/a-z/A-Z/; if ($ENV{"REQUEST_METHOD "} eq "GET ") { $buffer = $ENV{"QUERY_STRING "}; } # Разделяем строку запроса на пары вида ключ/значение @pairs = split(/&/, $buffer); foreach $pair (@pairs) { ($name, $value) = split(/=/, $pair); $value =~ tr/+/ /; $value =~ s/%(..)/pack("C", hex($1))/eg; $FORM{$name} = $value; # Сохраняем данные в массив } # Отправляем заголовок print "Content-type: text/html\n\n"; # Отправляем гипертекст print < CGI say Hello

Hello, $FORM{user} from $FORM{from}!

HTML exit;

Передача данных: метод POST

В общем случае более подходящим для передачи информации CGI-скрипту является метод POST. Блок передаваемых данных формируется так же, как и для метода GET, но непосредственно передача осуществляется в теле запроса. Данные поступают на вход CGI-приложения через стандартный ввод (stdin).

Для отправки данных этим методом, он должен быть явно задан в описании формы (action="POST").

Для обработки входных данных CGI-скрипт должен прочитать stdin, а чтобы это правильно сделать, ему нужно узнать размер сообщения из переменной CONTENT_LENGTH. Для иллюстрации этого модифицируем блок анализа окружения в листинге 4, заменив его следующим кодом:

... # Анализируем окружение $ENV{"REQUEST_METHOD"} =~ tr/a-z/A-Z/; if ($ENV{"REQUEST_METHOD "} eq "POST "){ read(STDIN , $buffer, $ENV{"CONTENT_LENGTH "}); } ...

Дальнейшая обработка сохраненных в переменной $buffer параметров и их значений выполняется так же, как и в при использовании метода GET.

Преимущества CGI

Многие возможности CGI сейчас дублируются такими технологиями, как например DHTML , ActiveX или Java-апплетами. Основными преимуществами использования серверных скриптов является то, что вы можете быть уверены, что все клиенты (за редким исключением, как правило связанным с блокировкой доступа к определенным ресурсам на уровне файрвола) смогут работать с серверным приложением. Клиентские-же программы могут быть просто отключены в браузере, или вовсе не поддерживаться.

Недостатки CGI

Самым большим недостатком этой технологии являются повышенные требования к производительности веб-сервера. Дело в том, что каждое обращение к CGI-приложению вызывает порождение нового процесса , со всеми вытекающими отсюда накладными расходами. Если же приложение написано с ошибками, то возможна ситуация, когда оно, например, зациклится. Браузер прервет соединение по истечении тайм-аута, но на серверной стороне процесс будет продолжаться, пока администратор не снимет его принудительно. В этом отношении клиентские скрипты имеют существенное преимущество, т.к. они выполняются локально.

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

Постоянный адрес этой страницы:

  • Tutorial

Добрый день.
В этой статье я бы хотел рассказать про протокол 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 #include #include #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("\r\n", request.out); FCGX_PutS("\r\n", request.out); FCGX_PutS("\r\n", request.out); FCGX_PutS("\r\n", request.out); FCGX_PutS("

FastCGI Hello! (multi-threaded C, fcgiapp library)

\r\n", request.out); FCGX_PutS("

Request accepted from host ", request.out); FCGX_PutS(server_name ? server_name: "?", request.out); FCGX_PutS("

\r\n", request.out); FCGX_PutS("\r\n", request.out); FCGX_PutS("\r\n", request.out); //"заснуть" - имитация многопоточной среды sleep(2); //закрыть текущее соединение FCGX_Finish_r(&request); //завершающие действия - запись статистики, логгирование ошибок и т.п. } return NULL; } int main(void) { int i; pthread_t id; //инициализация библилиотеки 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)

Спасибо всем, кто дочитал до конца.

Глава №9 .

Программирование с использованием CGI

Включение раздела о CGI в книгу по базам данных может показаться столь же странным, как если бы в кулинарную книгу была включена глава о ремонте автомобилей. Разумеется, для того чтобы съездить в магазин за продуктами, нужен исправный автомобиль, но уместно ли об этом говорить? Полное изложение CGI и веб-программирование в целом выходят за рамки данной книги, но краткого введения в эти темы достаточно для того, чтобы расширить возможности MySQL и mSQL по представлению данных в царстве Web.

В основном эта глава предназначена тем, кто изучает базы данных, но не прочь приобрести некоторые знания и в программировании для Web. Если ваша фамилия Бернерс-Ли или Андрессен, вряд ли вы найдете здесь то, чего еще не знаете. Но даже если вы не новичок в CGI, наличие под рукой краткого справочника во время погружения в тайны MySQL и mSQL может оказаться весьма полезным.

Что такое CGI?

Как и большинство акронимов, Common Gateway Interface (CGI - общий шлюзовый интерфейс) мало что говорит по сути. Интерфейс с чем? Где этот шлюз? О какой общности речь? Чтобы ответить на эти вопросы, вернемся немного назад и бросим взгляд на WWW в целом.

Тим Бернерс-Ли, физик, работавший в CERN, придумал Web в 1990 году, хотя план возник еще в 1988. Идея состояла в том, чтобы дать исследователям в области физики элементарных частиц возможность легко и быстро обмениваться мультимедийными данными - текстом, изображениями и звуком - через Интернет. WWW состояла из трех основных частей: HTML, URL и HTTP. HTML - язык форматирования, используемый для представления содержания в Web. URL - это адрес, используемый для получения содержимого в формате HTML (или каком-либо ином) с веб-сервера. И, наконец, HTTP - это язык, который понятен веб-серверу и позволяет клиентам запрашивать у сервера документы.

Возможность пересылки через Интернет информации всех типов явилась революцией, но вскоре была обнаружена и другая возможность. Если можно переслать через Web любой текст, то почему нельзя переслать текст, созданный программой, а не взятый из готового файла? При этом открывается море возможностей. Простой пример: можно использовать программу, выводящую текущее время, так, чтобы читатель видел правильное время при каждом просмотре страницы. Несколько умных голов в National Center for Supercomputing Applications (Национальный центр разработки приложений для суперкомпьютеров -NCSA), которые создавали веб-сервер, такую возможность увидели, и вскоре появился CGI.

CGI - это набор правил, согласно которым программы на сервере могут через веб-сервер посылать данные клиентам. Спецификация CGI сопровождалась изменениями в HTML и HTTP, вводившими новую характеристику, известную как формы.

Если CGI позволяет программам посылать данные клиенту, то формы расширяют эту возможность, позволяя клиенту посылать данные для этой CGI-программы. Теперь пользователь может не только видеть текущее время, но и устанавливать часы! Формы CGI открыли дверь для подлинной интерактивности в мире Web. Распространенные приложения CGI включают в себя:

  • Динамический HTML. Целые сайты могут генерироваться одной CGI-программой.
  • Поисковые механизмы, находящие документы с заданными пользователем словами.
  • Гостевые книги и доски объявлений, в которые пользователи могут добавлять свои сообщения.
  • Бланки заказов.
  • Анкеты.
  • Извлечение информации из размещенной на сервере базы данных.

В последующих главах мы обсудим все эти CGI-приложения, а также и некоторые другие. Все они дают прекрасную возможность соединения CGI с базой данных, что и интересует нас в этом разделе.

Формы HTML

Прежде чем изучать особенности CGI, полезно рассмотреть наиболее часто встречающийся способ, с помощью которого конечные пользователи получают интерфейс к CGI-программам: формы HTML. Формы являются частью языка HTML, предоставляющей конечному пользователю поля различных типов. Данные, вводимые в поля, могут быть пересланы веб-серверу. Поля могут служить для ввода текста или являться кнопками, которые пользователь может нажать или отметить галочкой. Вот пример страницы HTML, содержащей форму:

<НТМL><НЕАD><ТITLЕ>Моя страница с формами


<р>Это страница с формой.


Введите свое имя:



Данная форма создает строку длиной 40 символов, куда пользователь может ввести свое имя. Под строкой для ввода располагается кнопка, при нажатии которой данные формы передаются серверу. Ниже перечислены относящиеся к формам теги, поддерживаемые HTML 3.2 -наиболее распространенным в настоящее время стандартом. Названия тегов и атрибутов могут вводиться в любом регистре, но мы придерживаемся необязательного соглашения, согласно которому открывающие теги пишутся в верхнем регистре, а закрывающие - в нижнем.


Этот тег указывает на начало формы. В конце формы требуется закрывающий тег

. Между тегами
допускаются три атрибута: ACTION задает URL или относительный путь к CGI-програм-ме, которой будут посланы данные; METHOD указывает метод HTTP, посредством которого будет послана форма (это может быть GET или ч POST, но мы почти всегда будем использовать POST); ENCTYPE задает метод кодирования данных (его следует использовать только при четком понимании того, что вы делаете).


Предоставляет наиболее гибкий способ ввода данных пользователем. Фактически есть девять разных типов тега . Тип задается атрибутом TYPE. В предыдущем примере используются два тега : один с типом SUBMIT и другой с типом по умолчанию TEXT. Девять типов следующие:

TEXT

Поле для ввода пользователем одной строки текста.

PASSWORD

To же, что TEXT, но вводимый текст не отображается на экране.

CHECKBOX

Флажок, который пользователь может устанавливать и сбрасывать.

RADIO

Радиокнопка, которая должна объединяться еще хотя бы с одной радиокнопкой. Пользователь может выбрать только одну из них.

SUBMIT

Кнопка, при нажатии которой форма отправляется на веб-сервер.

RESET

Кнопка, при нажатии которой в форме восстанавливаются значения по умолчанию.

FILE

Аналогичен текстовому окну, но предполагает ввод имени файла, который будет отправлен на сервер.

HIDDEN

Невидимое поле, в котором могут храниться данные.

IMAGE

Аналогичен кнопке SUBMIT, но можно задать картинку для изображения на кнопке.

Кроме атрибута TYPE теги обычно имеют атрибут NAME, связывающий введенные в поле данные с некоторым именем. Имя и данные передаются серверу в стиле величина=значение. В предыдущем примере текстовое поле именовалось firstname . Можно использовать атрибут VALUE, чтобы присвоить полям типа TEXT, PASSWORD , FILE и HIDDEN предустановленные значения. Этот же атрибут, используемый с кнопками типа SUBMIT или RESET, выводит на них заданный текст. Поля типа RADIO и CHECKBOX можно отобразить как выставленные с помощью атрибута CHECKED без задания значения.

Атрибут SIZE используется для задания длины полей типа TEXT, PASSWORD и FILE. Атрибут MAXLENGTH можно использовать для ограничения длины вводимого текста. Атрибут SRC задает URL изображения, используемого в типе IMAGE. И наконец, атрибут ALIGN задает характер выравнивания изображения для типа IMAGE и может иметь значение TOP, MIDDLE, BOTTOM (по умолчанию), LEFT или RIGHT (вверх, в середину, вниз, влево, вправо).

.

Как и у тега , у тега , и в качестве текста по умолчанию будет принят любой текст, находящийся между тегами , аналогично атрибуту VALUE для тега . Для тега

, дающая место для ввода очерка. Данные получают имя "essay". Блок текста 70 символов в ширину и 10 строк в глубину. Пространство между тегами

можно использовать для образца очерка. -->

типов "SUBMIT" и "RESET" соответственно. Кнопка "SUBMIT" имеет переопределенную надпись "Ввести данные ", а кнопка "RESET" имеет надпись по умолчанию (определяемую броузером). Кликнув по кнопке "SUBMIT", вы пошлете данные на веб-сервер, Кнопка "RESET" восстановит данные R исходное состояние, удалив все введенные пользователем данные. -->


Единственный тип ввода, который мы здесь не использовали, - это тип IMAGE для тега . Можно было бы использовать его в качестве альтернативного способа отправки формы. Однако тип IMAGE редко совместим с текстовыми и не очень чуткими броузерами, поэтому благоразумно избегать его, если только ваш сайт не выполнен в насыщенном графическом стиле.

После знакомства с основами форм HTML можно приступить к изучению собственно CGI.

Спецификация CGI

Итак, что в точности представляет собой «набор правил», позволяющий CGI-программе, скажем, в Батавии, штат Иллинойс, обмениваться данными с веб-броузером во Внешней Монголии? Официальную спецификацию CGI наряду с массой других сведений о CGI можно найти на сервере NCSA по адресу http://hoohoo . ncsa.uluc.edu/ cgi/. Однако эта глава для того и существует, чтобы вам не пришлось долго путешествовать и самому ее искать.

Есть четыре способа, которыми CGI передает данные между CGI-npor-раммой и веб-сервером, а следовательно, и клиентом Web:

  • Переменные окружения.
  • Командная строка.
  • Стандартное устройство ввода.
  • Стандартное устройство вывода.

С помощью этих четырех методов сервер пересылает все данные, переданные клиентом, CGI-программе. Затем CGI-программа делает свое волшебное дело и пересылает выходные данные обратно серверу, который переправляет их клиенту.

Эти данные приводятся с прикидкой на сервер HTTP Apache. Apache - наиболее распространенный веб-сервер, работающий практически на любой платформе, включая Windows 9х и Windows NT. Однако они могут быть применимы ко всем HTTP-серверам, поддерживающим CGI. Некоторые патентованные серверы, например, от Microsoft и Netscape, могут иметь дополнительные функции или работать несколько иначе. Поскольку лицо Web продолжает изменяться с невероятной скоростью, стандарты все еще развиваются, и в будущем, несомненно, произойдут изменения. Однако, что касается CGI, то эта технология представляется устоявшейся - расплачиваться за это приходится тем, что другие технологии, такие как апплеты, ее потеснили. Все CGI-программы, которые вы напишете, используя эти сведения, почти наверное смогут работать еще долгие годы на большинстве веб-серверов.

Когда CGI-программа вызывается посредством формы - наиболее распространенного интерфейса, броузер передает серверу длинную строку, в начале которой стоит путь к CGI-программе и ее имя. Затем следуют различные другие данные, которые называются информацией пути и передаются CGI-программе через переменную окружения PATH_INFO (рис. 9-1). После информации пути следует символ «?», а за ним - данные формы, которые посылаются серверу с помощью метода HTTP GET. Эти данные становятся доступными CGI-программе через переменную окружения QUERY_STRING . Любые данные, которые страница посылает с использованием метода HTTP POST, который используется чаще всего, будут переданы CGI-программе через стандартное устройство ввода. Типичная строка, которую может получить сервер от броузера, показана на рис. 9-1. Программа с именем formread в каталоге cgi-bin вызывается сервером с дополнительной информацией пути extra/information и данными запроса choice=help - по-видимому, как часть исходного URL. Наконец, данные самой формы (текст «CGI programming» в поле «keywords») пересылаются через метод HTTP POST .

Переменные окружения

Когда сервер выполняет CGI-программу, то прежде всего передает ей некоторые данные для работы в виде переменных окружения. В спецификации официально определены семнадцать переменных, но неофициально используется значительно больше - с помощью описываемого ниже механизма, называемого HTTP_/nec/zams/n. CGI-программа

имеет доступ к этим переменным так же, как и к любым переменным среды командного процессора при запуске из командной строки. В сценарии командного процессора, например, к переменной окружения F00 можно обращаться как $F00; в Perl это обращение выглядит, как $ENV{"F00"} ; в С - getenv("F00") ; и т. д. В таблице 9-1 перечислены переменные, которые всегда устанавливаются сервером - хотя бы и в значение null. Помимо этих переменных данные, возвращаемые клиентом в заголовке запроса, присваиваются переменным вида HTTP_F00 , где F00 - имя заголовка. Например, большинство веб-броузеров включает данные о версии в заголовок с именем USEfl_AGENT . Ваша CGI-npor-рамма может получить эти данные из переменной HTTP_USER_AGENT .

Таблица 9-1. Переменные окружения CGI

Переменная окружения

Описание

CONTENT_LENGTH

Длина данных, переданных методами POST или PUT, в байтах.

CONTENT_TYPE

Тип MIME данных, присоединенных с помощью методов POST или PUT .

GATEWAY_INTERFACE

Номер версии спецификации CGI, поддерживаемой сервером.

PATH_INFO

Дополнительная информация пути, переданная клиентом. Например, для запроса http://www.myserver.eom/test.cgi/this/is/a/ path?field=green значением переменной РАТН_ INFO будет /this/is/a/path.

PATH_TRANSLATED

То же, что PATH_INFO , но сервер производит всю


Возможную трансляцию, например, расширение имен типа «-account». »

QUERY_STRING

Все данные, следующие за символом «?» в URL. Это также данные, передаваемые, когда REQ-UEST_METHOD формы есть GET.

REMOTE_ADDR

IP-адрес клиента, делающего запрос.

REMOTE_HOST

Имя узла машины клиента, если оно доступно.

REMOTE_IDENT

Если веб-сервер и клиент поддерживают идентификацию типа identd, то это имя пользователя учетной записи, которая делает запрос.

REQUEST_METHOD

Метод, используемый клиентом для запроса. Для CGI-программ, которые мы собираемся создавать, это обычно будет POST или GET.

SERVER_NAME Имя узла - или IP-адрес, если имя недоступно, -машины, на которой выполняется веб-сервер.
SERVER_PORT Номер порта, используемого веб-сервером.
SERVER_PROTOCOL
Протокол, используемый клиентом для связи с сервером. В нашем случае этот протокол почти всегда HTTP.
SERVER_SOFTWARE Данные о версии веб-сервера, выполняющего CGI-программу.

SCRIPT_NAME

Путь к выполняемому сценарию, указанный клиентом. Может использоваться при ссылке URL на самого себя, и для того, чтобы сценарии, ссылки на которые существуют в разных местах, могли выполняться по-разному в зависимости от места.

Приведем пример сценария CGI на Perl, который выводит все переменные окружения, установленные сервером, а также все унаследованные переменные, такие как PATH, установленные командным процессором, запустившим сервер.

#!/usr/bin/perl -w

print << HTML;

Content-type: text/html\n\n

<р>Переменные окружения

HTML

foreach (keys %ENV) { print "$_: $ENV{$_}
\n"; }

print <

HTML

Все эти переменные могут быть использованы и даже изменены вашей CGI-программой. Однако эти изменения не затрагивают веб-сервер, запустивший программу.

Командная строка

CGI допускает передачу CGI-программе аргументов в качестве параметров командной строки, которая редко используется. Редко используется она потому, что практические применения ее немногочисленны, и мы не будем останавливаться на ней подробно. Суть в том, что если переменная окружения QUERY_STRING не содержит символа « = », то CGI-программа будет выполняться с параметрами командной строки, взятыми из QUERY_STRING . Например, http://www.myserver.com/cgi- bin/finger?root запустит finger root на www.myserver.com.

Есть две основные библиотеки, обеспечивающие CGI-интерфейс для Perl. Первая из них - cgi-lib.pl Утилита cgi-lib.pl очень распространена, поскольку в течение долгого времени была единственной имеющейся большой библиотекой. Она предназначена для работы в Perl 4, но работает и с Perl 5. Вторая библиотека, CGI.pm, более новая и во многом превосходит cgi-lib.pl. CGI.pm написана для Perl 5 и использует полностью объектно-ориентированную схему для работы с данными CGI. Модуль CGI.pm анализирует стандартное устройство ввода и переменную QUERY_STRING и сохраняет данные в объекте CGI. Ваша программа должна лишь создать новый объект CGI и использовать простые методы, такие как paramQ, для извлечения нужных вам данных. Пример 9-2 служит короткой демонстрацией того, как CGI.pm интерпретирует данные. Все примеры на Perl в этой главе будут использовать CGI.pm.

Пример 9-2. Синтаксический анализ CGI-данных на Perl

#!/usr/bin/perl -w

use CGI qw(:standard);

# Используется модуль CGI.pm. qw(:standard) импортирует

# пространство имен стандартных CGI-функций,чтобы получить

# более понятный код. Это можно делать, если в сценарии

# используется только один объект CGI.

$mycgi = new CGI; #Создать объект CGI, который будет "шлюзом" к данным формы

@fields = $mycgi->param; # Извлечь имена всех заполненных полей формы

print header, start_html("CGI.pm test"); ft Методы "header" и "start_html",

# предоставляемые

# CGI.pm, упрощают получение HTML.

# "header" выводит требуемый заголовок HTTP, a

#"start_html" выводит заголовок HTML с данным названием,

#a также тег .

print "<р>Данные формы:
";

foreach (@fields) { print $_, ":",- $mycgi->param($_), "
"; }

# Для каждого поля вывести имя и значение, получаемое с помощью

# $mycgi->param("fieldname").

print end_html; # Сокращение для вывода завершающих тегов "".

Обработка входных данных в С

Поскольку основные API для MySQL и mSQL написаны на С, мы не будем полностью отказываться от С в пользу Perl, но там, где это уместно, приведем несколько примеров на С. Есть три широко используемые С-библиотеки для CGI-программирования: cgic Тома Бу-телла (Tom Boutell)*; cgihtml Юджина Кима (Eugene Kim)t и libcgi от EIT*. Мы полагаем, что cgic является наиболее полной и простой в использовании. В ней, однако, недостает возможности перечисления всех переменных формы, когда они не известны вам заранее. На самом деле, ее можно добавить путем простого патча, но это выходит за рамки данной главы. Поэтому в примере 9-3 мы используем библиотеку cgihtml, чтобы повторить на С приведенный выше сценарий Perl.

Пример 9-3. Синтаксический анализ CGI-данных на С

/* cgihtmltest.c - Типовая CGI-программа для вывода ключей и их значений

из данных, полученных от формы */

#include

#include "cgi-lib.h" /* Здесь содержатся все определения функций СGI */

#include "html-lib.h" /* Здесь содержатся" все определения вспомогательных функций для HTML */

void print_all(llist 1)

/* Эти функции выводят данные, переданные формой, в том же формате, что и приведенный выше сценарий Perl. Cgihtml предоставляет также встроенную функцию

Print_entries(), которая делает то же самое, используя формат списка HTML. */ {

node* window;

/* Тип "node" определен в библиотеке cgihtml и ссылается на связанный список, в котором хранятся все данные формы. */

window = I.head; /* Устанавливает указатель на начало данных формы */

while (window != NULL) { /* Пройти по связанному списку до последнего (первого пустого) элемента */

printf(" %s:%s
\n",window->entry. name,replace_ltgt(window->entry.value));

/* Вывести данные. Replace__ltgt() - функция, понимающая HTML-кодировку текста и обеспечивающая его правильный вывод на броузер клиента. */

window = window->next; /* Перейти к следующему элементу списка. */

} }

int main() {

llist entries; /* Указатель на проанализированные данные*/

int status; /* Целое число, представляющее статус */

Html__header(); /* Вспомогательная функция HTML, выводящая заголовок HTML*/

Html_begin("cgihtml test");

/* Вспомогательная функция HTML, выводящая начало страницы HTML с указанным заголовком. */

status = read_cgi_input(&entries); /* Производит ввод и синтаксический анализ данных формы*/

Printf("<р>Данные формы:
");

Print_all(entries); /* Вызывает определенную выше функцию print_all(). */

html_end(); /* Вспомогательная функция HTML, выводящая конец страницы HTML. */

List_clear(&entries); /* Освобождает память, занятую данными формы. */

return 0; }

Стандартное устройство вывода

Данные, посылаемые CGI-программой на стандартное устройство вывода, читаются веб-сервером и отправляются клиенту. Если имя сценария начинается с nph-, то данные посылаются прямо клиенту без вмешательства со стороны веб-сервера. В этом случае CGI-программа должна сформировать правильный заголовок HTTP, который будет понятен клиенту. В противном случае предоставьте веб-серверу сформировать HTTP-заголовок за вас.

Даже если вы не используете nph -сценарий, серверу нужно дать одну директиву, которая сообщит ему сведения о вашей выдаче. Обычно это HTTP-заголовок Content-Type , но может быть и заголовок Location . За заголовком должна следовать пустая строка, то есть перевод строки или комбинация CR/LF.

Заголовок Content-Type сообщает серверу, какого типа данные выдает ваша CGI-программа. Если это страница HTML, то строка должна быть Content-Type: text/html. Заголовок Location сообщает серверу другой URL - или другой путь на том же сервере, - куда нужно направить клиента. Заголовок должен иметь следующий вид: Location: http:// www. myserver. com/another/place/.

После заголовков HTTP и пустой строки можно посылать собственно данные, выдаваемые вашей программой, - страницу HTML, изображение, текст или что-либо еще. Среди CGI-программ, поставляемых с сервером Apache, есть nph-test-cgi и test-cgi, которые хорошо демонстрируют разницу между заголовками в стилях nph и не-nph, соответственно.

В этом разделе мы будем использовать библиотеки CGI.pm и cgic, в которых есть функции для вывода заголовков как HTTP, так и HTML. Это позволит вам сосредоточиться на выводе собственно содержания. Эти вспомогательные функции использованы в примерах, приведенных ранее в этой главе.

Важные особенности сценариев CGI

Вы уже знаете, в основном, как работает CGI. Клиент посылает данные, обычно с помощью формы, веб-серверу. Сервер выполняет CGI-программу, передавая ей данные. CGI-программа осуществляет свою обработку и возвращает свои выходные данные серверу, который передает их клиенту. Теперь от понимания того, как работают CGI-npor-раммы, нужно перейти к пониманию того, почему они так широко используются.

Хотя вам уже достаточно известно из этой главы, чтобы собрать простую работающую CGI-программу, нужно разобрать еще несколько важных вопросов, прежде чем создавать реально работающие программы для MySQL или mSQL. Во-первых, нужно научиться работать с несколькими формами. Затем нужно освоить некоторые меры безопасности, которые помешают злоумышленникам получить незаконный доступ к файлам вашего сервера или уничтожить их.

Запоминание состояния

Запоминание состояния является жизненно важным средством предоставления хорошего обслуживания вашим пользователям, а не только служит для борьбы с закоренелыми преступниками, как может показаться. Проблема вызвана тем, что HTTP является так называемым протоколом «без памяти». Это значит, что клиент посылает данные серверу, сервер возвращает данные клиенту, и дальше каждый идет своей дорогой. Сервер не сохраняет о клиенте данных, которые могут понадобиться в последующих операциях. Аналогично, нет уверенности, что клиент сохранит о совершенной операции какие-либо данные, которые можно будет использовать позднее. Это накладывает непосредственное и существенное ограничение на использование World Wide Web.

Составление сценариев CGI при таком протоколе аналогично неспособности запоминать разговор. Всякий раз, разговаривая с кем-либо, независимо от того, как часто вы общались с ним раньше, вам приходится представляться и искать общую тему для разговора. Нет нужды объяснять, что это не способствует продуктивности. Рисунок 9-2 показывает, что всякий раз, когда запрос достигает программы CGI, это совершенно новый экземпляр программы, не имеющий связи с предыдущим.

В части клиента с появлением Netscape Navigator появилось выглядящее наспех сделанным решение под названием cookies. Оно состоит в создании нового HTTP-заголовка, который можно пересылать туда-сюда между клиентом и сервером, похожего на заголовки Content-Type и Location. Броузер клиента, получив заголовок cookie, должен сохранить в cookie данные, а также имя домена, в котором действует этот cookie. После этого всякий раз при посещении URL в пределах указанного домена заголовок cookie должен возвращаться серверу для использования в CGI-программах на этом сервере.

Метод cookie используется в основном для хранения идентификатора пользователя. Сведения о посетителе можно сохранить в файле на машине сервера. Уникальный ID этого пользователя можно послать в качестве cookie броузеру пользователя, после чего при каждом посещении сайта пользователем броузер автоматически посылает серверу этот ID. Сервер передает ID программе CGI, которая открывает соответствующий файл и получает доступ ко всем данным о пользователе. Все это происходит незаметным для пользователя образом.

Несмотря на всю полезность этого метода, большинство больших сайтов не использует его в качестве единственного средства запоминания состояния. Для этого есть ряд причин. Во-первых, не все броузеры поддерживают cookie. До недавнего времени основной броузер для людей с недостаточным зрением (не говоря уже о людях с недостаточной скоростью подключения к сети) - Lynx - не поддерживал cookie. «Официально» он до сих пор их не поддерживает, хотя это делают некоторые его широко доступные «боковые ветви». Во-вторых, что более важно, cookie привязывают пользователя к определенной машине. Одним из великих достоинств Web является то, что она доступна из любой точки света. Независимо от того, где была создана или где хранится ваша веб-страница, ее можно показать с любой подключенной к Интернет машины. Однако если вы попытаетесь получить доступ к поддерживающему cookie сайту с чужой машины, все ваши персональные данные, поддерживавшиеся с помощью cookie, будут утрачены.

Многие сайты по-прежнему используют cookie для персонализации страниц пользователей, но большинство дополняет их традиционным интерфейсом в стиле «имя регистрации/пароль». Если доступ к сайту осуществляется из броузера, не поддерживающего cookie, то страница содержит форму, в которую пользователь вводит имя регистрации и пароль, присвоенные ему при первом посещении сайта. Обычно эта форма маленькая и скромная, чтобы не отпугивать большинство пользователей, не заинтересованных ни в какой персонализации, а просто желающих пройти дальше. После ввода пользователем в форму имени регистрации и пароля CGI находит файл с данными об этом пользователе, как если бы имя посылалось с cookie. Используя этот метод, пользователь может регистрироваться на персонализированном веб-сайте из любой точки света.

Помимо задач учета предпочтений пользователя и длительного хранения сведений о нем можно привести более тонкий пример запоминания состояния, который дают популярные поисковые машины. Осуществляя поиск с помощью таких служб, как AltaVista или Yahoo, вы обычно получаете значительно больше результатов, чем можно отобразить в удобном для чтения виде. Эта проблема решается тем, что показывается небольшое количество результатов - обычно 10 или 20 - и дается какое-либо средство перемещения для просмотра следующей группы результатов. Хотя обычному путешественнику по Web такое поведение кажется обычным и ожидаемым, действительная его реализация нетривиальна и требует запоминания состояния.

Когда пользователь впервые делает запрос поисковому механизму, тот собирает все результаты, возможно, ограничиваясь некоторым предустановленным предельным количеством. Фокус состоит в том, чтобы выдавать эти результаты одновременно в небольшом количестве, запомнив при этом, что за пользователь запрашивал эти результаты и какую порцию он ожидает следующей. Оставляя в стороне сложности самого поискового механизма, мы встаем перед проблемой последовательного предоставления пользователю некоторой информации по одной странице. Рассмотрим пример 9-4, в котором показан сценарий CGI, выводящий десять строк файла и предоставляющий ему возможность просмотреть следующие или предыдущие десять строк.

Пример 9-4. Сохранение состояния в сценарии CGI

#!/usr/bin/perl -w

use CGI;

Open(F,"/usr/dict/words") or die("He могу открыть! $!");

#Это файл, который будет выводиться, может быть любым.

$output = new CGI;

sub print_range { # Это главная функция программы, my $start = shift;

# Начальная строка файла, my $count = 0;

# Указатель, my $line = "";

# Текущая строка файла, print $output->header,

$output->start_html("Moй словарь");

# Создает HTML с заголовком "Мой словарь", print "\n";

while (($count < $start) and ($line = )) { $count++; }

# Пропустить все строки перед начальной, while (($count < $start+10) and ($line ? )) { print $line; $count++; }

# Напечатать очередные 10 строк.

my $newnext = $start+10; my $newprev = $start-10;

# Установить начальные строки для URL "Next" и "Previous",

print "

";

unless ($start == 0) { # Включить URL "Previous", если только вы

# уже не в начале.

print qq%Previous%; }

unless (eof) { # Включить URL "Next", если только вы # не в конце файла.

print qq% Next%;

}

print «HTML; HTML

exit(0); }

# Если данных нет, начать сначала,

if (not $output->param) {

&print_range(0); }

# Иначе начать со строки, указанной в данных.

&print_range($output->param("start"));

В этом примере запоминание состояния производится с помощью простейшего метода. Проблемы с сохранением данных нет, поскольку мы держим их в файле на сервере. Нам нужно только знать, откуда начать вывод, поэтому сценарий просто включает в URL начальную точку для следующей или предыдущей группы строк - все, что необходимо для генерации очередной страницы.

Однако если вам требуется нечто большее, чем возможность просто листать "файл, то полагаться на URL бывает обременительно. Облегчить эту трудность можно через использование формы HTML и включение данных о состоянии в теги типа HIDDEN. Этот метод с успехом используется на многих сайтах, позволяя делать ссылки между взаимосвязанными CGI-программами или расширяя возможности использования одной CGI-программы, как в предыдущем примере. Вместо ссылки на определенный объект, такой как начальная страница, данные URL могут указывать на автоматически генерируемый ID пользователя.

Так работают AltaVista и другие поисковые машины. При первом поиске генерируется ID пользователя, который скрыто включается в последующие URL. С этим ID связаны один или несколько файлов, содержащих результаты запроса. В URL включаются еще две величины: текущее положение в файле результатов и направление, в котором вы хотите перемещаться в нем дальше. Эти три значения - все, что нужно для работы мощных систем навигации больших поисковых машин.

Впрочем, не хватает еще кое-чего. Использовавшийся в нашем примере файл /usr/diet/words очень велик. Что если на середине чтения мы его бросим, но захотим вернуться к нему позднее? Если не запомнить URL следующей страницы, никакого способа вернуться назад нет, даже AltaVista это не позволит. Если вы перезагрузите свой компьютер или станете работать с другого, невозможно вернуться к результатам прежнего поиска, не вводя заново запрос. Однако такое долговременное запоминание состояния лежит в основе персонализации вебсайтов, о которой мы говорили выше, и стоит посмотреть, как им можно воспользоваться. Пример 9-5 является модифицированным вариантом примера 9-4.

Пример 9-5. Устойчивое запоминание состояния

#!/usr/bin/perl -w

use CGI;

umask 0;

Open(F,"/usr/dict/words") or die("He могу открыть! $!");

Chdir("users") or die("He могу перейти в каталог $!");

# Это каталог, где будут храниться все данные

# о пользователе.

Soutput = new CGI;

if (not$output->param) {

print $output->header,

$output->start_html("Мой словарь");

print «HTML;


<р>Введите свое имя пользователя:


HTML

exit(0); }

$user = $output->param("username");

## Если файла пользователя нет, создать его и установить

## начальное значение в "0",

if (not -e "$user") {

open (U, ">$user") or die("He могу открыть! $!");

print U "0\n";

close U;

&print_range("0");

## если пользователь существует и в URL не задано

## начальное значение, прочесть последнее значение и начать с него.

} elsif (not $output->param("start")) {

Open(U,"Suser") or die("He могу открыть пользователя! $!");

$start = ; close U;

chomp $starl;

uprint range($start);

## Если пользователь существует и в URL не указано

## начальное значение, записать начальное значение

## в файл пользователя и начать вывод.

} else{

Open(U,">$user") or die("He могу открыть пользователя для записи! $!");

print U $output->param("start"), "\n";

close U;

&print_range($output->param("start 1)); }

sub print_range {

my $start = shift;

my $count = 0;

my $line = " "

print $output->header,

$output->start_html(" Мой словарь ");

print "

\n";

while (($count < $start) and ($line = )) { $count++; }

while (($count < $start+10) and ($line = ))

print $line; $count++;

my $newnext = $start+10;

my $newprev = $start-10;

print "

unless (Sstart == 0)

{

print

qq%

Previous%;

}

unless (eof) { print qq% Next%;

# Заметьте, что имя пользователя "username" добавлено к URL.

# В противном случае CGI забудет, с каким пользователем имел дело.

}

print $output->end_html;

exit(0");

}

Меры безопасности

При работе серверов Интернет, будь они серверами HTTP или другого рода, соблюдение мер безопасности является важнейшей заботой. Обмен данными между клиентом и сервером, совершаемый в рамках

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

#!/usr/bin/perl -w

use CGI;

my $output = new CGI;

my $username = $output»param("username");

print $output->header, $output->start_html("Finger Output"),

"

",
 "finger $username", "
", $output->end_html;

Эта программа обеспечивает действующий CGI-интерфейс к команде finger. Если запустить программу просто как finger.cgi, она выведет список всех текущих пользователей на сервере. Если запустить ее как finger.cgi?username=fred, то она выведет информацию о пользователе «fred» на сервере. Можно даже запустить ее как finger. cgi?userna-me=bob@f oo.com для вывода информации об удаленном пользователе. Однако если запустить ее как finger.cgi?username=fred;[email protected]могут произойти нежелательные вещи. Оператор обратный штрих «"" » в Perl порождает процесс оболочки и выполняет команду, возвращающую результат. В данной программе "finger $username* используется как простой способ выполнить команду finger и получить ее результат. Однако большинство командных процессоров позволяет объединять в одной строке несколько команд. Например, любой процессор, подобный процессору Борна, делает это с помощью символа «; ». Поэтому "finger fred;mail [email protected]запустит сначала команду finger, а затем команду mail [email protected]которая может послать целиком файл паролей сервера нежелательному пользователю.

Одно из решений состоит в синтаксическом анализе поступивших от формы данных с целью поиска злонамеренного содержания. Можно, скажем, искать символ «;» и удалять все следующие за ним символы. Можно сделать такую атаку невозможной, используя альтернативные методы. Приведенную выше CGI-программу можно переписать так:

#!/usr/local/bin/perl -w

use CGI;

my $output = new CGI;

my $username = $output->param("username");

$|++;

# Отключить буферизацию с целью направления всех данных клиенту,

print $output->header, $putput->start_html("Finger Output"), "

\n";

$pid = open(C_OUT, "-|");# Эта идиома Perl порождает дочерний процесс и открывает

# канал между родительским и дочерним процессами,

if ($pid) {# Это родительский процесс.

print ; ft Вывести выходные данные дочернего процесса.

print "

", $output->end_html;

exit(O); ft Завершить программу. }

elsif (defined $pid) { # Это дочерний процесс.

$|++; # Отключить буферизацию.

ехес("/usr/bin/finger",$username) or die("exec() call failed.");

# Выполняет программу finger с Susername в качестве одного единственного
# аргумента командной строки. } else { die("неудачная попытка fork()"); }

# Проверка ошибок.

Как видите, это не на много более сложная программа. Но если запустить ее как finger.cgi?username=fred;[email protected]то программа finger будет выполняться с аргументом fred;mail [email protected]как одним именем пользователя.

В качестве дополнительной меры безопасности этот сценарий запускает finger явно как /usr/bin/finger. В маловероятном случае, когда веб-сервер передает вашей CGI-программе необычный PATH, запуск просто finger может заставить выполниться не ту программу, которая нужна. Еще одну меру безопасности можно принять, изучив переменную окружения PATH и убедившись, что она имеет приемлемое значение. Неплохо удалить из PATH текущий рабочий каталог, если только вы уверены, что это не тот случай, когда действительно нужно выполнить находящуюся в нем программу.

Другое важное соображение, касающееся безопасности, связано с правами пользователя. По умолчанию веб-сервер запускает программу CGI с правами того пользователя, который запустил сам сервер. Обычно это псевдопользователь, такой как «nobody», имеющий ограниченные права, поэтому у CGI-программы тоже мало прав. Обычно это хорошо, ибо, если злоумышленник сможет получить доступ к серверу через CGI-программу, ему не удастся причинить много вреда. Пример программы, крадущей пароли, показывает, что можно сделать, но фактический ущерб для системы, как правило, ограничен.

Однако работа в качестве пользователя с ограниченными правами ограничивает и возможности CGI. Если программе CGI нужно читать или записывать файлы, она может делать это только там, где у нее есть такое разрешение. Например, во втором примере запоминания состояния для каждого пользователя ведется файл. CGI-программа должна иметь разрешение на чтение и запись в каталоге, содержащем эти файлы, не говоря уже о самих файлах. Это можно сделать, создав каталог в качестве того же пользователя, что и сервер, с правами чтения и записи только для этого пользователя. Однако для такого пользователя, как «nobody», только root имеет подобную возможность. Если вы не суперпользователь, то вам придется общаться с администратором системы при каждом изменении в CGI.

Другой способ - сделать каталог свободным для чтения и записи, фактически сняв с него всякую защиту. Поскольку из внешнего мира получить доступ к этим файлам можно только через вашу программу, опасность не так велика, как может показаться. Однако если в программе обнаружится прореха, удаленный пользователь получит полный доступ ко всем файлам, в том числе возможность уничтожить их. Кроме того, законные пользователи, работающие на сервере, также получат возможность изменять эти файлы. Если вы собираетесь воспользоваться этим методом, то все пользователи сервера должны заслуживать доверия. Кроме того, используйте открытый каталог только для файлов, которые необходимы CGI-программе; иными словами, не подвергайте риску лишние файлы.

Если это ваше первое обращение к CGI-программированию, дальнейшее изучение можно продолжить разными путями. По этому предмету написаны десятки книг, многие из которых не предполагают никакого знакомства с программированием. «CGI Programming on the World Wide Web» издательства O"Reilly and Associates охватывает материал от простых сценариев на разных языках до действительно поразительных трюков и ухищрений. Общедоступная информация имеется также в изобилии в WWW. Неплохо начать с CGI Made Really Easy (Действительно просто о CGI) по адресу http://www.jmarshall.com/easy/cgi/ .

CGI и базы данных

С начала эпохи Интернет базы данных взаимодействовали с разработкой World Wide Web. На практике многие рассматривают Web просто как одну гигантскую базу данных мультимедийной информации.

Поисковые машины дают повседневный пример преимуществ баз данных. Поисковая машина не отправляется бродить по всему Интернету в поисках ключевых слов в тот момент, когда вы их запросили. Вместо этого разработчики сайта с помощью других программ создают гигантский указатель, который служит базой данных, откуда поисковый механизм извлекает записи. Базы данных хранят информацию в таком виде, который допускает быструю выборку с произвольным доступом.

Благодаря своей изменчивости базы данных придают Web еще большую силу: они превращают ее в потенциальный интерфейс для чего угодно. Например, системное администрирование можно производить удаленно через веб-интерфейс вместо требования регистрации администратора в нужной системе. Подключение баз данных к Web лежит в основе нового уровня интерактивности в Интернет.

Одна из причин подключения баз данных к Web регулярно дает о себе знать: значительная часть мировой информации уже находится в базах данных. Базы данных, существовавшие до возникновения Web, называются унаследованными (legacy) базами данных (в противоположность неподключенным к Web базам данных, созданным в недавнее время и которые следует назвать «дурной идеей»). Многие корпорации (и даже частные лица) стоят сейчас перед задачей обеспечения доступа к этим унаследованным базам данных через Web. Если только ваша унаследованная база не MySQL или mSQL, эта тема лежит за пределами данной книги.

Как сказано раньше, только ваше воображение может ограничить возможности связи между базами данных и Web. В настоящее время существуют тысячи уникальных и полезных баз данных, имеющие доступ из Web. Типы баз данных, действующих за пределами этих приложений, весьма различны. Некоторые из них используют CGI-программы в качестве интерфейса с сервером баз данных, таким как MySQL или mSQL. Эти типы представляют для нас наибольший интерес. Другие используют коммерческие приложения для взаимодействия с популярными настольными базами данных, такими как Microsoft Access и Claris FileMaker Pro. А другие просто работают с плоскими текстовыми файлами, являющимися самыми простыми базами данных изо всех возможных.

С помощью этих трех типов баз данных можно разрабатывать полезные веб-сайты любого размера и степени сложности. Одной из наших задач на протяжении нескольких следующих глав будет приложение мощи MySQL mSQL к Web с использованием CGI-программирования.

Новая страница 3

CGI

Скрипты CGI

Для того, чтобы Web-узлы были действительно интерактивными, она должны обмениваться информацией с пользователем, а не только позволять ему загружать документы. Используя программы Common Gateway Interface (называемые CGI-скриптами ), можно создавать Web-страницы, управляемые данными. Как вы узнаете, используя скрипты CGI, узел может получать запросы и отвечать пользователю.

Поскольку скрипты CGI формируют документы HTML во время работы системы, то пользователь имеет дело с динамическими Web-страницами. Можно даже генерировать страницы случайным образом, так что каждый пользователь, который посетит ваш узел, будет видеть несколько иную картину, чем предыдущий.

Почему Web-узлы используют CGI

Для создания динамических файлов HTML нет необходимости в применении CGI-скриптов. Однако без таких скриптов всякий раз, когда понадобится новая интерактивная динамическая страница Web, придется модифицировать программу-сервер. Спустя какое-то время программа-сервер может стать исключительно большой. Для того чтобы исключить такую модификацию сервера, разработчики используют CGI. Используя CGI-скрипты, сервер может переложить задачу создания линамических Web-документов на прикладную программу, созданную для этих специфических потребностей. Вы будете создавать вашу прикладную программу, используя C/C++, Perl, JavaScript, VBScript, или какой-либо другой язык программирования.

Программа-сервер должна вызвать CGI-скрипт

Вы не можете выполнять CGI-скрипт непосредственно из программы броузера. Для использования скрипта необходимо, чтобы он находился на том же компьютере, где расположен сервер. Для того, чтобы вы могли просмотреть выходные данные, формируемые скриптом на вашем броузере, требуется, чтобы сервер исполнил этот скрипт. Но это вопросы для администратора сервера.

Броузер, сервер и CGI

Как известно, для получения броузером HTML-документов, он сначала устанавливает контакт с сервером, затем запрашивает документ (обычно ваш броузер определяет GET- метод HTTP для получения информации от сервера). Далее, если документ существует, сервер отвечает броузеру, посылая ему HTML-документ, и затем закрывает соединение. Применение CGI-скриптов вносит изменения в этот процесс только на стороне сервера. Броузер не знает, что сервер вызывает CGI-скрипт, и он не заботится о том, каким именно способом поступают к нему данные от сервера. Как Web-программист, при написании CGI-скриптов вы заботитесь только о входах и выходах сервера. Броузер будет устанавливать контакт с программой сервера, которая, в свою очередь, исполняет CGI-скрипт. Ваш скрипт, в свою очередь, выполняет необходимую обработку данных, чтобы сформатировать требуемый выход. Обычно ваш сервер передает выходные данные из скрипта в форме HTML-броузеру. Чтобы выполнить это, верверная программа добавляет необходимую заголовочную информацию к выходным данным, сформированным скриптом, и посылает эту заголовочную информацию вместе с данными обратно броузеру. Затем сервер закрывает соединение и ждет новых запросов.

Как вы, возможно, знаете, серверы, которые работают под 32-битными операционными системами, такими как Windows 95/98 или Windows NT , могут обрабатывать запросы от многих пользователей одновременно. Отсюда следуте, что несколько пользователей могут одновременно использовать скрипт. Поэтому каждый из них, в зависимости от запроса, будет видетьь свою картину ответа сервера.

Взаимосвязь сервера и CGI-скрипта

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

Как вы уже знаете, HTTP является протоколом, с помощью которого клиенты и серверы Web обеспечиваются информацией. Заголовочная HTTP информация помогает программам эффективно выполнять обмен данными. Поэтому необходимо уделить особое внимание заголовочной информации, которой сервер снабжает броузер. Например, когда программа-сервер готова послать данные броузеру, она посылает заголовки, описывающие статус данных, тип данных и т.д. В свою очередь броузер использует заголовок (Content-type ) для подготовки к выводу данных на экран. Сервер отвечает за то, чтобы обеспечить этой метаинформацией броузер каждый раз, когда он посылает ему данные.

CGI т базы данных

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

Где находятся скрипты

Стандарты CGI не предписывают, куда должны помещаться скрипты, то есть не определяют заранее диск и каталог. Обычно Web-сервер ожидает найти скрипты в каталоге /CGI-BIN, который расположен нажи каталога самой программы сервера. Если вы помещаете свои скрипты на чей-то сервер, необходимо определить каталог для своих файлов, содержащих скрипты.

Расширение имен файлов CGI-скриптов

Серверы HTTP для Windows-систем обычно для CGI-файлов используют расшерение EXE или PL. Например, если вы создаете CGI-программу (скрипт), используя язык программирования С, то расширение ваших файлов-скриптов будет, вероятно, ЕХЕ. Аналогично, если вы создаете скрипт с помощью языка Perl, расширение ваших файлов будет PL.

Однако некоторые серверы ожидают использования для скриптов расшерения CGI. Фактически многие системы включают CGI-расширение как часть файла конфигурации сервера. Если вы не уверены, какое именно расширение файла поддерживает сервер, обратитесь к Web-мастеру. В противном случае сервре будет вызывать ваши скрипты некорректно.

Основы взаимодействия между Web-сервером и CGI-скриптом

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

Когда пользователь Web устанавливает связь, в результате которой сервер вызывает скрипты, серверу требуется большое число переменных окружения, используемых скриптом для получения информации. Эти переменные окружения содержат информацию о броузере, который посылает запрос, сервере, который его обрабатывает, данных, передаваемых вашему скрипту. Переменные окружения CGI чувствительны к регистру, и каждая из переменных определена в спецификации CGI.

Переменные окружения

Переменная AUTH_TYPE

Скрипты CGI используют переменную окружения AUTH_TYPE для идентификации пользователя, который пытается получить доступ к скрипту. Если сервер сконфигурирован так, чтобы поддерживать идентификацию пользователя, то пользователь, пытающийся получить доступ к скрипту, должен назвать свое имя и пароль. Например, следующее значение переменной означет, что от пользователя требуется основной уровень идентификации:

AUTH_TYPE = Basic

Переменная CONTENT_LENGTH

Скрипты используют переменную окружения CONTENT_LENGTH для того, чтобы определить точное число байт, содержащихся в просоединенных данных. Например, если запрос содержит документ длиной в 1,024 байта, то переменной окружения присваивается следующее значение:

CONTENT_LENGTH = 1024

Переменная CONTENT_TYPE

Скрипты используют эту переменную окружения для запросов, которые содержат присоединенную информацию. К такому типу запросов отоносится HTTP-операция POST. Содержащаяся в переменной информация указывает на тип присоединенных данных (MIME-тип.подтип). Например, если запрос содержит присоединенный документ HTML , то переменная окружения будет принимать следующие значения:

CONTENT_TYPE = text/html

Переменная GATEWAY_INTERFACE

Скрипты используют эту переменную для того, чтобы определить версию, номер выпуска спецификации CGI, которой удовлетворяет Web-сервер. Формат номера выпуска спецификации следующий: CGI/номер выпуска. Например, для CGI выпуска 1.1 переменная окружения будет иметь следующий вид:

GATEWAY_INTERFACE = CGI/1.1

Переменная PATH_INFO

Скрипты используют эту переменную для того, чтобы определить дополнительную информацию о путях, обеспечиваемую клиентам. Другими словами, сервер может обратиться к скрипту, используя виртуальный путь к скрипту, после которого стоит дополнительная информация о пути. Серверная программа должна декодировать эту дополнительную информацию, если она пришла от какого-то URL, перед тем как сервер передаст ее скрипту. Обычно эта дополнительная информация указывает на ресурс, который скрипт должен возвратить в случае успешного выполнения запроса.

Путь записывается в относительной форме, где за базу берется корневой каталог сервера. Иными словами, корневой каталог сервера является базисом для относительного пути, который и присваивается переменной PATH_INFO . Например, если задан путь c:/cgi-bin/example1.exe/sports.html, то переменная окружения будет иметь следующий вид:

PATH_INFO = /sports.html

Переменная PATH_TRANSLATED

Скрипты используют эту переменную для получения окончательной, пригодной для непосредственного использования информации относительно пути. Сервер переводит информацию переменной путем выполнения необходимых преобразований пути. Например, если переменная PATH_TRANSLATED имеет значение /sports.html, а корневым дирикторием сервера служит c:\, то переменная окружения будет иметь следующее значение:

PATH_TRANSLATED = c:\sports.html

Переменная QUERY_STIRNG

Скрипты используют эту переменную для того, чтобы получить информацию в текстовой форме (состоящую из аргументов), которая следует справа от знака вопроса после URL, переданного от пользователя скрипту для обработки. Эта текстовая сторока содежит вход для скрипта. Далее сервер заменяет в данном тексте каждый пробел на знак " + ", а все непечатные символы знаком " %dd", где d является базой десятичной системы счисления.

Скрипт должен содержать код для расшифровки этой текстовой строки. Сервер, передавая эту информацию скрипту, не должен заниматься декодированием информации запроса каким-либо образом. Сервер должен также установить переменную QUERY_STRING в случае, если пользователь обеспечивает какую-то информацию запроса. Например, для URL http://www.jamsa.com/cgi-bin/grandma.exe?name=margaret+alarcon переменная окружения имеет значением следующую величину:

QUERY_STRING = name=margaret+alarcon

Переменная REMOTE_ADDR

Скрипты используют эту переменную для получения IP-адресса удаленного узла (броузера), который делает запрос. Например, значение переменной окружения может быть следующим:

REMOTE_ADDR = 204.212.52.209

Переменная REMOTE_HOST

Скрипты используют эту переменную для того, чтобы получить имя узла, с которого делается запрос. Если сервер не знает имя узла, делающего запрос, то сервер должен присвоить значение переменной окружения REMOTE_ADDR и не присваивать значения переменной REMOTE_HOST . Напрмиер, для узла jamsa.com переменная окружения будет содержать следующее значение:

REMOTE_HOST = jamsa.com

Переменная REMOTE_IDENT

Используется для того, чтобы получиь имя удаленного пользователя, делающего запрос к серверу. Программа Web-сервера представляет собой программное обеспечение. вызывающее ваш скрипт. Если HTTP Web-сервер поддерживает протокол RFS 931 (Authentication Server Protocol), то сервер установит эту переменную равной значению имени пользователя, которое имеется у сервера. Скрипты могут использовать эту переменную только для регестрации пользователя. Напрмер, если имя удаленного пользователя pschmauder и он назодится на удаленном узле jamsa.com , то переменная примет следующее значение:

REMOTE_IDENT = pschmauder.www.jamsa.com

Переменная REMOTE_USER

Используется для того, чтобы получить имя удаленного пользователя без имени узла, с которого он производит запрос. Если сервер поддерживает идентификацию пользователя и скрипт является защищенным, то сервер установит имя пользователя и присвоит его этой переменной. Например, предположим, что именем удаленного пользователя является pschmauder . Тогда переменная будет выглядеть следующим образом:

REMOTE_USER = pschmauder

Переменная REQUEST_METHOD

Используется для того, чтобы определить тип HTTP-запроса, который послан броузером серверу и служит для вызова скриптов. Эта переменная может принимать значения GET, HEAD или POST. Например, если броузер посылает GET- метод, то переменная окружения содержит следующее:

REQUEST_METHOD = GET

Переменная SCRIPT_NAME

Используется для того, чтобы определить виртуальный путь к скрипту, который будет запущен сервером. Например, если имеется URL http://www.jamsa.com/cgi-bin/someprog.exe , то переменная окружения примет следующее значение:

SCRIPT_NAME = cgi-bin/example1.exe

Переменная SERVER_NAME

Использутся для того, чтобы определитьимя домена ли IP-адрес комрьютера, на котором раположен Web-сервер. Например, когда сервер возвращает IP-адрес, переменная окружения будет иметь вид, подобный следующему:

SERVER_NAME = 204.212.52.209

Переменная SERVER_PORT

Используется для того, чтобы определить номер порта, который пользователь (броузер) использует для связи с Web-сервером. Если используется HTTP-порт по умолчанию, то эта величина равна 80. Если используется какой-то другой порт, например, http://www.jamsa.com:3000 , то переменная принимает следующее значение:

SERVER_PORT = 3000

Переменная SERVER_PROTOCOL

Используется для того, чтобы определить имя и номер выпуска протокола, используемогоклиентом (броузером) для того, чтобы послать запрос к Web-серверу. Анализируя содержание переменной, скрипт может идентифицировать имя и номер выпуска протокола, который он должен использовать при передаче данных серверу. Формат имени протокола и номера выпуска следующий: протокол/номер выпуска. Например, для HTTP 1.1 переменная окружения будет иметь следующий вид:

SERVER_PROTOCOL = HTTP/1.1

Переменная SERVER_SOFTWARE

Как вы знаете, Web-сервер исполняет скрипты CGI. Поскольку скрипт может испольняться по-разному для различных серверных программ, скрипты используют эту переменную для того, чтобы определить имя программы Web-сервера и ее номер версии. Формат имени Web-сервера и номер версии должен передаваться CGI следующим образом: имя/версия. Например, для FOLK WEB - сервера версии 1.01 переменная окружения будет иметь седующий вид:

SERVER_SOFTWARE = FolkWeb/1.01 (Windows-32bit)

Дополнительные переменные окружения

В дополнение к переменным окружения. обсуждавшимся ранее, сервер также помещает данные из заголовка запроса, полученного от клиента, в переменные окружения. Сервер присваивает значения переменным, чьи имена начинаются с префикса HTTP_, после которого идет имя заголовка. Сервер заменяет все символы переноса (-) в заголовке на (_). Сервер может также исключать любые заголовки, которые он уже обработал, используя переменные окружения, такие как AUTH_TYPE, CONTENT_TYPE и CONTENT_LENGTH.

Переменная HTTP_ACCEPT

Используется для того, чтобы определить, какие MIME-типы может принимать броузер. Они определены в HTTP-заголовках, которые броузер послал серверу. Как известно, MIME-тип задается в виде тип/расширение. Если имеется насколько MIME-типов, то они разделяются запятыми. Например, переменная окружения может принимать следующее значение:

HTTP_ACCEPT = audio/aif, text/html, text/plain

Переменная HTTP_USER_AGENT

Используется для того, чтобы идентифицировать тип броузера, который делает запрос серверу. Например, переменная окружения может содержать следующее:

HTTP_USER_AGENT = Mozilla/2.01 Gold(Win95PE)

Опции командной строки CGI

Обычно CGI-скрипты используют командную строку в качестве входа для того, чтобы выполнить запрос ISINDEX, позволяющий добавить интерактивный поиск по ключевому слову к вашему HTML-документы. Однако не все серверные программы поддерживают ISINDEX- запрос. Броузер посылает запрос в виду командкной строки серверу. Программа сервера может идентифицировать входную командную строку, устанавливая, использовал ли броузер GET- метод HTTP и содержит ли строка URL символы uuencoded = .

Если броузер использует GET- метод HTTP и строка URL-поиска не содержит символы uuencoded =, то запрос осуществляется в форме командной строки. Перед тем как сервер вызовет соответствующий скрипт, серверная программа должна расщепить командную строку, используя знак (+), для отделения параметров. Затем сервер выполняет дополнительное декодирование (если необходимо) каждого параметра, переданного в URL-строке поиска, и хранит каждый параметр-строку в массиве, названную argv.

Дополнительное декодирование, выполняемое сервером, состоит в разделении отдельных строк, используя амперсанда (&) в качестве разделителя. Далее сервер расщепляет каждую из этих строк снова, используя знак (=) для того, чтобы отделить имя переменной, которое ставиться слева от знака (=), от значения переменной, которое стоит справа от (=). Сервер хранит число элементов, содержащихся в массиве argv , в переменной целого типа argс .

Если сервер нашел знак равенства внутри строки QUERY_STRING переменной окружения, то он не будет посылать командную строку в качестве входа в скрипт. Также если по какой-то причине программа сервера не может послать массив argv скрипту, она обеспечит недекодированную информацию запроса в переменной окружения QUERY_STRING.

Стандартный ввод (STDIN )

Когда броузер запрашивает сервер (например, используя HTTP-метод POST ), информация, которую получает скрипт, приходит со стандартного дескриптора ввода stdin. Серверная программа посылает скрипту переменную окружения CONTENT_LENGTH. Эта переменная содержит число байт, которое сервер посылает скрипту через этот дескриптор. Скрипт может использовать значение переменной CONTENT_LENGTH для того, чтобы определить, сколько данных должно поступить со стандартного ввода. Сервер также снабжает скрипт переменной окружения CONTENT_TYPE , которая помогает скрипту определить, как обрабатывать получаемые данные. В конце этого потока данных сервер может послать (а может и не посылать) маркер конца файла. Однако именно скрипт обязан определить, какой объем данных читать, и использует он для этого переменную окружения CONTENT_LENGTH.

Например, если форму использует HTTP-метод POST ( ) и посланные серверу данные закодированы следующим образом: name=alberta&husband=art , то сервер присвоит переменным CONTENT_LENGHT и CONTENT_TYPE следующие значения:

CONTENT_LENGHT = 24 CONTENT_TYPE = APPLICATION/x-www-form-urlencoded

Стандартный вывод (STDOUT )

После того как CGI-скрипт закончит обрабатывать полученные от сервера данные, он должен посылать свой ответ серверу. Для того, чтобы послать свои данные серверу, он должен посылать их файловому дескриптору стандартного выводы STDOUT . В общем случае данные, которые скрипт посылает назад серверу, представляет собой HTTP-ответ, включающий заголовок, после которого идет пустая строка и за ней остальная часть ответа. Обычно выходом скрипта служат документы HTML, созданные скриптом.

Прямой ответ скрипта броузеру

Обычно скрипт выдает ответ, который сервер интерпретирует и посылает назад броузеру. Преимуществом посылки выходных данных от скрипта серверу служит то, что скрипт в таком случае не обязан посылать полный заголовок HTTP на каждый запрос. Однако некоторые скрипты разгружают сервер и шлют свои выходные данные прямо броузеру. Для того, чтобы можно было отличить такие скрипты, которые шлют данные непосредственно броузеру, от скриптов, которые направляют их серверу, протокол CGI требует, чтобы имена скриптов, осуществляющих прямую посылку броузеру, начинались с букв nph- (которые говорят серверу Not to Parse the Header - не инициализировать заголовок). Если имя скрипта начинается так, то сервер не будет вставлять заголовок в данные скрипта. В таком случае ответственность возлагается на сам скрипт.

Заголовки CGI

Как вы узнали ранее, выходные данные скрипта начинаются с заголовка. Этот заголовок состоит из строки текста в том же формате, что и заголовок HTTP, оканчиваясь пустой строкой (строкой, которая содержит только CRLF). Если выходные данные скрипта содержат заголовки, не являющиеся директивами серверу , то сервер посылает эти заголовки броузеру в ответе на его запрос. Текущая спецификация CGI пределяет три директивы:

  • CONTENT-TYPE
  • LOCATION
  • STATUS

Поле Content-type в заголовке CGI определяет тип/расширение MIME-данных, которые скрипт посылает назад броузеру. Обычно скрипты выдают на выходе документ HTML. В этом случае заголовок CGI Content-type содержит следующее:

Content-type: text/html

Поле Location заголовка указывает на документ. Скрипты обычно используют поле Location для указания местонахождения документа. В случае, если документ находится не на сервере и содержимым поля является URL, сервер перенаправляет броузерна соответствующий узел. Если же документ находится на сервере и поле представляет собой виртуальный путь, сервер выдает этот документ броузеру. Например, для того чтобы указать на документ на удаленном сервере, значение поля в заголовке может иметь вид:

Location: htpp://www.cetis.ru

Поле Status заголовка содержит значение статуса HTTP, которое сервер направляет от скрипта броузеру. Сервер, который вызывает скрипт, использует различные коды статуса HTTP. Вы можете захотеть направлять код статуса прямо броузеру, особенно в случае возникновения ошибок.

В следующем примере проиллюстрирован типичный вывод скрипта, который он должен генерировать при посылке данных серверу. После того как данные достигнут серверной программы, сервер направит данные броузеру:

Content-type: text/html This is the title This is body generated by your CGI script.

Обратите внимание на пустую строку меду первыми строками. Эта пустая строка абсолютно необходима. Следующий фрагмент кода на языке С создает записанный выше документ с использованием функции printf:

// More code above: printf("Content-type: text/html\n"); printf("\n"); // Make sure to include this blank line printf("\n"); printf("\n"); printf("This is the title\n"); printf("\n"); printf("\n"); printf("This is the body generated by your CGI script.\n"); printf("\n"); printf("\n"); // More code bellow...

Похожие статьи