Сьогодні ми поговоримо про web-програмування на C++. Я розкажу, як налаштовувати http-сервер для того, щоб він підтримував виконання CGI-програм, і декілька прикладів найпростіших сторінок. В кінці статті, ми порівняємо швидкість видачі сторінки для С++ CGI-програми і аналогічного скрипта на PHP.

Конфігурування Apache2 для CGI-шлюзу

Якщо у Вас ще немає встановленого серверу Apache, чи іншого http-серверу, обов'язково встановіть його, інакше ви не зможете викликати свої CGI-програми. Сервер Apache можна сконфігурувати двома способами: через apache2.conf, або ж через файл .htaccess.

Файл apache2.conf

Для того, щоб дозволити виконання CGI-програм на сервері Apache2, необхідно відредагувати відповідний файл конфігурації apache2.conf. Він знаходиться, якщо у вас ОС Windwos і Ви встановили Appserv у директорію за умовчанням, за адресою “С:\AppServ\Apache2.2\conf\httpd.conf”, або ж, якщо у вас Юнікс-подібна ОС, то за адресою “/etc/apache2/apache2.conf”. Якщо у Вас ОС Windows, ви можете і не редагувати файл apache2.conf, а просто поміщати свої C++ CGI-програми у директорію “C:\AppServ\www\cgi-bin”, і в рядку адреси браузера вказувати “http://localhost/cgi-bin/myprogram.cgi” (рядок “myprogram.cgi” відповідно замінити на ім'я Вашої CGI-програми). Далі, необхідно у кінець файлу вставити наступний код:
LoadModule cgi_module /usr/lib/apache2/modules/mod_cgi.so 
<Directory /usr/www/html> 
    AddHandler cgi-script .cgi 
    SetHandler cgi-script  
    Options Indexes FollowSymLinks ExecCGI 
    AllowOverride None 
    Require all denied 
</Directory>
Після цього необхідно перезапустити веб-сервер командою
sudo apache2ctl restart
для Юнікс-систем, а для системи Windows запустити на виконання файл “C:\Appserv\Apache2.2\apache_stop” і після нього запустити “C:\Appserv\Apache2.2 \apache_start”.

Файл .htaccess

Створіть у необхідній директорії, яка буде доступна для web-сервера, текстовий файл з ім'ям “.htacces” і добавте в нього наступний код
AddHandler cgi-script .cgi
Options +ExecCGI

Створюємо CGI-програми на C++

Перш ніж компілювати код С++ у CGI-програми, Вам необхідно впевнитись, що Ви маєте компілятор C++. Без нього вихідний код неможливо буде перетворювати на виконувані файлі, і ви не зможете писати Web-сторінки. Я буду користуватися компілятором GNU GCC, викликаючи його з командного рядка.

“Привіт, Світ!”

Давайте напишемо найпростішу програму “Привіт, Світ!”. Для цього необхідно створити новий файл вихідного коду С++ (Я обираю розширення “.cxx”) і вставити у нього наступний код:
#include <iostream> 
using namespace std; 

