RDM/2 The Russian Electronic Developer Magazine  
RDM/2 Русский электронный журнал разработчика  
ДомойОт редактораПишите намОбратная связьRU/2

Web технологии. Программирование ICAPI для ICS. Часть 1

В этих записках не собираюсь учить кого-либо программированию или открывать Америку. Мало того, понимаю, что часть из того, о чём будет идти, лежит пока за пределами моих знаний. Но, тем не менее, мне показалось, что некоторые использованные мной на практике приёмы помогут программирующим под OS/2.

Речь пойдёт о создании распределённых приложений с использованием IBM Internet Connection Server (Lotus Go Server, Web Traffic Express) и языка Java. Точнее, о том, как написать на С серверную часть, а клиентскую на Java.

Начнём с более сложной, на мой взгляд, части - серверной. Я для себя остановился на ICAPI, программном интерфейсе ICS. Такой подход позволяет сосредоточится на разработке алгоритмов приложения, т.к. заботу о многопоточности, распределении памяти, вводе/выводе на себя берёт ICS. Кроме того, производительность будет гораздо выше, чем при использовании CGI.

В работе ICS выделены следующие шаги:

  1. Инициализация сервера - Server Initialisation (здесь и далее терминологи из документации на ICS). Выполняется один раз при запуске сервера или по команде Restart.

  2. Обработка запроса состоит из:

    1. Шаг PreExit. Первый шаг при обработке запроса, сервер уже получил запрос, но ещё не знает, что с ним делать.

    2. Authentication - шаг авторизации. Как сказано в документации : разбор, проверка и сохранение ключей шифрования, доступа и т.п. Сам не пользовался, поэтому комментировать не буду.

    3. Name Translation - на этом шаге определется какой файл соответствует запросу.

    4. Authorization - так и есть - авторизация. Проверка полномочий запрашивающего на данный объект сервера.

    5. ObjectType - разбор типа объекта (текст, картинка, звук и т.д.) и определение его местоположения в файловой системе.

    6. Service - отправка объекта пользователю или выполнение каких-либо других полезных действий. Очень удобное место, чтобы поместить сюда основной алгоритм прикладной программы (вы ещё не забыли зачем мы этим занимаемся?)

    7. Data Filter - доступ к выходному потоку данных, который направлется сервером клиенту. Очень удобно перехватывать информацию, уходящую с сервера.

    8. Log - в документации - позволяет регистрировать транзакции.

    9. Error - обработка ошибок.

    10. PostExit - шаг "подчистки" за собой ресурсов, выделенных в процессе обработки запроса. Правда, как это сделать, реально не совсем понтяно, что-то IBM-еры здесь темнят и не договаривают. На этом обработка запроса заканчивается.

  3. Server Termination - выполнется один раз при отключении или рестарте сервера. Здесь можно закрыть (если таковое были) файлы и освободить память, остановить дополнительные нити.

Программно все эти шаги реализованы в виде хуков в определённые места серверного кода. Каждому хуку соответствует директива из файла конфигурации (httpd.cnf). Хук - это функци на С (С++) скомпилированная, экспортированная и помещённая в dll-ку. На каждый шаг может быть "навешано" несколько хуков. Первым управление получит тот, который первым встретится в директивах конфигурации. А вопрос получит ли следующий хук управление? - решается кодом возврата (мне правда кажется, что не на всех шагах), если код HTTP_OK, значит функция-хук успешно обработала запрос на своем шаге и никаких дополнительных действий не требуется, другие хуки (даже стандартный обработчик) вызываться не будут. Если же код HTTP_NOACTION, то следующий хук получит управление по цепочке. Если же в цепочке хуков ни один не вернул код HTTP_OK, будет выполнена стандартная процедура обработки. Более конкретно эти вопросы надо рассматривать на каждом отдельном шаге. Кстати, коды возврата функций-хуков имеют префикс "HTTP_".

При написании своих обработчиков, в код необходимо включать файл . В нем описаны коды возврата и шаблоны стандартных функций ICAPI (об этом немного позже). Кроме того, при линковке необходимо использовать HTTPDAPI.LIB из поставки ICS (WTE).

Я для работы использую IBM VA C++ 3.0 for OS/2, поэтому все примеры будут для него.

Итак, любая функция обработчик должна выглядеть примерно так:

#include ...                // нужные вам H-файлы
#include <HTAPI.H>          // это файл из поставки ICS

