Сьогодні ми поговоримо про 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”.
Відкрийте свій Web-переглядач, і введіть у рядку адреси шлях до вашої CGI-програми, використовуючи протокол HTTP. У мене вона буде виглядати як “http://localhost/index.cgi”:
Запрацювало! Тепер Ви вмієте писати серверну сторону сайту використовуючи мову С++. Однак, як Ви, напевне, здогадалися, 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-браузера ми можемо спостерігати наступне:
В результаті ми можемо побачити, створені 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-програма просто видала повідомлення про пусте значення 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-програми у адресному рядку браузера, ми побачимо наступне:
Якщо ми введемо у поле значення “hallo, world”, ми можемо отримати наступний результат:
Очевидно, що значення змінної потрібно обробити: відділити ім'я змінної “user_input” від її значення, яке є наступним після знаку “=” і перетворити усі системні знаки (типу “%”) у відповідні символьні значення. В даному випадку “%2C” означає знак коми, а знак “+” - означає пробіл. Це саме стосується і для Української мови. Якщо ми введемо значення “текст” у поле вводу, тоді ми отримаємо наступний результат:
Це відбувається тому, що Українська мова кодується в системі кодування символів 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
Iteration | http://localhost/similar_code.cgi | http://localhost/similar_code.php |
1 | 0.100417 | 0.215538 |
2 | 0.03266 | 0.217287 |
3 | 0.030545 | 0.209635 |
4 | 0.031703 | 0.212948 |
5 | 0.032758 | 0.210367 |
6 | 0.0350649 | 0.222671 |
7 | 0.0347111 | 0.212881 |
8 | 0.029933 | 0.212047 |
9 | 0.032315 | 0.212958 |
10 | 0.033339 | 0.208902 |
11 | 0.034723 | 0.21012 |
12 | 0.0363641 | 0.231266 |
13 | 0.0349441 | 0.210756 |
14 | 0.0304 | 0.21135 |
15 | 0.0314689 | 0.208119 |
16 | 0.0307999 | 0.20966 |
17 | 0.033576 | 0.211673 |
18 | 0.0305049 | 0.222937 |
19 | 0.0297348 | 0.211989 |
20 | 0.032429 | 0.212751 |
21 | 0.0301301 | 0.211259 |
22 | 0.0336859 | 0.34106 |
23 | 0.033222 | 0.210725 |
24 | 0.0319359 | 0.220448 |
25 | 0.042484 | 0.210795 |
26 | 0.058656 | 0.210431 |
27 | 0.038228 | 0.209331 |
28 | 0.0368471 | 0.212536 |
29 | 0.0326459 | 0.208579 |
30 | 0.0316532 | 0.221948 |
31 | 0.033097 | 0.215846 |
32 | 0.038533 | 0.20977 |
33 | 0.0346291 | 0.212108 |
34 | 0.032402 | 0.209163 |
35 | 0.0334499 | 0.211018 |
36 | 0.0340371 | 0.221504 |
37 | 0.0327311 | 0.211893 |
38 | 0.0362759 | 0.213149 |
39 | 0.033623 | 0.214826 |
40 | 0.0372491 | 0.210866 |
41 | 0.0322862 | 0.210695 |
42 | 0.0330598 | 0.219759 |
43 | 0.0319269 | 0.211324 |
44 | 0.0324099 | 0.223353 |
45 | 0.0303729 | 0.212082 |
46 | 0.0349729 | 0.211824 |
47 | 0.0316849 | 0.211603 |
48 | 0.034198 | 0.220934 |
49 | 0.0809 | 0.210128 |
50 | 0.031795 | 0.210655 |
Minimal | 0.0297348 | 0.208119 | Maximum | 0.100417 | 0.34106 | Average | 0.0361503 | 0.216109 |
Для 10000 ітерацій:
9995 | 0.0221729 | 0.112787 |
9996 | 0.0203612 | 0.117057 |
9997 | 0.0237498 | 0.115194 |
9998 | 0.022198 | 0.113579 |
9999 | 0.0216808 | 0.116882 |
10000 | 0.0223711 | 0.113654 |
Minimal | 0.0181792 | 0.108869 | Maximum | 0.241094 | 0.470374 | Average | 0.0222144 | 0.115382 |
Увесь результат для 10000 ітерацій тесту, Я не включив в дану сторінку по відомим причинам. Результату для мільйона ітерацій тесту, мені так і не вдалося дочекатись.
Якщо ми поглянемо на дані результати, то в середньому С++ CGI-програма в 5 разів швидша ніж аналогічна програма на PHP. В п'ять разів...
Уявіть, якщо сумарний час обчислень С++ CGI-програми склав би 1 день, тоді для аналогічної програми на PHP сумарний час виконання склав би 5 днів і декілька годин. Уявіть собі Ваших клієнтів сайту, які додатково чекали 4 дні. Уявіть себе, який 4 дня чекає, поки програма видасть результат. Подумайте про ці п'ятикратні обчислення, які виконуються на серверах. Весела картина.
Завдяки С++ і бінарному коду, ви вмістили ці надмірні 5-кратні обчислення в час компілювання програми, яке відбувається не так вже і довго - точно не 5 днів. Я думаю, це хороший виграш сайтам, на які кожного дня будуть надсилатися мільйони запитів. Оптимізація часу виконання, в середньому, складає 80%. Завдяки використання С++, на Вашому сервері можна розмістити 5 різних за змістом сайтів, які використовують подібне ПЗ.
В наступних статтях Я розкажу вам, як видати через CGI-шлюз не text/html файли і як перетворити символи з %-представлення у читабельний вигляд.