Пример простейшего cgi сервера

Думаю многие знают, как работает CGI взаимодействие между клиентом и сервером: клиент получает от сервера и отдает серверу данные через стандартные stdin и stdout. Многие наверное даже сами писали CGI клиентов, ведь по сути — любой скрипт для веб-сервера это и есть CGI-клиент.А многие ли задавались вопросом, как именно происходит эта «магия»? Каким образом стандартные функции для ввода/вывода вместо экрана взаимодействуют с сервером? Результаты поиска ответа в сети меня не удовлетворили и я решил сам написать простейший CGI сервер, который сможет:

Запускать дочерний процес — CGI скрипт Передавать скрипту переменные окружения и переменные командной строки Принимать от скрипта ответ Завершаться, когда завершится процесс клиента Кроме этого, мне хотелось, чтобы клиент и сервер компилировались как в Windows, так и в Linux.CGI-клиентНачну я все-таки с самого простого и общеизвестного: опишу своего клиента для CGI сервера. Простой «hello world» меня не устроил, потому что нужно было проверить не только возможность передачи сообщений через stdout, но и корректность приема переменных окружения и сообщений из stdin.Кроме этого чтобы убедиться, что получилось самое настоящее CGI взаимодействие, было решено написать не один, а сразу два клиента. На с++ и на python.Исходник CGI-клиента на С++ #include #include #include #include #include

#ifdef _WIN32 #include #define getpid () GetCurrentProcessId () #define sleep (n) Sleep (n*1000); #else #include #endif

using namespace std;

int main (int argc, char *argv[]) { //Отдаем в stdout переменные командной строки, которые получили от родителя cout << "Child process started\n"; for (int n=0; n

//Отдаем в stdout переменные окружения, которые получили от родителя const int nContentLength = atoi (getenv («Content-Length»)); cout << "\n" << "Content-Length = " << nContentLength << "\n" << "VARIABLE2 = " << getenv("VARIABLE2") << "\n" << "VARIABLE3 = " << getenv("VARIABLE3") << "\n" << "\n\n"; fflush(stdout);

sleep (5); //Паузы сделаны для наглядности

vector vBuffer (nContentLength);

//Получаем из stdin все, что прислал туда родительский процесс const size_t nBytes = fread (&vBuffer[0], 1, nContentLength, stdin);

//Отдаем в stdout то, что только что получили от родителя и добавляем свое cout << "Request body:\n"; fwrite(&vBuffer[0], 1, nBytes, stdout); fflush(stdout);

sleep (5); //Паузы сделаны для наглядности

return 0; } Исходник CGI-клиента на Python #!/usr/bin/python

import sys import os

print «Content-Length = » + os.environ[«Content-Length»] print «VARIABLE2 = » + os.environ[«VARIABLE2»] print «VARIABLE3 = » + os.environ[«VARIABLE3»]

body = sys.stdin.read (int (os.environ[«Content-Length»])) print body

Пояснения к коду клиентов Если из CGI-сервера запустить клиента на С++, то на экран выведется информация о переменных командной строки, три переменные окружения с именами «Content-Length», «VARIABLE2» и «VARIABLE3», а также все содержимое которое получено от сервера в stdin.Если из CGI-сервера запустить клиента на Python, то на экран выведется информация о переменных окружения с именами «Content-Length», «VARIABLE2» и «VARIABLE3», а также все содержимое которое получено от сервера в stdin.Надо отметить, что переменная окружения «Content-Length» должна быть сформирована сервером таким образом, чтобы быть числом меньше либо равным количеству байт в stdin. Это необходимо потому, что клиент никаким другим образом не может узнать данную информацию кроме как от сервера.

CGI-сервер В отличии от клиентских скриптов, код CGI сервера в сети найти совсем не просто, поэтому мой код собран из различных обрывистых и часто содержащих ошибки примеров. Кое-что добавил от себя, чтобы было более наглядно.Исходник CGI-сервера на С++ #include #include #include #include #include

#ifdef _WIN32 #include /* Required for _spawnv */ #include #include

#define pipe (h) _pipe (h, 1024×16, _O_BINARY|_O_NOINHERIT) #define getpid () GetCurrentProcessId () #define dup _dup #define fileno _fileno #define dup2 _dup2 #define close _close #define read _read #define write _write #else #include #include #include #include #endif

using namespace std;

//Формируем в глобальных переменных тело запроса и его длинну static const string strRequestBody = »===this is request body===\n»; static const string strRequestHeader = «Content-Length=» + to_string ((long long)strRequestBody.length ());

//Формируем переменные окружения которые будут отосланы дочернему процессу static const char *pszChildProcessEnvVar[4] = {strRequestHeader.c_str (), «VARIABLE2=2», «VARIABLE3=3», 0};

//Формируем переменные командной строки для дочернего процесса. Первая переменная — путь к дочернему процессу. static const char *pszChildProcessArgs[4] = {»./Main_Child.exe», «first argument», «second argument», 0}; //При желании можно запустить интерпретатор какого-нибудь скрипта. //Тогда первый аргумент — путь к интерпретатору, второй — к скрипту //static const char *pszChildProcessArgs[3] = {«python»,»./test.py», 0};