void    HTTPD_LINKAGE Some_MyFunc (
            unsigned char *h, long *rcode )
{

...
*rcode = HTTP_OK;           // код возврата обязателен
}
Конкретный набор параметров обработчика зависит от шага (см.выше) для которого вы его пишите. В принципе, нужные шаблоны описаны в документации на ICS, но я думаю не будет лишним привести его здесь.
void HTTPD_LINKAGE ServerInit (
    unsigned char *handle, unsigned long *maj_ver,
    unsigned long *min_ver, long *rcode );
Обратите внимание на параметр handle, его назначение примерно такое же как и у файлового handle, но мне не удалось найти указаний на то, что этот handle связан с конкретным сеансом или хотя бы запросом (т.е. он уникален для каждого запроса и не меняет своего значения на разных шагах обработки), поэтому я побоялся использовать его для идентификации.
void HTTPD_LINKAGE PreExit (
    unsigned char *handle, long *rcode );

void HTTPD_LINKAGE Authentication (
    unsigned char *handle, long *rcode );

void HTTPD_LINKAGE NameTrans (
    unsigned char *handle, long *rcode );

void HTTPD_LINKAGE Authorization (
    unsigned char *handle, long *rcode );

void HTTPD_LINKAGE ObjectType (
    unsigned char *handle, long *rcode );

void HTTPD_LINKAGE Service (
    unsigned char *handle, long *rcode );

    А для шага DataFilter можно задать три функции. Первая:

void HTTPD_LINKAGE Open (
    unsigned char *handle, long *rcode );
Вызывается один раз перед началом отправки данных сервером клиенту (причем, что не описано в документации, вызавается до шага PreExit, т.е. сначала идет Open, и только затем PreExit).

Вторая вызывается столько раз, сколько нужно чтобы передать все данные в ответ на запрос (понятно, что весь документ может не поместиться в буфер сервера). И параметров у этой функции больше, ей передается указатель на буфер с данными и указатель на переменную содержащую размер этих данных. Хочу предостеречь вас от прямой модификации значений этих переменных, так как вы рискуете налететь на "Memory protection violation".

void HTTPD_LINKAGE Write (
    unsigned char *handle, unsigned char *data,
    unsigned long *datalength, long *rcode );
И третья функция предназначена для "закрытия" выходного потока.
void HTTPD_LINKAGE Close (
    unsigned char *handle, long *rcode );
Далее...
void HTTPD_LINKAGE Log (
    unsigned char *handle, long *rcode );

void HTTPD_LINKAGE Error (
    unsigned char *handle, long *rcode );

void HTTPD_LINKAGE PostExit (
    unsigned char *handle, long *rcode );

void HTTPD_LINKAGE ServerTerm (
    unsigned char *handle, long *rcode );
Имена функций даны условно, для иллюстрации к какому шагу относится тот или иной шаблон. Вы будете давать свои имена.

Скорее всего у вас уже возникли вопросы, да действительно, информации передаваемой через параметры функций во многих случаях недостаточно. Недостающее можно в ICAPI получить через переменные сервера, назовем из именованными переменными (или переменными окружения, в оригинале) чтобы отличать от переменных языка C. Набор именованных переменныех в ICAPI и CGI для ICS совпадают. Т.е. вы можете получить значение такой переменной CGI как "REMOTE_ADDR" (IP адрес клиента). Здесь надо обратить внимание на то, что не на всех шагах обработки запроса доступны все переменные. Так, например, "PATH_TRANSLATED" на шаге PreExit еще не определена, просто потому, что сервер еще не выполнил необходимых для этого действий и отдал управление вашей функции. Описания, конкретно, какие переменные, когда действительны я не нашел, но мне пока помогал здравый смысл.

