Проблемы конечно были всегда, но тут они стали быстро переходить в разряд неразрешимых. Как привязать новый офис к информационной структуре фирмы, тем более что руководству хочется видеть что происходит в филиале. И выручку, и складские остатки, и кто кому и сколько должен. Набирать штат программистов в филиал никто особенно не хотел. Устанавливать там сервер и постоянно его обслуживать не хотелось уже нам. Тем более непонятно было как сводить два одновременно работающих предприятия в одно. Выставить наш SQL сервер в интернет мы тоже как-то не решились.
Вначале подумали создать какое-нибудь WEB приложение и заставить филиал работать с ним через браузер, тем более что Apache сервер у нас уже был, и какой-никакой опыт работы с html имелся. Но тут выяснилось, что к браузеру придется привязать сканеры, кассы и весы. Как это делать и во что это выльется мы представить себе не могли.
Немного подумав, решили что если браузер не может работать с периферией, то нужно написать что-то свое. В таком случае отпадает необходимость создавать сложные и громоздкие web странички, несущие в себе как данные так и кучу разметочной информации с кодом на java-script и можно ограничиться простейшим протоколом связи web-server'а с нашим клиентом. А весь функционал, отображение и взаимодействие компонентов возложить на клиентское приложение. К тому же по возможностям любой нормальный язык гораздо гибче чем java-script. В таком случае нужно создать надстройку над web сервером, реализующую какой-нибудь язык программирования и чтобы разгрузить SQL server, возложить на эту настройку отработку всех алгоритмов и бизнес-правил, а SQL серверу оставить роль которая ему и предназначена изначально - хранение и выборка данных. Тем более что количество stored-procedures росло лавинообразно да и убогенький полуязык на котором они писались затруднял решение некоторых задач.
После некоторых раздумий для сервера приложений был выбран mod_perl, благодаря в основном хорошим отзывам в инете и огромной библиотеке бесплатных модулей. Программу-клиент решили реализовать на Delphi, сказался наш многолетний опыт работы и имеющиеся наработки. Руководство спешило, поэтому раздумывать времени не было.
В этой статье будут описаны наши первые шаги по созданию распределенной системы. Все примеры сопровождаются полным набором исходников, так что вы сможете либо повторить наш путь, или пойти своим.. Кроме этих примеров скоро будет выложена для свободного скачивания в исходных кодах небольшая складская система. В будущем, если будет время, мы собираемя добавить информацию по шифрации и сжатию передаваемых данных, аутентификации, развитию протоколов связи и т.д..
Установка Web сервера Apache и mod_perl.
Начинаем работу: разархивируем файлы, производим конфигурацию, сборку, тестирование и установку дистрибутивов:
root> tar -zxvf ./apache_1.3.31.tzr.gz root> tar -zxvf ./mod_perl-1.0-current.tar.gz root> cd ./mod_perl-1.29 root> perl Makefile.PL APACHE_PREFIX=/usr/local/apache \ > APACHE_SRC=../apache-1.3.31/src \ > DO_HTTPD=1 \ > USE_APACI=1 \ > EVERYTHING=1 root> make root> make test root> make install |
root> cd /usr/local/apache |
Кроме этого нам понадобится библиотека, поддерживающая объект Request, который позволяет облегчить разбор html заголовков.
Ее можно скачать с сайта CPAN или у . Разархивируем, конфигурируем, устанавливаем:
root> tar -zxvf ./libapreq-1.3.tar.gz |
Настройка httpd.conf
PerlFreshRestart On |
Существует несколько способов использования mod_perl - например в режиме эмуляции CGI-вызовов, для чего служат модули Apache::Registry или Apache::Run. Однако они только эмулируют CGI и при этом теряются многие преимущества mod-perl. Для уменьшения времени реакции системы и повышения быстродействия авторами mod_perl рекомендуется создание специальных модулей-обработчиков.
Теперь немного теории. Для того чтобы сервер выполнил ваш модуль необходимо чтобы:
- он был правильно зарегистрирован в конфигурационном файле,
- был расположен в соответствующем каталоге файловой системы,
- имел права на чтение и исполнение для пользователя под которым работает сервер,
- а также должен быть доступным для адресации в системе каталогов Perl.
Предположим, что основным каталогом, относительно которого будет происходить поиск исполняемых модулей будет стандартная директория Apache /usr/local/apache/cgi-bin. Создадим в ней еще один каталог ./ThreeTier в котором и будем создавать файлы модулей. Для этого выполним следующие команды:
| root> cd /usr/local/apache/cgi-bin root> mkdir ./ThreeTier |
Теперь нам необходимо передать серверу информацию об этом каталоге. Точнее эта информация нужна не серверу, а Perl, который в момент старта сервера должен добавить в хеш %INC информацию о каталогах в которых нужно искать исполняемые файлы. Например, если серверу потребуется найти мoдуль ThreeTier::ExampleOne.pm, он должен начать поиск с /cgi-bin, найти в нем каталог ThreeTier и, уже там считать и выполнить файл ExampleOne.pm. Для решения этого вопроса нужно использовать опцию PerlRequire в httpd.conf. Аргументом этой опции служит абсолютный путь к программе Perl, которая запускается один раз при старте сервера. Поскольку указывается абсолютный путь, то не имеет особого значения где расположен и как называется файл программы. В этом примере мы назовем его PerlRequire.pl поместим в каталог cgi-bin/ThreeTier вместе с модулями. В самом же файле вызовем директиву use lib для указания начального каталога поиска модулей. Кроме назначения начального каталога этот файл может служить для предзагрузки некоторых модулей Perl, стандартных, или созданных программистом. Например для работы с базами данных, или взаимодействия с Apache.
Итак создаем файл /usr/local/apache/cgi-bin/ThreeTier/PerlRequire.pl.
| #!/usr/bin/perl use Apache # Предзагрузка модуля из стандартных путей Perl use lib '/usr/local/apache/cgi-bin # Добавление дополнительного пути поиска 1; # Стандартный код возврата |
Назначаем права доступа для этого файла. Владелец - пользователь от имени которого запускается Web сервер, в данном случае nobody. Права для пользователя - только чтение и исполнение.
| root> cd /usr/local/apache/cgi-bin/ThreeTier root> chown nobody ./PerlRequire.pl root> chmod 500 ./PerlRequire.pl |
Теперь добавляем следующую строку в httpd.conf:
| PerlRequire /usr/local/apache/cgi-bin/ThreeTier/PerlRequire.pl |
Наш сервер готов к тому чтобы мы написали и зарегистрировали наш первый пример.
Пример №1
Сервер
SetHandler perl-script PerlHandler ThreeTier::ExampleOne |
| package ThreeTier::ExampleOne; use strict; use warnings; sub handler { my $r = shift; Apache->request( $r ); $r->send_http_header('text/plain'); if( $r->method ne 'POST') { print "Неверный метод вызова"; return 0; } my $req = print $req; } 1; |
В качестве аргумента handler получает объект типа Apache::Request, вначале мы сохраняем его в локальной переменной $r, а в следующей строке приводим к необходимому типу. Отсылаем клиенту заголовок ответа с типом контента - в данном случае - простой текст (можно применить text/html или еще что-нибудь - это может зависеть от файрволов и прокси-серверов, которые вы применяете, например некоторые файрволы проверяют документ на соответствие его содержания объявленному типу). Далее анализируем передан ли поступивший запрос по методу POST? Для этого служит свойство method объекта Apache::Request. Если метод не является POST - мы выходим из обработчика с признаком ошибки return 0 (нулевое значение возвращаемое функцией обычно служит для обозначения ошибки в mod_perl) и пересылаем клиенту строку с текстом предупреждения о неверном вызове. В случае если метод равен POST, то считываем текст присланного от клиента сообщения в локальную переменную $req (mod_perl переопределяет файл стандартного ввода на прием POST сообщений, а стандартный вывод - на ответ сервера), после чего отсылаем ее обратно клиенту. На этом работа сервера завершается и он начинает ожидать следующий запрос.
Клиент
Создаем новое приложение средствами DELPHI. Объявляем переменную HTTP типа TIdHTTP, при обработке создания главной формы вызываем конструктор для этого объекта, при закрытии - соответственно деструктор.
| use ....... IdHTTP; ...... private HTTP : TIdHTTP; ....... procedure TfExample01.FormCreate(Sender: TObject); begin HTTP := TIdHttp.Create(Self); end; ........ procedure TfExample01.FormDestroy(Sender: TObject); begin HTTP.Free; end; |
| procedure TfExample01.BitBtn1Click(Sender: TObject); var tmpStream : TStringStream; begin tmpStream := TStringStream.Create(''); memReceive.Text := ''; try HTTP.Post('http://your_server/exampleone', memPost.Lines, tmpStream); memReceive.Text := tmpStream.DataString; except on E : Exception do memReceive.Text := E.Message; end; tmpStream.Free; end; |
- URL вашего сервера,
- объект типа TStrings содержащий текст сообщения и
- поток, возвращаемый сервером.
Если у вас произошла ошибка то посмотрите файлы /usr/local/apache/logs/access_log и /usr/local/apache/error_log. Они обычно помогают диагностировать ее причину. И не забывайте перезапускать сервер после каждого обновления текстов модулей и изменений в конфигурации!
Таким образом мы смогли передать сообщение на сервер и принять ответ от него. Конечно, практической пользы от этого примера немного - это своеобразный тест для диагностики работы сервера и правильной установки всех компонентов.
Пример №2
Итак мы получили весьма бесполезную программу. Попробуем немного преобразовать ее, чтобы она могла очень много, буквально все что можно.Сервер
Для этого нужно совсем ничего, копируем файл ExampleOne.pm в ExampleTwo.pm :root> cd /usr/local/apache/cgi-bin/ThreeTier |
| package ThreeTier::ExampleTwo; use strict; use warnings; sub handler { my $r = shift; Apache->request( $r ); $r->send_http_header('text/plain'); if( $r->method ne 'POST') { print "Неверный метод вызова"; return 0; } my $req = print eval( $req ); } 1; |
SetHandler perl-script PerlHandler ThreeTier::ExampleTwo |
Клиент
Теперь делаем минимальные изменения в программе - клиенте:| procedure TfExample01.BitBtn1Click(Sender: TObject); var tmpStream : TStringStream; begin tmpStream := TStringStream.Create(''); memReceive.Text := ''; try HTTP.Post('http://your_server/exampletwo', memPost.Lines, tmpStream); memReceive.Text := tmpStream.DataString; except on E : Exception do memReceive.Text := E.Message; end; tmpStream.Free; end; |
Итак мы увидели что несмотря на практически безграничные возможности, этот модуль открыл огромную дыру в защите нашей системы. Такую же как например telnet без пароля. Любой шалунишка, способный ронять сервера только со стола на пол, в состоянии причинить нам массу неприятностей. Вся следующая писанина будет посвящена только тому, как найти компромисс между примером №1 и примером №2, между полной бесполезностью, но защищенностью, и полным подчинением сервера клиенту.
Никаких скомпилированных примеров под наш сервер по понятным причинам тут нету, а файл конфигурации естественно не содержит код для доступа к exampletwo. Так что не пытайтесь...
Пример №3
Прежде чем начинать создание новой программки, подумаем насчет структуризации предаваемой и получаемой информации. Ясно что к серверу должна приходить не просто какой-то массив байт, а структурированный запрос, который сервер мог бы разбить на элементы, а затем после некоторой интерпретации выполнить только ту работу, на которую мы рассчитываем. Ни в коем случае не больше, но и не меньше. Кроме того и возвращаемые данные должны быть как-то разделены друг от друга. В этом примере мы пока поразбираемся и предложим простейшую модель синтеза на стороне клиента и парсинга на стороне сервера некоторого, наперед неограниченного количества именованных аргументов. Существует множество способов передать информацию вида ключ-значение. От простейших разделителей, ограничителей, системы кодирования запросов в http-протоколе и заканчивая всеобъемлющим и модным сейчас XML (добавим от себя и крайне громоздким). Мы пойдем по пути простейших ограничителей (усложнить вы сможете и сами), где каждая посылка будет представлена в виде:| arg1=vol1,arg2=vol2,arg3=vol3, ....,argX=volX,......,argN=volN, |
A в этом примере оставим знак "," разделителем пар, а знак "=" - разделителем имен аргументов от их значений.
Клиент
Начнем на этот раз с программы-клиента. Объявим символы-разделители как константы:| const d_fld = '='; d_rec = ','; |
| for i:= 0 to memPost.Lines.Count - 1 do begin s := s + 'args' + IntToStr(i) + d_fld + memPost.Lines[i] + d_rec; end; |
| args0=234,args1=546,args2=66,args3=67,args4=324, |
| 'http://11.0.0.1/examplethree' |
SetHandler perl-script PerlHandler ThreeTier::ExampleThree |
| package ThreeTier::ExampleThree; |
| use vars qw ($d_fld $d_rec); |
| $d_fld = "="; $d_rec = ","; |
| sub requestParser { my @params = split /$d_rec/, shift; my %args; grep {my @param = split /$d_fld/; $args{$param[0]} = $param[1]; } @params; return \%args; } |
| $params[0] = "args0=234"; $params[1] = "args1=546"; и т.д. |
| $args{'args0'}='234'; $args{'args1'}='546'; и т.д. |
| return \%args; |
| my $args = requestParser($req); my $tmpSum = 0; my $tmpStr = ''; foreach my $par (sort keys(%$args)) { $tmpStr .= "$par=$args->{$par}\r\n"; $tmpSum += $args->{$par}; } print "Сумма аргументов:\r\n$tmpStrРавна : $tmpSum"; |
Конечно, этот пример имеет массу недостатков - отсутствие проверки на соответствие аргументов числовым значениям, неформатированный вывод (вряд ли "\r\n" можно назвать форматированием), что не дает возможность вернуть массив или таблицу со значениями, да и хотелось бы заставить сервер выполнять несколько функций. Со всеми этими недостатками мы и поборемся в следующих примерах.
Пример №4
В этом разделе мы заставим сервер выполнять несколько функций из разных модулей. В сложных программах, когда количество функций и процедур насчитывает сотни и тысячи, хотелось бы группировать их в отдельных модулях, и вызывать не только по имени, но и по имени файла в которых они находятся. Для решения этой проблемы мы поступим довольно просто, введем дополнительные параметры с предопределенными именами в строку вызова. Сервер должен будет их опознать, после чего сначала попытаться найти и загрузить исполняемый файл с именем модуля, а затем вызвать функцию по ее имени и передать этой функции оставшиеся параметры. Мы совершенно свободны в выборе имен для параметров, назначим для модуля имя параметра _mod, а для функции - _func. Подчеркивания мы ввели только для того чтобы показать, что это служебные параметры и они не будут считаться аргументами для вызываемых функций, с другой стороны их проще искать отладчиком в строке вызова, если возникнут какие-то проблемы.
- ExampleFourMath
- ExampleFourString
- undefined
cbFunc, в свою очередь, будет заполнен именами функций:
- min
- max
- average
- sum
- undefined
Теперь после цикла с созданием строки запросов добавим следующие операторы и изменения (выделены цветом):
| for i:= 0 to memPost.Lines.Count - 1 do begin s := s + 'args' + IntToStr(i) + d_fld + memPost.Lines[i] + d_rec; end; s := s + '_func' + d_fld + cbFunc.Text + d_rec; s := s + '_mod' + d_fld + cbMod.Text + d_rec; reqStream := TStringStream.Create(s); HTTP.Post('http://11.0.0.1/examplefour', reqStream, tmpStream); |
Сервер
SetHandler perl-script PerlHandler ThreeTier::ExampleFour |
| my $args = requestParser($req); my $func = $args->{_func}; delete($args->{_func}); my $mod = $args->{_mod}; delete($args->{_mod}); unless( eval "require ThreeTier::$mod") { print "Неизвестный модуль $mod"; Apache::exit(); } no strict 'refs'; my $f; $f = ("ThreeTier::" . $mod . "::" . $func); if(defined(&$f)) { $f->($args); } else { print "Неопределенная функция $func в модуле ThreeTier::$mod"; Apache::exit(); } use strict 'refs'; return 1; |
Теперь вызываем интерпретацию (eval) строки вида "require ThreeTier::ExampleFourMath", тем самым mod_perl пытается откомпилировать и загрузить в свое пространство имен файл ExampleFourMath.pm из каталога ThreeTier находящегося в свою очередь в одном из каталогов, указанных для Perl как по умолчанию так и директивами use lib из PerlRequire.pl. Если файл существует, и может быть откомпилирован, eval возвращает ненулевой результат и программа продолжается. В противном случае возвращается сообщение об ошибке и handler прерывает свою работу (Apache::exit()). Заметим, что просто выйти из mod_perl, используя например, halt, нельзя, это может привести к краху системы. Рекомендуется return 1, если это главный handler, или Apache::exit - в любом месте программы или вложенной функции.
Дальше используется директива no strict 'refs'. Она необходима чтобы снять ограничение на вызов функции по имени. Так как у нас вначале модуля указано use strict - то выполнение такого вызова приведет к ошибке. И это правильно практически всегда, но только не здесь. Объявляем переменную $f которая будет хранить ссылку на функцию, определяемую строкой вида "ThreeTier::ExampleFourMath::max". Для получения ссылки на функцию по строке применяются круглые скобки (). Ну такой уж синтаксис в Perl ! Ужас конечно, а есть еще регулярные выражения... Но как ни странно он живет и процветает последние 20 лет.
Проверяем создалась ли ссылка if(defined(&$f)) (тоже перл !!!) и если да - то вызываем эту функцию по ссылке, передавая ссылку на хеш с аргументами $f->($args) (и тут не лучше...). А если нет, печатаем сообщение что забыли мы эту функцию создать, или не оттуда вызываем, после чего выходим из обработки. После вызова функции возвращаем на место директиву use strict и тоже выходим.
Теперь нам надо создать файлы-модули содержащие реализацию наших функций. Файлов будет два - один ExampleFourMath.pm и второй ExampleFourString.pm. Оба они будут находиться в каталоге ..../ThreeTier. Вообще-то их можно запрятать куда-нибудь подальше от любопытных глаз в файловую систему, в том числе и не в дерево файлов Apache, но для этого придется поменять как PerlRequire.pl (добавив use lib) так, возможно и префиксы пакетов как в handler, так и в самих объявлениях пакетов. Но оставим все как есть, у нас тут все-таки примеры, а не корпоративный кластерный сервер.
Итак фрагмент файла ExampleFourMath.pm:
| package ThreeTier::ExampleFourMath; use strict; use warnings; sub sum { my $args = shift; my $tmpVal = 0; my $tmpStr = ''; foreach my $par (sort keys(%$args)) { $tmpVal += $args->{$par}; $tmpStr .= "$par = $args->{$par}\r\n"; } print "Сумма параметров :\r\n$tmpStrРавна: $tmpVal"; Apache::exit(); } sub average { my $args = shift; my @pars = sort keys(%$args); my $tmpVal = 0; my $count = $#pars + 1; my $tmpStr = ''; foreach my $par (@pars) { $tmpVal += $args->{$par}; $tmpStr .= "$par = $args->{$par}\r\n"; } $tmpVal /= $count; print "Среднее значение параметров :\r\n$tmpStrРавно: $tmpVal"; Apache::exit(); } ............ |
Теперь посмотрим на ExampleFourString.pm:
| package ThreeTier::ExampleFourString; use strict; use warnings; sub sum { my $args = shift; my $tmpVal = ''; my $tmpStr = ''; foreach my $par (sort keys(%$args)) { $tmpVal .= $args->{$par}; $tmpStr .= "$par = $args->{$par}\r\n"; } print "Конкатенация параметров :\r\n$tmpStrРавна: $tmpVal"; Apache::exit(); } |
Осталось еще много проблем, которые надо решить, например типизация, форматированный вывод и парсинг результатов, но мы все ближе к цели.
Пример №5
В предыдущих примерах мы получали от сервера неформатированную строку. Попробуем упорядочить вывод результатов. Применим тот же метод что и при отправке аргументов в запросе - то есть разделители. Предположим что наш сервер должен возвращать набор нескольких таблиц за один раз. Это довольно распространенный случай - например для накладной нужно вернуть ее заголовок из нескольких полей (массив, или таблица из одной строки) и табличную часть. Поэтому будем использовать три различных разделителя:- разделитель полей в записи (скаляров в одномерном массиве)
- разделитель записей в двумерном массиве
- разделитель таблиц в возвращаемом наборе данных
- '=' - разделитель полей
- ',' - разделитель
- '!' - разделитель таблиц
Сервер
Создаем файл ExampleFive.pm по образцу ExampleFour.pm и, дополнительно, ExamleFiveGlobal.pm - в который мы вынесем все константы и функции из ExampleFive.pm, оставив там только handler. Функции из ExampleFiveGlobal будут интенсивно использоваться другими модулями, формирующими ответ сервера.
Файл ExampleFive.pm:
| package ThreeTier::ExampleFive; use strict; use warnings; use ThreeTier::ExampleFiveGlobal; sub handler { my $r = shift; Apache->request($r); $r->send_http_header('text/plain'); if($r->method ne 'POST') { print " Bad method request "; return 0; } my $req = my $args = requestParser($req); $strRespond = ''; callUserFunction($args); sendOK(); return 1; } |
requestParser($req) - создает хеш аргументов запроса так же как и в предыдущих примерах:
| sub requestParser { my @params = split /$d_rec/, shift; my %args; grep {my @param = split /$d_fld/; $args{$param[0]} = $param[1]; } @params; return \%args; } |
| sub callUserFunction { my $args = shift; my $parentPackage = 'ThreeTier::'; my $strFunc = $args->{_func}; my $strMod = $parentPackage . $args->{_mod}; unless( eval "require $strMod") { sendError("Неизвестный модуль $strMod"); } no strict 'refs'; my $func = ($strMod . "::" . $strFunc); unless(defined(&$func)) { sendError("Неопределенная функция $strFunc в модуле $strMod"); } $func->($args); use strict 'refs'; } |
Заголовок ExampleFiveGlobal.pm:
| package ThreeTier::ExampleFiveGlobal; use strict; use warnings; use Exporter; use vars qw(@ISA @EXPORT $d_fld $d_rec $d_set $strRespond); @ISA = qw(Exporter); @EXPORT = qw($d_fld $d_rec $d_set $strRespond &callUserFunction &requestParser &addEmptySet &addField &addRecord &addSet &sendError &sendOK &isDecimal &isInt); $d_fld = chr(20); $d_rec = chr(19); $d_set = chr(18); $strRespond = ''; |
@ISA = qw(Exporter) - опять же служит для удовлетворения стандартов экспорта в Perl. Массиву @EXPORT присваивается список всех функций и переменных, которые будут экспортированы из этого модуля. Если вы упустите какую-нибудь из них в этом списке, то внешние модули не смогут к ним обратиться. Естественно, что внутренние функции и служебные переменные модуля в этом списке присутствовать не должны. Далее следует инициализация глобальных переменных.
Теперь создадим функции, которые будут принимать скаляры, массивы и массивы массивов и преобразования в строку ответа. Ниже приводится описание функции addSet, которая получает в качестве аргумента ссылку на массив ссылок на массив (в нормальном языке - двумерный массив), преобразует ее в строку и прибавляет результат к глобальной переменной $strRespond:
| sub addSet { my ($prs) = shift; foreach my $pr (@$prs) { $strRespond .= join($d_fld , @$pr) . $d_fld . $d_rec; } $strRespond .= $d_set; } |
| 1 2 3 4 5 6 7 8 9 11 12 13 |
| 1=2=3=,4=5=6=,7=8=9=,11=12=13=,! |
| sub addRecord { my $pr = shift; $strRespond .= join($d_fld , @$pr) . $d_fld . $d_rec . $d_set; } |
| 1 2 3 4 5 6 7 |
| 1=2=3=4=5=6=7=,! |
| sub addField { $strRespond .= shift() . $d_fld . $d_rec . $d_set; } |
| 1 |
| 1=,! |
Теперь, вызывая эти функции несколько раз, мы можем сформировать довольно сложный по структуре ответ сервера клиенту.
Но осталась еще одно небольшое дело - обработка ошибок на сервере. До этого мы отсылали простую неформатированную строку и в общем случае невозможно было определить нормальный это ответ, или произошел какой-то сбой. Теперь мы можем условиться, что первая таблица будет содержать код статуса со строкой ошибки. Например, если сервер отработал корректно, то в первом поле первой записи содержится строка "OK", а если нет - то в ней находится строка "Error", а в следующем поле - строковое описание ошибки. Все остальные наборы будут следовать за первой таблицей. Создадим функцию sendOK:
| sub sendOK { print 'OK' . $d_fld . $d_rec . $d_set . $strRespond; } |
И функцию sendError которая принимает любую строку:
| sub sendError { $strRespond = ''; print 'Error' . $d_fld . shift() . $d_fld . $d_rec . $d_set; Apache::exit(); } |
Добавим в ExampleFiveGlobal пару полезных функций isInt и isDecimal которые позволяют определить является ли значение некоторой переменной соответственно целым или дробным числом (они будут полезны для генерации ошибок, если мы передадим серверу неверные аргументы):
| sub isDecimal { return ($_[0] =~ m/^\s*[-+]?\d+\.?\d*\s*$/); } sub isInt { return ($_[0] =~ m/^\s*[-+]?\d+\s*$/); } |
Начнем с ExampleFiveField:
| package ThreeTier::ExampleFiveField; use strict; use warnings; use ThreeTier::ExampleFiveGlobal; sub sum { my $args = shift; sendError('Первое слагаемое не является цифрой !') unless(isDecimal($args->{add_1})); sendError('Второе слагаемое не является цифрой !') unless(isDecimal($args->{add_2})); addField($args->{add_1} + $args->{add_2}); } ......................................... |
Попробуем вернуть одномерный массив.
| package ThreeTier::ExampleFiveRecord; use strict; use warnings; use ThreeTier::ExampleFiveGlobal; sub arProgress { my $args = shift; sendError('Начальное значение не является цифрой !') unless(isDecimal($args->{add_1})); sendError('Шаг итерации не является цифрой !') unless(isDecimal($args->{add_2})); sendError('Количество итераций не является цифрой !') unless(isDecimal($args->{add_3})); sendError('Количество итераций должно быть больше 0 !') if($args->{add_3} < 1); sendError('Количество итераций должно быть целым !') unless(isInt($args->{add_3})); sendError('Количество итераций должно быть меньше 1000 !') if($args->{add_3} >= 1000); my @res = (); for( my $i = 0; $i < $args->{add_3}; $i++ ) { push @res, $args->{add_1} + $i * $args->{add_2}; } addRecord(\@res); } ......................................... |
Функции последнего модуля ExampleFiveSet.pm возвращают таблицы:
| package ThreeTier::ExampleFiveSet; use strict; use warnings; use ThreeTier::ExampleFiveGlobal; sub arTable { my $args = shift; sendError('Кол-во по горизонтали не является целым числом !') unless(isInt($args->{add_1})); sendError('Кол-во по вертикали не является целым числом !') unless(isDecimal($args->{add_2})); sendError('Количество горизонтали должно быть меньше 50 но больше 1!') if(($args->{add_1} >= 50) || ($args->{add_1} <= 0)); sendError('Количество вертикали должно быть меньше 50 но больше 1!') if(($args->{add_2} >= 50) || ($args->{add_2} <= 0)); my @res = (); my @rec = (); push @rec, ''; for( my $i = 1; $i <= ($args->{add_1}); $i++ ) { push @rec, $i ; } push @res, \@rec; for( my $j = 1; $j <= ($args->{add_2}); $j++ ) { my @rec = (); push @rec , $j; for(my $i = 1; $i <= ($args->{add_1}); $i++ ) { push @rec, $i + $j; } push @res, \@rec; } addSet(\@res); } ......................................... |
Добавляем в конец httpd.conf строки:
SetHandler perl-script PerlHandler ThreeTier::ExampleFive |
| root> /usr/local/apache/bin/apachectl restart |
Клиент
| TConnector = class private HTTP : TIdHTTP; Composer : TRequestComposer; Parser : TRespondParser; .................................... |
Определим константы для символов - ограничителей:
| const d_fld = Chr(20); d_rec = Chr(19); d_set = Chr(18); |
| procedure TRequestComposer.AddParam(AName,AValue : String); begin CheckParam(AName); paramList.Add(AName + d_fld + StringToServer(AValue)); end; |
Кроме того, определены два свойства Module и Func - для хранения имени модуля и функции. При установке хоть одного из них происходит очистка paramList - незачем хранить параметры, если функция или модуль изменились.
После установки всех параметров вызывается свойство RequestString через внутреннюю функцию FGetRequestStrng:
| function TRequestComposer.FGetRequestString : String; var res : String; i : Integer; begin if Length(_module) = 0 then raise Exception.Create('Имя модуля не может быть пустым !'); if Length(_func) = 0 then raise Exception.Create('Имя функции не может быть пустым !'); res := '_mod' + d_fld + _module + d_rec + '_func' + d_fld + _func + d_rec; for i := 0 to paramList.Count - 1 do res := res + paramList[i] + d_rec; FGetRequestString := res; end; |
Некоторые из этих свойств и функций через переопределения в TConnector доступны программисту для прямого использования. Обычный метод применения модуля TConnector для отправки запроса на сервер выглядит следующим образом:
| try Connector.Module := 'ExampleFiveField'; Connector.Func := 'sum'; Connector.AddParam('add_1', '3'); Connector.AddParam('add_2', '4'); Connector.Execute; ...................... //Обработка ответа ...................... except on E: Exception do Memo1.Text := E.Message; end; |
| try reqStream := TStringStream.Create(Composer.RequestString); resStream := TStringStream.Create(''); HTTP.Post(_URL,reqStream,resStream); Parser.Parse(resStream); except on E : Exception do begin reqStream.Free; resStream.Free; raise Exception.Create(E.Message); end; end; |
| procedure TRespondParser.ParseToList(AInputString : PChar; chrDvd : Char; AOutputList : TStringList); var pBeg,pPos,pEnd : PChar; fullLen,iLen, i : Integer; PTemp : PChar; tmp : real; begin AOutputList.Clear; pBeg := AInputString; pPos := nil; pEnd := StrEnd(AInputString); fullLen := Length(AInputString); pPos := FastStrScan(pBeg, chrDvd, pEnd - pBeg); while (pPos <> nil) and (pBeg < pEnd) do begin iLen := pPos - pBeg; PTemp := AllocMem(iLen + 1); strMove(PTemp,pBeg,iLen); AOutputList.Add(PTemp); FreeMem(PTemp); pBeg := pPos + 1; pPos := FastStrScan(pBeg, chrDvd, pEnd - pBeg); end; end; |
| function TRespondParser.FastStrScan(const Str: PChar; Chr: Char; Length : Integer): PChar; assembler; asm PUSH EDI MOV EDI,Str MOV AL,Chr MOV ECX,Length REPNE SCASB MOV EAX,0 JNE @@1 MOV EAX,EDI DEC EAX @@1: POP EDI end; |
Символы, расположенные от начала строки до найденной позиции копируются в TStringList. В классе TRespondParser содержатся три списка строк :
- SetList : TStringList; - сохраняет строки-таблицы
- RecList : TStringList; - строки-записи для одной из таблиц в SetList
- FldList : TStringList; - строки, соответствующие полям одной из записей в RecList
| function TRespondParser.Parse(strm : TStringStream) : Integer; var s : String; begin strm.Position := 0; s := strm.DataString; SetList.Clear; ParseToList(PChar(s), d_set , SetList); if SetList.Count < 1 then raise Exception.Create('Ответ сервера не содержит таблиц!'); if ParseToList(PChar(SetList[0]), d_rec, RecList) <> 1 then raise Exception.Create('Ответ сервера не содержит таблицу статуса!'); if ParseToList(PChar(RecList[0]), d_fld, FldList) < 1 then raise Exception.Create('Ответ сервера не содержит код статуса!'); if FldList[0] <> 'OK' then if FldList.Count < 2 then raise Exception.Create('Ответ сервера не содержит поле ошибки!') else raise Exception.Create('Ошибка сервера: ' + FldList[1]); SetList.Delete(0); Parse := SetList.Count; end; |
Для удобства дальнейшего использования этого класса, создадим несколько служебных функций -
- TRespondParser.DoRecList(ASetNumber : Integer = 0) : Integer;
- TRespondParser.DoFldList(ARecNumber : Integer = 0) : Integer;
Еще одна функция одна служебная функция GetField нужна для доступа к полям по индексу:
| function TRespondParser.GetField(AFldNumber : Integer = 0) : String; begin if FldList.Count <= AFldNumber then raise Exception.Create('Номер поля слишком велик!'); GetField := FldList[AFldNumber]; end; |
Теперь можно перейти к примерам. Попробуем вызвать на сервере функцию sum из ExampleFiveFiled - она, как вы помните, должна вернуть только одно значение:
| Memo1.Text := ''; Edit3.Text := ''; try Connector.Module := 'ExampleFiveField'; Connector.Func := 'sum'; Connector.AddParam('add_1', '2'); Connector.AddParam('add_2', '3'); Connector.Execute; Connector.DoRecList(0); Connector.DoFieldList(0); Edit3.Text := Connector.GetField(0); except on E: Exception do Memo1.Text := E.Message; end; |
Теперь вызовем arProgress из ExampleFiveRecord чтобы получить и обработать одномерный массив.
| Memo2.Text := ''; lbRes.Items.Clear; try Connector.Module := 'ExampleFiveRecord'; Connector.Func := 'arProgress'; Connector.AddParam('add_1', '10'); Connector.AddParam('add_2', '3'); Connector.AddParam('add_3', '5'); Connector.Execute; Connector.DoRecList; Connector.DoFieldList; for i := 0 to Connector.FieldCount - 1 do lbRes.Items.Add(Connector.GetField(i)); except on E: Exception do Memo2.Text := E.Message; end; |
И последний пример - возврат таблицы. Для этого нужно вызвать arTable из ExampleFiveSet:
| Memo3.Text := ''; grid.RowCount := 2; grid.ColCount := 2; try Connector.Module := 'ExampleFiveSet'; Connector.Func := 'arTable'; Connector.AddParam('add_1', '5'); Connector.AddParam('add_2', '6'); Connector.Execute; Connector.DoRecList; grid.RowCount := Connector.RecCount ; Connector.DoFieldList; grid.ColCount := Connector.FieldCount ; for j := 0 to Connector.RecCount - 1 do begin Connector.DoFieldList(j); for i := 0 to Connector.FieldCount - 1 do begin grid.Cells[i , j ] := Connector.GetField(i); end; end; except on E: Exception do Memo3.Text := E.Message; end; |
Пример №6
для сервера:
- Сервер MySQL - можно взять с сайта производителя www.mysql.com или у нас
- DBI - универсальный коннектор баз данных для PERL. Можно взять с нашего сайта или найти на CPAN.
- Msql-Mysql-module - драйвер для доступа из DBI к Msql и Mysql серверам. Находится у нас или на .
- RXLib - хорошо известная библиотека для Delphi. В этом примере мы используем из нее только TRxMemoryData - таблицу, хранимую в памяти. Можно взять c .
Установка MySQL хорошо описана во многих статьях. Но если вам не хочется изучать всю документацию мы приведем пример быстрой установки.
Для начала создадим пользователя и группу от имени которых будет работать демон сервера MySQL:
| root> groupadd mysql root> useradd -g mysql mysql |
| root> cd /usr/local root> tar -zxvf /your/path/to/file/mysql-standard-4.0.17-pc-linux-i686.tar.gz |
| root> ln -s ./mysql-standard-4.0.17-pc-linux-i686 ./mysql |
| root> cd ./mysql |
| root> ./scripts/mysql_install_db --user=mysql |
| root> chown -R root . root> chown -R mysql data root> chgrp -R mysql . |
| root> ./bin/mysqld_safe --user=mysql --default-character-set=cp1251 & |
Установка DBI. Разархивируем дистрибутивный файл и войдем в каталог:
| root> tar -zxvf ./DBI-1.42.tar.gz root> cd ./DBI-1.42 |
| root> perl ./Makefile.PL root> make root> make test root> make install |
Установка Msql-Mysql-module. Аналогично DBI разархивируем и входим в каталог с исходниками:
| root> tar -zxvf ./Msql-Mysql-modules-1.2219.tar.gz root> cd ./Msql-Mysql-modules-1.2219 |
| root> perl ./Makefile.PL Enter the appropriate number: [3] 1 Do you want to install the MysqlPerl emulation? You might keep your old Mysql module (to be distinguished from DBD::mysql!) if you are concerned about compatibility to existing applications! [n] n Where is your MySQL installed? Please tell me the directory that contains the subdir 'include'. [/usr/local/mysql] /usr/local/mysql Which database should I use for testing the MySQL drivers? [test] test On which host is database test running (hostname, ip address or host:port) [localhost] localhost User name for connecting to database test? [undef] root Password for connecting to database test? [undef] root> make root> make test root> make install |
Создадим базу данных и одну таблицу для наших экспериментов:
Открываем новую базу и заходим в нее:
| root> /usr/local/mysql/bin/mysql mysql> create database example; Query OK, 1 row affected (0.00 sec) mysql> use example; Database changed |
| mysql> CREATE TABLE tovar( -> tv_id int(8) NOT NULL auto_increment, -> tv_kod varchar(10) NOT NULL default '', -> tv_name varchar(150) NOT NULL default '', -> tv_cena decimal(12,2) NOT NULL default '0.00', -> PRIMARY KEY (tv_id), -> FULLTEXT KEY tv_name_idx (tv_name)); Query OK, 0 rows affected (0.02 sec) |
| mysql> \. ~/tovar.sql Query OK, 430 rows affected (0.05 sec) Records: 430 Duplicates: 0 Warnings: 0 |
| mysql> GRANT SELECT,INSERT,UPDATE,DELETE -> ON example.* -> TO example_user@localhost -> IDENTIFIED BY 'example_password'; Query OK, 0 rows affected (0.01 sec) mysql> FLUSH PRIVILEGES; Query OK, 0 rows affected (0.01 sec) |
Копируем файл ExampleFive.pm в ExampleSix.pm. ExampleSix практически не изменится, необходимо только поменять в заголовке имя пакета и вызов use:
| package ThreeTier::ExampleSix; use strict; use warnings; use ThreeTier::ExampleSixGlobal; |
| package ThreeTier::ExampleSixGlobal; use strict; use warnings; use DBI; use Exporter; .................................. @EXPORT = qw($d_fld $d_rec $d_set $strRespond &callUserFunction &requestParser &addEmptySet &addField &addRecord &addSet &sendError &sendOK &isDecimal &isInt &getSQLResult); |
| sub getSQLResult { my $sql = shift; my $dbh; unless( eval { $dbh = DBI->connect("DBI:mysql:example:localhost", 'example_user','example_password', {PrintError=>0, RaiseError=>0 }) or sendError("Ошибка соединения с БД: ".$DBI::errstr); } ) { sendError("Ошибка соединения с БД : $@"); }; my $sth = $dbh->prepare($sql) or sendError('Ошибка при подготовке запроса:' . $DBI::errstr); $sth->execute or sendError('Ошибка SQL сервера : ' . $DBI::errstr); my $res = $sth->fetchall_arrayref; $sth->finish; return $res; } |
Теперь все готово для того чтобы написать небольшую функцию, для выборки информации из таблицы по критерию, заданному клиентом. Создадим еще один файл ExampleSixTable.pm и определим в нем единственную функцию getTovar:
| package ThreeTier::ExampleSixTable; use strict; use warnings; use ThreeTier::ExampleSixGlobal; sub getTovar { my $args = shift; my $table = getSQLResult(qq[SELECT tv_id,tv_kod,tv_name,tv_cena FROM tovar WHERE tv_name LIKE '%$args->{pattern}%' ORDER BY tv_name]); addSet($table); return 1; } 1; |
Добавляем в конец httpd.conf строки:
SetHandler perl-script PerlHandler ThreeTier::ExampleSix |
| root> /usr/local/apache/bin/apachectl restart |
Установка RxLib хорошо описана в поставляемом с пакетом файле документации и обычно не вызывает затруднений.
Начнем модификацию объекта TConnector так, чтобы он мог преобразовывать принимаемые от сервера поля в различные типы данных. Создадим несколько дополнительных функций, ядром которых останется описанная в предыдущем примере GetField:
- GetFieldAsString - возвращает поле как строку
- GetFieldAsInteger - возвращает целое
- GetFieldAsDouble - возвращает число с плавающей запятой
| function TConnector.GetFieldAsString(FieldNumber : Integer = 0) : String; begin GetFieldAsString := GetField(FieldNumber); end; |
| function TConnector.GetFieldAsInteger(FieldNumber : Integer = 0) : Integer; begin GetFieldAsString := StrToInt(GetField(FieldNumber)); end; |
| function TConnector.GetFieldAsDouble(FieldNumber : Integer = 0) : Double; var s,tmpStr:String; begin s := GetField(FieldNumber); if (DecimalSeparator <> '.') and (Pos('.',s) <> 0) then tmpStr:=StringReplace(s,'.',DecimalSeparator,[rfReplaceAll]) else tmpStr:=s; GetFieldAsDouble := StrToFloat(tmpStr); end; |
| procedure TConnector.ParseToDataSet(ADataSet : TDataSet; SetNumber : Integer = 0); var i,j, RecNum, FieldNum : integer; begin ADataSet.First; while not ADataSet.Eof do ADataSet.Delete; RecNum := DoRecList(SetNumber); for i := 0 to RecNum - 1 do begin DoFieldList(i); AddRecordToTable(ADataSet); end; ADataSet.First; end; |
| procedure TConnector.AddRecordToTable(ATable : TDataSet); var i : integer; begin ATable.Append; for i := 0 to ATable.Fields.Count - 1 do begin if ATable.Fields[i].Calculated then continue; if ATable.Fields[i].DataType = ftString then begin ATable.Fields[i].AsString := GetFieldAsString(i); continue; end; if ATable.Fields[i].DataType = ftInteger then begin ATable.Fields[i].AsInteger := GetFieldAsInteger(i); continue; end; if ATable.Fields[i].DataType = ftFloat then begin ATable.Fields[i].AsFloat := GetFieldAsDouble(i); continue; end; end; ATable.Post; end; |
Во первых создадим временную таблицу в памяти:
| tv_table: TRxMemoryData; ......................................... tv_table := TRxMemoryData.Create(Self); |
| tv_table.FieldDefs.Add('tv_id', ftInteger); tv_table.FieldDefs.Add('tv_kod', ftString, 10); tv_table.FieldDefs.Add('tv_name', ftString, 150); tv_table.FieldDefs.Add('tv_cena', ftFloat); |
| tv_table.Active := true; |
| tv_DS: TDataSource; ........................................ tv_DS := TDataSource.Create(Self); tv_DS.DataSet := tv_table; |
| tv_grid: TDBGrid; ........................................ var tmpColumn : TColumn; ........................................ tv_grid := TDBGrid.Create(Self); tv_grid.Top := 90; tv_grid.Left := 10; tv_grid.Height := 200; tv_grid.Width := 700; tmpColumn := tv_grid.Columns.Add; tmpColumn.FieldName := 'tv_id'; tmpColumn.Width := 50; tmpColumn := tv_grid.Columns.Add; tmpColumn.FieldName := 'tv_kod'; tmpColumn.Width := 50; tmpColumn := tv_grid.Columns.Add; tmpColumn.FieldName := 'tv_name'; tmpColumn.Width := 500; tmpColumn := tv_grid.Columns.Add; tmpColumn.FieldName := 'tv_cena'; tmpColumn.Width := 60; tv_grid.DataSource := tv_DS; tv_grid.Parent := Self; |
| Memo1.Text := ''; try Connector.Module := 'ExampleSixTable'; Connector.Func := 'getTable'; Connector.AddParam('pattern', '7200'); tmpCursor := Screen.Cursor; Screen.Cursor := crHourGlass; Connector.Execute; Connector.ParseToDataSource(tv_DS); except on E: Exception do Memo1.Text := E.Message; end; Screen.Cursor := tmpCursor; |
Пример №7
Как последний пример позволим клиенту не только просматривать, но и изменять данные на сервере - добавлять новые товары, редактировать и удалять существующие.
| package ThreeTier::ExampleSeven; use strict; use warnings; use ThreeTier::ExampleSevenGlobal; |
| package ThreeTier::ExampleSevenGlobal; use strict; use warnings; use DBI; use Exporter; use vars qw(@ISA @EXPORT $d_fld $d_rec $d_set $strRespond $dbh); @ISA = qw(Exporter); @EXPORT = qw($d_fld $d_rec $d_set $strRespond &callUserFunction &requestParser &addEmptySet &addField &addRecord &addSet &sendError &sendOK &isDecimal &isInt &getSQLResult &execSQL); |
| sub execSQL { my $sql = shift; $dbh->do($sql) or sendError($DBI::errstr . $sql); } |
Скопируем ExampleSixTable.pm в ExampleSevenTable.pm и как обычно изменим имя пакета и директиву use:
| package ThreeTier::ExampleSevenTable; use strict; use warnings; use ThreeTier::ExampleSevenGlobal; |
| sub getTovar { my $args = shift; my $table = getSQLResult(qq[SELECT tv_id,tv_kod,tv_name,tv_cena FROM tovar ORDER BY tv_name]); addSet($table); return 1; } |
- addTovar
- editTovar
- delTovar
| sub addTovar { my $args = shift; my $sql = qq[INSERT INTO tovar (tv_kod,tv_name,tv_cena) VALUES ('$args->{kod}','$args->{name}','$args->{cena}')]; execSQL($sql); my $ident = getSQLResult("SELECT LAST_INSERT_ID()")->[0]->[0]; getTovar($args); addField($ident); } |
| INSERT INTO tovar (tv_kod,tv_name,tv_cena) VALUES('56-76','Новый товар','12.50') |
Функция editTovar позволяет менять все поля в таблице tovar кроме индексного:
| sub editTovar { my $args = shift; my $sql = qq[UPDATE tovar SET tv_kod = '$args->{kod}', tv_name = '$args->{name}', tv_cena = '$args->{cena}' WHERE tv_id = '$args->{id}']; execSQL($sql); getTovar($args); } |
Функция delTovar удаляет строку из таблицы по индексному полю:
| sub delTovar { my $args = shift; my $sql = qq[DELETE FROM tovar WHERE tv_id = '$args->{id}']; execSQL($sql); getTovar($args); } |
Добавляем в httpd.conf строки:
SetHandler perl-script PerlHandler ThreeTier::ExampleSeven |
| root> /usr/local/apache/bin/apachectl restart |
Создадим сервисные процедуры для того чтобы не приходилось каждый раз преобразовывать различные типы в строку при формировании запросов к серверу (конечно в реальной программе их должно быть больше, например для даты/времени, логического значения и т.д.):
- procedure AddParam(AName,AValue : String); overload;
- procedure AddParam(AName : String; AValue : Integer); overload;
- procedure AddParam(AName : String; AValue : Double); overload;
Функция AddParam(String,Integer):
| procedure TConnector.AddParam(AName : String;AValue : Integer); var s : String; begin Str(AValue, s); AddParam(AName,s); end; |
Функция AddParam(String,Double) чуть посложнее:
| procedure TConnector.AddParam(AName : String;AValue : Double); var s : String; begin s := FloatToStr(AValue); if DecimalSeparator<> '.' then AddParam(AName,StringReplace(s,DecimalSeparator,'.',[rfReplaceAll]); else AddParam(AName,s); end; |
Рассмотрим добавление записи в нашу таблицу на стороне клиента:
| Memo1.Text := ''; try Connector.Module := 'ExampleSevenTable'; Connector.Func := 'addTovar'; Connector.AddParam('kod', '56-76'); Connector.AddParam('name', 'Новый товар'); Connector.AddParam('cena', 12.50); tmpCursor := Screen.Cursor; Screen.Cursor := crHourGlass; Connector.Execute; Connector.ParseToDataSet(tv_table); Connector.DoRecList(1); Connector.DoFieldList(0); id := Connector.GetFieldAsInteger(0); tv_table.Locate('tv_id', id, []); except on E: Exception do Memo1.Text := E.Message; end; Screen.Cursor := tmpCursor; |
Операция модификации несколько проще:
| Memo1.Text := ''; try Connector.Module := 'ExampleSevenTable'; Connector.Func := 'editTovar'; Connector.AddParam('kod', '56-77'); Connector.AddParam('name', 'Старый товар'); Connector.AddParam('cena', 12.70); Connector.AddParam('id', 7777456); tmpCursor := Screen.Cursor; Screen.Cursor := crHourGlass; Connector.Execute; Connector.ParseToDataSource(tv_DS); tv_table.Locate('tv_id', 7777456, []); except on E: Exception do Memo1.Text := E.Message; end; Screen.Cursor := tmpCursor; |
Удаление тоже не представляет сложностей:
| Memo1.Text := ''; try Connector.Module := 'ExampleSevenTable'; Connector.Func := 'delTovar'; Connector.AddParam('id', 7777456); tmpCursor := Screen.Cursor; Screen.Cursor := crHourGlass; Connector.Execute; Connector.ParseToDataSource(tv_DS); if tv_table.RecordCount < (oldPos) then tv_table.RecNo := tv_table.RecordCount else tv_table.RecNo := oldPos ; except on E: Exception do Memo1.Text := E.Message; end; Screen.Cursor := tmpCursor; |
Эта статья показала как можно использовать Apache не только как WEB сервер, но и как сервер приложений. Можно сказать что подобные результаты можно получить с любым WEB сервером, поддерживающим языковые расширения. С Apache можно использовать Java или PHP, mod_python или mod_mono, CGI или FastSGI. Клиентские средства тоже могут быть какими угодно - главное чтобы они могли поддерживать сокет IP.
Осталось еще множество нерешенных вопросов и проблем - например сжатие передаваемой информации, ее шифрация и защита, аутентификация, разделение прав доступа. Конечно эти проблемы можно частично решить используя протокол https, но это сразу приведет к существенному падению быстродействия. Используемый в этих примерах компонент IdHTTP также далек от идеала с точки зрения минимизации количества пересылаемых пакетов - например вместо стандартных 8 пакетов без KeepAlive и 4 с KeepAlive он пересылает соответственно 24 и 14. На медленных или спутниковых каналах это приводит к значительным задержкам. Потому стоит поэкспериментировать с другими свободными или платными продуктами.
Хотелось бы ввести полную автоматическую сериализацию и десериализацию передаваемых данных, но это снова приводит к увеличению трафика. Так что до совершенства еще очень далеко.
P.S. Если вы найдете какие-нибудь ошибки, неточности, или у вас появятся вопросы или предложения - пишите на почту info@bizonlie.ru