//Это функция, которая породит дочерний процесс и передаст ему переменные из родительского int spawn_process (const char *const *args, const char * const *pEnv) { #ifdef _WIN32 return _spawnve (P_NOWAIT, args[0], args, pEnv); #else /* Create copy of current process */ int pid = fork (); /* The parent`s new pid will be 0 */ if (pid == 0) { /* We are now in a child progress Execute different process */ execvpe (args[0], (char* const*)args, (char* const*)pEnv);

/* This code will never be executed */ exit (EXIT_SUCCESS); }

/* We are still in the original process */ return pid; #endif }

int main () { int fdStdInPipe[2], fdStdOutPipe[2]; fdStdInPipe[0] = fdStdInPipe[1] = fdStdOutPipe[0] = fdStdOutPipe[1] = -1; if (pipe (fdStdInPipe) != 0 || pipe (fdStdOutPipe) != 0) { cout << "Cannot create CGI pipe"; return 0; }

// Duplicate stdin and stdout file descriptors int fdOldStdIn = dup (fileno (stdin)); int fdOldStdOut = dup (fileno (stdout));

// Duplicate end of pipe to stdout and stdin file descriptors if ((dup2(fdStdOutPipe[1], fileno (stdout)) == -1) || (dup2(fdStdInPipe[0], fileno (stdin)) == -1)) return 0;

// Close original end of pipe close (fdStdInPipe[0]); close (fdStdOutPipe[1]);

//Запускаем дочерний процесс, отдаем ему переменные командной строки и окружения const int nChildProcessID = spawn_process (pszChildProcessArgs, pszChildProcessEnvVar);

// Duplicate copy of original stdin an stdout back into stdout dup2(fdOldStdIn, fileno (stdin)); dup2(fdOldStdOut, fileno (stdout));

// Close duplicate copy of original stdin and stdout close (fdOldStdIn); close (fdOldStdOut);

//Отдаем тело запроса дочернему процессу write (fdStdInPipe[1], strRequestBody.c_str (), strRequestBody.length ());

while (1) { //Читаем ответ от дочернего процесса char bufferOut[100000]; int n = read (fdStdOutPipe[0], bufferOut, 100000); if (n > 0) { //Выводим ответ на экран fwrite (bufferOut, 1, n, stdout); fflush (stdout); }

//Если дочерний процесс завершился, то завершаем и родительский процесс #ifdef _WIN32 DWORD dwExitCode; if (!:: GetExitCodeProcess ((HANDLE)nChildProcessID, &dwExitCode) || dwExitCode!= STILL_ACTIVE) break; #else int status; if (waitpid (nChildProcessID, &status, WNOHANG) > 0) break; #endif } return 0; } Пояснения к коду сервера Сервер писался так, чтобы его можно было скомпилировать как в Windows, так и в Linux, поэтому первые несколько строк это кроссплатформенные определения.Дальше в глобальных переменных задается: тело запроса который будет послан скрипту (строка »===this is request body===\n»), длина сообщения запоминается в переменной strRequestHeader переменные окружения в виде массива строк {strRequestHeader.c_str (), «VARIABLE2=2», «VARIABLE3=3», 0} переменные командной строки в виде массива {»./Main_Child.exe», «first argument», «second argument», 0} При такой инициализации сервер будет взаимодействовать с процессом »./Main_Child.exe». Так я назвал скомпилированного клиента на С++.Если в качестве переменных командной строки задать массив {«python»,»./test.py», 0}, то сервер будет взаимодействовать со скриптом на питоне.

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

Заканчивается сервер функцией «main», большую часть которой (как можно догадаться по английским комментариям) я взял из разных сторонних источников. Из кода этой функции видно, что «вся соль» перенаправления ввода/вывода между процессами организована с помощью «pipe» (каналов).Механизм каналов довольно простой и стандартный, он почти одинаково реализован как в Windows так и в Linux. Чтобы связать эти два подхода, в самом начале исходника я добавил простое переопределение:

#ifdef _WIN32 #define pipe (h) _pipe (h, 1024×16, _O_BINARY|_O_NOINHERIT) #endif В конце функции «main» организован бесконечный цикл, в котором сервер принимает от клиента ответ и передает этот ответ на экран. Выход из цикла произойдет когда клиентский процесс завершится.

Заключение Несмотря на свой солидный возраст, CGI — интерфейс остается одним из самых распространенных интерфейсов межпроцессного взаимодействия. Подавляющее большинство веб-серверов и веб-сайтов взаимодействуют именно с помощью этого интерфейса. Надеюсь данная статья будет полезна тем, кто захочет понять, а может и реализовать в собственных проектах не только клиентскую, но и серверную часть CGI.Все исходники можно найти: тут.В папках «cgi_main» и «child» находятся проекты для Visual Studio.Чтобы запустить пример под Линукс, достаточно скопировать содержимое папки «src» и запустить скрипт «compile.py». Должно получиться что-то вроде этого: c57700faa55b421f80fe06b19dde93bc.jpg

© Habrahabr.ru