Для работы с переменными, а также для выполнения других полезных действий интерфейс ICAPI предоставляет в ваше распоряжение набор стандартных функций и макросов. Я приведу перевод части документации связанной с этим вопросом.

  1. Проверяет имя пользователя и пароль можно использовать только на шагах PreExit и Authorization.
    void HTTPD_authenticate (
            unsigned char *handle,
            long *rcode );
    

  2. Получить значение именованной переменной (см.выше).
    void HTTPD_extract (
            unsigned char *handle,
            unsigned char *name,    // именованая переменная
            unsigned long *name_length,// длина имени переменной
            unsigned char *value,   // буфер под значение
            unsigned long *val_length,// размер буфера под значение
            long *rcode );
    
    Возникает вопрос, как определить размер буфера, здесь используется следующий прием :
    {
        unsigned char Buf [2];
        unsigned long BufSize;
        unsigned char *Name;
        unsigned long NameSize;
        unsigned char *BigBuf;
        long    RetCode;
    
    Name = "REMOTE_ADDR";
    NameSize = strlen(Name);
    BufSize = sizeof(Buf);
    HTTPD_extract( handle,Name,&NameSize,Buf,&BufSize,&RetCode);
    if ( RetCode == HTTPD_BUFFER_TOO_SMALL )
        {
        if ( (BigBuf=malloc(BufSize+1)) != NULL )
           HTTPD_extract(
                  handle,Name,&NameSize,Buf,&BufSize,&RetCode);
        }
    
    То есть, если после первого вызова получили код ошибки - "буфер мал", то в переменную BufSize ICS помещает нужный размер буфера, после чего можно смело его динамически выделять и повторять попытку извлечения значения именованной переменной.

  3. Установить значение именованной переменной (например, чтобы передать ее CGI-программе). Кроме того, можно создавать свои переменные (нестандартные). При этом действует соглашение, если имя перменной начинается на "HTTP_", то она и ее значение будут включены в заголовок ответа (но уже без префикса "HTTP_").
    void HTTPD_set (
            unsigned char *handle,
            unsigned char *name,    // именованая переменная
            unsigned long *name_length,// длина имени переменной
            unsigned char *value,   // буфер значения
            unsigned long *val_length,// размер буфера значение
            long *rcode );
    

  4. Отправить клиенту файл. Можно использовать только на шаге Service.
    void HTTPD_file (
        unsigned char *handle,
        unsigned char *Filename,
        unsigned long *Filename_length,
        long *rcode );
    

  5. Выполнить CGI-скрипт для удовлетворения запроса. Можно использовать только на шагах PreExit и Service.
    void HTTPD_exec (
        unsigned char *handle,
        unsigned char *Progname,
        unsigned long *Progname_length,
        long *rcode );
    

  6. Прочитать клиентский запрос. Используется для получения заголовков. Можно использовать только на шагах PreExit и Service.
    void HTTPD_read (
        unsigned char *handle,
        unsigned char *value,
        unsigned long *value_length,
        long *rcode );
    
    От себя добавлю, что с длиной буфера дело обстоит также как и в функции HTTPD_extract.

  7. Записать данные в выходной поток. Допустима на шагах Service и DataFilter. Если не установить с помощью HTTPD_set content-type, то сервер считает что данные отправляются GCI-скриптом.
    void HTTPD_write (
        unsigned char *handle,
        unsigned char *data,
        unsigned long *data_length,
        long *rcode );
    

  8. Записать сообщение об ошибке в файл ошибок (если разрешено в настройках, то текст вообщений будет выведен и в окно ErrorLog сервера). Перевод строки в конце текста не обязателен :-)
    void HTTPD_log_error (
        unsigned char *handle,
        unsigned char *text,
        unsigned long *text_length,
        long *rcode );
    

  9. Записать сообщение об ошибке в файл протокола (если разрешено в настройках, то текст вообщений будет выведен и в окно TraceLog сервера).
    void HTTPD_log_trace (
        unsigned char *handle,
        unsigned char *text,
        unsigned long *text_length,
        long *rcode );
    

  10. Перезапустить сервер.
    void HTTPD_restart ( long *rcode );
    
Стандартные функции также возвращают коды ошибок, эти коды следует отличать от тех, что должны возвращать ваши обработчики. Следите за тем, чтобы код возврата стандартной функции не попал выше (будете долго искать ошибку). Коды приведены в и имеют префикс "HTTPD_".

Еще одно замечание. Постарайтесь избегать статических и глобальных переменных (помните, сервер - существо многопоточное, нужны блокировки), обработчики должны быть реентерабельными. И последнее - не злоупотребляйте стеком, большой стек приводит к большим трапам :-)

