Шаг 10 - Как запускать CGI.
Если у Вас от чего-то все работает, то очень СТРАННО. Как правило новичку, чтобы запустить скрипт приходится попотеть. Какие же основные проблемы возникают при запуске скрипта ?
Настройки сервера
Первой, но не главной причиной может послужить неправильная настройка сервера. Скорее всего попросту он не имеет права запускать скрипты из этой директории. Я всегда рассматриваю только Web-сервер Apache, поэтому приведу настройки для него. Кстати сегодня в ComputerWorld(8.02.2000) опубликовали результаты исследований RUNet'a. Оказалось, что Apache установлен на 78% всех серверов, веб-сервера от Microsoft - 19%, доля остальных серверов составляет по 1%. Так что я опять говорю, лучше Apache Вы не найдете !!!
Ладно отвлеклись от темы... Все настройки Apache для каждой директории задаются с помощью файла .htaccess. Если такового в вашей директории не имеется, то создавайте его. В него запишите следующее: Options ExecCGI
Или даже посоветую обратиться к документации, но думаю она не понадобится.
Теперь любые скрипты в этом каталоге будут загружаться без проблем.
Неверные атрибуты на файле скрипта
Если на вашем сервере установлена система подобная Windows, то эта проблема вас не касается, так как все программы *.exe "эта" система загружает без вопросов.
В случае если Ваша система Unix, то вам повезло меньше, особенно если Вы до этого видели только Windows.
В кратце поясню... Во всех системах Unix для каждого файла устанавливаются атрибуты файлов. Этих атрибутов (как правило) девять. Даю список таковых. Owner Read
Owner Write
Owner Execute
Group Read
Group Write
Group Execute
Other Read
Other Write
Other Execute
Если кратко, то в Unix системах создаются пользователи и разделяются на группы. Вы вот при входе в систему набираете свой логин и пароль, т.е. Вы являетесь пользователем. Также по FTP и т.д. Все кто имеет доступ к системе является ее пользователем.
Атрибуты типа Owner задают параметры для Вас, т.е. для владельца файлов. Атрибуты Group определяют уровень доступа для вашей группы, т.е. если Вы принадлежите к группе Webmasters, то при установке атрибута Group Write любой другой пользователь, который принадлежит к группе Webmasters сможет записывать в этот файл информацию. Думаю для чего Other понятно, это значит всем остальным.
При записывании файла через FTP атрибуты файла устанавливаются по умолчанию rw.r..r..
Т.е. Вы можете писать и читать, а остальные могут только читать. Как видите ни один атрибут не указывает на то, что файл загружаемый. Вы должны добиться такого: rwxr.xr.x
Т.е. установить атрибут Execute во всех группах.
Как это сделать это другой вопрос, давайте рассмотрим с вами работу с некоторыми FTP клиентами. Сразу скажу, что не использую никакие Виндовые программы, т.е. графические "проги" рассчитанные на любителей делать все одним кликом мыши...
Я пользуюсь нашим, т.е. российским файловым менеджером FAR, если у вас его нет, то Вы много потеряли... И я вам сочувствую.
Так вот в нем надо нажать Ctrl-A на том файле, который Вы закачали на сервер (только делаете это не на локальном диске, а на FTP, а то увидите вместо атрибутов Unix атрибуты Досовской файловой системы) Делаете следующую картинку: R W X R W X R W X
[x][x][x] [x][ ][x] [x][ ][x]
И нажимаете Okey. Теперь все классно.
Если у Вас нет FAR, то у Вас ОБЯЗАНА быть программа в системе, которая занимается сервисом FTP. В большинстве систем (и в Винде) такая программа называется ftp. Запустите ее. Наверняка она обладает только командной строкой, так что потейте... :-) Я рассмотрю программу ftp.exe, которая входит в виндовс.
Первое, что надо сделать открыть Ваш сайт, делается это командой open ftp> open www.mjk.msk.ru
Связь с mjk.
220 mjk-gw.mjk.msk.ru FTP server (Version wu-2.4.2-academ[BETA-18](1)
Mon Aug 3 19:17:20 EDT 1998) ready.
Пользователь (mjk:(none)): dron
331 Password required for dron.
Пароль:
230-Please read the file README.linux
230- it was last modified on Sat Feb 5 16:31:50 2000 - 3 days ago
230 User dron logged in.
ftp>
Теперь Вы в системе. Наберите help для получения основных команд. Попробуйте набрать dir. Пример вывода: 200 PORT command successful.
150 Opening ASCII mode data connection for /bin/ls.
total 48
drwx------ 3 dron mjkusers 1024 Feb 6 16:58 .
drwxr-xr-x 75 root root 2048 Feb 1 00:03 ..
-rw-r--r-- 1 dron mjkusers 1155 Jun 24 1999 .Xdefaults
-rw------- 1 dron mjkusers 24 Jan 8 11:35 .bash_history
-rw-r--r-- 1 dron mjkusers 24 Jun 24 1999 .bash_logout
-rw-r--r-- 1 dron mjkusers 230 Jun 24 1999 .bash_profile
-rw-r--r-- 1 dron mjkusers 124 Jun 24 1999 .bashrc
-rw-r--r-- 3 dron mjkusers 1324 Jan 8 11:32 123.cgi
-rw-r--r-- 1 dron mjkusers 37165 Feb 5 16:31 README.linux
226 Transfer complete.
691 байт получено за 0.33 с (2.09 КБ/с)
Мне например надо теперь установить атрибут загрузки на файл 123.cgi, как видите у него такого атрибута нет. Такую возможность ftp.exe не предоставляет, зато он может посылать команды непосредственно FTP - серверу, т.е. что нам и требуется. Если вы вызывали помощь, то знаете, что такую функцию выполняет команда quote. Чтобы Вам особо не разбираться просто приведу команду. quote SITE CHMOD 755 123.cgi
Теперь на файле 123.cgi будут установлены необходимые атрибуты. Если Ваш файл располагается в другой директории, то пользуйтесь командой cd (change directory).
Неправильный атрибут на каталоге скрипта
Многие скрипты не только выводят какую-то информацию, но и еще записывают что-то в определенные папки или файлы. Тут надо предусмотреть правильный доступ к этим ресурсам. Любой Web-сервер работает не от вашего имени, а от другого, поэтому запуская скрипт он не предоставляет ему Ваши возможности. Вы должны поставить атрибуты на директорию, в которую записан скрипт, для полного доступа, т.е. для записи всем кому угодно. Для установки таких атрибутов в FAR'е поставьте все крестики. В ftp.exe команда такая quote SITE CHMOD 777 <директория>
Хочу Вас также предостеречь, делая полный доступ на директорию помните, что любой "злоумышленник" может вам подпортить жизнь, стереть Ваш скрипт и например записать свой, или записывать неправильные данные в ваши файлы. Короче он может делать, что угодно. Особенно опасно делать полный доступ к директории в которой лежит страничка, потому как рано или поздно вы ее там не обнаружите :-(.
Поэтому СОВЕТ. Создавайте специально для скриптов отдельные каталоги и используйте их для записи данных. А лучше всего делать доступ только отдельному файлу (в этом случае маска доступа не 777, а 666 !!!) Причем думаю не плохо было бы позаботиться и о шифровании, т.к. любой скрипт может записывать конфиденциальную информацию, такую как номера кридитных карт, почтовые адреса, имена и фамилии. Любой "спаммер" или "хакер" скажет вам большое спасибо за такой подарок, хотя скорее всего "спасибо" вам скажут "дяди в погонах".
Вобщем-то все, надеюсь у Вас все заработает :-)
Шаг 11 - Как работают гостевые книги ?
Гостевая книга очень полезная штука на сайте, да и что тут объяснять ? Все хотят иметь на сайте хоть какой-то механизм обратной связи. Именно для этого и существуют гостевые книги.
Общий метод, я думаю, прост и ясен всем. Вы создаете страничку с формой, которую будут заполнять ваши посетители и посылать вам. С этим думаю не будет возникать проблем. Проблемы возникнут, когда надо будет писать обрабатывающий скрипт. Я могу лишь дать некоторые советы для создания хороших гостевых книг.
Первое о чем надо позаботиться, это обеспечение обработки нескольких пользователей одновременно. Думаю не секрет, что в интернете миллионы людей, и никогда не известно кто из них зайдет на вашу страничку, а самое главное когда... Вероятность появления ситуации, что два или более пользователя одновременно записывают данные в гостевую книгу очевидна. Да это и вовсе не относится только к гостевым книгам, существуют другие более "нагруженные" приложения, такие как чаты, форумы, счетчики и т.д.
Второе, что я считаю важным - это обеспечение настроек формата вывода данных. Очень важно для того, чтобы использовать один и тот же скрипт где угодно и когда угодно. Если Ваш скрипт "не мобилен", то Вам прийдется для добавления какой-нибудь новой информации переписывать скрипт.
Ну, а третье может всем и не нужно, хотя стоило бы обеспечить обработку сразу нескольких гостевых книг одним скриптом. Это позволит пользоваться гостевыми книгами не только Вам.
Если соблюдать такие вот простые правила можно создать не плохие скрипты, обеспеченные стойкостью и мобильностью.
О том как контролировать одновременные вызовы скрипта несколькими клиентами я писал раньше... Если Вы не помните, то "алгоритм" прост. При запуске скрипт создает какой-либо файл на время работы, затем его удаляет. Другой же запущенный скрипт контролирует появление этого файла, и если он есть, то ждет некоторое время пока он не исчезнет.
Как обеспечить настройки выводимой информации я могу лишь только посоветовать, потому как каждый может придумать разные способы и методы. Лично моя гостевая книга устроена в этом отношении просто. Есть файл, который полностью описывает структуру нового добавляемого сообщения. Т.е. это своего рода файл html, который скрипт попросту копирует в гостевую книгу, НО при этом подставляет какие-то данные в обозначенные места. Как их обозначить ? Это тоже Вам решать... Я обозначал просто, между двумя амперсандами & задается имя поля формы, значение которого надо записать в гостевую книгу. Таким образом изначально сам скрипт гостевой книги даже не знает какие поля формы ему сообщаются. Давайте приведу кусок кода, который реализует чтение файла с форматом и подставляет вместо полей их содержимое. char parname[150];
char *par;
int parnamelen=0;
char charbuf;
int ampfound=0, dolfound=0;
if ( (f_form=fopen(guest_path,"rt")) != NULL )
{
if ( (f_guest = fopen(guestbook,"at")) != NULL)
{
while (!feof(f_form))
{
fscanf(f_form,"%c",&charbuf);
if ((ampfound==0)&&(dolfound==0))
{
if ((charbuf!='&') && (charbuf!='$'))
{
fprintf(f_guest,"%c",charbuf);
}
else
{
if (charbuf=='&') ampfound=1; else dolfound=1;
parnamelen=0;
};
}
else
{
if ((charbuf=='&') || (charbuf=='$'))
{
if (parnamelen==0)
{
if (charbuf=='&') fprintf(f_guest,"&");
else fprintf(f_guest,"$");
}
else
{
parname[parnamelen]='\0';
if (ampfound==1) par=getparam(buf,parname);
else par=getenv(parname);
fprintf(f_guest,"%s",par);
};
if (ampfound==1) ampfound=0; else dolfound=0;
}
else
{
parname[parnamelen]=charbuf;
parnamelen++;
};
};
};
fclose(f_guest);
};
fclose(f_form);
};
Алгоритм прост, считываете посимвольно, если встречается амперсанд, то начинаете накапливать имя поля, которое необходимо вставить в гостевую книгу. Ну, а когда дойдете до второго амперсанда, то выводите содержимое этого параметра в файл гостевой книги. Забыл сказать, да Вы наверно это уже заметили, что я еще использую знак доллара, он служит для того чтобы выводить не содержимое формы, а переменные среды. Это очень полезно, если Вы хотите записывать в гостевую книгу какие-то данные о соединении, например, IP адрес клиента.
Ну, а третье условие "хорошей" гостевой книги - один скрипт на всех :), попробуйте реализовать сами, это не очень сложно... Создайте конфигурационный файл для гостевой книги, в котором Вы будете прописывать положение файлов других гостевых книг. Тут также надо будет позаботиться о выборе гостевой книги, т.е. в какую гостевую книгу будут записываться поступаемые данные. Я для этого рекомендую использовать метод GET, а сами данные будете посылать через POST.
Конечно тут я описал самые "зачатки" продвинутой гостевой книги, но у нас еще все впереди. Можно сделать автоматическое занесение в архив устаревших записей, затем если сообщений слишком много, то создавать перелистывающиеся страницы и еще много чего...
Шаг 12 - Посылка письма.
Посылка писем внутри скрипта бывает очень полезной. Имея такую возможность Вы, например, можете оповещать себя или еще кого-то о запуске или работе скрипта. Например, очень хорошо использовать посылку писем в регистрационных скриптах для подтверждения по почте пользователя о том, что он зарегистрирован. Думаю можно найти сотни применений для этой технологии, причем иногда настолько простых и эффективных, что трудно будет найти нечто другое.
Ну, а теперь от слов к делу. Сразу вопрос: Вы знаете как работать по протоколу SMTP ? Я вообще-то знаю, но реализовывать этот механизм в каждом скрипте что-то не хочется. Слишком это сложно и громоздко. А потом не уверен я, что любой может без проблем написать программу работающую с сокетами, да еще и по определенному прококолу. Вот как раз для таких функций, т.е. отправки почты, существуют программы работающие через командную строку. На большинстве серверов, да и вообще компьютерах с Unix системами, функцию отправки писем может возлагать на себя непосредственно SMTP демон. Их существует достаточно много, хотя если я скажу, что самый распространенный из них это sendmail, то не сильно ошибусь.
Конкретно sendmail позволяет посылать письмо из командной строки. Как это делать лучше посмотреть (да и вообще лучше всегда смотреть :) в мануале к программе. В моем случае :) чтобы послать письмо надо набрать следующую командную стоку: /usr/lib/sendmail -t
Далее набирается письмо в формате: To: email@adr
From: mymail@adr
Subject: Hello !!!
This Is Letter !!!
.
Признаком конца письма служит строка с одной единственной точкой, поэтому прошу Вас обратить на это внимание.
Все хорошо, но как все это дело использовать в скрипте ? А просто !!! В Юниксе существует уникальная просто возможность запускать программу и ассоциировать с ней файловые потоки для обмена информацией !!!
Делает такой вызов программы функция практически аналогичная fopen, но только popen :-) !!! Так что теперь делаем просто файловый указатель и открываем нашу программу посылки письма. #include ...
FILE *sendmail;
sendmail=popen("/usr/lib/sendmail -t","w");
fprintf(sendmail,"To: email@adr\n");
fprintf(sendmail,"From: myemail@adr\n");
fprintf(sendmail,"Subject: Hello\n");
fprintf(sendmail," HEHEHEHEH !!!\n");
fprintf(sendmail,".\n");
pclose(sendmail);
Теперь ваше письмо пошлется другу :). Можно ли использовать такой механизм в Виндовсе ? Не знаю :). По крайней мере стандартная программа посылки почты, т.е. OutLook не позволит вам этого сделать совершенно точно, да и кроме того ДОС-система не умеет вызывать программу как файл !!! У Юникса здесь нет конкурентов :). Остается только найти программу под Виндовс, которая будет посылать письма из командной строки, а у меня такая раньше была. И вызывать ее через стандартую exec-процедуру. Только при этом не забывайте создавать временный файл с почтовым сообщением !!! :)
Хотя если Вы будете писать программы только для Unix, то у Вас не возникнет проблем :)
Шаг 13 - Разработка класса CGIContent.
Код получения параметров во многих скриптах CGI одинаков и кочует от одного скрипта к другому практически без изменений. Поэтому стоит написать библиотеку классов для работы с CGI и подключать ее при разработке нового скрипта.
Прежде чем разработать какую-либо программу надо поставить себе цель, поэтому давайте ее наметим. Естественно самое главное, что должны обеспечивать классы - это простоту работы с ними. Потом любой скрипт должен уметь получать данные большого размера, при этом обеспечивая их удобное размещение в памяти и маленький размер.
До сих пор мы просто выделяли кусок памяти и целиком помещали в него все переданные данные. Из-за этого при передаче огромного количества данных скрипт мог зависнуть. Кроме того наши данные никогда не декодировались и хранились закодированными, что сильно тратило нашу память и содержимое параметров могло занимать в три раза больше, чем на самом деле.
Первое, что нам потребуется для разработки нашего класса - это класс для хранения больших объемов данных. Самым простым способом хранения помоему является список. Поэтому создадим класс CGIContentItem, который будет элементом списка управляемого классом CGIContent: class CGIContent;
class CGIContentItem{
friend CGIContent;
char *Content;
CGIContentItem *Next;
public:
CGIContentItem();
~CGIContentItem();
};
Простой элемент списка у нас будет содержать текстовую строку Content и указывать на следующий элемент. Реализация деструктора и конструктора тоже просты: CGIContentItem::CGIContentItem(){
Content=NULL;
Next=NULL;
};
CGIContentItem::~CGIContentItem(){
if (Content!=NULL) free(Content);
if (Next!=NULL) delete Next;
};
Для всех классов элементы нашей ячейки скрыты, поэтому класс CGIContent, который является управляющей оболочкой для списка описан как дружественный.
Теперь требуется реализовать сам этот класс, чтобы получить полнофункциональный список со всякими возможностями. class CGIContent{
public:
CGIContentItem *Content;
CGIContent();
~CGIContent();
CGIContentItem* Add(char *Buffer);
int Write(FILE *f_out);
};
Самое главное в списке это указатель на его начало, эту функцию у нас выполняет указатель Content. Для начала нам потребуется только функция добавления в список Add() и вывода Write(). Для возможности вывода содержимого списка в любое место в функции Write() надо использовать указатель типа FILE, вместо которого можно будет поставлять поток stdout или любой другой открытый файл. Вот реализация методов этого класса: CGIContent::CGIContent(){
Content=NULL;
};
CGIContent::~CGIContent(){
if (Content!=NULL) delete Content;
};
CGIContentItem* CGIContent::Add(char *Buffer){
CGIContentItem *temp,*p;
temp=new CGIContentItem();
temp->Content=strdup(Buffer);
if (Content==NULL){
Content=temp;
} else {
p=Content;
while (p->Next!=NULL) p=p->Next;
p->Next=temp;
};
return temp;
};
int CGIContent::Write(FILE *f_out){
CGIContentItem *temp=Content;
while (temp!=NULL){
fprintf(f_out,"%s",temp->Content);
temp=temp->Next;
};
return 0;
};
Теперь у нас готов полнофункциональный класс способный хранить в себе большие массивы информации.
Шаг 14 - Класс CGIParam.
Вторым классом, который требуется для класса CGIApp должен быть класс хранящий имена передаваемых параметров и их значение.
Для хранения значений параметров мы уже сделали класс CGIContent. Нам осталось добавить в класс переменную для имени и методы для обеспечения добавления параметров и их быстрого поиска. Структура класса очень похожа на класс CGIContent. Класс CGIParamItem будет элементом списка, а класс CGIParam управляющим: class CGIParam;
class CGIParamItem {
friend CGIParam;
char *Name;
CGIContent *Content;
CGIParamItem *Next;
public:
CGIParamItem();
~CGIParamItem();
};
CGIParamItem::CGIParamItem(){
Name=NULL;
Next=NULL;
Content=new CGIContent();
};
CGIParamItem::~CGIParamItem(){
if (Name!=NULL) free(Name);
delete Content;
if (Next!=NULL) delete Next;
};
Класс CGIParamItem содержит имя параметра в переменной Name, а содержимое в Content.
Теперь класс CGIParam: class CGIParam{
CGIParamItem *List;
public:
CGIParam();
~CGIParam();
CGIContent *Add(char *name,char *Buffer);
CGIContent *Find(char *name);
};
CGIParam::CGIParam(){
List=NULL;
};
CGIParam::~CGIParam(){
if (List!=NULL) delete List;
};
CGIContent *CGIParam::Add(char *name,char *Buffer){
CGIParamItem *temp,*p;
temp=new CGIParamItem();
temp->Name=strdup(name);
temp->Content->Add(Buffer);
if (List==NULL){
List=temp;
} else {
p=List;
while (p->Next!=NULL) p=p->Next;
p->Next=temp;
};
return temp->Content;
};
CGIContent *CGIParam::Find(char *name){
CGIParamItem *temp=List;
while (temp!=NULL){
if (strcmp(temp->Name,name)==0) return temp->Content;
temp=temp->Next;
};
return NULL;
};
Удобство этого класса очевидно. Позволяет хранить большие объемы, обеспечивает очень быстрый поиск нужного параметра. Теперь вместо просмотра всех данных, как раньше, требуется всего лишь просмотреть имена параметров, что, собственно говоря, и требуется. Кроме того эти классы в будущем будут хранить уже декодированные данные. Раньше нам нельзя было декодировать данные потому, что мы могли потерять связки разделяющие имена параметров и их содержимое между собой, т.е. могли бы появиться нежелательные символы & и = и испортить все картину. Теперь этого никогда не произойдет, т.к. данные хранятся отдельно друг от друга и бережно охраняются своими управляющими классами. Преимущество ООП вообще еще состоит в и том, что создав класс вы можете не беспокойться о динамическом распределении памяти, правильно написанные конструкторы и деструкторы будут заботиться об этом без вашего участия.
Шаг 15 - Класс CGIApp.
Давайте теперь подведем черту под тем, что было сделано в прошлых шагах. Все эти классы были предназначены для одного самого главного CGIApp.
Вот его описание: class CGIApp{
public:
CGIParam *Param;
int Method;
char *Query_String;
long Content_Length;
CGIApp();
~CGIApp();
int Init();
CGIContent *FindParam(char *name);
};
Все просто как всегда, Param хранит полученные данные, Method содержит данные о методе передачи данных, который определяется следующими константами: #define METHOD_GET 1
#define METHOD_POST 2
Переменные Query_String и Content_Length содержат значения полученные из соответствующих переменных окружения.
В конструкторе и деструкторе класса также все по старому: CGIApp::CGIApp(){
Param=new CGIParam();
Query_String=NULL;
Method=0;
Content_Length=0;
};
CGIApp::~CGIApp(){
delete Param;
if (Query_String!=NULL) free(Query_String);
};
Особую "ценность" содержит в себе метод Init(). Эта процедура инициализирует все данные необходимые для работы приложения CGI, т.е. получает и декодирует все полученные от клиента данные. Сначала приведу текст этого метода: int CGIApp::Init(){
char *Buffer;
char *Content_Len=NULL;
char *Query_Str=NULL;
char *ParamName=NULL;
long Buffer_Len=0;
long Buffer_Size=0;
char Mode=0, ToMode=0;
long i=0;
char ch=0;
CGIContent *temp=NULL;
Buffer=getenv("REQUEST_METHOD");//получаем метод передачи данных
if (Buffer==NULL) return 0;//если нет такой переменной то ошибка
Query_Str=getenv("QUERY_STRING");//строка в URL после "?"
Content_Len=getenv("CONTENT_LENGTH");//длинна передаваемых данных
if (strcmp(Buffer,"GET")==0){
if (Query_Str!=NULL) Content_Length=strlen(Query_Str);
else Content_Length=0;
Method=METHOD_GET;
} else {
if (strcmp(Buffer,"POST")==0){
Method=METHOD_POST;
if (Content_Len!=NULL) Content_Length=atol(Content_Len);
else Content_Len=0;
} else goto proc_exit;
};
Buffer=NULL;
Buffer_Size=10000;
//инициализируем буффер для работы
while (Buffer==NULL){
Buffer=(char *)malloc(Buffer_Size);
if (Buffer==NULL){
if ((Buffer_Size-=1000)<0) return 0;
};
};
//основной цикл
while (i<Content_Length){
//получаем следующий байт данных в
//зависимости от метода передачи
switch (Method){
case METHOD_GET:{
ch=*(Query_Str+i);
break;
};
case METHOD_POST:{
fread(&ch,1,1,stdin);
break;
};
};//switch;
switch (Mode){
case 0:{//режим накопления имени параметра
if (ch!='='){
if (ch=='+') ch=' '; else
if (ch=='%'){
ToMode=0;
Mode=2;
break;
};
*(Buffer+Buffer_Len)=ch;
Buffer_Len++;
} else {
Mode=1;
*(Buffer+Buffer_Len)=0;
if (ParamName!=NULL) free(ParamName);
ParamName=strdup(Buffer);
Buffer_Len=0;
};
break;
};//case 0;
case 1:{//режим накопления содержимого атрибута
if (ch!='&'){
if (ch=='+') ch=' '; else
if (ch=='%'){
ToMode=1;
Mode=2;
break;
};
*(Buffer+Buffer_Len)=ch;
if ((Buffer_Len+=1)>=Buffer_Size){
*(Buffer+Buffer_Len)=0;
if (ParamName!=NULL){
temp=Param->Add(ParamName,Buffer);
free(ParamName);
ParamName=NULL;
} else {
temp->Add(Buffer);
};
Buffer_Len=0;
};
} else {//если найден символ &, то добавляется параметр и значение
*(Buffer+Buffer_Len)=0;
if (ParamName!=NULL)
temp=Param->Add(ParamName,Buffer);
else
temp->Add(Buffer);
Buffer_Len=0;
Mode=0;
};
break;
};//case 1;
case 2:{//режим преобразования из HEX первого символа после %
*(Buffer+Buffer_Len)=(gethex(ch)<<4);
Mode=3;
break;
};//case
case 3:{//второго символа HEX
*(Buffer+Buffer_Len)+=gethex(ch);
Buffer_Len++;
Mode=ToMode;
break;
};
};//switch (Mode)
i++;
};
//если осталось что-то после работы то добавляем
if (Buffer_Len>0){
*(Buffer+Buffer_Len)=0;
if (ParamName!=NULL)
temp=Param->Add(ParamName,Buffer);
else
temp->Add(Buffer);
};
proc_exit:
if ((Method==METHOD_GET)&&(Query_Str!=NULL))
Query_String=strdup(Query_Str);
return 0;
};
Этот метод получает все переменные окружения необходимые для считывания приходящих данных. После чего инициализирует буффер и считывает их в зависимости от метода передачи.
В зависимости от считанного символа происходит смена режима и символ добавляется либо в имя параметра, либо в содержимое, либо декодируется.
Последний на этот момент необходимый метод поиска: CGIContent *CGIApp::FindParam(char *name){
CGIContent *temp=Param->Find(name);
return temp;
};
Стоило бы заметить о некоторых особенностях работы класса на разных платформах. Изначально писал я его в среде Borland C++ 3.1 и в среде Windows все скрипты просто "летали". При отладке в Linux начались большие проблемы. После поиска ошибок, которых по идее не было, в течении получаса оказалось, что нельзя использовать процедуру free(*char), если указатель был получен с помощью getenv(). Т.е. я предполагаю в Линуксе процедура getenv() не копирует содержимое переменной окружения в отдельную строку, а возвращает адрес, в котором она содержится. В Досе таких ошибок не возникало, правда тут нельзя утверждать, что getenv() создает отдельную строку для этого, вероятно процедура free() просто не вызывает того аварийного завершения, которое получается в Линуксе.
Вобщем, что не система, то свои проблемы, да и что вообще там система... Компиляторы чего только стоят :-) Отладка CGI написанных на языке С++ всегда будет иметь некоторую зависимость от платформы или реализации компилятора, хотя считается, что все везде работает по одному алгоритму. Оказывается, что не все и не всегда.
Шаг 16 - Графика на лету это просто.
Не буду продолжать рассказ о классе CGIApp, потому что считаю там должно быть все ясно. У него конечно еще много недочетов, и вероятно будут проявляться глюки, но всеже это лучше чем раньше. Единственное, что там еще надо доработать это разбор данных из строки QUERY_STRING при обращении методом POST, а то получается, что данные просто хранятся в переменной класса и не разобраны в приемлимый вид. Так что работайте...
Сейчас думаю многим будет интересна возможность создания графики на "лету". Наверняка тысячи раз видели графики на сайтах, диаграммы, таблички и т.д. Все это не рисуют бедные вебмастера каждый час или даже минуту :-)
Вывод графики интересная скажу вам штука... Особенно когда эта операция должна выполняться быстро. Пару лет назад я крепко этим занимался и целыми днями сидел за оптимизацией ассемблерного кода. Иногда уменьшив размер процедуры на пару байт (например, заменив add ax,1 на inc ax) скорость возрастала на 50 и больше процентов. Странно было, но очень приятно... Сейчас уже никто не думает о скоростях и оптимизации, пишут на 500Мб то, что можно "упаковать" в 100 :-)) Вобщем хватит философии, приступим...
При создании картинки монитора может и не быть под рукой, мало того в компьютере может попросту не быть видео адаптера... Так, что все операции надо проводить в памяти на так называемом "виртуальном экране".
Чтобы зря не тратить память лучше при инициализации приложения выделить память под виртуальный экран ровно столько, сколько потребуется для создания картинки. Для начала опишем "болванку" класса, который потом будем постоянно наращивать. class CGIScreen {
protected:
int width, height;
char *scr;
public:
CGIScreen(int w,int h);
~CGIScreen();
};
Переменные width и height хранят размеры виртуального экрана, которые необходимы для процедур вывода, чтобы отрезать все ненужные или по другому не видимые пикселы.
Указатель scr это и есть тот самый экран. Теперь конструктор и деструктор: CGIScreen::CGIScreen(int w,int h)
{
if (w*h<=64000)
scr=(char *)malloc(w*h);
else {
scr=NULL;
return;
}
width=w;
height=h;
};
CGIScreen::~CGIScreen()
{
if (scr!=NULL) free(scr);
};
В конструкторе я не стал выделять много памяти. Во-первых, 64000=320*200 чего вполне хватит, чтобы создать картинку немеренного размера (в байтах :-) и разозлить посетителя :-). Во-вторых, я не могу предугадать в каких системах будет использоваться этот код. В ДОСе максимальный блок памяти 64Кб, ну а если у Вас возможно хранить больше, то меняйте код сами :-)
Следующей думаю будет процедура заполнения "экрана" одним цветом. Догадались как это можно быстро сделать ? int CGIScreen::FillScreen(char color)
{
if (scr!=NULL) setmem((void *)scr,width*height,color);
};
Теперь не забудьте вставить заголовок процедуры в описание класса и можно начинать работать :-) Будете получать виртуальные экранчики заполненные одним цветом. Помоему не плохо для начала... Подойдет для создания фонов :-)
Шаг 17 - Вывод примитивов на экран.
Экран у нас теперь готов, научимся выводить примитивы на этот экран. Самым "примитивным" из примитивов является точка или пиксел, как кто хочет. Если Вы никогда не занимались графикой, то Вам вероятно будет сложно сообразить, что значит вывести точку.
Видео память в PC-совместимых системах линейна, если не считать древних режимов EGA или CGA. Поэтому для того, чтобы вывести точку на экран достаточно просто записать байт-цвет в соответствующую ячейку в памяти. "Линейная память" надо понимать так: каждая последующая строка пикселов размещается в памяти точно за предыдущей строчкой. Небольшой пример: RRRGGGGBBB
GGGBBBRRRB
..........
Допустим это картинка на экране, тут буквы RBG это какие-то цвета. Так вот в нашей линейной памяти эти строчки будут располагаться так: RRRGGGGBBBGGGBBBRRRB..........
Думаю Вам уже должна быть понятна следующая формула: адрес_ячейки=Y_точки * Ширину_Экрана + Х_точки. Именно по ней мы будем вычислять адрес пиксела в памяти. Так вот давайте напишем процедуру вывода этого пиксела: int CGIScreen::PutPixel(int x,int y,char color)
{
if ((x<0)||(x>=width)||(y<0)||(y>=width))
return -1;
*(scr+y*width+x)=color;
};
Естественно надо не забывать, что точка может заходить за границы экрана, поэтому ее надо проверять. Это делает оператор if (...). Теперь Вы можете выводить точку в любое место и все, что надо будет отображаться.
Дальнейней процедурой должна быть линия. Алгоритмов вычерчивания линии насколько я знаю есть предостаточно, но повсюду используется один - это алгоритм Брезенхейма. Объяснить по нормальному это дело я Вам сейчас без подготовки не смогу, да и не та у нас тут тематика, по этому просто принимаем "as is" :-) int CGIScreen::Line(int x1,int y1,int x2,int y2, unsigned char Color)
{
int dx=abs(x2-x1);
int dy=abs(y2-y1);
int sx=x2 >= x1 ? 1:-1;
int sy=y2 >= y1 ? 1:-1;
int d,d1,d2,x,y,i;
if (dy<=dx)
{ d=(dy << 1)-dx;
d1=dy << 1;
d2=(dy - dx) << 1;
PutPixel(x1,y1,Color);
for (x=x1+sx,y=y1,i=1;i<=dx;i++,x+=sx)
{
if (d>0){d+=d2;y+=sy;}
else d+=d1;
PutPixel(x,y,Color);
}
}
else
{
d=(dx << 1) - dy;
d1=dx << 1;
d2=(dx - dy) << 1;
PutPixel(x1,y1,Color);
for (x=x1,y=y1+sy,i=1;i<=dy;i++,y+=sy)
{
if (d>0){d+=d2; x+=sx;}
else d+=d1;
PutPixel(x,y,Color);
}
}
return 0;
};/*Line*/
Вычерчивание линии происходит по точкам, а значит будет действовать наш механизм "отсечки" ненужных пикселов.
Помоему уже не плохо. Самые главные можно сказать примитивы мы научились строить. Дело за немногим, использовать их для создания более сложных объектов.
Шаг 18 - Вывод примитивов на экран (часть 2).
Еще остался один примитив, который является одним из главных. Это естественно окружность, а как примитив с расширенными возможностями лучше сразу рассмотреть эллипс. Над прорисовкой эллипса тоже постарался Брезенхейм и облегчил задачу всем программистам до сегодняшних дней.
Вот код рисования эллипса: int CGIScreen::Ellipse(int exc, int eyc, int ea, int eb , unsigned char Color)
{
int elx, ely;
long aa, aa2, bb, bb2, d, dx, dy;
elx = 0; ely = eb; aa = (long)ea * ea; aa2 = 2 * aa;
bb = (long)eb * eb; bb2 = 2 * bb;
d = bb - aa * eb + aa/4; dx = 0; dy = aa2 * eb;
PutPixel(exc, eyc - ely, Color); PutPixel(exc, eyc + ely, Color);
PutPixel(exc - ea, eyc, Color); PutPixel(exc + ea, eyc, Color);
while (dx < dy)
{
if (d > 0) { ely--; dy-=aa2; d-=dy;}
elx++; dx+=bb2; d+=bb+dx;
PutPixel(exc + elx, eyc + ely, Color);
PutPixel(exc - elx, eyc + ely, Color);
PutPixel(exc + elx, eyc - ely, Color);
PutPixel(exc - elx, eyc - ely, Color);
};
d+=(3 * (aa - bb)/2 - (dx + dy))/2;
while (ely > 0)
{
if (d < 0) {elx++; dx+=bb2; d+=bb + dx;}
ely--; dy-=aa2; d+=aa - dy;
PutPixel(exc + elx, eyc + ely, Color);
PutPixel(exc - elx, eyc + ely, Color);
PutPixel(exc + elx, eyc - ely, Color);
PutPixel(exc - elx, eyc - ely, Color);
};
return 0;
};/*Ellipse*/
Здесь координатами центральной точки эллипса являются параметры exc,eyc. Параметры ea,eb это ширина и высота эллипса, подставив вместо них одинаковые значения Вы сможете нарисовать окружность.
Теперь еще два более крупных примитива состоящих из линий. Первый это прямоугольник: int CGIScreen::Rectangle(int x1,int y1,int x2,int y2, unsigned char color)
{
Line(x1,y1,x2,y1,color);
Line(x1,y1,x1,y2,color);
Line(x2,y1,x2,y2,color);
Line(x1,y2,x2,y2,color);
}/*Rectangle*/
и закращенный прямоугольник: int CGIScreen::FillRectangle(int X1,int Y1,int X2,int Y2, unsigned char Color)
{
int I;
if (Y1>Y2) {I=Y1;Y1=Y2;Y2=I;};
for (I=Y1;I<=Y2;I++) line(X1,I,X2,I,Color);
}/*FillRectangle*/
Теперь на этой базе вы сможете строить более сложные объекты.
Шаг 19 - Вывод спрайтов на экран.
Эта возможность должна всем очень понравиться, т.к. наверняка кто-то из Вас захочет кроме вывода простых линий вывести какую-то картинку нарисованную в Фотошопе :-)
Естественно запрограммировать вывод этой картинки попиксельно в программе очень сложно. Тут приходят на помощь спрайты. Спрайтами принято называть графические объекты(кусочки изображения) помещенные в память или в файл на диске. Для тех кто слабо себе представляет понятие спрайт поясню: в играх, например, это любой человечек, цветочек, стена и т.д. Когда много спрайтов одного и того же объекта(но с изменениями) быстро выводят на экран получается анимация. Но нам анимация естественно не пригодится :-)
Физически спрайт в наших понятиях это такой же экран, но меньшего размера и заполненный информацией. Поэтому для него также справедливо понятие линейности хранения в памяти.
Вывод спрайта на экран состоит в том, чтобы выделить из последовательности хранящихся в памяти байтов полоску шириной равной ширине спрайта и вывести ее в соответствующее положение на экране. Выделение таких полосок(строк изображения) происходит начиная с первой и до конечной, при этом на экране по одной строчке появляется картинка. Для осуществления такой вот операции "копирования" полоски байтов на экран предназначается следующая процедура: void MoveLineOfImage(char *Source,char *Dest,unsigned int Count,char Bol)
{
for (int i=0; i<Count; i++){
if ((*Source)!=0)
*Dest=*Source;
else {
if (Bol!=1)
*Dest=*Source;
};
Source++;
Dest++;
};
}
Здесь Source - указатель на источник в памяти, Dest - указатель на получателя, Count - количество копируемых байт. Признак Bol - это признак копирования нулевых байтов. Если этот признак установлен в 1, то процедура не копирует нулевые байты, при этом не стирается изображение, которое уже было на экране, т.е. достигается эффект прозрачности областей спрайта, которые состоят из нулей.
Теперь на базе этой процедуры можно строить процедуру, которая будет выводить спрайт на виртуальный экран. Разработка данной процедуры и доведение ее до окончательного нормального вида заняла у меня в свое время пару месяцев. Сейчас я Вам привожу код этой процедуры, но только пришлось убрать все ассемблерные вставки (над которыми я так долго работал) на их менее быстродействующие аналоги языка C++. Рассказывать о выводе спрайта не буду, так как сам доходил до этого долго и не та у нас тематика. void CGIScreen::PutImage(char *p,int x,int y,int bol)
{
int xs = *p; //ширина изображения
int ys = *(p+2); //высота изобажения
int lsx;
if ((x>width) || (y>height) || (x+xs<=0) || (y+ys<=0)) return;
int x1,y1,x2,y2;
x1=y1=0;
x2=xs;
y2=ys;
if (x<0) x1=-x;
if (y<0) y1=-y;
if (x+xs>width) x2=width-x;
if (y+ys>height) y2=height-y;
lsx=x2-x1;
char *p1=p+4+y1*xs+x1;
int i;
long Addr=(y+y1)*width+x+x1;
for (i=y1;i<y2;i++)
{
MoveLineOfImage(p1,scr+Addr,lsx,bol);
p1+=xs;
Addr+=width;
}
};/*PutImage*/
Указатель *p указывает на область памяти, в которой содержится спрайт. Координаты вывода задаются параметрами x,y причем они могут быть меньше нуля. Если при этом кусок спрайта попадает на экран, то будет выводиться именно этот кусок. Признак bol указывает на прозрачность спрайта и используется процедурой копирования строк изображения описанной выше.
Осталось только сказать о формате хранения спрайта в памяти. Как некоторые наверно уже успели заметить кроме самого содержимого спрайта в нем еще содержатся его физические размеры. Эти размеры записаны в самом начале спрайта и занимают 4 байта, т.е. два числа типа int - горизонтальный и вертикальный размер. Естественно формат спрайтов не подчиняется ни одному стандартному формату графических данных и при загрузке спрайта надо обеспечить его преобразование во внутренний формат. В будущем для облегчения работы мы напишем процедуру считывания спрайтов из файлов какого-нибудь типа, например, как самый легкий возьмем BMP. Но для этого надо еще обеспечить работу с палитрами. Скорее всего этим займемся в дальнейших шагах.
|