int main () 
{ 
 cout << "Content-Type: text/html; charset=UTF-8\n\n" ; 

 cout << "<h1>Привіт, Світ!</h1>" ; 

 return 0; 
}
Зверніть увагу на рядок “cout << "Content-Type: text/html; charset=UTF-8\n\n" ; ”, якщо Ви не виведете в стандартний вивід “Content-Type: text/html;\n\n” (обов'язково з двома новими рядками - “\n\n”), тоді Ви не побачите результат виконання CGI-програми. Скомпілюйте даний файл і помістіть його у Вашу директорію, яка призначена для CGI-програм. Якщо необхідно, перейменуйте її у “index.cgi”. halloworld_CPP_CGI_compilation Відкрийте свій Web-переглядач, і введіть у рядку адреси шлях до вашої CGI-програми, використовуючи протокол HTTP. У мене вона буде виглядати як “http://localhost/index.cgi”: halloworld_CGI_in_browser Запрацювало! Тепер Ви вмієте писати серверну сторону сайту використовуючи мову С++. Однак, як Ви, напевне, здогадалися, C++ CGI-програми це тільки серверна сторона. Браузер не “розуміє” С++ чи бінарний код, він може коректно інтерпретувати тільки HTML, CSS, JavaScript і інші технології. С++ Вас не звільняє від необхідності знань HTML технологій.

Змінні середовища

Якщо Ви прочитали мою попередню статтю, про специфікацію CGI інтерфейсу, тоді Ви знаєте, що перед викликом CGI-програми, Web-сервер створює для неї декілька спеціалізованих мета-змінних, тобто змінних середовища системи. Оскільки нам для коректної обробки буде необхідно отримувати їхні значення, давайте напишемо просту демонстраційну CGI-програму, яка буде відображати ім'я усіх наявних змінних середовища у HTML-документ і відправляти його клієнту. Для цього ми будемо використовувати С-змінну середовища environ, яка містить вказівники на рядки вигляду “змінна=значення”. Програма має наступний вигляд:
#include <iostream> 
#include <unistd.h> 
#include <stdio.h> 
using namespace std; 

int main () 
{ 
    cout << "Content-Type: text/html; charset=UTF-8\n\n" ; 

    cout << "<h1>Наявні змінні середовища:</h1>" ; 
    
    for (int iter=0; environ[iter]!=NULL; iter++) 
    { 
        cout << environ[iter] << "<br>" ; 
    } 

    return 0; 
}
Після компілювання і виконання даного CGI-скрипту на моїй системі, у робочій області Web-браузера ми можемо спостерігати наступне: show_env_var_CGI_CPP_in_browser В результаті ми можемо побачити, створені HTTP-сервером, декілька специфічних для протоколу HTTP змінних середовища: HTTP_HOST, HTTP_CONNECTION, HTTP_ACCEPT, HTTP_USER_AGENT і інші. Кожна з них несе певну інформацію про користувача, чи про систему. Наприклад, змінна HTTP_USER_AGENT вміщує назву програми користувача, яка надіслала запит до серверу, або змінна HTTP_CONNECTION представляє значення заголовкового поля запиту (або пакету) HTTP “Connection”, значення “keep-alive” найчастіше означає, що програма-клієнт і сервер використовують версію протоколу HTTP/1.1, і обидві сторони підключення бажають не переривати його, а залишити для подальшої співпраці (тобто “persistent connection” - постійне підключення). Далі ми можемо побачити характерні для інтерфейсу CGI мета-змінні з їхніми відповідними значеннями: QUERY_STRING, REQUEST_METHOD, REMOTE_ADDR, SCRIPT_NAME і інші. Значення цих мета-змінних описані в моїй попередній статті про інтерфейс CGI/1.1.

Перевіряємо значення конкретних мета-змінних середовища

Для того, щоб отримати значення конкретної змінної середовища, нам необхідно скористатися функцією getenv з стандартної бібліотеки С. Дана функція приймає один параметр — назву змінної середовища значення якої ми бажаємо взнати, у вигляді типу “const char *”. Однак слід пам'ятати, що не можна модифікувати повернуте значення або вивільняти масив функцією free чи оператором “delete []”, оскільки це призведе до подальшої неправильно роботи усієї програми.
#include <iostream> 
#include <unistd.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string> 
#include <string.h> 
using namespace std; 

int main (int argc, char** argv) 
{ 
    /* показуємо серверу, що ми повертаємо звичайний текст */ 
    cout << "Content-Type: text/html; charset=UTF-8\n\n" ; 

    /* дістаємо значення необхідної змінної і зберігаємо його в вказівник */ 
    char * user_query = getenv("QUERY_STRING") ; 

    /* перевіряємо, чи повернене значення не пусте */ 
    if (user_query==NULL || strlen(user_query)==0) 
    { 
        /* якщо пусте - повідомляємо про це користувача */ 
        cout << "<h1>Користувач нічого не ввів!</h1>" ; 
    } 
    else 
    { 
        /* якщо існує значення - виводимо його у документ */ 
        cout << "QUERY_STRING = " << user_query ; 
    } 

    /* виходимо з програми */ 
    return 0 ; 
}
Після того, як Ви скомпілюєте дану програму і запустите її через http-сервер, ви отримаєте приблизно наступний результат: CGI_empty_query_string_in_browser Оскільки ми не передали через браузер ніяких параметрів, CGI-програма просто видала повідомлення про пусте значення QUERY_STRING.

Передаємо значення скрипту

В основному для передачі певних значень серверним web-програмам, використовуються спеціальні HTML конструкції — форми. Для нашого випадку, ми можемо скористатися HTML-кодом наступної форми:
<form ACTION="http://localhost/index.cgi" METHOD="GET"> 

    Введіть який-небудь текст:<input type="text" name="user_input"> 
    <input type="submit" value="Надіслати на сервер"> 
    
</form>
Її необхідно включити у нашу програму, за допомогою створення константного вказівника на масив символів html_form. Тепер, якщо програма переконається, що значення мета-змінної QUERY_STRING пусте, вона виведе у документ дану форму, що дасть змогу користувачеві ввести деякий текст у поле для введення. Але якщо значення змінної QUERY_STRING виявиться не нульовим — вона виведе його у документ. CGI-програма на С++ тепер отримає вигляд:
#include <iostream> 
#include <unistd.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string> 
#include <string.h> 
using namespace std; 

const char * html_form = "<form ACTION=\"http://localhost/index.cgi\" METHOD=\"GET\">" 
                         " Введіть який-небудь текст:<input type=\"text\" name=\"user_input\">" 
                         " <input type=\"submit\" value=\"Надіслати на сервер\"> " 
                         "</form>" ; 

int main (int argc, char** argv) 
{ 
    /* показуємо серверу, що ми повертаємо звичайний текст */ 
    cout << "Content-Type: text/html; charset=UTF-8\n\n" ; 

    /* дістаємо значення необхідної змінної і зберігаємо його в вказівник */ 
    char * user_query = getenv("QUERY_STRING") ; 

    /* перевіряємо, чи повернене значення не пусте */ 
    if (user_query==NULL || strlen(user_query)==0) 
    { 
        /* якщо пусте - повідомляємо про це користувача */ 
        cout << html_form ; 
    } 
    else 
    { 
        /* якщо існує значення - виводимо його у документ */ 
        cout << "QUERY_STRING = " << user_query ; 
    } 

    /* виходимо з програми */ 
    return 0 ; 
}
Після компілювання і виклику даної CGI-програми у адресному рядку браузера, ми побачимо наступне: simple_form_for_user_input Якщо ми введемо у поле значення “hallo, world”, ми можемо отримати наступний результат: simple_form_hallo_world_input Очевидно, що значення змінної потрібно обробити: відділити ім'я змінної “user_input” від її значення, яке є наступним після знаку “=” і перетворити усі системні знаки (типу “%”) у відповідні символьні значення. В даному випадку “%2C” означає знак коми, а знак “+” - означає пробіл. Це саме стосується і для Української мови. Якщо ми введемо значення “текст” у поле вводу, тоді ми отримаємо наступний результат: html_form_with_uk-UA Це відбувається тому, що Українська мова кодується в системі кодування символів UTF-8. І браузер, для коректного передавання запиту користувача через мережу, повинен перетворити усі не US-ASCII-символи у дане представлення. Ми розглянемо проблему перетворення у наступних статтях.

Порівнюємо швидкість C++ і PHP5 через CGI

Розглянемо дві аналогічні програми на С++:
#include <iostream>
using namespace std ;

int main (int argc, char** argv)
{
    cout << "Content-Type: text/html; charset=utf-8\n\n" ;
    
    cout << "<h1>Simple page C++</h1><br>" ;
    
    for (int i=-1; i<1000001; ++i)
    {
        if ( i % 100  == 0 )
        {
            cout << i << " " ;
        }
    }
    
    return 0 ;
}
і PHP:
<?php

    echo "<h1>Simple page PHP</h1><br>" ;
    
    for ($i=-1; $i<1000001; ++$i)
    {
        if ( $i % 100 == 0 )
        {
            echo $i . " " ;
        }
    }
    
?>
І помістимо їх в директорію, призначену для http-сервера. С++ програму, відповідно, необхідно спочатку скомпілювати в файл similar_code.cgi. Далі нам необхідна програма, яка буде виконувати тести.
#include <iostream>
#include <vector>
#include <string>
#include <fstream>
#include <algorithm>
#include <functional>
#include <numeric>
using namespace std ;

#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

/* Функція яка повертає секунди з мантисою */
/* 1 c = 1.0 * 10+E6 мс*/
double get_secs ()
{
    struct timeval value ;
    
    /* обнуляємо пам'ять структури */
    memset ((void*)&value,'',sizeof (timeval)) ;

    /* отримуємо значення секунд і мікросекунд */
    if ( gettimeofday(&value, NULL) == 0 )
    {
        /* повертаємо значення секунд */
        return  ((double)value.tv_sec) +  (((double)value.tv_usec) / 1000000) ;
    }
    else
    {
        /* в разі помилки */
        perror("GetUSec(): FAIL : getttimeofday()") ;
        return -1 ;
    }

    return 0;
}

/* функція виконує команду в системі */
int sys_execute (string& command)
{
    if (system(command.c_str())<0)
    {
        cerr << "WHILE TRYING TO EXECUTE: " << command << endl ;
        perror ("ERROR") ;
        return -1 ;
    }
    
    return 0 ;
}

/* Функція тестування швидкості видачі */
/* 
** Для підключення і виконання запитів ми будемо використовувати 
** системну програму wget. Впевніться, що вона у вас є.
*/
int make_url_test (string& URL, /* URL для тестування */
                   int times, /* кількість повторення тесту */
                   vector<double>& vals) /* результати у дельта секундах */
{
    /* формуємо команду для виконання */
    /* все, що завантажується записується у файл downloaded */
    string command = "wget -q -O downloaded \"" + URL + "\" > output.txt" ;
    
    /* очищуєм буфер */
    vals.clear() ;   
    /* резервуємо місце */
    vals.reserve (times+1) ;
    
    double t0, /* початковий час */
           t1 ; /* кінцевий час */
    
    for (int iter=0; iter<times; ++iter)
    {
        /* нічого між ними! */
        /* дістаємо початковий час */
        t0 = get_secs () ;
        /* виконуємо команду */
        sys_execute (command) ;   
        /* дістаємо кінцевий час */
        t1 = get_secs () ;
        
        /* зберігаємо значення різниці */
        vals.push_back ( t1 - t0 ) ;
    }
    
    
    return 0 ;
}

int main (int argc, char** argv)
{
    if (argc<4)
    {
        cout << "Usage: " << argv[0] << " <test_times> <URL1> <URL2>" ;
        return 0 ;
    }
    
    int times = abs(atoi(argv[1])) ; /* кількість повторень тесту */
    string u1 = argv[2] ; /* URL1 */
    string u2 = argv[3] ; /* URL2 */
    vector<double> result_u1 ; /* результати для URL1 */
    vector<double> result_u2 ; /* результати для URL2 */
 
    /* виконуємо тести для обох URL */
    make_url_test (u1, times, result_u1) ;
    make_url_test (u2, times, result_u2) ;
    
    /* зберігаємо результати у файлі з таблицею "results.html" */

    fstream file ;
    
    //cout.precision(15) ;
    
    file.open("results.html",fstream::in | fstream::out | fstream::trunc) ;

    file << "<head>" ;
    file << "<style>\ntable { border-collapse: collapse; border: 1px solid #000; padding:5px; }\n "
         << "table td { border-collapse: collapse; border: 1px solid #000; padding:5px; }"
         << "</style>\n</head>" ;

    file << "<center>\n<h1>Test results</h1>\n<table>" ;

    /* заголовок таблиці */
    file << "<tr><td><b>Iteration<b></td><td><b>" << u1 << "</b></td><td><b>" 
         << u2 << "</b></td></tr>" ;
         
    for (int i=0; i<times; ++i)
    {
        file << "<tr>" ;
        
        /* номер ітерації, перше поле */
        file << "<td>" << i + 1 << "</td>" ;
        /* для першої URL */
        file << "<td>" << result_u1[i] << "</td>" ;
        /* для другої URL */
        file << "<td>" << result_u2[i] << "</td>" ;
        
        file << "</tr>" ;
    }

    /* мінімальний елемент */
    file << "<td><b>Minimal</b></td>" ;
        
    file << "<td>"
         << (double)*(min_element(result_u1.begin(), result_u1.end()))
         << "</td>" ;
    file << "<td>"
         << (double)*(min_element(result_u2.begin(), result_u2.end()))
         << "</td>" ;
    file << "</tr>" ;
 
     /* максимальний елемент */
    file << "<td><b>Maximum</b></td>" ;
    file << "<td>"
         << (double)*(max_element(result_u1.begin(), result_u1.end()))
         << "</td>" ;
    file << "<td>"
         << (double)*(max_element(result_u2.begin(), result_u2.end()))
         << "</td>" ;
        
    file << "</tr>" ;

    /* обчислюємо середні значення */
    file << "<tr>" ;
    file << "<td><b>Average</b></td>" ;
        
    /* середнє арифметичне значення для першої URL */
    file << "<td>"
         << (double)(accumulate(result_u1.begin(), result_u1.end(), 0.0) / (double) times)
         << "</td>" ;
        
    /* для другої URL */
    file << "<td>"
         << (double)(accumulate(result_u2.begin(), result_u2.end(), 0.0) / (double) times)
         << "</td>" ;
        
    file << "</tr>" ;    
 
 
    file << "</table>\n</center>\n" ;
 
    file.close() ;
       
    return 0 ;
}
Тестеру необхідно передати три параметри: 1-й, для кількості повторень тесту для одного URL, і два наступні параметри - самі URL, які необхідно тестувати. Після компілювання і виконання даної програми з'явиться файл “result.html”, в якому відображаються дані тестувань у зручному табличному форматі. Ось, наприклад, результат для 50 ітерацій тесту

Test results

Iterationhttp://localhost/similar_code.cgihttp://localhost/similar_code.php
10.1004170.215538
20.032660.217287
30.0305450.209635
40.0317030.212948
50.0327580.210367
60.03506490.222671
70.03471110.212881
80.0299330.212047
90.0323150.212958
100.0333390.208902
110.0347230.21012
120.03636410.231266
130.03494410.210756
140.03040.21135
150.03146890.208119
160.03079990.20966
170.0335760.211673
180.03050490.222937
190.02973480.211989
200.0324290.212751
210.03013010.211259
220.03368590.34106
230.0332220.210725
240.03193590.220448
250.0424840.210795
260.0586560.210431
270.0382280.209331
280.03684710.212536
290.03264590.208579
300.03165320.221948
310.0330970.215846
320.0385330.20977
330.03462910.212108
340.0324020.209163
350.03344990.211018
360.03403710.221504
370.03273110.211893
380.03627590.213149
390.0336230.214826
400.03724910.210866
410.03228620.210695
420.03305980.219759
430.03192690.211324
440.03240990.223353
450.03037290.212082
460.03497290.211824
470.03168490.211603
480.0341980.220934
490.08090.210128
500.0317950.210655
Minimal0.02973480.208119
Maximum0.1004170.34106
Average0.03615030.216109
Для 10000 ітерацій:
99950.02217290.112787
99960.02036120.117057
99970.02374980.115194
99980.0221980.113579
99990.02168080.116882
100000.02237110.113654
Minimal0.01817920.108869
Maximum0.2410940.470374
Average0.02221440.115382
Увесь результат для 10000 ітерацій тесту, Я не включив в дану сторінку по відомим причинам. Результату для мільйона ітерацій тесту, мені так і не вдалося дочекатись. Якщо ми поглянемо на дані результати, то в середньому С++ CGI-програма в 5 разів швидша ніж аналогічна програма на PHP. В п'ять разів... Уявіть, якщо сумарний час обчислень С++ CGI-програми склав би 1 день, тоді для аналогічної програми на PHP сумарний час виконання склав би 5 днів і декілька годин. Уявіть собі Ваших клієнтів сайту, які додатково чекали 4 дні. Уявіть себе, який 4 дня чекає, поки програма видасть результат. Подумайте про ці п'ятикратні обчислення, які виконуються на серверах. Весела картина. Завдяки С++ і бінарному коду, ви вмістили ці надмірні 5-кратні обчислення в час компілювання програми, яке відбувається не так вже і довго - точно не 5 днів. Я думаю, це хороший виграш сайтам, на які кожного дня будуть надсилатися мільйони запитів. Оптимізація часу виконання, в середньому, складає 80%. Завдяки використання С++, на Вашому сервері можна розмістити 5 різних за змістом сайтів, які використовують подібне ПЗ. В наступних статтях Я розкажу вам, як видати через CGI-шлюз не text/html файли і як перетворити символи з %-представлення у читабельний вигляд.
Категорії: