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

Результаты поиска ответа в сети меня не удовлетворили и я решил сам написать простейший CGI сервер, который сможет:
  • Запускать дочерний процес — CGI скрипт
  • Передавать скрипту переменные окружения и переменные командной строки
  • Принимать от скрипта ответ
  • Завершаться, когда завершится процесс клиента

Кроме этого, мне хотелось, чтобы клиент и сервер компилировались как в Windows, так и в Linux.


CGI-клиент


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

Исходник CGI-клиента на С++
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <fcntl.h>
#include <vector>

#ifdef _WIN32
#include <windows.h>
#define getpid() GetCurrentProcessId()
#define sleep(n) Sleep(n*1000);
#else
#include <unistd.h>
#endif

using namespace std;

int main(int argc, char *argv[])
{
	//Отдаем в stdout переменные командной строки, которые получили от родителя
	cout << "Child process started\n";
	for (int n=0; n<argc; n++)
		cout << "argv[" << n << "] = " << argv[n] << "\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<unsigned char> 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 <stdio.h>
#include <iostream>
#include <fcntl.h>
#include <string>
#include <vector>

#ifdef _WIN32
#include <process.h> /* Required for _spawnv */
#include <windows.h>
#include <io.h>

#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 <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#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». Должно получиться что-то вроде этого:

Комментарии (2)


  1. evnuh
    01.04.2015 19:29
    +5

    Замечательный пример cgi сервера — github.com/lighttpd/spawn-fcgi. И да, кто-то пользуется CGI? Все на бинарном fast-cgi.


  1. kreon
    01.04.2015 22:02

    В 2009 году я на C написал веб-приложение, которое управляло сетью провайдера. Контроль железок, отчетики из биллнга, тулзы для монтажников и подключальщиков и все такое прочее. До сих пор горжусь той разработкой.
    Но время конечно было потрачено дико неоптимально :-)