В качестве примера рассмотрим часть кода RHTTP-перекодировщика "на-лету". Код приведен не полностью (интересующиеся смогут забрать текущий вариант со всеми потрохами на ftp://cbs-edu.chel.su/pub/OS2/RHTTP, когда я доделаю программу).

Необходимо :

  1. проинициализировать подсистему (ServerInit)
    void HTTPD_LINKAGE  RHTTP_Init (unsigned char *handle,
        unsigned long *major_version,
        unsigned long *minor_version,
        long *rcode )
    {
        char     Buf [256];
        unsigned long length;
        unsigned char *serverROOT;
    
    ...
    /*
        Теперь необходимо определить каталог, в который помещена программа
    */
    if ( (serverROOT=RHTTP_GetCGIvar(handle,SERVER_ROOT)) != NULL )
        {                           // я сделал свою функцию для
                                    // получения значений  именованых
                                    // переменных.
        ...
        }
    ...
    *rcode = HTTP_OK;		// нужно вернуть именно HTTP_OK,
    }				// иначе это будет ошибкой инициализации
    
    // сервера с вытекающими последствиями.

  2. Фильтровать данные (DataFilter). Нужно определить три функции.
    void HTTPD_LINKAGE RHTTP_Open ( unsigned char *handle, long *rcode )
    {
           unsigned char *remoteADDR = NULL;
           unsigned char *querySTRING = NULL;
           unsigned char *userAGENT  = NULL;
           unsigned char CODEPAGE [_MAX_CPAGENAME];
           RHTTP_User volatile *UserPtr = NULL;
           short volatile Codepage;
    
    /*
       Необходимо выделить адрес запрашивающего и тип его броузера.
       Это переменные REMOTE_ADDR и HTTP_USER_AGENT
    */
    if( (remoteADDR=RHTTP_GetCGIvar(handle,REMOTE_ADDR)) != NULL &&
        (userAGENT=RHTTP_GetCGIvar(handle,HTTP_USER_AGENT)) != NULL )
        {                           // нормализуем имя агента пользователя и поищем
        if ( (UserPtr=RHTTP_FindUser(remoteADDR,userAGENT)) == NULL )
            {                       // если не найдем, то добавим в таблицу, рано
            if ( (UserPtr=RHTTP_AddUser(remoteADDR,userAGENT)) == NULL )
                {                   // или поздно он к нам прийдет
                RHTTP_LogError(handle,Err_NotEnoughMemory);
                }
            }
        else
            {                       // если нашли, то сбросим исходную
            DosEnterCritSec();      // кодировку для пользователя
            UserPtr->SourceCP = SourceCodepage;
            DosExitCritSec();
            }                       // проверим, есть ли запрос на
                                    // смену кодовой страницы, для этого выделим
        if ( UserPtr != NULL )
            {
            if ( (querySTRING=RHTTP_GetCGIvar(handle,QUERY_STRING)) != NULL )
                {                   // пользовательский запрос, затем поищем
                CODEPAGE [0] = '\0';// в нем переменную на смену кодовой стр.
                RHTTP_VarValue ( querySTRING, CGI_Codepage,
                                             CODEPAGE, sizeof(CODEPAGE)-1 );
                if ( CODEPAGE [0] )
                    {
                    Codepage = RHTTP_GetCodepage(CODEPAGE);
                    DosEnterCritSec();
                    UserPtr->Codepage = Codepage;
                    DosExitCritSec();
                    }
                }
            RHTTP_SetContentEncoding ( handle, UserPtr->Codepage );
            }
        }
    if ( remoteADDR != NULL )
        free ( remoteADDR );
    if ( querySTRING != NULL )
        free ( querySTRING );
    if ( userAGENT != NULL )
        free ( userAGENT );
    *rcode = HTTP_OK;
    }
    
    Обратите внимание, эта функция должна вернуть HTTP_OK, иначе оставшиеся две ваших функции будут проигнорированы, т.е. считается что у вас произошла ошибка при открытии и смысла вызывать остальные функции нет.
    void HTTPD_LINKAGE RHTTP_Close ( unsigned char *handle, long *rcode )
    {
    *rcode = HTTP_NOACTION;		// по закрытию соединения ничего не делаем
    }				// о чем и сообщаем соответствующим кодом.
    
    
    
    void HTTPD_LINKAGE RHTTP_Write ( unsigned char *handle,
                            unsigned char *data, unsigned long *datalength,
                            long *rcode )
    {
           RHTTP_User volatile *UserPtr = NULL;
           unsigned char *remoteADDR = NULL;
           unsigned char *userAGENT  = NULL;
           unsigned char *pathTRANSLATED = NULL;
    
    /*
       Необходимо выделить адрес запрашивающего и тип его броузера.
       Это переменные REMOTE_ADDR и HTTP_USER_AGENT, точно также как
       и на шаге Open.
    */
    ...
    *rcode = HTTP_OK;
    if( (remoteADDR=RHTTP_GetCGIvar(handle,REMOTE_ADDR)) != NULL &&
        (userAGENT=RHTTP_GetCGIvar(handle,HTTP_USER_AGENT)) != NULL )
        {                         // нормализуем имя агента пользователя и поищем
        UserPtr=RHTTP_FindUser(remoteADDR,userAGENT);
        }
    /*
       Выделим запрашиваемый URL.
    */
    if ( (pathTRANSLATED=RHTTP_GetCGIvar(handle,PATH_TRANSLATED)) == NULL &&
         (pathTRANSLATED=RHTTP_GetCGIvar(handle,DOCUMENT_URL)) == NULL )
        {                           // если были ошибки при выделении, то
        HTTPD_write ( handle, data, datalength, rcode );
        *rcode = HTTP_OK;           // просто отдадим данные
        }
    else
        {
        if ( RHTTP_GetDataType(pathTRANSLATED) == _CONTENT_TEXT )
            {                       // проверим тип данных, обрабатывать
                                    // будем только текстовые данные
            if ( !RHTTP_CheckSite(pathTRANSLATED) || UserPtr == NULL )
                {                   // проверим, надо ли перекодировать данный
                                    // источник или нет, если не надо, то
                HTTPD_write ( handle, data, datalength, rcode );
                *rcode = HTTP_OK;   // просто отдадим данные
                }
            else
                {
                if ( UserPtr->SourceCP != UserPtr->Codepage )
                    RHTTP_Translate ( data, *datalength,
                                    To866_Codepages[UserPtr->SourceCP],
                                    From866_Codepages[UserPtr->Codepage]);
                RHTTP_write ( handle, data, datalength, rcode );
                *rcode = HTTP_CREATED;
                }
            }
        else                        // см. выше
            {                       // если тип данных двоичный, просто отдадим
            HTTPD_write ( handle, data, datalength, rcode );
            *rcode = HTTP_OK;
            }
        }
    if ( remoteADDR != NULL )
        free ( remoteADDR );
    if ( userAGENT != NULL )
        free ( userAGENT );
    if ( pathTRANSLATED != NULL )
        free ( pathTRANSLATED );
    *rcode = HTTP_OK;
    }
    
Функцию для шага PreExit я опускаю, в ней нет ничего примечательного.

Для сборки DLL-ки нужен DEF-файл. В моем случае я воспользовался шаблоном IBM VA C++ и он выглядит так :

;========================================================================
; rhttp.def - Intermediate dynamic linking library definition file
;========================================================================
LIBRARY rhttp INITINSTANCE TERMINSTANCE
PROTMODE
DATA MULTIPLE NONSHARED READWRITE LOADONCALL
CODE LOADONCALL
EXPORTS
       RHTTP_Open
       RHTTP_Close
	   RHTTP_Write
	   RHTTP_Init
       RHTTP_PreExit
Для подключения полученных функций в файл httpd.cnf добавлены следующие строки (в версии WTE 1.1.2 следует имя DLL писать полностью):
ServerInit   E:\WWW\RHTTP\rhttp.dll:RHTTP_Init
DataFilter   E:\WWW\RHTTP\rhttp.dll:RHTTP_Open:RHTTP_Write:RHTTP_Close
PreExit      E:\WWW\RHTTP\rhttp.dll:RHTTP_PreExit
Несколько слов об отладке. Для отладки удобно пользоваться опцией сервера "Trace On". В этом случае в окно "TraceLog" сервера выводится достаточно подробная и разнообразная инфорамция о том что, в какой последовательности и как вызывается.

В заключение хочу привести полностью код функции получающей значение именованной переменной.

#define _MAX_BUF    2

unsigned char *RHTTP_GetCGIvar ( unsigned char *handle, CGI_Var Var )
{
      unsigned char *Buf;
      unsigned long  maxsize = _MAX_BUF;
      long     rcode = HTTPD_SUCCESS;

if ( (Buf=malloc(maxsize)) != NULL )     // размещаем буфер
    {                                    // если все ОК, выделяем переменную
    HTTPD_extract(handle,Var.name,&Var.size,Buf,&maxsize,&rcode);
    if ( rcode == HTTPD_BUFFER_TOO_SMALL )
        {                                // если ошибка размера буфера
        free ( Buf );                    // освободим занимаемую память
        if ( (Buf=malloc(maxsize+1)) != NULL )
            {                            // выделим новый, побольше
            HTTPD_extract(handle,Var.name,&Var.size,Buf,&maxsize,&rcode);
            Buf [maxsize] = '\0';        // и снова выделяем переменную
            }
        else
            {
            RHTTP_LogError(handle,Err_NotEnoughMemory);
            }
        }
    }
else
    {
    RHTTP_LogError(handle,Err_NotEnoughMemory);
    }
if ( rcode != HTTPD_SUCCESS && rcode != HTTPD_PARAMETER_ERROR )
    {
    RHTTP_LogErrorCode(handle,rcode);
    }
return ( Buf );
}

Это было описание серверной части, так сказать stand-alone. Про взаимодействие сервера и java-клиента немного позже.

Андрей Породько
Программирование ICAPI для ICS. Часть 2

---
Интересные ссылки:

---

---
Комментариев к странице: 0 | Добавить комментарий
---
Редактор: Дмитрий Бан
Оформление: Евгений Кулешов


(C) Russian Underground/2