2. Подключение пользователя к серверу

В этой главе мы рассмотрим процедуры получения списка активных серверов в сети и подключения пользователей к серверам. Вы научитесь составлять программы, выполняющие действия, аналогичные сетевым утилитам slist.exe, login.exe и attach.exe. Эти утилиты были подробно описаны нами в томе "Библиотеки системного программиста", посвященном установке аппаратного и программного обеспечения локальных сетей компьютеров.

Пользователь подключается к файл-серверу обычно при помощи утилит login.exe и attach.exe, которые поставляются в комплекте с сетевой операционной системой Novell NetWare. Программа login.exe подключает пользователя только к одному серверу. Если необходимо подключиться сразу к нескольким серверам, это можно сделать при помощи программы attach.exe.

Программа login.exe прежде всего создает канал с сервером, записывая данные о сервере в две внутренние таблицы сетевой оболочки - в таблицу номеров каналов (Connection ID Table) и в таблицу имен серверов (Server Name Table). Для этой процедуры не нужны имя пользователя и пароль, запрашиваемые утилитой login.exe. Заметим, что сама сетевая оболочка netx.exe при запуске создает канал с ближайшим сервером. Этот сервер с точки зрения сетевой оболочки становится первичным (Primary) сервером. После создания канала на рабочей станции появляется новый диск, отображаемый на сетевой каталог SYS:LOGIN. В этом каталоге есть две программы - slist.exe и login.exe, предназначенные соответственно для получения списка серверов и для подключения пользователя к серверу.

Затем утилита login.exe пытается подключить пользователя к серверу, проверяя имя пользователя и пароль. Если база данных объектов сервера содержит пользователя с введенным именем и паролем, пользователь получает доступ к файл-серверу.

Файл-сервер имеет таблицу каналов (File Server Connection Table) и таблицу паролей (Password Table). После того как сетевая оболочка создает канал с сервером, в таблицу каналов файл-сервера записывается номер канала, используемого сервером для связи с рабочей станцией. Разумеется, номер канала на рабочей станции не равен номеру канала на сервере. Рабочая станция может создать до 8 каналов с серверами, а сервер - до 250 (в зависимости от версии сетевой операционной системы).

Если подключение пользователя к файл-серверу завершилось успешно, Netware заносит идентификатор пользователя в таблицу паролей.

Программа attach.exe может создать новый канал с другим сервером, отличным от первичного, записав его номер в таблицу номеров каналов, а также подключить пользователя к еще одному серверу.

Подключив пользователя к файл-серверу, программа login.exe переходит в каталог SYS:PUBLIC и считывает системный файл автоматической настройки System Login Script, который находится в файле с именем net$log.dat. Программа login.exe интерпретирует все команды, записанные в этом файле.

Далее программа login.exe извлекает из базы данных BINDERY идентификатор пользователя и ищет в каталоге SYS:MAIL каталог с именем, совпадающим с десятичным представлением идентификатора пользователя, - личный каталог пользователя. В личном каталоге пользователя находится личный файл автоматической настройки Login Script с именем login (имя не имеет расширения). Если этот файл есть, программа login.exe считывает и интерпретирует его.

Только что мы рассмотрели упрощенный алгоритм работы программы login.exe, подключающей пользователя к файл-серверу. Если вам потребуется создать собственную программу подключения пользователей, вы должны выполнить все или некоторые из описанных выше действий.

2.1. Проверка присутствия сетевой оболочки

Прежде чем обращаться к функциям сетевой оболочки рабочей станции, ваша программа должна убедиться, что эта оболочка загружена. Следует также узнать ее версию, так как состав функций сетевой оболочки может меняться от версии к версии.

Для проверки присутствия сетевой оболочки и определения ее версии проще всего воспользоваться функцией GetShellVersionInformation(), входящей в состав библиотеки NetWare C Interface. Приведем прототип указанной функции:

int GetShellVersionInformation(BYTE *MajorVersion,
                BYTE *MinorVersion, BYTE *RevisionLevel);

Тип BYTE описан в include-файле prolog.h, входящем в состав NetWare C Interface:

#define BYTE unsigned char


Если функция возвратила значение 0xFF, поля MajorVersion, MinorVersion, RevisionLevel будут содержать соответственно верхний (major) номер версии, нижний (minor) номер версии и номер изменения (revision). Если функция GetShellVersionInformation() вернула нулевое значение, версия сетевой оболочки слишком стара (номер версии меньше 2.1) и не поддерживает данную функцию.

Для того чтобы определить наличие сетевой оболочки, перед вызовом функции GetShellVersionInformation() запишите нулевое значение в переменную MajorVersion. Если после возврата из функции переменная MajorVersion не изменила своего значения, сетевая оболочка не загружена.

К сожалению, функция GetShellVersionInformation() не сохраняет содержимое регистра SI, поэтому у нас возникли проблемы с использованием этой функции в среде сетевой оболочки версии 3.26. Мы вышли из положения простым способом - сохранили содержимое этого регистра в стеке сами перед вызовом функции, а после возврата из функции восстановили его.

Приведем программу, определяющую версию сетевой оболочки (листинг 1):

// ===================================================
// Листинг 1. Программа для обнаружения сетевой
// оболочки и определения ее версии
// Файл version\version.cpp
//
// (C) A. Frolov, 1993
// ===================================================

#include <stdlib.h>
#include <stdio.h>

extern "C" int GetNetWareShellVersion(char *,char *, char *);

void main(void) {

        char MajorVersion=0;
        char MinorVersion=0;
        char Revision=0;

        asm push si

        GetNetWareShellVersion(&MajorVersion,
                        &MinorVersion, &Revision);

        asm pop si

        if(MajorVersion == 0) {
                printf("\nОболочка NetWare не загружена\n");
                return;
        }

        printf("\nВерсия оболочки NetWare: %d.%d.%d\n",MajorVersion,
                        MinorVersion, Revision);
}


Приведенная программа составлена на языке программирования С++, поэтому внешняя функция GetNetWareShellVersion() должна быть описана как внешняя функция, использующая соглашения об именах и передаче параметров в стандарте С:

extern "C" int GetNetWareShellVersion(char *,char *, char *);


Если бы программа была составлена на языке С, можно было бы использовать описание этой функции, приведенное в одном из include-файлов библиотеки Netware C Interface. Для включения всех include-файлов библиотеки Netware C Interface вы должны добавить в вашу программу следующую строку:

#include <nit.h>


