Локальные сети персональных компьютеров. Использование протоколов IPX, SPX, NETBIOS© Александр Фролов, Григорий ФроловТом 4, М.: Диалог-МИФИ, 1993, 160 стр. 2.2. Работа с драйвером IPX/SPXПервое, что должна сделать программа, желающая работать в сети с протоколом IPX или SPX, - проверить, установлен ли драйвер соответствующего протокола. Затем необходимо получить адрес вызова этого драйвера - точку входа API (Application Programm Interface - интерфейс для приложений). В дальнейшем программа вызывает драйвер при помощи команды межсегментного вызова процедуры по адресу точки входа API драйвера IPX/SPX. 2.2.1. Точка входа API драйвера IPX/SPXДля того чтобы проверить, загружен ли драйвер IPX, необходимо загрузить в регистр AX значение 7A00h и вызвать мультиплексное прерывание INT 2Fh. Если после возврата из прерывания в регистре AL будет значение FFh, драйвер IPX загружен. Адрес точки входа для вызова API драйвера при этом будет находиться в регистровой паре ES:DI. Если же содержимое регистра AL после возврата из прерывания INT 2Fh будет отлично от FFh, драйвер IPX/SPX не загружен. Это означает, что на данной рабочей станции не загружены резидентные программы ipx.exe или ipxodi.exe, обеспечивающие API для работы с протоколами IPX и SPX. Для вызова API в регистр BX необходимо загрузить код выполняемой функции. Значения, загружаемые в другие регистры, зависят от выполняемой функции. Например, функция с кодом 10h используется для проверки присутствия в системе протокола SPX (может быть такая ситуация, когда протокол IPX присутствует, а SPX - нет). Для того, чтобы определить наличие SPX, необходимо загрузить в BX значение 10h, в AX значение 00h и вызвать API драйвера IPX. Если после возврата регистр AX будет содержать значение FFh, протокол SPX присутствует и может быть использован. В регистрах CX и DX передаются параметры SPX - максимальное число каналов связи, которое данная станция может установить с программами, работающими на других станциях, и количество каналов, доступное в настоящее время. О назначении этих параметров мы будем говорить в главе, посвященной протоколу SPX. Приведем текст программы, определяющей наличие драйвера протоколов IPX и SPX (листинг 1). Программа вызывает функции ipx_init() и ipxspx_entry(), тексты которых находятся в листинге 2. Текст сокращенного варианта include-файла ipx.h представлен в листинге 3. Вы можете попробовать запустить эту программу на рабочей станции сети Novell NetWare под управлением MS-DOS или на виртуальной машине MS Windows, работающей в расширенном (Enchanced) режиме. // =================================================== // Листинг 1. Программа для обнаружения драйвера // протокола IPX/SPX и определения его версии // // Файл ipxver.c // // (C) A. Frolov, 1993 // =================================================== #include <stdio.h> #include <stdlib.h> #include "ipx.h" void main(void) { // Точка входа в IPX/SPX API, переменная находится // в файле ipxdrv.asm и инициализируется функцией ipx_init(). extern far char *ipxspx_drv_entry; // Структура для вызова API IPX struct IPXSPX_REGS iregs; unsigned error; unsigned spx_ver; unsigned spx_max_connections, spx_avail_connections; printf("\n*Детектор IPX/SPX*, (C) Фролов А., 1993\n\n"); // Проверяем наличие драйвера IPX и определяем // адрес точки входа его API if(ipx_init() == 0xff) printf("IPX загружен! "); else { printf("IPX не загружен!\n"); exit(-1); } printf("Точка входа в IPX API - %Fp\n",ipxspx_drv_entry); // Проверяем доступность протокола SPX error = NO_ERRORS; // Вызываем функцию проверки доступности SPX // Здесь мы вызываем API драйвера IPX/SPX iregs.bx = SPX_CMD_INSTALL_CHECK; iregs.ax = 0; ipxspx_entry( (void far *)&iregs ); if(iregs.ax == 0x00) error = ERR_NO_SPX; if(iregs.ax != 0xff) error = UNKNOWN_ERROR; if(error != NO_ERRORS) { printf("SPX не загружен!\n"); exit(-1); } // Запоминаем параметры IPX/SPX spx_ver = iregs.bx; spx_max_connections = iregs.cx; spx_avail_connections = iregs.dx; printf("SPX загружен! "); printf("Версия SPX: %d.%d\n", (spx_ver>>8) & 0xff, spx_ver & 0xff); printf("Всего соединений: %d, ", spx_max_connections); printf("из них доступно: %d\n", spx_avail_connections); exit(0); } Далее расположен исходный текст модуля инициализации IPX (листинг 2). В этом модуле находится функция ipxspx_entry(), необходимая для вызова драйвера IPX/SPX. Ее имя начинается с символа "_", что необходимо для выполнения соглашения об именах в языке Си. Здесь же имеется функция ipx_init(), которая проверяет наличие драйвера в системе, получает адрес API драйвера и сохраняет его в области памяти _ipxspx_drv_entry. ; =================================================== ; Листинг 2. Инициализация и вызов драйвера IPX/SPX ; Файл ipxdrv.asm ; ; (C) A. Frolov, 1993 ; =================================================== .286 .MODEL SMALL ; --------------------------------------- ; Структура для вызова драйвера IPX/SPX ; --------------------------------------- IPXSPX_REGS struc rax dw ? rbx dw ? rcx dw ? rdx dw ? rsi dw ? rdi dw ? res dw ? IPXSPX_REGS ends .DATA ; Точка входа в драйвер IPX/SPX _ipxspx_drv_entry dd ? .CODE PUBLIC _ipxspx_entry, _ipx_init PUBLIC _ipxspx_drv_entry ; --------------------------------------- ; Процедура, вызывающая драйвер IPX/SPX ; --------------------------------------- _ipxspx_entry PROC FAR ; Готовим BP для адресации параметра функции push bp mov bp,sp ; Сохраняем регистры, так как драйвер IPX/SPX ; изменяет содержимое практически всех регистров push es push di push si push dx push cx push bx push ax ; Загружаем регистры из структуры, ; адрес которой передается как параметр push ds mov bx, [bp+6] ; смещение mov ds, [bp+8] ; сегмент mov es, ds:[bx].res mov di, ds:[bx].rdi mov si, ds:[bx].rsi mov dx, ds:[bx].rdx mov cx, ds:[bx].rcx mov ax, ds:[bx].rax mov bx, ds:[bx].rbx pop ds ; Вызываем драйвер IPX/SPX call [dword ptr _ipxspx_drv_entry] ; Сохраняем регистры push ds push dx mov dx, bx ; Записываем в структуру содержимое регистров после вызова драйвера mov bx, [bp+6] ; смещение mov ds, [bp+8] ; сегмент mov ds:[bx].rax, ax mov ds:[bx].rcx, cx mov ds:[bx].rbx, dx pop dx mov ds:[bx].rdx, dx pop ds ; Восстанавливаем регистры pop ax pop bx pop cx pop dx pop si pop di pop es pop bp retf _ipxspx_entry ENDP ; --------------------------------------------- ; Процедура инициализации драйвера IPX/SPX ; --------------------------------------------- _ipx_init PROC NEAR push bp mov bp,sp ; Определяем наличие драйвера в системе и его точку входа mov ax, 7a00h int 2fh ; Если драйвера нет, завершаем процедуру cmp al, 0ffh jne _ipx_init_exit ; Сохраняем адрес точки входа mov word ptr _ipxspx_drv_entry+2, es mov word ptr _ipxspx_drv_entry, di _ipx_init_exit: ; В регистре AX - код завершения процедуры mov ah, 0 pop bp ret _ipx_init ENDP end Описания типов и констант, а также прототипы функций для программы ipxver.c находятся в файле ipx.h (листинг 3). // =================================================== // Листинг 3. Include-файл для работы с IPX // Сокращенный вариант для программы ipxver.c // Файл ipx.h // // (C) A. Frolov, 1993 // =================================================== // ----------------------- // Команды интерфейса IPX // ----------------------- #define IPX_CMD_OPEN_SOCKET 0x00 #define IPX_CMD_CLOSE_SOCKET 0x01 #define IPX_CMD_GET_LOCAL_TARGET 0x02 #define IPX_CMD_SEND_PACKET 0x03 #define IPX_CMD_LISTEN_FOR_PACKET 0x04 #define IPX_CMD_SCHEDULE_IPX_EVENT 0x05 #define IPX_CMD_CANCEL_EVENT 0x06 #define IPX_CMD_GET_INTERVAL_MARKER 0x08 #define IPX_CMD_GET_INTERNETWORK_ADDRESS 0x09 #define IPX_CMD_RELINQUISH_CONTROL 0x0a #define IPX_CMD_DISCONNECT_FROM_TARGET 0x0b // ----------------------- // Команды интерфейса SPX // ----------------------- #define SPX_CMD_INSTALL_CHECK 0x10 // ----------------------- // Коды ошибок // ----------------------- #define NO_ERRORS 0 #define ERR_NO_IPX 1 #define ERR_NO_SPX 2 #define NO_LOGGED_ON 3 #define UNKNOWN_ERROR 0xff // ----------------------- // Константы // ----------------------- #define SHORT_LIVED 0 #define LONG_LIVED 0xff #define IPX_DATA_PACKET_MAXSIZE 546 // Внешние процедуры для инициализации и вызова драйвера IPX/SPX void far ipxspx_entry(void far *ptr); int ipx_init(void); // Структура для вызова драйвера IPX/SPX struct IPXSPX_REGS { unsigned int ax; unsigned int bx; unsigned int cx; unsigned int dx; unsigned int si; unsigned int di; unsigned int es; }; 2.2.2. Использование API драйвера IPXТеперь, после того как мы научились проверять наличие драйвера IPX и определять точку входа для вызова его API, нам предстоит научиться пользоваться этим API. Без преувеличения можно сказать, что от того, насколько хорошо вы освоите API драйвера IPX, зависят ваши успехи в создании программного обеспечения для сетей Novell NetWare. Однако, прежде чем мы займемся программным интерфейсом IPX, нам необходимо разобраться со структурой программ, способных обмениваться данными по сети, и понять, каким образом программы, работающие на различных станциях, могут передавать друг другу эти самые данные. Такие знания помогут вам и в том случае, если вы будете создавать программы, использующие протокол NETBIOS. Схема "клиент-сервер"Обычно в сети одна из рабочих станций принимает запросы на выполнение каких-либо действий от других рабочих станций. Так как станция обслуживает запросы, она называется сервером (serve - обслуживать, server - обслуживающее устройство). Выполнив запрос, сервер посылает ответ в запросившую станцию, которая называется клиентом. В сети может быть много серверов и много клиентов. Одни и те же клиенты могут посылать запросы разным серверам. Говоря более строго, сервером или клиентом является не рабочая станция, а запущенная на ней программа. В мультизадачной среде разные программы, запущенные одновременно на одной и той же рабочей станции могут являться и клиентами, и серверами. Программа-сервер, выполнив очередной запрос, переходит в состояние ожидания. Она ждет прихода пакета данных от программы-клиента. В зависимости от содержимого этого пакета программа-сервер может выполнять различные действия, в соответствии с логикой работы программы. Например, она может принять от программы-клиента дополнительные пакеты данных или передать свои пакеты. Сервер и клиент при необходимости на какое-то время или навсегда могут поменяться местами, изменив свое назначение на противоположное. Для того, чтобы создавать программы-серверы и программы-клиенты, нам необходимо научиться выполнять две задачи:
Инициализация сервера и клиентаДля инициализации программ сервера и клиента, работающих на базе IPX, недостаточно убедиться в наличии соответствующего драйвера и получить точку входа в его API. Вам необходимо выполнить некоторые подготовительные действия для того, чтобы программа могла принимать и передавать пакеты данных. Прежде всего необходимо, чтобы программа-сервер или программа-клиент идентифицировали себя в сети при помощи механизма сокетов. Для хранения сокета используется двухбайтовое слово, так что диапазон возможных значений простирается от 0 до FFFFh. Однако вы не можете использовать произвольные значения. Некоторые значения зарезервированы для использования определенными программами. Это так называемые "хорошо известные" сокеты ("well-known" sockets). Так как протокол IPX является практической
реализацией протокола Xerox Internetwork Packet Protocol,
первоначальное распределение сокетов
выполняется фирмой Xerox. Согласно этому
распределению сокеты от 0 до 3000 зарезервированы
статически за определенным программным
обеспечением. В частности, фирма Novell получила от
фирмы Xerox диапазон сокетов для своей Динамически распределяемые сокеты выдаются программам как бы во временное пользование (на время их работы) по специальному запросу. Перед началом работы программа должна запросить сокет у протокола IPX, а перед завершением - освободить его. Распределение сокетов в сети Novell NetWare несколько
отличается от Вы, как разработчик программного обеспечения для сетей NetWare, можете получить у Novell для своей программы персональный сокет (если сумеете это сделать) или воспользоваться сокетом, полученным динамически. Можно задавать сокет в качестве параметра при запуске программы. Если вы обнаружите, что используемое вами значение сокета конфликтует с другим программным обеспечением, вы легко сможете изменить его, просто задавая новое значение для соответствующего параметра. При реализации схемы обмена данными "клиент-сервер" сервер обычно принимает пакеты на сокете, значение которого известно программам-клиентам. Сами же программы-клиенты могут использовать либо то же самое значение сокета, либо получать свой сокет динамически. Клиент может сообщить серверу свой сокет просто передав его в пакете данных (так как мы предполагаем, что сокет сервера известен программе-клиенту). После определения сокета необходимо узнать сетевой адрес станций-получателей. Для того чтобы клиент мог послать запрос серверу, необходимо кроме сокета сервера знать его сетевой адрес - номер сети и адрес рабочей станции в сети. Если программа-клиент знает только сокет программы-сервера, но не знает его сетевой адрес, последний можно запросить у сервера, послав запрос во все станции одновременно. Такой запрос в пределах одного сегмента сети можно выполнить, если в качестве адреса рабочей станции указать специальное значение FFFFFFFFFFFFh. Это так называемый "широковещательный" (broadcast) адрес. Клиент посылает запрос на известный ему сокет программы-сервера и использует адрес FFFFFFFFFFFFh. Такой запрос принимают все программы на всех рабочих станциях, ожидающие пакеты на данном сокете. Получит его и наша программа-сервер. А она может определить свой собственный сетевой адрес (выполнив вызов соответствующей функции IPX) и послать его клиенту. Адрес же клиента программа-сервер может взять из заголовка принятого пакета. Разумеется, существует способ определения адреса рабочей станции по имени пользователя, подключившегося на ней к файл-серверу. Это можно сделать при помощи API сетевой оболочки рабочей станции (резидентная программа netx.exe). Однако этот способ не позволит вам определить адрес станции, на которой не выполнено подключение к файл-серверу или не запущена сетевая оболочка netx.exe. Пакет, переданный по адресу FFFFFFFFFFFFh, будет принят всеми станциями сети даже в том случае, если файл-сервер выключен или его вовсе нет. Поэтому способ определения сетевого адреса через запрос по всей сети более универсален. Прием и передача пакетов данныхРассмотрим теперь процедуру приема пакетов данных средствами IPX. Прием и передачу пакетов выполняет сетевой адаптер, работающий с использованием прерываний. Некоторые сетевые адаптеры работают с памятью через канал прямого доступа DMA. Прерывание от сетевого адаптера обрабатывает драйвер сетевого адаптера. Например, в операционной системе MS-DOS для адаптеров, совместимых с адаптером Novell NE2000 в составе Novell NetWare поставляется драйвер ne2000.com, реализованный в виде резидентной программы. Прикладные программы не работают напрямую с драйвером сетевого адаптера. Все свои запросы на прием и передачу пакетов они направляют драйверу IPX (программа ipx.exe или ipxodi.exe), который, в свою очередь, обращается к драйверу сетевого адаптера. Для приема или передачи пакета прикладная программа должна подготовить пакет данных, сформировав его заголовок, и построить так называемый блок управления событием ECB (Event Control Block). В блоке ECB задается адресная информация для передачи пакета, адрес самого передаваемого пакета в оперативной памяти и некоторая другая информация. Подготовив блок ECB, прикладная программа передает его адрес соответствующей функции IPX для выполнения операции приема или передачи пакета. Функции IPX, принимающие или передающие пакет, не выполняют ожидания завершения операции, а сразу возвращают управление вызвавшей их программе. Прием или передача выполняются сетевым адаптером автономно и асинхронно по отношению к программе, вызвавшей функцию IPX для передачи данных. После того, как операция передачи данных завершилась, в соответствующем поле блока ECB устанавливается признак. Программа может периодически проверять ECB для обнаружения признака завершения операции. Есть и другая возможность. В блоке ECB можно указать адрес процедуры, которая будет вызвана при завершении выполнения операции передачи данных. Такой способ предпочтительнее, так как прикладная программа не будет тратить время на периодическую проверку блока ECB. Формат блока ECBФормат блока ECB представлен на рис. 3.
Рис. 3. Формат блока ECB Блок ECB состоит из фиксированной части размером 36 байт и массива дескрипторов, описывающих отдельные фрагменты передаваемого или принимаемого пакета данных. Приведем структуру, которую вы можете использовать для описания блока ECB в программах, составленных на языке Си: struct ECB { void far *Link; void far (*ESRAddress)(void); unsigned char InUse; unsigned char CCode; unsigned int Socket; unsigned int ConnectionId; unsigned int RrestOfWorkspace; unsigned char DriverWorkspace[12]; unsigned char ImmAddress[6]; unsigned int FragmentCnt; struct { void far *Address; unsigned int Size; } Packet[2]; }; Рассмотрим назначение отдельных полей блока ECB. Поле Link предназначено для организации списков, состоящих из блоков ECB. Драйвер IPX использует это поле для объединения переданных ему блоков ECB в списки, записывая в него полный адрес в формате [сегмент:смещение]. После того, как IPX выполнит выданную ему команду и закончит все операции над блоком ECB, программа может распоряжаться полем Link по своему усмотрению. В частности, она может использовать это поле для организации списков или очередей свободных или готовых для чтения блоков ECB. Поле ESRAddress содержит полный адрес программного модуля (в формате [сегмент:смещение]), который получает управление при завершении процесса чтения или передачи пакета IPX. Этот модуль называется программой обслуживания события ESR (Event Service Routine). Если ваша программа не использует ESR, она должна записать в поле ESRAddress нулевое значение. В этом случае о завершении выполнения операции чтения или передачи можно узнать по изменению содержимого поля InUse. Поле InUse, как мы только что заметили, может
служить индикатором завершения операции приема
или передачи пакета. Перед тем как вызвать
функцию IPX, программа записывает в поле InUse
нулевое значение. Пока опе-
Функции асинхронного управления AES будут рассмотрены позже. Программа может постоянно опрашивать поле InUse, ожидая завершения процесса передачи или приема данных. Как только в этом поле окажется нулевое значение, программа может считать, что запрошенная функция выполнена. Результат выполнения можно получить в поле CCode. Поле CCode после выполнения функции IPX (после того, как в поле InUse будет нулевое значение) содержит код результата выполнения. Если с данным ECB была связана команда приема пакета, в поле CCode могут находиться следующие значения:
Если ECB использовался для передачи пакета, в поле CCode после завершения передачи могут находиться следующие значения:
Поле Socket содержит номер сокета, связанный с данным ECB. Если ECB используется для приема, это поле содержит номер сокета, на котором выполняется прием пакета. Если же ECB используется для передачи, это поле содержит номер сокета передающей программы (но не номер сокета той программы, которая должна получить пакет). Поле IPXWorkspace зарезервировано для использования драйвером IPX. Ваша программа не должна инициализировать или изменять содержимое этого поля, пока обработка ECB не завершена. Поле DriverWorkspace зарезервировано для использования драйвером сетевого адаптера. Ваша программа не должна инициализировать или изменять содержимое этого поля, так же как и поля IPXWorkspace, пока обработка ECB не завершена. Поле ImmAddress (Immediate Address - непосредственный адрес) содержит адрес узла в сети, в который будет направлен пакет. Если пакет передается в пределах одной сети, поле ImmAddress будет содержать адрес станции-получателя (такой же, как и в заголовке пакета IPX). Если же пакет предназначен для другой сети и будет проходить через мост, поле ImmAddress будет содержать адрес этого моста в сети, из которой передается пакет. Поле FragmentCnt содержит количество фрагментов, на
которые надо разбить принятый пакет, или из
которых надо собрать передаваемый пакет. В
простейшем случае весь пакет, состоящий из
заголовка и данных, может представлять собой
непрерывный массив. Однако часто удобнее хранить
отдельно Значение, записанное вами в поле FragmentCnt, не должно быть равно нулю. Если в этом поле записано значение 1, весь пакет вместе с заголовком записывается в один общий буфер. Сразу вслед за полем FragmentCnt располагаются дескрипторы фрагментов, состоящие из указателя в формате [сегмент:смещение] на фрагмент Address и поля размера фрагмента Size. Если программе надо разбить принятый пакет на несколько частей, она должна установить в поле FragmentCnt значение, равное количеству требуемых фрагментов. Затем для каждого фрагмента необходимо создать дескриптор, в котором указать адрес буфера и размер фрагмента. Аналогичные действия выполняются и при сборке пакета перед передачей из нескольких фрагментов. Отметим, что самый первый фрагмент не должен быть короче 30 байт, так как там должен поместиться заголовок пакета IPX. |