Для программ, составленных на языке С++, вам придется создавать собственные include-файлы на базе поставляемых вместе с библиотекой Netware C Interface.

Если у вас нет библиотеки Netware C Interface, вы можете узнать номер версии, вызвав непосредственно функцию 0xEA01 прерывания INT 21h.

Перед вызовом функции вам нужно соответствующим образом загрузить регистры:

На входе: AX = EA01h.
ES:DI = Указатель на буфер размером 40 байт, в который будет записано текстовое описание среды рабочей станции. Это описание состоит из четырех строк:- название операционной системы;
- версия операционной системы;
- модель компьютера;
- фирма-производитель компьютера.
Последняя текстовая строка в буфере закрыта двумя двоичными нулями.
На выходе: BH = Верхний (major) номер версии или 0, если сетевая оболочка не загружена или ее версия меньше 2.1.
BL = Нижний (minor) номер версии.
CL = Номер изменения (revision).

Приведем вариант предыдущей программы, не использующий библиотеку NetWare C Interface (листинг 2). Кроме версии сетевой оболочки программа выводит содержимое буфера с текстовым описанием среды рабочей станции.

// ================================================================
// Листинг 2. Программа для обнаружения сетевой оболочки, определе-
// ния ее версии и вывода строк описания среды рабочей станции
// Файл version1\version1.cpp
//
// (C) A. Frolov, 1993
// ================================================================

#include <stdlib.h>
#include <stdio.h>
#include <dos.h>
#include <string.h>

void    PrintBuffer(char*);

void main(void) {

        char MajorVersion=0;
        char MinorVersion=0;
        char Revision=0;
        char Buffer[40];
        union REGS regs; struct SREGS sregs;

        regs.x.ax = 0xea01;
        regs.x.di = FP_OFF(Buffer);
        sregs.es = FP_SEG(Buffer);
        intdosx(&regs, &regs, &sregs);

        MajorVersion = regs.h.bh;
        MinorVersion = regs.h.bl;
        Revision     = regs.h.cl;

        printf("\nВерсия оболочки NetWare: %d.%d.%d\n",MajorVersion,
                        MinorVersion, Revision);
        printf("\nСтроки описания среды: ");

        PrintBuffer(Buffer);
}

void PrintBuffer(char *Buffer) {
        char *ptr;

                for(ptr = Buffer; *ptr != '\0';) {
                        printf("'%s' ", ptr);
                        ptr = ptr + strlen(ptr) + 1;
                }
}


2.2. Поиск серверов в сети

Если в сети имеется более одного сервера, то, прежде чем подключиться к файл-серверу, вам необходимо узнать его имя, заданное супервизором при запуске сервера. Для этого предназначена утилита slist.exe, которая находится в каталоге SYS:LOGIN и всегда доступна, если на рабочей станции загружена сетевая оболочка.

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

Для поиска серверов вы можете воспользоваться диагностическим сервисом, описанным в предыдущем томе "Библиотеки системного программиста". Однако существует более удобный протокол, позволяющий средствами IPX найти все активные серверы и, что самое главное, определить их имена. Этот протокол называется протоколом объявления сервиса в сети (Service Advertising Protocol - SAP).

Использование протокола SAP основано на том факте, что все серверы в сети идентифицируют себя периодической посылкой пакета IPX специального типа - пакета объявления сервиса (Servise Advertising Packet). Кроме того, рабочие станции и серверы могут посылать пакеты запроса (Service Query) по адресу 0xFFFFFFFFFFFF, в ответ на который все серверы присылают запросившей станции пакеты объявления сервиса. Последнее обстоятельство роднит сервис SAP с диагностическим сервисом.

Для того чтобы найти все активные серверы в сети, ваша программа должна подготовить массив буферов и блоков ECB для приема IPX-пакетов объявления сервиса и послать по адресу 0xFFFFFFFFFFFF пакет запроса на сокет 0x452. Через некоторое время программа получит пакеты объявления сервиса. Просмотрев их, она сможет определить имена серверов, а также другую информацию об активных серверах.

Пакет запроса состоит из стандартного IPX-заголовка и блока данных, который может быть описан структурой следующего вида:

struct QPacket {
        unsigned QueryType;
        unsigned ServerType;
};


Поле QueryType задает тип запроса и может содержать одно из двух значений: 1 или 3. Значение 1 соответствует общим запросам и позволяет получить информацию о всех серверах во всех сетях. Значение 3 позволяет найти ближайший сервер нужного типа.

Тип сервера, который нужно найти, задается в поле ServerType. Для определения значения, соответствующего файл-серверу, можно воспользоваться списком типов объектов, хранящихся в базе данных объектов сервера:

Значение Описание
0 Не классифицируемый (неизвестный) объект
1 Пользователь
2 Группа пользователей
3 Очередь печати
4 Файл-сервер
5 Сервер заданий
6 Шлюз
7 Сервер печати
8 Очередь для архивирования
9 Сервер для архивирования
A Очередь заданий
B Администратор
24 Сервер удаленного моста

Номера типов объектов назначаются фирмой Novell; при необходимости вы можете создавать объекты своих типов, если получите в Novell номер специально для создаваемого вами объекта.

Для поиска всех файловых серверов вам надо указать в поле ServerType значение 4, а в поле QueryType - значение 1.

После посылки пакета запроса вы получите несколько пакетов объявления типа, состоящих из обычного IPX-заголовка и блока данных в следующем формате (описанном в файле sap.h, входящем в библиотеку NetWare C Interface):

typedef struct SAPHeader {
        WORD     checksum;
        WORD     length;
        BYTE     transportControl;
        BYTE     packetType;
        IPXAddress    destination;
        IPXAddress    source;
        WORD     SAPPacketType;
        WORD     serverType;        
        BYTE     serverName[48];
        IPXAddress    serverAddress;
        WORD     interveningNetworks;
} SAPHeader;


Тип WORD описан в include-файле prolog.h, входящем в состав NetWare C Interface:

#define WORD unsigned int


Поля checksum, length, transportControl, packetType, destination и source представляют собой заголовок IPX-пакета. Тип IPXAddress описывает сетевой адрес и также определен в файле sap.h:

typedef struct IPXAddress {
        BYTE network[4];
        BYTE node[6];
        WORD socket;
} IPXAddress;


Все эти поля мы подробно описали в предыдущем томе "Библиотеки системного программиста".

Поле SAPPacketType содержит значение 2, если пакет пришел в ответ на общий запрос, или 4 - для ближайших запросов.

Поле serverType содержит номер типа сервера, описываемого данным пакетом. Если мы запрашивали информацию о файл-серверах, в этом поле должно быть значение 4.

В поле serverName расположена текстовая строка имени сервера, закрытая двоичным нулем. Именно это поле нам и нужно для получения списка имен активных серверов в сети.

Поле serverAddress является структурой, в которой находится сетевой адрес сервера. Именно по этому адресу сервер принимает запросы от сетевых оболочек рабочих станций. Вам не следует использовать в своих программах сокет, номер которого возвращается в поле serverAddress.socket, если вы не знаете точно, что и как собираетесь с ним делать.

Поле interveningNetworks отражает количество мостов, которые прошел пакет на своем пути от сервера до рабочей станции. Если значение в этом поле равно 1, сервер и рабочая станция находятся в одном сегменте сети.

Таким образом, самое интересное для нас в пакете объявления типа - это поля типа сервера serverType и имени сервера serverName. Для подключения к сети нам потребуются только имена файловых серверов.

В разделе "Программа SLIST" мы приведем исходный текст программы, выводящей на экран список активных серверов и другую интересную информацию о серверах, например серийные номера операционных систем. Для того чтобы вы смогли в ней разобраться, в мы расскажем вам в следующем разделе о каналах, создаваемых между сетевыми оболочками рабочих станций и серверами.

2.3. Создание канала с файл-сервером

Каналы, создаваемые между сетевыми оболочками рабочих станций и файл-серверами, похожи на каналы, создаваемые протоколом SPX (или протоколом NETBIOS). Однако для повышения производительности эти каналы сделаны на базе протокола IPX, а не на базе протокола SPX, как это можно было бы предположить.

И сервер, и каждая рабочая станция имеют таблицы номеров каналов, в которых находятся различные характеристики партнеров, такие, как имена или сетевые адреса. Таблица каналов рабочей станции содержит 8 элементов, поэтому каждая рабочая станция может подключиться не более чем к 8 различным серверам. Размер таблицы каналов файл-сервера может меняться в зависимости от версии операционной системы Novell NetWare в пределах от 5 до 250. Этот размер определяет максимальное количество пользователей, которые могут подключиться к файл-серверу.

Подробно формат таблицы номеров каналов и других таблиц сетевой оболочки мы рассмотрим в разделе "Отображение дисков рабочей станции" этой главы. Сейчас для нас важно, что при создании канала с файл-сервером в таблице номеров каналов появляется новая запись. При уничтожении канала соответствующая запись также удаляется.

Для создания канала с файл-сервером следует использовать функцию AttachToFileServer(), определенную в библиотеке Novell NetWare C Interface следующим образом:

int AttachToFileServer(char *ServerName, WORD *ConnectionID);


Функции надо передать указатель на текстовую строку с именем файл-сервера и адрес переменной типа WORD, в которую будет записан номер созданного канала. При успешном создании канала функция возвращает нулевое значение, в противном случае - код ошибки:

Код ошибки Значение
0xF8 Рабочая станция уже подключена к этому серверу
0xF9 Нет места в таблице номеров каналов рабочей станции
0xFA Нет места в таблице номеров каналов сервера
0xFC Сервера с указанным именем нет в сети
0xFE База объектов сервера заблокирована
0xFF Сервер не отвечает на запрос

Для уничтожения канала вы можете использовать функцию DetachFromFileServer():

void DetachFromFileServer(WORD ConnectionID);


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

Таким образом, все, что вам нужно знать для создания канала с файл-серве-
ром, - это имя файл-сервера. Пользователь может ввести имя нужного файл-сервера, спросив его у супервизора. Однако вы можете предоставить пользователю меню активных файл-серверов. Для получения меню можно воспользоваться методикой обнаружения файл-серверов, изложенной нами ранее и основанной на протоколе SAP. Соответствующая программа, иллюстрирующая использование SAP-протокола, приведена дальше в разделе "Программа SLIST" этой главы.

Заметим, что сетевая оболочка сразу после своего запуска создает канал с ближайшим файл-сервером. Этот файл-сервер становится первичным (Primary).

Диски рабочей станции могут отображаться на каталоги файл-сервера. Если на рабочей станции текущим (т. е. используемым по умолчанию) является диск, отображенный на каталог файл-сервера, то этот файл-сервер называется текущим или используемым по умолчанию (Default).

Кроме того, существует понятие предпочтительного (Preferred) файл-сервера. Этот сервер должен быть задан явно специальной функцией.

Когда программа, запущенная на рабочей станции, обращается к файл-серверу, вначале проверяется, был ли задан предпочтительный файл-сервер. Если он задан не был, запрос адресуется текущему серверу. Если же текущий диск рабочей станции локальный (т. е. текущий сервер не определен), запрос адресуется первичному серверу.

В библиотеке NetWare C Interface есть несколько функций, позволяющих определить номера каналов первичного, текущего и предпочтительного сервера, задать предпочтительный сервер и изменить первичный сервер.

Функция GetPrimaryConnectionID() возвращает номер канала первичного сервера:

WORD GetPrimaryConnectionID(void);


Функция GetDefaultConnectionID() возвращает номер канала для текущего сервера:

WORD GetDefaultConnectionID(void);


Функция GetPreferredConnectionID() возвращает номер канала предпочтительного сервера или 0, если предпочтительный сервер не был задан.

Напомним, что номер канала соответствует индексу в таблице номеров каналов и лежит в пределах от 1 до 8.

Функция SetPreferredConnectionID() предназначена для определения предпочтительного сервера. Номер канала для сервера, который должен стать предпочтительным, передается функции в качестве параметра:

void SetPreferredConnectionID(BYTE ConnectionID);


Если у вас нет библиотеки NetWare C Interface, вы можете создать канал с сервером или удалить его с помощью функции F1h прерывания INT 21h.

Перед вызовом функции вам нужно загрузить регистры следующим образом:

На входе: AH = F1h;
AL = 0 - создать канал с файл-сервером, использовать номер канала, заданный в регистре DL;
1 - отключить пользователя и удалить канал, номер которого задан в регистре DL;
2 - отключить пользователя от файл-сервера, номер канала которого задан в регистре DL;
DL = Номер канала.
На выходе: AL = Код ошибки или 0, если операция выполнена без ошибок.

При помощи функции F0h прерывания INT 21h вы сможете определить первичный и текущий сервер, а также задать новый первичный или предпочтительный сервер:

На входе: AH = F0h;
AL = 0 - установить предпочтительный файл-сервер, номер канала которого задан в регистре DL;
1 - определить текущий предпочтительный сервер, номер сервера возвращается в регистре AL;
2 - получить в регистре AL номер текущего сервера;
4 - установить первичный файл-сервер, номер канала которого задан в регистре DL;
5 - получить в регистре AL номер первичного файл-сервера;
DL = Номер канала.
На выходе: AL = Код ошибки или 0, если операция выполнена без ошибок.

2.4. Подключение к файл-серверу

Создав канал с файл-сервером, программа еще не получила доступ к томам сервера и другому сервису. Следующим после создания канала этапом должно быть подключение пользователя к файл-серверу.

Для подключения пользователя к файл-серверу вы должны использовать функцию LoginToFileServer() из библиотеки Novell NetWare C Interface:

int LoginToFileServer(char *ObjectName, WORD ObjectType,
 char *ObjectPassword);


Первый параметр ObjectName - указатель на имя пользователя, под которым его зарегистрировал супервизор сети или руководитель группы. Второй параметр определяет тип объекта. Для пользователя вы долны задать значение 1. Последний параметр - указатель на текстовую строку, содержащую пароль пользователя. Учтите, что и имя пользователя, и его пароль должны задаваться заглавными буквами.

Функция LoginToFileServer() выполняет достаточно сложную процедуру шифровки пароля, поэтому без использования библиотеки NetWare C Interface или аналогичных средств вы только с большим трудом сможете выполнить процедуру подключения к серверу без этой функции. Кстати, в отличие от других функций, исходный текст функции LoginToFileServer() и некоторых других не входит в комплект поставки библиотеки NetWare C Interface.

Есть функции и для отключения пользователя от одного или сразу ото всех файл-серверов.

С помощью функции Logout() вы можете отключиться сразу ото всех файл-серверов:

void Logout(void);


Функция LogoutFromFileServer() предназначена для отключения только от одного сервера, номер канала которого задается в качестве единственного параметра функции:

void LogoutFromFileServer(WORD ConnectionID);


В разделе "Программа LOG" мы приведем программу, которая умеет подключать пользователя к файл-серверу, а сейчас займемся тем, что определим список активных файл-серверов.

2.4.1. Программа SLIST

Мы подготовили для вас программу, которая, пользуясь протоколом SAP, определяет список активных серверов и запоминает имена серверов. Затем для всех активных серверов программа получает дополнительную информацию и выводит ее в стандартный поток вывода.

Программа создает объект класса SLIST. Конструктор этого объекта получает всю необходимую информацию, которая при помощи функции SLIST::PrintServersName(), определенной в классе SLIST, выводится в стандартный поток (листинг 3).

// ================================================================
// Листинг 3. Просмотр списка активных серверов и вывод в стандарт-
// ный поток имен и другой информации об активных серверах
// Файл slist!\slist.cpp
//
// (C) A. Frolov, 1993
// ================================================================

#include <stdlib.h>
#include <stdio.h>
#include <mem.h>
#include <string.h>
#include <dos.h>
#include <direct.h>
#include "sap.hpp"

void main(void) {

  SLIST *ServerList;
  int ccode = 0;

  printf("\n*SLIST!*, v.1.0, (C) Фролов А.В., 1993\n");

// Создаем объект класса SLIST. Конструктор этого объекта
// получает всю необходимую информацию о серверах и
// записывает ее в область данных объекта

  ServerList = new SLIST(GENERAL_SERVICE);

// Если при создании объекта были ошибки, завершаем
// выполнение программы

  ccode = ServerList->Error();

  if(ccode) {
                printf("Ошибка %d\n", ccode);
                return;
  }

// Выводим список серверов

  printf("\nОбнаружены серверы:\n");

  printf(  "---------------------------------------------"
                "------------------------------\n");

  ServerList->PrintServersName();

  printf(  "---------------------------------------------"
                "------------------------------\n");
}


Файл slist.cpp содержит определения функций-членов класса SLIST (листинг 4).

Конструктор SLIST() проверяет наличие сетевой оболочки, проверяет и запоминает тип запроса (получить сведения о ближайшем сервере или о всех серверах сети) и запоминает его. Затем конструктор инициализирует драйвер протокола IPX и открывает динамический короткоживущий сокет для работы с протоколом SAP. Далее в цикле создаются блоки ECB и ставятся в очередь на прием пакетов. Эти блоки ECB будут использованы для приема SAP-пакетов. После подготовки ECB конструктор посылает пакет запроса, ожидает примерно одну секунду и при помощи функций SLIST::GetServersName() и SLIST::GetServersInfo() получает и запоминает имена серверов и другую информацию.

Для работы с IPX-пакетами мы использовали функции из библиотеки NetWare C Interface. Назначение этих функций вам будет понятно из их названия, если вы прочитали предыдущий том "Библиотеки системного программиста".

Функция IPXInitialize() проверяет наличие драйвера протокола IPX и выполняет все инициализирующие действия, необходимые для использования протокола IPX.

Функция IPXOpenSocket() предназначена для открытия сокета. Первый параметр функции - указатель на переменную типа WORD, содержащую значение открываемого сокета или ноль, если надо получить динамический сокет. Байты в этой переменной расположены в обратном порядке, т. е. старший байт расположен по младшему адресу. Второй параметр функции IPXOpenSocket() определяет тип открываемого сокета - долгоживущий или короткоживущий. В нашем случае мы открываем динамический короткоживущий сокет.

После открытия сокета конструктор с помощью функции SLIST::ReceiveSAPPacket() подготавливает массив блоков ECB для приема ответных пакетов и, вызывая функцию IPXListenForPacket(), ставит эти блоки в очередь на прием. Функция IPXListenForPacket() имеет в качестве единственного параметра указатель на блок ECB.

Далее конструктор вызывает функцию SLIST::SendSAPPacket(), которая подготавливает блок ECB и заголовок IPX-пакета для SAP-запроса. При этом с помощью функции IPXGetInternetworkAddress() программа определяет свой собственный сетевой адрес. Функция IPXGetInternetworkAddress() имеет в качестве параметра указатель на структуру, в которую будет записан номер сети и сетевой адрес узла в сети.

Подготовив пакет, функция SLIST::SendSAPPacket() ставит его в очередь на передачу при помощи функции IPXSendPacket(), передавая ей в качестве параметра указатель на соответствующий блок ECB.

Когда пакет будет передан, конструктор ждет примерно одну секунду. В течение этого времени приходят ответные пакеты от серверов. После ожидания вызываются функции SLIST::GetServersName() и SLIST::GetServersInfo(), получающие соответсвенно имена серверов и дополнительную информацию.

Функция SLIST::GetServersName() переписывает имена откликнувшихся на запрос серверов из принятых SAP-пакетов во внутренний массив объекта класса SLIST.

Функция SLIST::GetServersInfo() выполняет более сложные действия.

Вначале с помощью функций GetPrimaryConnectionID() и GetDefaultConnectionID() она получает номера каналов первичного и текущего серверов, записывая их во внутренние переменные объекта класса SLIST. Затем запускается цикл по всем обнаруженным в сети серверам.

Внутри этого цикла для каждого сервера функция получает его номер канала при помощи функции GetConnectionID(). Если канала нет, рабочая станция создает его, подключаясь к серверу. Для подключения используется функция AttachToFileServer().

Затем сервер делается предпочтительным, для чего вызывается функция SetPreferredConnectionID(). Теперь все запросы будут идти к предпочтительному серверу. Внутри цикла мы по очереди будем делать все имеющиеся серверы предпочтительными и, направляя запросы, получать от серверов интересующую нас информацию.

Далее функция SLIST::GetServersInfo() вызывает функцию GetServerInformation(), которая записывает сведения о сервере в структуру ServerInfo. Первый параметр функции GetServerInformation() задает размер этой структуры, а второй является указателем на нее.

Перед возвратом функция SLIST::GetServersInfo() пытается получить серийный номер операционной системы Novell NetWare, работающей на предпочтительном файл-сервере, вызывая функцию GetNetworkSerialNumber(). Этой функции в качестве первого параметра необходимо передать указатель на переменную типа long, в качестве второго - указатель на переменную типа WORD. В первую переменную функция запишет серийный номер операционной системы, во вторую - серийный номер приложения, работающего на файл-сервере.

Надо заметить, что данная функция возвращает серийный номер только для тех серверов, к которым было выполнено подключение пользователя функцией LoginToFileServer(). Поэтому перед вызовом функции GetNetworkSerialNumber() мы записываем в поле серийного номера и номера приложения нулевое значение. Если содержимое этих полей останется нулевым, значит, пользователь не подключился к данному файл-серверу. Для сокращения размера листинга мы не проверяем код ошибки, возвращаемый функцией GetNetworkSerialNumber().

Функция SLIST::PrintServersName() в цикле для всех обнаруженных серверов выводит в стандартный поток вывода имя сервера, напротив которого указывается, является ли он первичным (Primary) или текущим (Default). Затем выводится версия Novell NetWare, взятая из полей netwareVersion и netwareSubVersion структуры ServerInfo. Для подключенных серверов выводится серийный номер и номер приложения.

Далее для всех серверов выводится номер канала, используемого сервером и записанного ранее в массив ConnID[].

После этого для каждого сервера выводится содержимое полей maxConnectionsSupported и connectionsInUse структуры ServerInfo, которые содержат максимальное количество каналов для сервера и количество каналов, используемых в данный момент.

Перед окончанием работы программы вызывается деструктор, который отменяет все ожидающие приема блоки ECB и закрывает динамический сокет. Для отмены блоков ECB используется функция IPXCancelEvent(), которой в качестве параметра передается указатель на отменяемый блок ECB. Сокет закрывается при помощи функции IPXCloseSocket(). Номер закрываемого сокета передается этой функции в качестве параметра.

// ===================================================
// Листинг 4. Функции для программы SLIST.CPP
// Файл slist!\sap.cpp
//
// (C) A. Frolov, 1993
// ===================================================

#include <stdlib.h>
#include <stdio.h>
#include <mem.h>
#include <string.h>
#include <dos.h>
#include "sap.hpp"

// ====================================================
// Конструктор класса SLIST
// ====================================================

SLIST::SLIST(int ServiceType) {

// Проверяем наличие сетевой оболочки и определяем ее версию

        MajorVersion = 0;

        asm push si
        GetNetWareShellVersion(&MajorVersion,
                        &MinorVersion, &Revision);
        asm pop si

// Если оболочка не загружена, завершаем работу
// программы с сообщением об ошибке

        if(MajorVersion == 0) {
                printf("\nОболочка NetWare не загружена\n");
                errno = 0xff;
                return;
        }

// Проверяем тип SAP-запроса

        if (ServiceType != 1 && ServiceType != 3)       {
                errno = NOT_SUPPORTED;
                return;
        }
// Запоминаем тип запроса

        QueryType = ServiceType;

// Инициализируем драйвер протокола IPX

        IPXInitialize();

// Открываем короткоживущий динамический сокет

        SrcSocket = 0x00;
        errno = IPXOpenSocket(&SrcSocket, SHORT_LIVED);

// Заполняем таблицу имен серверов нулями

        memset(ServerName,0,sizeof(ServerName));

// Подготавливаем блоки ECB для приема
// пакетов от SAP-протокола

        for(int i=0;i<MAX_SERVERS;i++) {

// Заполняем блок ECB

                 ReceiveSAPPacket(&Query[i]);

// Ставим в очередь на прием пакета

                 IPXListenForPacket(&Query[i].theECB);
        }

// Если не было ошибок, посылаем запрос

        if (!errno) {
                SendSAPPacket();

// Ждем примерно одну секунду

                sleep(1);

// Переписываем имена серверов и другую информацию

                GetServersName();
                GetServersInfo();
        }
}

// ====================================================
// Деструктор класса SLIST
// ====================================================

SLIST::~SLIST() {

// Отменяем ожидающие блоки ECB

         for(int i=0;i<MAX_SERVERS;i++) {
                 IPXCancelEvent(&Query[i].theECB);
         }

// Закрываем сокет

         IPXCloseSocket(SrcSocket);
}

// ====================================================
// Посылка SAP-запроса
// ====================================================
void SLIST::SendSAPPacket(void) {

// Сбрасываем поле inUseFlag и ESRAddress, устанавливаем тип пакета 0

        SendPacket.theECB.inUseFlag = 0;
        SendPacket.theECB.ESRAddress = 0;
        SendPacket.SAPq.packetType = 0;

// SAP-пакет состоит из одного фрагмента. Записываем в ECB 
// количество фрагментов, адрес и размер буфера

        SendPacket.theECB.fragmentCount = 1;
        SendPacket.theECB.fragmentDescriptor[0].address =
                        &SendPacket.SAPq;
        SendPacket.theECB.fragmentDescriptor[0].size =
                        sizeof(SAPQueryPacket);

// Записываем в ECB номер своего сокета

        SendPacket.theECB.socketNumber = SrcSocket;

// Устанавливаем адрес назначения - все станции в текущей сети,
// сокет SAP_SOCKET. Устанавливаем поле непосредственного адреса

        memset(SendPacket.SAPq.destination.network, '\x00', 4);
        memset(SendPacket.SAPq.destination.node, '\xFF', 6);
        SendPacket.SAPq.destination.socket = IntSwap(SAP_SOCKET);
        memset(SendPacket.theECB.immediateAddress, '\xFF', 6);

// Устанавливаем свой адрес в заголовке запроса

        IPXGetInternetworkAddress(SendPacket.SAPq.source.network);
        SendPacket.SAPq.source.socket = IntSwap(SrcSocket);

// Заполняем передаваемый пакет. Устанавливаем тип запроса
// и тип сервера

        SendPacket.SAPq.queryType = IntSwap(QueryType);
        SendPacket.SAPq.serverType = IntSwap(0x0004);

// Посылаем SAP-пакет

        IPXSendPacket(&SendPacket.theECB);

// Ожидаем завершения процесса передачи пакета

        while (SendPacket.theECB.inUseFlag)
                 IPXRelinquishControl();

// Сохраняем код возврата

        errno = SendPacket.theECB.completionCode;
}

// ====================================================
// Прием SAP-пакетов
// ====================================================

void  SLIST::ReceiveSAPPacket(RECEIVE_PACKET     *Query) {

// Сбрасываем поле inUseFlag и ESRAddress

        Query->theECB.inUseFlag = 0;
        Query->theECB.ESRAddress = 0;
// Записываем в ECB количество фрагментов, адрес и размер буфера

        Query->theECB.fragmentCount = 1;
        Query->theECB.fragmentDescriptor[0].address =
                                &Query->SB;
        Query->theECB.fragmentDescriptor[0].size =
                                sizeof(Query->SB);

// Устанавливаем в ECB свой номер сокета

        Query->theECB.socketNumber = SrcSocket;
}

// ====================================================
// Процедура переписывает имена серверов из тех
// блоков ECB, для которых пришли пакеты
// ====================================================

void  SLIST::GetServersName(void) {

        for(int i=0,j=0; i<MAX_SERVERS; i++) {
                if(!Query[i].theECB.inUseFlag) {
                        strcpy(ServerName[j],Query[i].SB.ServerName);
                        j++;
                }
        }
}

// ====================================================
// Процедура получает информацию о серверах
// ====================================================

void  SLIST::GetServersInfo(void) {

// Получаем номера каналов первичного сервера
// и сервера по умолчанию

        PrimaryConnID   = GetPrimaryConnectionID();
        DefaultConnID   = GetDefaultConnectionID();

// Цикл по всем обнаруженным в сети активным серверам

        for(int i=0; i<MAX_SERVERS; i++) {
                if(ServerName[i][0]) {

// Получаем номер канала сервера

                        errno = GetConnectionID(ServerName[i], &ConnID[i]);

// Если канала нет, создаем его, подключаясь к серверу

                        if(errno) {
                                AttachToFileServer(ServerName[i], &ConnID[i]);
                        }

// Делаем текущий сервер предпочтительным, так как
// именно к нему должны поступать запросы

                        errno = SetPreferredConnectionID(ConnID[i]);

// Получаем информацию о текущем сервере

                        if(!errno)
                          errno = GetServerInformation(sizeof(ServerInfo[i]),
                                                                &ServerInfo[i]);

// Получаем серийный номер и номер приложения

                        SerialNumber[i]=ApplicationNumber[i]=0L;
                        errno = GetNetworkSerialNumber(&SerialNumber[i],
                         &ApplicationNumber[i]);
                        errno = 0;
                }
        }
}

// ============================================================
// Процедура распечатывает имена и другую информацию о серверах
// ============================================================

void  SLIST::PrintServersName(void) {

// Цикл по всем обнаруженным в сети активным серверам

        for(int i=0; i<MAX_SERVERS; i++) {
                if(ServerName[i][0]) {

// Выводим имя сервера

                        printf("%s",ServerInfo[i].serverName);

// Если номер канала текущего сервера совпадает с
// номером канала первичного сервера, выводим строку "\t[Primary]"

                        if(ConnID[i] == PrimaryConnID)
                                printf("\t[Primary]");
                        else
                                printf("\t[       ]");

// Если номер канала текущего сервера совпадает с
// номером канала сервера по умолчанию, выводим строку " [Default]"

                        if(ConnID[i] == DefaultConnID)
                                printf(" [Default]");
                        else
                                printf(" [       ]");

// Выводим версию сетевой операционной системы,
// работающей на текущем сервере

                        printf(" v.%d.%d, ",
                                ServerInfo[i].netwareVersion,
                                ServerInfo[i].netwareSubVersion);

// Для подключенных серверов выводим серийный
// номер и номер приложения

                        if(SerialNumber[i] != 0L)
                                printf("s/n %08.8lX/%04.4X",
                                        SerialNumber[i], ApplicationNumber[i]);
                        else
                                printf("- Not Logged In -");
// Выводим номер канала, используемого для связи с текущим сервером

                        printf("\tConnID: %d,",ConnID[i]);

// Выводим максимальное число каналов, поддерживаемых
// сервером, и количество используемых каналов

                        printf(" (%d-%d)\n",
                                ServerInfo[i].maxConnectionsSupported,
                                ServerInfo[i].connectionsInUse);
                }
        }
}


Файл sap.hpp содержит все определения констант и описания структур, необходимые для программы SLIST. В частности, в этом файле описан класс SLIST.

// ===================================================
// Листинг 5. Include-файл для программы SLIST.CPP
// Файл slist!\sap.hpp
//
// (C) A. Frolov, 1993
// ===================================================

// Максимальное количество серверов, для которых выполняется опрос

#define MAX_SERVERS         8

// Типы сервиса SAP

#define GENERAL_SERVICE     1
#define NEAREST_SERVICE     3
#define NOT_SUPPORTED       1

// Короткоживущий сокет

#define SHORT_LIVED         0x00

// Сокет для SAP-протокола

#define SAP_SOCKET          0x452

// Тип пакета SAP

#define SAP_PACKET_TYPE     2

// Определения используемых типов данных

#define BYTE  unsigned char
#define WORD  unsigned short

// Сетевой адрес

typedef struct IPXAddress {
        BYTE    network[4];
        BYTE    node[6];
        WORD    socket;
} IPXAddress;

// Заголовок IPX-пакета

typedef struct IPXHeader {
         WORD         checkSum;
         WORD         length;
         BYTE         transportControl;
         BYTE         packetType;
         IPXAddress     destination;
         IPXAddress     source;
} IPXHeader;

// Заголовок SAP-пакета

typedef struct SAPHeader {
                 WORD          checksum;
                 WORD          length;
                 BYTE          transportControl;
                 BYTE          packetType;
                 IPXAddress  destination;
                 IPXAddress  source;
                 WORD          SAPPacketType;
                 WORD          serverType;
                 BYTE          serverName[48];
                 IPXAddress  serverAddress;
                 WORD          interveningNetworks;
} SAPHeader;

// Пакет для посылки SAP-запроса

typedef struct SAPQueryPacket {
                 WORD          checksum;
                 WORD          length;
                 BYTE          transportControl;
                 BYTE          packetType;
                 IPXAddress  destination;
                 IPXAddress  source;
                 WORD          queryType;
                 WORD          serverType;
} SAPQueryPacket;

// Структуры для описания блока ECB

typedef struct ECBFragment {
                        void    far *address;
                        WORD    size;
} ECBFragment;

typedef struct ECB {
                void          far *linkAddress;
                void          (far *ESRAddress)();
                BYTE          inUseFlag;
                BYTE          completionCode;
                WORD          socketNumber;
                BYTE          IPXWorkspace[4];
                BYTE          driverWorkspace[12];
                BYTE          immediateAddress[6];
                WORD          fragmentCount;
                ECBFragment     fragmentDescriptor[2];
} ECB;

// SAP-пакет

typedef struct {
                 IPXHeader    Header;
                 WORD       ResponseType;
                 WORD       ServerType;
                 BYTE       ServerName[48];
                 BYTE       Network[4];
                 BYTE       Node[6];
                 WORD       Socket;
                 WORD       InterveningNetworks;
} SAP;

// Структура для передачи SAP-пакета

typedef struct {
                 ECB             theECB;
                 SAPQueryPacket  SAPq;
} SEND_PACKET;

// Структура для приема SAP-пакета

typedef struct {
                 ECB   theECB;
                 SAP   SB;
} RECEIVE_PACKET;

// Информация о файл-сервере

typedef struct  {
         char    serverName[48];
         BYTE    netwareVersion;
         BYTE    netwareSubVersion;
         WORD    maxConnectionsSupported;
         WORD    connectionsInUse;
         WORD    maxVolumesSupported;
         BYTE    revisionLevel;
         BYTE    SFTLevel;
         BYTE    TTSLevel;
         WORD    peakConnectionsUsed;
         BYTE    accountingVersion;
         BYTE    VAPversion;
         BYTE    queingVersion;
         BYTE    printServerVersion;
         BYTE    virtualConsoleVersion;
         BYTE    securityRestrictionLevel;
         BYTE    internetBridgeSupport;
 } FILE_SERV_INFO;

// Описания функций библиотеки NetWare C Interface

extern "C" int IPXInitialize(void);
extern "C" int IPXOpenSocket(WORD *, BYTE);
extern "C" int IPXListenForPacket(ECB *);
extern "C" int IPXCancelEvent(ECB *);
extern "C" int IPXCloseSocket(WORD);
extern "C" WORD IntSwap(WORD);
extern "C" void IPXGetInternetworkAddress(BYTE *);
extern "C" void IPXSendPacket(ECB *);
extern "C" void IPXRelinquishControl(void);
extern "C" IPXGetLocalTarget(BYTE *, BYTE *, int*);
extern "C" WORD IPXGetIntervalMarker(void);
extern "C" long LongSwap(long);
extern "C" int AttachToFileServer(char *, WORD *);
extern "C" int SetPrimaryConnectionID(int);
extern "C" int GetServerInformation(int, FILE_SERV_INFO *);
extern "C" WORD GetPreferredConnectionID(void);
extern "C" WORD GetPrimaryConnectionID(void);
extern "C" WORD GetDefaultConnectionID(void);
extern "C" int SetPreferredConnectionID(WORD);
extern "C" int GetConnectionID(char *, WORD *);
extern "C" void DetachFromFileServer(WORD);
extern "C" int GetNetWareShellVersion(BYTE *,BYTE *, BYTE *);
extern "C" int IsConnectionIDInUse(WORD);
extern "C" int GetNetworkSerialNumber(long *, int*);

// Класс SLIST

class SLIST {
private:

        WORD    QueryType;     // тип запроса
        WORD    SrcSocket;     // сокет

// Массив для приема SAP-пакетов

        RECEIVE_PACKET   Query[MAX_SERVERS];

// Передаваемый SAP-пакет

        SEND_PACKET   SendPacket;

// Таблицы имен файл-серверов, серийных
// номеров и номеров приложений

        char ServerName[MAX_SERVERS][48];
        long SerialNumber[MAX_SERVERS];
        int  ApplicationNumber[MAX_SERVERS];

// Таблица информации о файл-серверах

        FILE_SERV_INFO ServerInfo[MAX_SERVERS];

// Таблица номеров каналов файл-серверов

        WORD    ConnID[MAX_SERVERS];

// Функции для приема и передачи SAP-пакетов

        void    ReceiveSAPPacket(RECEIVE_PACKET  *Query);
        void    SendSAPPacket(void);
// Функции для получения имен файл-серверов и
// другой информации о файл-серверах

        void  GetServersName(void);
        void  GetServersInfo(void);

public:

        int errno;              // код ошибки
        WORD PreferredConnID;   // предпочтительный сервер
        WORD PrimaryConnID;     // первичный сервер
        WORD DefaultConnID;     // сервер по умолчанию

        BYTE MajorVersion;      // верхний номер версии
        BYTE MinorVersion;      // нижний номер версии
        BYTE Revision;          // номер изменений

        SLIST(int);  // конструктор
        ~SLIST();    // деструктор

// Функция для вывода имен серверов

        void  PrintServersName(void);

// Проверка ошибок

        int Error(void) { return errno; }
};


2.4.2. Пограмма LOG

В этом разделе мы приведем исходный текст программы, выполняющей подключение пользователя к файл-серверу. Возможности этой программы ограничены по сравнению со стандартной утилитой login.exe: она, например, не выполняет интерпретацию файлов Login Script и System Login Script. После подключения к файл-серверу диск "S:" рабочей станции отображается на том SYS:. Вы можете использовать нашу программу как прототип собственной процедуры подключения к файл-серверу.

После проверки присутствия сетевой оболочки программа LOG с помощью функции GetConnectionNumber() получает номер канала текущего файл-сервера и затем, вызвав функцию GetFileServerName(), определяет имя текущего файл-сервера. Имя и номер канала текущего сервера выводятся в стандартный поток вывода.

Далее программа запрашивает имя сервера, имя пользователя и его пароль, при помощи функции AttachToFileServer() создает канал с указанным файл-сервером. Если канал уже есть или его удалось создать, новый сервер делается предпочтительным, для чего вызывается функция SetPreferredConnectionID().

Затем вызывается функция LoginToFileServer(). Она пытается подключить пользователя к предпочтительному серверу.

После подключения программа с помощью функции CheckConsolePrivileges() проверяет, имеет ли данный пользователь права оператора консоли, и выводит соответствующее сообщение.

Для того чтобы получить информацию о сервере, к которому только что подключился пользователь, программа LOG вызывает функцию GetFileServerDescriptionStrings(), которая записывает в четыре буфера имя фирмы-изготовителя, изменения и дату изменений, права на сетевую операционную систему. Содержимое всех этих буферов выводится в стандартный поток вывода.

Затем вызывается функция GetServerInformation(). С ее помощью определяется максимальное количество пользователей для данного сервера.

Так как мы только что подключились к файл-серверу, он должен стать первичным, поэтому на следующем шаге программа LOG вызывает функцию SetPrimaryConnectionID() и делает новый сервер первичным.

Подключившись к файл-серверу, вы еще не имеете доступа к его томам. Для того чтобы вы могли работать с дисками файл-сервера, вам необходимо отобразить один или несколько локальных дисков на сетевые каталоги. В нашей программе мы отображаем диск "S:" на корневой каталог тома SYS: нового первичного сервера. Для этого мы вызываем функцию AllocPermanentDirectoryHandle(). Эту функцию, а также все, что связано с дисками сервера, мы рассмотрим в следующей главе.

// ===================================================
// Листинг 6. Подключение к серверу
// Файл log\log.c
//
// (C) A. Frolov, 1993
// ===================================================

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "nit.h"         // include-файлы из библиоткеи
#include "niterror.h"    //  NetWare C Interface

// Эта функция не описана в include-файлах
// библиотеки NetWare C Interface, поэтому опишем ее сами.

void GetServerInformation(int, FILE_SERV_INFO*);

void main(void) {

  int ccode;
  char ServerName[48];
  char UserName[48];
  char Password[128];
  WORD ConnID, ConnNumber;

  char companyName[80],  revision[80];
  char revisionDate[24], copyrightNotice[80];

  FILE_SERV_INFO serverInfo;
  BYTE newDirectoryHandle, effectiveRightsMask;
  char driveLetter;

  char MajorVersion=0;
  char MinorVersion=0;
  char Revision=0;

  printf("NetWare Login, (C) Фролов А.В., 1993\n");

  asm push si
  GetNetWareShellVersion(&MajorVersion,
                        &MinorVersion, &Revision);
  asm pop si

  if(MajorVersion == 0) {
                printf("\nОболочка NetWare не загружена\n");
                return;
  }

// Получаем номер канала, используемого сервером
// по умолчанию (default) для связи с рабочей станцией, на
// которой была запущена эта программа

  ConnNumber = GetConnectionNumber();

// Получаем имя файл-сервера, используемого по умолчанию (default)

  GetFileServerName(0, ServerName);

// Выводим имя и номер канала для
// сервера, используемого по умолчанию

  if(ConnNumber)
                printf("Сервер по умолчанию '%s', ConnNumber=%04.4X\n",
                        ServerName, ConnNumber);

// Вводим имя сервера, имя пользователя и пароль.
// Преобразуем все введенные буквы в заглавные.

  printf("\nВведите имя сервера: ");
  gets(ServerName);
  strupr(ServerName);

  printf("\nВведите ваше имя: ");
  gets(UserName);
  strupr(UserName);

  printf("\nВведите пароль: ");
  gets(Password);
  strupr(Password);

// Создаем канал с сервером

  ccode = AttachToFileServer(ServerName, &ConnID);

// Если канал удалось создать или он уже был создан раньше,
// выводим имя сервера и номер канала, используемого
// на рабочей станции для идентификации сервера.

  if(ccode == 0 || ccode == ALREADY_ATTACHED_TO_SERVER) {
                printf("\nServerName='%s', ServerID=%04.4X",
                        ServerName, ConnID);

// Делаем данный сервер предпочтительным для того,
// чтобы все запросы направлялись к нему в первую очередь
                SetPreferredConnectionID(ConnID);

// Подключаем пользователя к файл-серверу

                ccode = LoginToFileServer(UserName,OT_USER,Password);
                if(!ccode) {

// Если подключение произошло успешно, проверяем, есть ли
// у подключившегося пользователя права оператора консоли

                        if(!CheckConsolePrivileges())
                                printf("Вы оператор консоли\n");

// Получаем строки описания сервера и выводим их
// в стандартный поток

                        GetFileServerDescriptionStrings(companyName,
                                revision, revisionDate, copyrightNotice);

                        printf("Описание сервера:\n%s\n%s\n\n%s\n%s\n",
                                companyName,revision, revisionDate,
                                copyrightNotice);

// Получаем информацию о сервере, выводим максимальное количество
// пользователей, которые могут подключиться к 
// данному файл-серверу.

                        GetServerInformation(sizeof(serverInfo),                                                &serverInfo);
                        printf("Версия на %d пользователей\n",
                                serverInfo.maxConnectionsSupported);

// Делаем данный сервер первичным.

                        SetPrimaryConnectionID(ConnID);

// Отображаем диск S: рабочей станции на
// корневой каталог тома SYS: сервера

                        driveLetter = 'S';

                        ccode = AllocPermanentDirectoryHandle(0,"SYS:\\",
                                driveLetter,
                                &newDirectoryHandle,&effectiveRightsMask);
                        printf("Диск отображен, код CCode = %d\n",ccode);
                }
  }
  else {
         printf("Ошибка при подключении: %04.4X\n",ccode);
         return;
  }
}