close

Вход

Забыли?

вход по аккаунту

?

1791.Основы разработки сетевых Windows-приложений

код для вставкиСкачать
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Министерство образования и науки Российской Федерации
Федеральное агентство по образованию
Ярославский государственный университет им. П.Г.Демидова
В.В. Васильчиков
Основы разработки сетевых
Windows-приложений
Учебное пособие
Рекомендовано
Научно-методическим советом университета
для студентов специальностей Прикладная математика
и информатика и Математическое обеспечение
и администрирование информационных систем
Ярославль 2007
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
УДК 004.4:004.7
ББК 3973.202я73
В 19
Рекомендовано
Редакционно-издательским советом университета
в качестве учебного издания. План 2007 года
Рецензенты:
кандидат физико-математических наук С.И. Щукин;
кафедра теории и методики обучения информатике
ЯГПУ им. К.Д. Ушинского
В 19
Васильчиков, В.В. Основы разработки сетевых Windowsприложений : учебное пособие / В.В. Васильчиков ; Яросл. гос.
ун-т. – Ярославль : ЯрГУ, 2007. – 212 с.
ISBN 978-5-8397-0533-3
Рассмотрены основные моменты разработки сетевых
приложений для платформы Win32, сетевые функции Windows, а
также наиболее распространенные сетевые протоколы.
Рекомендуется студентам, обучающимся по специальностям
010501 Прикладная математика и информатика (дисциплина
специализации "Программирование в Windows и сетях Windows")
и 010503 Математическое обеспечение и администрирование
информационных систем (дисциплина специализации "Программирование в сетях Windows") очной формы обучения.
Библиогр.: 5 назв.
УДК 004.4:004.7
ББК 3973.202я73
© Ярославский
государственный
университет, 2007
© В.В. Васильчиков, 2007
ISBN 978-5-8397-0533-3
2
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Введение
Данное учебное пособие написано на основе лекционного курса по
программированию в сетях Windows, читавшегося автором для студентов
факультета ИВТ ЯрГУ, обучающихся по специальности "Математическое
обеспечение и администрирование информационных систем".
В пособии рассмотрены существующие технологии и Win32 APIфункции, использующиеся для создания сетевых приложений различных
версий ОС Windows. При этом все, что говорится о Windows 2000,
остается в силе и для более свежих версий операционной системы.
Первые главы посвящены рассмотрению несколько устаревших,
однако все еще использующихся технологий: интерфейса NetBIOS,
перенаправителей Windows, почтовых ящиков и именованных каналов.
Далее рассматривается API-интерфейс Winsock, наиболее распространенные сетевые протоколы Winsock, базовая модель программирования
клиент-сервер и соответствующие функции, предоставляемые Win32 API.
При этом основное внимание уделялось программированию протоколов,
построенных на базе протокола IP. Наконец, последняя глава коротко
описывает основные моменты работы с сервером удаленного доступа RAS,
используемого для коммутируемого доступа к Интернет.
Основная часть учебного материала и исходных кодов взята из книги
Джонса и Оланда [1]. Все примеры программ написаны на языке С и не
привязаны к использованию какой-либо конкретной среды программирования. При этом для организации работы с сетью используются только
Win32 API-функции. Читателям, интересующимся более высокоуровневыми средствами, например классами библиотеки MFC для работы в
сети, можно порекомендовать обратиться к книгам Олафсена, Скрайбера
и Уайта [3], Круглински, Уингоу и Шеферда [4], а также к документации
MSDN. Однако для понимания устройства и работы этих классов все
же настоятельно рекомендуется изучить использование соответствующих функций Win32 API.
Для удобства использования все исходные коды, сгруппированные по
темам учебного курса (а на них в тексте пособия постоянно встречаются
ссылки), доступны в локальной сети факультета.
Для лабораторных занятий студентам предлагаются задания по разработке программ, функционально эквивалентных рассмотренным, однако
имеющих привычный оконный интерфейс. В качестве среды разработки
предполагается использование Microsoft Visual Studio версии 6.0. Проекты
структурированы по темам учебного курса. Предполагаемый результат
находится в папке Solution. Стартовый проект (заготовка с предлагаемым
интерфейсом) находится в папке Starter. Инструкции по выполнению
задания помещены в файл ToDo.doc.
3
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Интерфейс NetBIOS
Network Basic Input/Output System (NetBIOS) – стандартный
интерфейс прикладного программирования, разработанный еще в 1983
году. Он определяет программный интерфейс для сетевой связи, но не
обусловливает физический способ передачи данных по сети. В 1985 году
фирма IBM сформировала цельный протокол NetBIOS Extended User
Interface (NetBEUI), интегрированный с интерфейсом NetBIOS.
Впоследствии его реализовали и для протоколов TCP/IP и IPX/SPX. В
настоящее время NetBIOS используется многими приложениями и
операционными системами, включая все варианты Windows (Windows CE
не дает возможности использовать NetBIOS API).
NetBIOS и сетевая модель OSI
Модель Open Systems Interconnect (OSI) обеспечивает высокоуровневое представление сетевых систем. Для описания сетевых
концепций она использует семь уровней (см. рисунок):
4
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
• прикладной – предоставляет пользовательский интерфейс для
передачи данных между программами;
• представительский – форматирует данные;
• сеансовый – управляет связью между двумя узлами;
• транспортный – обеспечивает передачу данных (надежную или
ненадежную);
• сетевой – поддерживает механизм адресации между узлами и
маршрутизацию пакетов данных;
• канальный – управляет взаимодействием между узлами на
физическом уровне, отвечает за группировку данных,
передаваемых по физическому носителю;
• физический – физический носитель, ответственный за передачу
данных в виде электрических сигналов.
Здесь, правда, следует заметить, что, несмотря на частое упоминание
в литературе по компьютерным сетям, модель OSI обладает изрядной
условностью. Она была изначально спроектирована без готовой
реализации, и потому распределение функций между уровнями не всегда
очевидно, ряд функций выполняется сразу на нескольких уровнях.
Конкретные реализации могут использовать не все уровни, редко,
например, используются уровень представления и сеансовый уровень.
Если говорить о NetBIOS, то в модели OSI он относится к сеансовому
и транспортному уровню.
Интерфейс Microsoft NetBIOS
В известном смысле интерфейс NetBIOS не зависит от сетевого
протокола. Если приложение разработано согласно спецификации
NetBIOS, оно может использовать и протокол TCP/IP, и IPX/SPX, и
NetBEUI. Однако оба связывающихся компьютера должны иметь хотя бы
один общий сетевой протокол.
Кроме того, не все протоколы по умолчанию реализуют интерфейс
NetBIOS. Например, Microsoft TCP/IP и NetBEUI поддерживают, а
IPX/SPX нет. Однако при установке IPX/SPX можно в окне свойств
поставить флажок, с помощью которого включается поддержка NetBIOS
для данного сетевого протокола.
Следует также учитывать, что NetBEUI – немаршрутизируемый
протокол. Если между клиентом и сервером есть маршрутизатор, то
связаться они не смогут. Для TCP/IP и IPX/SPX такого ограничения не
существует.
Номера LANA
Основное понятие интерфейса NetBIOS – это номера сетевых
адаптеров (Local Area Network Adapter, LANA). Первоначально каждому
5
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
физическому сетевому адаптеру присваивалось уникальное значение –
номер LANA.
Номер LANA соответствует уникальным сочетаниям сетевого
адаптера с транспортным протоколом. Например, если на компьютере
установлено две сетевых платы и два поддерживающих NetBIOS сетевых
протокола, то их сочетаниям будет присвоено четыре номера LANA. Эти
номера лежат в диапазоне от 0 до 9, и операционная система назначает их
без какого-либо определенного порядка, за исключением того, что LANA 0
– номер по умолчанию. Некоторые старые приложения жестко
запрограммированы на работу только с LANA 0. Для работы с ними можно
вручную назначить этот номер конкретному сетевому протоколу.
Имена NetBIOS
Приложение регистрирует имя на каждом номере LANA, с которым
ему требуется связаться. Имя NetBIOS имеет длину 16 символов, причем
16 символ зарезервирован для специальных целей (определяет сетевую
службу Microsoft, более подробно этот вопрос не рассматриваем). В среде
Win32 каждый процесс имеет таблицу имен для каждого доступного
номера LANA (не более 254 имен с номерами от 1 до 254, номера 0 и 255
зарезервированы для системы).
Есть два типа имен NetBIOS: уникальное и групповое. Имена
компьютеров в сетях Microsoft – имена NetBIOS. Когда компьютер
загружается, он регистрирует свое уникальное имя на сервере WINS
(Windows Internet Naming Server). Сервер WINS поддерживает список всех
зарезервированных имен NetBIOS. Вместе с именем может храниться
информация и о протоколе. Например, в сетях TCP/IP запоминает IP-адрес
компьютера.
Групповые имена используются, чтобы отправлять или получать
данные, предназначенные для множества получателей.
NetBIOS предлагает как службы, требующие установления
соединения (сеанс связи), так и службы, которые его не требуют
(дейтаграммные). В первом случае доставка данных гарантируется. Сервер
обычно регистрирует себя под определенным известным именем, а
клиенты ищут это имя, чтобы связаться с сервером.
При использовании дейтаграммных служб сервер регистрируется под
конкретным именем, а клиент просто посылает данные в сеть на NetBIOSимя серверного процесса, не устанавливая соединения. При этом не
тратится время и ресурсы на установление соединения, однако
отсутствуют гарантии доставки и порядка сообщений.
Основы программирования NetBIOS
API-интерфейс NetBIOS содержит всего одну функцию:
UCHAR Netbios (PNCB pncb);
6
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Все объявления функций, константы и т.п. определены в
заголовочном файле Nb30.h. Для компоновки требуется только одна
библиотека – Netapi32.lib.
Параметр функции Netbios является указателем на блок сетевого
управления (network control block, NCB). Структура NCB определена
следующим образом:
typedef struct _NCB {
UCHAR
ncb_command;
UCHAR
ncb_retcode;
UCHAR
ncb_lsn;
UCHAR
ncb_num;
PUCHAR
ncb_buffer;
WORD
ncb_length;
UCHAR
ncb_callname[NCBNAMSZ];
UCHAR
ncb_name[NCBNAMSZ];
UCHAR
ncb_rto;
UCHAR
ncb_sto;
void
(CALLBACK *ncb_post) (struct _NCB *);
UCHAR
ncb_lana_num;
UCHAR
ncb_cmd_cplt;
UCHAR
ncb_reserve[10];
HANDLE
ncb_event;
} NCB, *PNCB;
Не все поля структуры используются при каждом вызове, поэтому
перед заполнением структуры ее есть смысл обнулить. Некоторые из полей
являются выходными значениями. Ниже описано значение каждого поля:
• ncb_command – указывает выполняемую команду Netbios.
Логическое сложение с флагом ASYNCH (0x80) приводит к
асинхронному выполнению;
• ncb_retcode – код возврата для данной операции;
• ncb_lsn – номер локального сеанса;
• ncb_num – номер локального сетевого имени;
• ncb_buffer – буфер данных;
• ncb_length – размер буфера в байтах;
• ncb_callname – имя удаленного приложения;
• ncb_name – имя приложения;
• ncb_rto – тайм-аут для операций приема в единицах по 500 миллисекунд.
• ncb_sto – тайм-аут для операций отправки в единицах по 500
миллисекунд.
• ncb_post – адрес процедуры, которую надо вызвать по завершении
асинхронной команды. Ее формат:
7
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
void CALLBACK PostRoutine (PNCB pncb);
•
•
•
•
Здесь pncb – указатель на блок сетевого управления завершенной
команды.
ncb_lana_num – номер LANA для выполнения команды;
ncb_cmd_cplt – код возврата, то же самое значение, что и
ncb_retcode;
ncb_reserve – зарезервировано, должно быть равно 0;
ncb_event – указывает описатель объекта события Windows в
свободном состоянии, после завершения асинхронной команды
событие переходит в занятое состояние.
Синхронный и асинхронный вызов
Большинство команд, выполняемых через вызов функции, можно
вызвать синхронно (т.е. функция не возвратит управление, пока команда
не будет выполнена), либо асинхронно. Для асинхронного вызова команды
необходимо в поле ncb_command установить флаг ASYNCH. В этом случае
требуется либо определить вызываемую после выполнения команды
процедуру (поле ncb_post), либо дескриптор события (поле ncb_event).
Типовые процедуры NetBIOS
Общие функции приложений NetBIOS
Ниже приводится исходный код основных функций типичного
клиентского или серверного приложения, реализующего асинхронную
модель NetBIOS. Полный код содержится в файле Nbcommon.c.
#include
#include
#include
#include
<windows.h>
<stdio.h>
<stdlib.h>
"nbcommon.h"
// Перечислить все номера LANA
int LanaEnum(LANA_ENUM *lenum)
{
NCB
ncb;
ZeroMemory(&ncb, sizeof(NCB));
ncb.ncb_command = NCBENUM;
ncb.ncb_buffer = (PUCHAR)lenum;
ncb.ncb_length = sizeof(LANA_ENUM);
if (Netbios(&ncb) != NRC_GOODRET)
{
printf("ERROR: Netbios: NCBENUM: %d\n",
ncb.ncb_retcode);
8
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
return ncb.ncb_retcode;
}
return NRC_GOODRET;
}
//
// Сбор сведений о LANA, перечисленных в структуре LANA_ENUM.
// Настройка среды NetBIOS (макс. число сеансов, макс. размер
// таблицы имен), использование первого NetBIOS–имени.
//
int ResetAll(LANA_ENUM *lenum, UCHAR ucMaxSession,
UCHAR ucMaxName, BOOL bFirstName)
{
NCB
ncb;
int
i;
ZeroMemory(&ncb, sizeof(NCB));
ncb.ncb_command = NCBRESET;
ncb.ncb_callname[0] = ucMaxSession;
ncb.ncb_callname[2] = ucMaxName;
ncb.ncb_callname[3] = (UCHAR)bFirstName;
for(i = 0; i < lenum->length; i++)
{
ncb.ncb_lana_num = lenum->lana[i];
if (Netbios(&ncb) != NRC_GOODRET)
{
printf("ERROR: Netbios: NCBRESET[%d]: %d\n",
ncb.ncb_lana_num, ncb.ncb_retcode);
return ncb.ncb_retcode;
}
}
return NRC_GOODRET;
}
//
// Добавление указанного имени данному LANA.
// Возвращает номер для зарегистрированного имени.
//
int AddName(int lana, char *name, int *num)
{
NCB
ncb;
ZeroMemory(&ncb, sizeof(NCB));
ncb.ncb_command = NCBADDNAME;
ncb.ncb_lana_num = lana;
9
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
memset(ncb.ncb_name, ' ', NCBNAMSZ);
strncpy(ncb.ncb_name, name, strlen(name));
if (Netbios(&ncb) != NRC_GOODRET)
{
printf("ERROR: Netbios: NCBADDNAME[lana=%d;name=%s]: %d\n",
lana, name, ncb.ncb_retcode);
return ncb.ncb_retcode;
}
*num = ncb.ncb_num;
return NRC_GOODRET;
}
//
// Добавление данного группового имени NetBIOS данному LANA.
// Возвращает номер добавленного имени.
//
int AddGroupName(int lana, char *name, int *num)
{
NCB
ncb;
ZeroMemory(&ncb, sizeof(NCB));
ncb.ncb_command = NCBADDGRNAME;
ncb.ncb_lana_num = lana;
memset(ncb.ncb_name, ' ', NCBNAMSZ);
strncpy(ncb.ncb_name, name, strlen(name));
if (Netbios(&ncb) != NRC_GOODRET)
{
printf("ERROR: Netbios: NCBADDGRNAME[lana=%d;name=%s]: %d\n",
lana, name, ncb.ncb_retcode);
return ncb.ncb_retcode;
}
*num = ncb.ncb_num;
return NRC_GOODRET;
}
//
// Удаление данного имени NetBIOS из таблицы имен
// для данного LANA
//
int DelName(int lana, char *name)
{
NCB
ncb;
ZeroMemory(&ncb, sizeof(NCB));
ncb.ncb_command = NCBDELNAME;
ncb.ncb_lana_num = lana;
10
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
memset(ncb.ncb_name, ' ', NCBNAMSZ);
strncpy(ncb.ncb_name, name, strlen(name));
if (Netbios(&ncb) != NRC_GOODRET)
{
printf("ERROR: Netbios: NCBADDNAME[lana=%d;name=%s]: %d\n",
lana, name, ncb.ncb_retcode);
return ncb.ncb_retcode;
}
return NRC_GOODRET;
}
//
// Отправка len байтов по указанному сеансу(lsn) и номеру lana
//
int Send(int lana, int lsn, char *data, DWORD len)
{
NCB
ncb;
int
retcode;
ZeroMemory(&ncb, sizeof(NCB));
ncb.ncb_command = NCBSEND;
ncb.ncb_buffer = (PUCHAR)data;
ncb.ncb_length = len;
ncb.ncb_lana_num = lana;
ncb.ncb_lsn = lsn;
retcode = Netbios(&ncb);
return retcode;
}
//
// Прием до len байтов по указанному сеансу(lsn) и номеру lana
//
int Recv(int lana, int lsn, char *buffer, DWORD *len)
{
NCB
ncb;
ZeroMemory(&ncb, sizeof(NCB));
ncb.ncb_command = NCBRECV;
ncb.ncb_buffer = (PUCHAR)buffer;
ncb.ncb_length = *len;
ncb.ncb_lana_num = lana;
ncb.ncb_lsn = lsn;
11
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if (Netbios(&ncb) != NRC_GOODRET)
{
*len = -1;
return ncb.ncb_retcode;
}
*len = ncb.ncb_length;
return NRC_GOODRET;
}
//
// Прекращение указанного сеанса на данном номере lana
//
int Hangup(int lana, int lsn)
{
NCB
ncb;
int
retcode;
ZeroMemory(&ncb, sizeof(NCB));
ncb.ncb_command = NCBHANGUP;
ncb.ncb_lsn = lsn;
ncb.ncb_lana_num = lana;
retcode = Netbios(&ncb);
return retcode;
}
//
// Отмена данной асинхронной команды, указанной в структуре NCB
//
int Cancel(PNCB pncb)
{
NCB
ncb;
ZeroMemory(&ncb, sizeof(NCB));
ncb.ncb_command = NCBCANCEL;
ncb.ncb_buffer = (PUCHAR)pncb;
ncb.ncb_lana_num = pncb->ncb_lana_num;
if (Netbios(&ncb) != NRC_GOODRET)
{
printf("ERROR: NetBIOS: NCBCANCEL: %d\n", ncb.ncb_retcode);
return ncb.ncb_retcode;
}
return NRC_GOODRET;
}
12
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
//
// Форматирование указанного имени NetBIOS для печати
// Размер выходного буфера outname не менее NCBNAMSZ + 1 символов
//
int FormatNetbiosName(char *nbname, char *outname)
{
int
i;
strncpy(outname, nbname, NCBNAMSZ);
outname[NCBNAMSZ - 1] = '\0';
for(i = 0; i < NCBNAMSZ - 1; i++)
{
// Непечатные символы заменяются точками
//
if (!((outname[i] >= 32) && (outname[i] <= 126)))
outname[i] = '.';
}
return NRC_GOODRET;
}
Большинство из использованных в коде процедур далее
рассматриваются более подробно. Здесь мы ограничимся краткими
комментариями.
Функция LanaEnum используется практически всеми приложениями
NetBIOS. Она перечисляет доступные номера LANA в данной системе. В
случае ее успешного завершения в предоставленную структуру
LANA_ENUM будут записаны количество номеров LANA на данном
компьютере и их фактические номера. Формат структуры:
typedef struct _LANA_ENUM {
UCHAR length;
UCHAR lana[MAX_LANA];
} LANA_ENUM, *PLANA_ENUM;
Хорошо написанная программа NetBIOS должна обнулить каждый
номер LANA, который собирается применить. Это делает функция
ResetAll, которая также присутствует почти в каждом приложении
NetBIOS. Кроме того, здесь же задаются определенные параметры среды
NetBIOS.
Назначение остальных использованных функций достаточно понятно
из комментариев.
Сервер сеансов: модель асинхронного обратного вызова
Ниже приводится пример исходного кода простого эхо-сервера,
использующего функции обратного вызова. Текст содержится в файле
Cbnbsvr.c.
13
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include "..\Common\nbcommon.h "
#define MAX_BUFFER
#define SERVER_NAME
2048
"TEST-SERVER-1"
DWORD WINAPI ClientThread(PVOID lpParam);
//
// Функция: ListenCallback
// Вызывается при положительном результате прослушивания клиента.
// Если нет ошибок, то создает поток для работы с клиентом.
// Дается команда на продолжение прослушивания.
//
void CALLBACK ListenCallback(PNCB pncb)
{
HANDLE
hThread;
DWORD
dwThreadId;
if (pncb->ncb_retcode != NRC_GOODRET)
{
printf("ERROR: ListenCallback: %d\n", pncb->ncb_retcode);
return;
}
Listen(pncb->ncb_lana_num, SERVER_NAME);
hThread = CreateThread(NULL, 0, ClientThread,
(PVOID)pncb, 0, &dwThreadId);
if (hThread == NULL)
{
printf("ERROR: CreateThread: %d\n", GetLastError());
return;
}
CloseHandle(hThread);
return;
}
14
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
//
// Функция: ClientThread
// Клиентский поток получает данные от клиентов
// (в блокирующем режиме) и отправляет их назад.
// Работает, пока не закрыт сеанс, или не произойдет ошибка.
//
DWORD WINAPI ClientThread(PVOID lpParam)
{
PNCB
pncb = (PNCB)lpParam;
NCB
ncb;
char
szRecvBuff[MAX_BUFFER];
DWORD
dwBufferLen = MAX_BUFFER,
dwRetVal = NRC_GOODRET;
char
szClientName[NCBNAMSZ+1];
FormatNetbiosName(pncb->ncb_callname, szClientName);
while (1)
{
dwBufferLen = MAX_BUFFER;
dwRetVal = Recv(pncb->ncb_lana_num, pncb->ncb_lsn,
szRecvBuff, &dwBufferLen);
if (dwRetVal != NRC_GOODRET)
break;
szRecvBuff[dwBufferLen] = 0;
printf("READ [LANA=%d]: '%s'\n", pncb->ncb_lana_num,
szRecvBuff);
dwRetVal = Send(pncb->ncb_lana_num, pncb->ncb_lsn,
szRecvBuff, dwBufferLen);
if (dwRetVal != NRC_GOODRET)
break;
}
printf("Client '%s' on LANA %d disconnected\n",
szClientName, pncb->ncb_lana_num);
if (dwRetVal != NRC_SCLOSED)
{
// Непредусмотренная ошибка. Соединение закрывается.
ZeroMemory(&ncb, sizeof(NCB));
ncb.ncb_command = NCBHANGUP;
ncb.ncb_lsn = pncb->ncb_lsn;
ncb.ncb_lana_num = pncb->ncb_lana_num;
15
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if (Netbios(&ncb) != NRC_GOODRET)
{
printf("ERROR: Netbios: NCBHANGUP: %d\n",
ncb.ncb_retcode);
dwRetVal = ncb.ncb_retcode;
}
GlobalFree(pncb);
return dwRetVal;
}
GlobalFree(pncb);
return NRC_GOODRET;
}
//
// Функция : Listen
// Инициирует асинхронное прослушивание с помощью функции
// обратного вызова. Создается глобальная структура NCB
// для использования функцией обратного вызова
//
int Listen(int lana, char *name)
{
PNCB pncb = NULL;
pncb = (PNCB)GlobalAlloc
(GMEM_FIXED | GMEM_ZEROINIT, sizeof(NCB));
pncb->ncb_command = NCBLISTEN | ASYNCH;
pncb->ncb_lana_num = lana;
pncb->ncb_post = ListenCallback;
// Имя, с которым клиенты будут соединяться
memset(pncb->ncb_name, ' ', NCBNAMSZ);
strncpy(pncb->ncb_name, name, strlen(name));
// Имя клиента,
// '*' означает, что примем соединение от любого клиента
memset(pncb->ncb_callname, ' ', NCBNAMSZ);
pncb->ncb_callname[0] = '*';
if (Netbios(pncb) != NRC_GOODRET)
{
printf("ERROR: Netbios: NCBLISTEN: %d\n",
pncb->ncb_retcode);
return pncb->ncb_retcode;
}
return NRC_GOODRET;
}
16
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
//
// Функция: main
// Инициализирует интерфейс NetBIOS, выделяет ресурсы,
// добавляет имя сервера к каждому номеру LANA.
// Слушает каждый номер LANA с соответствующим обратным вызовом.
// Больше главный поток ничего не делает.
//
int main(int argc, char **argv)
{
LANA_ENUM
lenum;
int
i,
num;
// Перечисляются и инициализируются все номера LANA
//
if (LanaEnum(&lenum) != NRC_GOODRET)
return 1;
if (ResetAll(&lenum, 254, 254, FALSE) != NRC_GOODRET)
return 1;
// Каждому номеру LANA добавляется имя сервера
// и дается команда слушать
for(i = 0; i < lenum.length; i++)
{
AddName(lenum.lana[i], SERVER_NAME, &num);
Listen(lenum.lana[i], SERVER_NAME);
}
while (1)
{
Sleep(5000);
}
}
Пример сервера, основанный на модели событий
Приведенный ниже пример исходного кода (файл Envbsvr.c) – это
также код простого эхо-сервера, однако в качестве механизма оповещения
о завершении операции используются события Win32.
Последовательность действий аналогична действиям сервера
обратного вызова:
1. Перечисляются номера LANA.
2. Каждый номер LANA сбрасывается.
3. Имя сервера добавляется к каждому номеру LANA.
4. На каждом номере LANA инициируется прослушивание.
17
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
При этом необходимо отслеживать все невыполненные команды
прослушивания,
чтобы
сопоставить
завершение
события
с
соответствующими блоками NCB, инициализирующими конкретную
команду.
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include "..\Common\nbcommon.h"
#define MAX_SESSIONS
#define MAX_NAMES
254
254
#define MAX_BUFFER
#define SERVER_NAME
2048
"TEST-SERVER-1"
NCB *g_Clients=NULL; // Глобальная структура NCB для клиентов
//
// Функция: ClientThread
// Поток берет структуру NCB из сеанса соединения и в цикле,
// пока не будет закрыт, ждет входящих данных
// и отправляет их обратно.
//
DWORD WINAPI ClientThread(PVOID lpParam)
{
PNCB
pncb = (PNCB)lpParam;
NCB
ncb;
char
szRecvBuff[MAX_BUFFER],
szClientName[NCBNAMSZ + 1];
DWORD
dwBufferLen = MAX_BUFFER,
dwRetVal = NRC_GOODRET;
// Отправка и прием сообщений, пока сеанс не закрыт
FormatNetbiosName(pncb->ncb_callname, szClientName);
while (1)
{
dwBufferLen = MAX_BUFFER;
dwRetVal = Recv(pncb->ncb_lana_num, pncb->ncb_lsn,
szRecvBuff, &dwBufferLen);
if (dwRetVal != NRC_GOODRET)
break;
szRecvBuff[dwBufferLen] = 0;
18
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
printf("READ [LANA=%d]: '%s'\n", pncb->ncb_lana_num,
szRecvBuff);
dwRetVal = Send(pncb->ncb_lana_num, pncb->ncb_lsn,
szRecvBuff, dwBufferLen);
if (dwRetVal != NRC_GOODRET)
break;
}
printf("Client '%s' on LANA %d disconnected\n",
szClientName, pncb->ncb_lana_num);
// Обработка ошибок в ходе чтения или записи
if (dwRetVal != NRC_SCLOSED)
{
ZeroMemory(&ncb, sizeof(NCB));
ncb.ncb_command = NCBHANGUP;
ncb.ncb_lsn = pncb->ncb_lsn;
ncb.ncb_lana_num = pncb->ncb_lana_num;
if (Netbios(&ncb) != NRC_GOODRET)
{
printf("ERROR: Netbios: NCBHANGUP: %d\n",
ncb.ncb_retcode);
GlobalFree(pncb);
dwRetVal = ncb.ncb_retcode;
}
}
// Удаление динамически выделенной структуры NCB
GlobalFree(pncb);
return NRC_GOODRET;
}
//
// Функция: Listen
// Дается команда асинхронно слушать на указанном LANA.
// В переданной структуре NCB полю ncb_event
// уже присвоено значение дескриптора события.
//
int Listen(PNCB pncb, int lana, char *name)
{
pncb->ncb_command = NCBLISTEN | ASYNCH;
pncb->ncb_lana_num = lana;
//
// Имя, с которым клиенты будут соединяться
memset(pncb->ncb_name, ' ', NCBNAMSZ);
strncpy(pncb->ncb_name, name, strlen(name));
19
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Имя клиента,
// '*' означает, что примем соединение от любого клиента
memset(pncb->ncb_callname, ' ', NCBNAMSZ);
pncb->ncb_callname[0] = '*';
if (Netbios(pncb) != NRC_GOODRET)
{
printf("ERROR: Netbios: NCBLISTEN: %d\n",
pncb->ncb_retcode);
return pncb->ncb_retcode;
}
return NRC_GOODRET;
}
//
// Функция: main
// Инициализирует интерфейс NetBIOS, выделяет ресурсы,
// дает асинхронную команду слушать каждому LANA,
// используя события. Ждет, пока событие не сработает,
// затем обрабатывает клиентское соединение.
//
int main(int argc, char **argv)
{
PNCB
pncb=NULL;
HANDLE
hArray[64],
hThread;
DWORD
dwHandleCount=0,
dwRet,
dwThreadId;
int
i,
num;
LANA_ENUM
lenum;
// Перечисление и сброс всех номеров LANA
if (LanaEnum(&lenum) != NRC_GOODRET)
return 1;
if (ResetAll(&lenum, (UCHAR)MAX_SESSIONS,
(UCHAR)MAX_NAMES, FALSE) != NRC_GOODRET)
return 1;
// Выделение массива структур NCB (по одному на каждый LANA)
g_Clients = (PNCB)GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT,
sizeof(NCB) * lenum.length);
20
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Создание событий, добавление имени сервера
// каждому номеру LANA, начало асинхронного прослушивания
for(i = 0; i < lenum.length; i++)
{
hArray[i] = g_Clients[i].ncb_event =
CreateEvent(NULL, TRUE, FALSE, NULL);
AddName(lenum.lana[i], SERVER_NAME, &num);
Listen(&g_Clients[i], lenum.lana[i], SERVER_NAME);
}
while (1)
{
// Ожидание подключения клиента
dwRet = WaitForMultipleObjects(lenum.length, hArray,
FALSE, INFINITE);
if (dwRet == WAIT_FAILED)
{
printf("ERROR: WaitForMultipleObjects: %d\n",
GetLastError());
break;
}
// Проверка всех структур NCB для определения,
// достигла ли успеха более, чем одна структура. Если
// поле ncb_cmd_plt не содержит значение NRC_PENDING,
// значит существует клиент, необходимо создать поток
// и выделить ему новую структуру NCB.
// Старая структура используется повторно.
for(i = 0; i < lenum.length; i++)
{
if (g_Clients[i].ncb_cmd_cplt != NRC_PENDING)
{
pncb = (PNCB)GlobalAlloc(GMEM_FIXED, sizeof(NCB));
memcpy(pncb, &g_Clients[i], sizeof(NCB));
pncb->ncb_event = 0;
hThread = CreateThread(NULL, 0, ClientThread,
(LPVOID)pncb, 0, &dwThreadId);
CloseHandle(hThread);
// Описатель сбрасывается, начинаем еще
// одно прослушивание
ResetEvent(hArray[i]);
Listen(&g_Clients[i], lenum.lana[i], SERVER_NAME);
}
}
}
21
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Очистка
for(i = 0; i < lenum.length; i++)
{
DelName(lenum.lana[i], SERVER_NAME);
CloseHandle(hArray[i]);
}
GlobalFree(g_Clients);
return 0;
}
В рассмотренных вариантах программирования сервера существует
небольшая вероятность того, что клиенту будет отказано в обслуживании.
Это произойдет в том случае, если сервер обслужил одного клиента на
данном номере LANA, а запрос от второго пришел раньше, чем сервер
успел поставить номер на следующее прослушивание.
Клиент сеанса NetBIOS
Клиент выполняет стандартные шаги инициализации имени:
добавляет свое имя к таблице имен каждого номер LANA, затем дает
асинхронную команду соединения. Далее он ждет, пока не произойдет
одно из событий.
Ниже приводится пример кода клиента, основанного на модели
событий (файл Nbclient.c).
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include "..\Common\nbcommon.h"
#define MAX_SESSIONS
#define MAX_NAMES
254
254
#define MAX_BUFFER
1024
char
szServerName[NCBNAMSZ];
//
// Функция: Connect
// Устанавливает соединение с сервером на заданном номере LANA.
// В переданной структуре NCB поле ncb_event уже заполнено
int Connect(PNCB pncb, int lana, char *server, char *client)
{
pncb->ncb_command = NCBCALL | ASYNCH;
pncb->ncb_lana_num = lana;
22
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
memset(pncb->ncb_name, ' ', NCBNAMSZ);
strncpy(pncb->ncb_name, client, strlen(client));
memset(pncb->ncb_callname, ' ', NCBNAMSZ);
strncpy(pncb->ncb_callname, server, strlen(server));
if (Netbios(pncb) != NRC_GOODRET)
{
printf("ERROR: Netbios: NCBCONNECT: %d\n",
pncb->ncb_retcode);
return pncb->ncb_retcode;
}
return NRC_GOODRET;
}
//
// Функция: main
// Инициализирует интерфейс NetBIOS, распределяет ресурсы,
// дает команду NCBCALL для каждого номера LANA на сервере.
// Когда соединение создано, отменяет или разрывает
// неудавшиеся соединения. Затем посылает и получает данные.
// Наконец, выполняет очистку.
//
int main(int argc, char **argv)
{
HANDLE
*hArray;
NCB
*pncb;
char
szSendBuff[MAX_BUFFER];
DWORD
dwBufferLen,
dwRet,
dwIndex,
dwNum;
LANA_ENUM
lenum;
int
i;
if (argc != 3)
{
printf("usage: nbclient CLIENT-NAME SERVER-NAME\n");
return 1;
}
// Перечисление и сброс всех номеров LANA
if (LanaEnum(&lenum) != NRC_GOODRET)
return 1;
23
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if (ResetAll(&lenum, (UCHAR)MAX_SESSIONS,
(UCHAR)MAX_NAMES, FALSE) != NRC_GOODRET)
return 1;
strcpy(szServerName, argv[2]);
// Размещение массива дескрипторов событий.
// Выделение массива структур NCB.
hArray = (HANDLE *)GlobalAlloc(GMEM_FIXED,
sizeof(HANDLE) * lenum.length);
pncb
= (NCB *)GlobalAlloc(GMEM_FIXED | GMEM_ZEROINIT,
sizeof(NCB) * lenum.length);
// Создание события и присвоение его структуре NCB,
// начало асинхронного соединения (NCBCALL).
for(i = 0; i < lenum.length; i++)
{
hArray[i] = CreateEvent(NULL, TRUE, FALSE, NULL);
pncb[i].ncb_event = hArray[i];
AddName(lenum.lana[i], argv[1], &dwNum);
Connect(&pncb[i], lenum.lana[i], szServerName, argv[1]);
}
// Ждем, пока не сработает хотя бы одно соединение
dwIndex = WaitForMultipleObjects(lenum.length, hArray,
FALSE, INFINITE);
if (dwIndex == WAIT_FAILED)
{
printf("ERROR: WaitForMultipleObjects: %d\n",
GetLastError());
}
else
{
// Если успешно хотя бы одно соединение, остальные мы
// разрываем. Используем соединение, возвращенное
// WaitForMultipleObjects.
// Остальные, если они не установлены еще, отменим
for(i = 0; i < lenum.length; i++)
{
if (i != dwIndex)
{
if (pncb[i].ncb_cmd_cplt == NRC_PENDING)
Cancel(&pncb[i]);
else
Hangup(pncb[i].ncb_lana_num, pncb[i].ncb_lsn);
}
}
24
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
printf("Connected on LANA: %d\n",
pncb[dwIndex].ncb_lana_num);
// Отправка и прием сообщений
for(i = 0; i < 20; i++)
{
wsprintf(szSendBuff, "Test message %03d", i);
dwRet = Send(pncb[dwIndex].ncb_lana_num,
pncb[dwIndex].ncb_lsn, szSendBuff,
strlen(szSendBuff));
if (dwRet != NRC_GOODRET)
break;
dwBufferLen = MAX_BUFFER;
dwRet = Recv(pncb[dwIndex].ncb_lana_num,
pncb[dwIndex].ncb_lsn,
szSendBuff, &dwBufferLen);
if (dwRet != NRC_GOODRET)
break;
szSendBuff[dwBufferLen] = 0;
printf("Read: '%s'\n", szSendBuff);
}
Hangup(pncb[dwIndex].ncb_lana_num,
pncb[dwIndex].ncb_lsn);
}
// Очистка
/
for(i = 0; i < lenum.length; i++)
{
DelName(lenum.lana[i], argv[1]);
CloseHandle(hArray[i]);
}
GlobalFree(hArray);
GlobalFree(pncb);
return 0;
}
Дейтаграммные операции
Обмен дейтаграммами – это способ связи без установления
логического соединения. Отправитель просто направляет каждый пакет по
соответствующему имени NetBIOS. При этом целостность данных и
порядок доставки не гарантируется.
25
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Есть три способа отправить дейтаграмму:
1. Отправить ее на определенное (уникальное) имя;
2. Отправить дейтаграмму на групповое имя, тогда ее смогут
получить только процессы, зарегистрировавшие это имя;
3. Широковещательно разослать ее по всей сети.
Для отправки дейтаграммы на уникальное или групповое имя
используется команда NCBDGSEND, а для широковещательной отправки
– NCBDGSENDBC.
Для отправки нужно выполнить следующие действия:
1. Полю ncb_num присвоить номер имени, возвращенный командой
NCBADDNAME или NCBADDGRNAME.
2. Полям ncb_buffer и ncb_length присвоить адрес буфера и
количество отправляемых данных в байтах.
3. Полю ncb_lana_num присвоить номер LANA приемника.
4. Полю ncb_callname присвоить значение NetBIOS-имени
приемника. Для широковещательной отправки этого делать не
надо.
Для получения дейтаграмм также есть три способа:
1. Использование команды NCBDGRECV для сообщений,
направленных на конкретное имя (уникальное или групповое);
2. Использование команды NCBDGRECV для любой дейтаграммы,
предназначенной для любого имени в таблице имен NetBIOS;
3. Использование команды NCBDGRECVBC для приема широковещательных дейтаграмм.
Для приема нужно выполнить следующие действия:
1. Полю ncb_num присвоить номер имени, возвращенный командой
NCBADDNAME или NCBADDGRNAME. Этот номер определяет,
от какого имени вы слушаете входящие дейтаграммы. Если его
значение равно 0xFF, то вы будете получать дейтаграммы,
предназначенные для любого имени из таблицы имен NetBIOS.
2. Полям ncb_buffer и ncb_length присвоить адрес приемного буфера
и его размер в байтах.
3. Полю ncb_lana_num присвоить номер LANA, на котором вы ждете
дейтаграммы.
После успешного приема поле ncb_length будет содержать
фактическое количество полученных байтов, а поле ncb_callname –
NetBIOS-имя процесса-отправителя.
В файле Nbdgram.c содержится пример программы для обмена
дейтаграммами, однако из-за ее объемности здесь этот код не приводится.
Однако в учебных целях ее рекомендуется скомпилировать и запустить на
двух сетевых компьютерах в различных вариантах, а именно:
26
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Команда клиента
Команда сервера
Nbdgram /n:CLIENT01
Nbdgram /s /n:SERVER01 /r:CLIENT01
Nbdgram /n:CLIENT01 /b
Nbdgram /s /n:SERVER01 /b
Nbdgram /g:CLIENTGROUP
Nbdgram /s /r: CLIENTGROUP
Параметры командной строки данной программы имеют следующие
значения
• /n:my-name – регистрирует уникальное имя my-name;
• /g:group-name – регистрирует групповое имя group-name;
• /s – отправляет дейтаграммы (по умолчанию принимает);
• /c:n – отправляет или получает n дейтаграмм;
• /r:receiver – определяет NetBIOS-имя получателя;
• /b – использует широковещательную посылку дейтаграмм;
• /a – принимает данные для любого имени NetBIOS;
• /l:n – выполняет все операции только на номере LANA n;
• /d:n – ждет n миллисекунд между отправками.
Дополнительные команды NetBIOS
Проверка состояния адаптера (команда NCBASTAT)
Команда опроса состояния адаптера возвращает
ADAPTER_STATUS,
за
которой
следует
массив
NAME_BUFFER. Они определены следующим образом:
typedef struct _ADAPTER_STATUS {
UCHAR
adapter_address[6];
UCHAR
rev_major;
UCHAR
reserved0;
UCHAR
adapter_type;
UCHAR
rev_minor;
WORD
duration;
WORD
frmr_recv;
WORD
frmr_xmit;
WORD
iframe_recv_err;
WORD
xmit_aborts;
DWORD
xmit_success;
DWORD
recv_success;
WORD
iframe_xmit_err;
WORD
recv_buff_unavail;
WORD
t1_timeouts;
WORD
ti_timeouts;
DWORD
reserved1;
WORD
free_ncbs;
27
структуру
структур
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
WORD
max_cfg_ncbs;
WORD
max_ncbs;
WORD
xmit_buf_unavail;
WORD
max_dgram_size;
WORD
pending_sess;
WORD
max_cfg_sess;
WORD
max_sess;
WORD
max_sess_pkt_size;
WORD
name_count;
} ADAPTER_STATUS, *PADAPTER_STATUS;
typedef struct _NAME_BUFFER {
UCHAR name[NCBNAMSZ];
UCHAR name_num;
UCHAR name_flags;
} NAME_BUFFER, *PNAME_BUFFER;
Детальную информацию о смысле полей можно получить из
документации, мы же ограничимся несколькими замечаниями.
Наибольший интерес в первой структуре представляют поля
adapter_address – адрес адаптера, max_dgram_size – максимальный размер
дейтаграммы и max_sess – максимальное количество сеансов.
Поле name_count сообщает, сколько было возвращено структур
NAME_BUFFER. Для того, чтобы узнать необходимый размер буфера,
можно осуществить вызов функции Netbios со значением ncb_length,
равным нулю, по возвращении это поле будет содержать необходимый
размер буфера.
Поля, необходимые для вызова команды NCBASTAT: ncb_command,
ncb_buffer, ncb_length, ncb_lana, ncb_callname. Если первый символ поля
ncb_callname – звездочка, то команда проверки состояния выполняется, но
возвращает только NetBIOS-имена, добавленные вызывающим процессом.
Допускается проверка состояния адаптера не с того компьютера, на
котором выполняется команда. Для этого в поле ncb_callname следует
указать имя удаленного компьютера. Проверка в этом случае должна
осуществляться по общему для двух компьютеров транспортному
протоколу.
Команда поиска имени (NCBFINDNAME)
Команда доступна только в Windows NT, и в версиях, начиная с
Windows 2000. Она сообщает, кто зарегистрировал данное имя NetBIOS.
Чтобы выполнить этот запрос, процесс должен добавить свое уникальное
имя в таблицу имен. Необходимые для заполнения поля: команда, номер
LANA, буфер и длина буфера.
28
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Запрос возвращает структуру FIND_NAME_HEADER, за которой
следует массив структур FIND_NAME_BUFFER. Они определены
следующим образом:
typedef struct _FIND_NAME_HEADER {
WORD node_count;
UCHAR reserved;
UCHAR unique_group;
} FIND_NAME_HEADER, *PFIND_NAME_HEADER;
typedef struct _FIND_NAME_BUFFER {
UCHAR length;
UCHAR access_control;
UCHAR frame_control;
UCHAR destination_addr[6];
UCHAR source_addr[6];
UCHAR routing_info[18];
} FIND_NAME_BUFFER, *PFIND_NAME_BUFFER;
Как и в предыдущем случае, выполнение команды со значением поля
ncb_length, равным нулю, позволяет определить необходимый размер
буфера.
После успешного выполнения команды поле unique_group содержит 0,
если имя уникальное, и 1, если групповое. Поле node_count содержит
количество
структур
FIND_NAME_BUFFER.
Из
информации,
содержащейся в этой структуре, наиболее интересны поля destination_addr
и source_addr, содержащие адрес адаптера, выполнившего запрос, и адрес
адаптера, зарегистрировавшего данное имя, соответственно.
Сопоставление протоколов номерам LANA
В зависимости от использования того или иного транспортного
протокола могут возникать разные проблемы. Поэтому важно иметь
возможность программно определять доступные транспорты. В Winsock 2
для этой цели обычно используется функция WSAEnumProtocols, которая
будет рассмотрена далее. Она возвращает информацию в структуре
WSAPROTOCOL_INFO. Если в поле iAddressFamily содержится значение
AF_NETBIOS, то абсолютное значение поля iProtocol – номер LANA для
протокола, указанного в поле szProtocol. Отметим, что в Windows NT и в
Windows 2000 поле iProtocol для любого протокола, установленного на
LANA 0, имеет значение 0x80000000, поскольку он зарезервирован для
специального использования.
29
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Перенаправитель
При работе с файлами операционная система передает запросы вводавывода локальному драйверу данного устройства. Аналогичным образом
ОС поступает и с запросами, направленными к удаленному устройству в
сети. Этот процесс называется перенаправлением ввода-вывода (I/O
redirection).
Так, можно назначить имя локального диска (например, E:) общей
папке на удаленном компьютере. Тогда при обращении приложений к
диску E: операционная система будет перенаправлять запросы на
устройство, называемое перенаправителем (redirector), а оно формировать
канал связи с удаленным компьютером, для доступа к нужной общей папке.
Ниже обсуждается, как ссылаться на файлы в сети с помощью
универсальных правил именования (Universal Naming Convention, UNC) и
указателя ресурса поставщика нескольких UNC (Multiple UNC Provider, MUP).
Универсальные правила именования
Имена UNC – это стандартный способ доступа к файлам и
устройствам без назначения этим объектам буквы локального диска. Они
имеют вид
\\сервер\ресурс\путь
Например, если на сервере с именем Myserver находится папка
D:\Myfiles\Music, представленная как общий ресурс с именем Myshare, а в
ней – файл Sample.mp3, то для доступа к нему с другого компьютера надо
указать следующее UNC-имя:
\\Myserver\Myshare\Sample.mp3
Клиент
Сервер
Приложение
MUP
Локальный
ввод-вывод
Перенаправитель
Компонент доступа
Серверная служба
перенаправителя
Транспортные
драйверы
Транспортные
драйверы
Сетевой адаптер
Сетевой адаптер
30
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
На рисунке изображены основные компоненты, формирующие UNCсоединение в сетевой операционной системе (network operating system,
NOS).
Поставщик нескольких UNC
MUP ответственен за выбор компонента доступа для обслуживания
соединений по UNC-именам. Компонент сетевого доступа или сетевой
поставщик (network provider) – это служба, способная использовать
сетевые устройства для доступа к ресурсам, расположенным на удаленном
компьютере. MUP использует компонент сетевого доступа для
организации связи в ответ на все запросы ввода-вывода к файлам и
принтерам по UNC-именам.
В операционных системах Windows может быть несколько
компонентов доступа одновременно, например, клиент для сетей Microsoft,
клиент для сетей Novell и т.п. Основная функция MUP – выбор сетевого
компонента, который должен обслужить UNC-запрос. Он просто
направляет запросы всем установленным компонентам и выбирает того,
который ответит, что способен обслужить такой запрос. Если таковых
окажется несколько, будет выбран тот, у кого выше приоритет
(компоненты прописаны в реестре в порядке приоритета).
Компоненты сетевого доступа
Компонент сетевого доступа – это служба, использующая сетевые
устройства для доступа к файлам и принтерам на удаленном компьютере.
В комплект Windows входит клиент для сетей Microsoft (Client for
Microsoft Networks), ранее называвшийся Microsoft Networking Provider
(MSNP).
Перенаправитель
Перенаправитель формирует запросы и отправляет их серверной
службе на удаленном компьютере, которая генерирует локальные запросы
ввода-вывода. Поскольку он предоставляет услуги службам верхнего
уровня (типа MUP), он скрывает детали сетевого уровня от приложений. В
итоге компонент сетевого уровня не зависит от протокола, и приложения
могут работать практически в любой сетевой конфигурации.
Перенаправитель MSNP связывается с другими рабочими станциями,
посылая сообщения серверной службе перенаправителя на этих станциях.
Эти сообщения задаются в строго определенной структуре, называемой
Server Message Block (SMB). Так же называется и соответствующий
протокол.
31
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Протокол SMB
Структура данных SMB содержит три основных компонента: код
команды, параметры команды и пользовательские данные.
Протокол SMB основан на простой модели запросов клиента и
ответов сервера. В качестве иллюстрации рассмотрим последовательность
действий при открытии файла \\Myserver\Myshare\Sample.mp3:
1.
Приложение направляет запрос на открытие файла локальной ОС
(например, CreateFile).
2. Локальная ОС определяет по имени, что запрос адресован
компьютеру \\Myserver и передает его MUP.
3. MUP определяет, что запрос предназначен компоненту доступа
MSNP, поскольку именно этот компонент обнаруживает
\\Myserver путем разрешения NetBIOS-имени.
4. Запрос передается перенаправителю MSNP.
5. Перенаправитель форматирует запрос как сообщение SMB.
6. Сообщение SMB передается по сетевому транспортному
протоколу.
7. Сервер \\Myserver получает SMB-запрос и передает его серверной
службе своего перенаправителя MSNP.
8. Серверная служба выполняет локальный запрос ввода-вывода на
открытие файла Sample.mp3 в общей папке \Myshare.
9. Перенаправитель сервера формирует ответное сообщение SMB с
информацией об успехе или неудаче попытки открытия файла.
10. Ответ сервера посылается по сетевому транспортному протоколу
клиенту.
11. Перенаправитель MSNP получает ответ SMB и передает код
возврата локальной ОС.
12. Локальная ОС передает код возврата приложению, вызвавшему
функцию CreateFile.
Пример
Приложения Win32 могут использовать API-функции CreateFile,
ReadFile и WriteFile для создания, открытия и изменения файлов по сети с
использованием перенаправителя MSNP. В предлагаемом примере
приведен исходный код приложения, создающего файл по UNCсоединению:
#include <windows.h>
#include <stdio.h>
void main(void)
{
HANDLE FileHandle;
DWORD BytesWritten;
32
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Открытие файла \\Myserver\Myshare\sample.txt
if ((FileHandle =
CreateFile("\\\\Myserver\\Myshare\\Sample.txt",
GENERIC_WRITE | GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL))
== INVALID_HANDLE_VALUE)
{
printf("CreateFile failed with error %d\n",
GetLastError());
return;
}
// Запись строки длиной 14 байт в новый файл
if (WriteFile(FileHandle, "This is a test", 14,
&BytesWritten, NULL) == 0)
{
printf("WriteFile failed with error %d\n",
GetLastError());
return;
}
if (CloseHandle(FileHandle) == 0)
{
printf("CloseHandle failed with error %d\n",
GetLastError());
return;
}
}
33
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Почтовые ящики
В версиях Windows, начиная с Windows 95 (за исключением Windows
CE) реализован простой однонаправленный механизм межпроцессной
связи (interprocess communications, IPC), называемый почтовыми ящиками
(mailslots). Они позволяют клиентскому процессу передавать сообщения
одному или нескольким серверным процессам. Разработка приложений,
использующих почтовые ящики, не требует знания сетевых протоколов.
Функционирование почтовых ящиков основано на широковещании, и
соответственно они не гарантируют надежной передачи данных.
Почтовые ящики основаны на интерфейсе файловой системы
Windows. Для ввода и вывода используются стандартные функции Win32,
такие как ReadFile и WriteFile и стандартные правила именования
файловой системы Win32. Для создания и идентификации почтовых
ящиков перенаправитель Windows использует файловую систему Mailslot
File System (MSFS).
Имена почтовых ящиков
Формат имени почтового ящика:
\\сервер\mailslot\[путь]имя
Здесь \mailslot – обязательная строка, уведомляющая систему, что имя
файла относится к MSFS. Примеры:
\\Testserver\mailslot\Mydirectory\Subdirectory\Mymailslot
\\.\mailslot\Mymailslot
\\*\mailslot\Mymailslot
Строка сервер может представлять собой точку, звездочку, имя
домена или сервера. Домен – это группа рабочих станций и серверов с
общим групповым именем. Что означает использование символов '.' и '*',
объясняется ниже.
Размеры сообщений
Обычно для передачи сообщений в сети почтовые ящики используют
дейтаграммы (datagram) – небольшие порции данных, передаваемые без
установления соединения. Доставка при этом не гарантируется и
уведомление о получении не пересылается. Однако этот механизм
позволяет использовать широковещание. В Windows NT/2000 в этом
случае размер сообщения не должен превышать 424 байта. Сообщения
размером более 426 байтов в этих операционных системах передаются с
использованием протокола, требующего соединения в сеансе SMB.
Ограничения на размер сообщений почтовых ящиков перечислены в
следующей таблице
34
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Направление
передачи
Передача посредством
дейтаграмм
Передача с
установлением
соединения
Windows 95/98 →
Windows 95/98
Размер сообщения
не более 64 Кб
Не поддерживается
Windows NT/2000 →
Windows NT/2000
Размер сообщения
не более 424 байтов
Сообщения должны
быть более 426 байтов
Windows NT/2000 →
Windows 95/98
Размер сообщения
не более 424 байтов
Не поддерживается
Windows 95/98 →
Windows NT/2000
Размер сообщения
усекается до 424 байтов
Не поддерживается
Сборка приложения и коды возврата
При сборке приложений, использующих почтовые ящики, в Microsoft
Visual C++ необходимо включать заголовочный файл Winbase.h (впрочем,
если включается Windows.h), то это не обязательно. Приложение должно
компоноваться с библиотекой Kernel32.lib.
Все API-функции Win32, используемые при работе с почтовыми
ящиками, в случае неудачи возвращают 0. Исключение составляют
функции CreateFile и CreateMailslot, которые возвращают при неудаче
INVALID_HANDLE_VALUE.
Использование архитектуры клиент-сервер
Почтовые ящики используют простую архитектуру клиент-сервер, в
которой данные передаются от клиента серверу однонаправленно. Сервер
создает почтовый ящик, и только он может читать из него данные.
Клиенты создают ссылки на существующие почтовые ящики, и только они
могут записывать в них данные.
Сервер почтовых ящиков
Последовательность действий сервера:
1. Создать описатель (дескриптор) почтового ящика посредством
API-функции CreateMailslot.
2. Получить данные от любого клиента через вызов API-функции
ReadFile с дескриптором почтового ящика в качестве параметра.
3. Закрыть описатель почтового ящика посредством API-функции
CloseHandle.
35
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Функция CreateMailslot определена следующим образом:
HANDLE CreateMailslot(
LPCTSTR lpName,
// Имя почтового ящика
DWORD nMaxMessageSize,
// Макс. размер сообщения в бт
DWORD lReadTimeout,
// Тайм-аут при чтении в мс
LPSECURITY_ATTRIBUTES lpSecurityAttributes // Права доступа
);
При этом уникальное имя почтового ящика (параметр lpName)
должно быть локальным, то есть иметь вид:
\\.\mailslot\[путь]имя
Если в качестве значения третьего параметра (lReadTimeout) взять
MAILSLOT_WAIT_FOREVER, то приложение будет ждать бесконечно,
пока данные не будут доступны для чтения.
Параметру lpSecurityAttributes рекомендуется дать значение NULL,
поскольку в Windows 95/98 соответствующая система безопасности не
реализована, а в Windows NT/2000 – реализована частично.
Для чтения из почтового ящика сервер может использовать функцию
ReadFile:
BOOL ReadFile(
HANDLE hFile,
// описатель файла
LPVOID lpBuffer,
// буфер для данных
DWORD nNumberOfBytesToRead, // сколько байтов читать
LPDWORD lpNumberOfBytesRead, // количество прочитанных байтов
LPOVERLAPPED lpOverlapped
// для перекрытого ввода-вывода
);
В качестве параметра hFile передается значение, возвращенное
функцией CreateMailslot. Размер буфера ввода должен быть не меньше,
чем параметр nMaxMessageSize функции CreateMailslot, и чем размер
входящих сообщений. При недостаточном размере буфера функция
ReadFile возвращает значение ERROR_INSUFFICIENT_BUFFER. Через
параметр lpNumberOfBytesRead возвращается количество реально
прочитанных байтов. Последний параметр используется для асинхронного
ввода в модели перекрытого ввода-вывода, в Windows 9x он должен быть
равен NULL.
Следующий пример кода представляет собой простой сервер
почтовых ящиков:
#include <windows.h>
#include <stdio.h>
void main(void)
{
HANDLE Mailslot;
36
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
char buffer[256];
DWORD NumberOfBytesRead;
// Создание почтового ящика
if ((Mailslot = CreateMailslot("\\\\.\\mailslot\\Myslot",
0, MAILSLOT_WAIT_FOREVER, NULL))
== INVALID_HANDLE_VALUE)
{
printf("Failed to create a mailslot %d\n",
GetLastError());
return;
}
// Бесконечное чтение из почтового ящика
while(ReadFile(Mailslot, buffer, 256, &NumberOfBytesRead,
NULL) != 0)
{
buffer[NumberOfBytesRead] = 0;
printf("%s\n", buffer);
}
}
Клиент почтовых ящиков
Для реализации клиента нужно организовать ссылку на
существующий почтовый ящик и писать туда данные. Необходимая
последовательность шагов:
1.
2.
3.
Открыть описатель-ссылку на почтовый ящик посредством APIфункции CreateFile.
Записать данные в почтовый ящик через вызов API-функции
WriteFile с описателем почтового ящика в качестве параметра.
Закрыть описатель почтового ящика посредством API-функции
CloseHandle.
Формат функции CreateFile:
HANDLE CreateFile(
LPCTSTR lpFileName,
// имя ящика
DWORD dwDesiredAccess,
// режим доступа
DWORD dwShareMode,
// режим разделения
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // права
DWORD dwCreationDisposition,
// как создавать
DWORD dwFlagsAndAttributes,
// атрибуты файла
HANDLE hTemplateFile
// временный файл
);
37
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Имя почтового ящика должно удовлетворять одному из следующих
форматов:
•
\\.\mailslot\имя – определяет локальный почтовый ящик
на том же компьютере;
•
\\имя_сервера\mailslot\имя – определяет удаленный
сервер (с именем имя_сервера) почтового ящика;
•
\\имя_домена\mailslot\имя – определяет все почтовые
ящики с именем имя в домене имя_домена;
•
\\*\mailslot\имя – определяет все почтовые ящики с
именем имя в основном домене системы.
Параметр dwDesiredAccess в этом случае должен иметь значение
GENERIC_WRITE, а параметр dwShareMode – FILE_SHARE_READ,
чтобы позволить серверу читать данные из почтового ящика. Значение
параметра lpSecurityAttributes не влияет на почтовые ящики и должно быть
равно NULL. Флаг dwCreationDisposition должен быть равен
OPEN_EXISTING. Если сервер не создал почтовый ящик с таким именем,
то функция вернет ошибку. В случае если сервер удаленный, значение параметра dwCreationDisposition не имеет значения. Наконец, значения двух
последних параметров должны быть равны FILE_ATTRIBUTE_NORMAL
и NULL соответственно.
Запись в почтовый ящик происходит посредством вызова функции
WriteFile:
BOOL WriteFile(
HANDLE hFile,
// описатель файла
LPCVOID lpBuffer,
// буфер с данными
DWORD nNumberOfBytesToWrite,
// сколько байтов писать
LPDWORD lpNumberOfBytesWritten, // кол-во записанных байтов
LPOVERLAPPED lpOverlapped // для перекрытого ввода-вывода
);
Смысл параметров достаточно понятен (аналогично параметрам
функции ReadFile).
Ниже приводится простой пример клиента почтового ящика:
#include <windows.h>
#include <stdio.h>
void main(int argc, char *argv[])
{
HANDLE Mailslot;
DWORD BytesWritten;
CHAR ServerName[256];
38
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Ввод имени сервера через аргументы командной строки
if (argc < 2)
{
printf("Usage: client <server name>\n");
return;
}
sprintf(ServerName, "\\\\%s\\Mailslot\\Myslot", argv[1]);
if ((Mailslot = CreateFile(ServerName, GENERIC_WRITE,
FILE_SHARE_READ, NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL, NULL)) == INVALID_HANDLE_VALUE)
{
printf("CreateFile failed with error %d\n",
GetLastError());
return;
}
if (WriteFile(Mailslot, "This is a test", 14,
&BytesWritten, NULL) == 0)
{
printf("WriteFile failed with error %d\n",
GetLastError());
return;
}
printf("Wrote %d bytes\n", BytesWritten);
CloseHandle(Mailslot);
}
Дополнительные API-функции почтовых ящиков
Функция GetMailslotInfo позволяет определить размер прибывшего
сообщения. Она может также применяться для определения наличия
входящих данных.
BOOL GetMailslotInfo(
HANDLE hMailslot,
// описатель почтового ящика
LPDWORD lpMaxMessageSize, // макс. размер сообщения
LPDWORD lpNextSize,
// размер следующего сообщения
LPDWORD lpMessageCount,
// количество сообщений
LPDWORD lpReadTimeout
// тайм-аут в мс
);
Параметр hMailslot – описатель почтового ящика, возвращенный
функцией CreateMailslot, lpMaxMessageSize указывает на переменную,
содержащую максимальный размер сообщения (в байтах), которое можно
записать в почтовый ящик. Параметр lpNextSize указывает на размер
следующего сообщения. При отсутствии сообщений этот параметр
принимает специальное значение MAILSLOT_NO_MESSAGE.
Последние два параметра используются при работе в модели
перекрытого ввода-вывода (асинхронно). lpMessageCount указывает на
переменную, в которую записывается общее количество сообщений,
39
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
ожидающих прочтения, lpReadTimeout – на переменную, куда
записывается время тайм-аута, в течение которого операция чтения
(ReadFile) ждет записи сообщения в почтовый ящик.
Функция SetMailslotInfo задает значение тайм-аута для почтового
ящика, в течение которого операция чтения ожидает входящих сообщений.
Через ее вызов приложение может изменить способ чтения от
блокирующего к неблокирующему и наоборот.
BOOL SetMailslotInfo(
HANDLE hMailslot,
// описатель почтового ящика
DWORD lReadTimeout
// тайм-аут в мс
);
Если здесь lReadTimeout равно 0, то в отсутствие сообщений операции
чтения завершаются немедленно, а если MAILSLOT_WAIT_FOREVER, то
они будут ждать бесконечно долго.
Особенности работы в Windows 9x
Правила наименования
Windows 95 и Windows 98 ограничивают размеры имен почтовых
ящиков форматом "8.3". Это создает некоторые проблемы совместимости
при общении с почтовыми ящиками на компьютерах с более новыми
версиями Windows. Например, при попытке создания почтового ящика с
именем Mymailslot оно будет усечено до Mymailsl и создание завершится
успешно, однако если затем от Windows 2000 будет отправлено сообщение
на имя Mymailslot, оно не будет получено из-за несовпадения имен.
Неспособность отменить блокирующие запросы
ввода-вывода
Почтовые ящики в Windows 95 и Windows 98, созданные с
параметром
MAILSLOT_WAIT_FOREVER
при
попытке
чтения
блокируются вплоть до получения данных. Единственный способ снять
приложение – перезагрузка системы. Чтобы решить эту проблему, можно
заставить сервер открыть описатель почтового ящика в отдельном потоке и
отправить данные, чтобы прервать блокирующий запрос чтения.
В файле Server2.cpp предложен исправленный в соответствии с этими
замечаниями код сервера. Здесь он для краткости не приводится.
Утечки памяти
Если почтовый ящик в Windows 95 или Windows 98 создаются
функцией CreateMailslot со значением тайм-аута, большим 0, функция
ReadFile вызывает утечку памяти, когда время ожидания истекает. После
многократных вызовов система становится нестабильной.
40
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Именованные каналы
Именованные каналы – это простой механизм межпроцессной связи,
реализованный в ОС Windows, начиная с Windows 95 (кроме Windows CE).
Именованные каналы обеспечивают надежную одностороннюю и
двустороннюю передачу данных между процессами на одном или разных
компьютерах.
Работа с именованными каналами не требует от программиста знаний
механизма работы с транспортными протоколами, так как детали работы
скрыты от приложения. Для обмена данными именованные каналы
используют перенаправитель сети Microsoft Network Provider (MSNP).
Именованные каналы позволяют воспользоваться встроенными
механизмами защиты ОС Windows NT, Windows 2000 и более свежих
версий. Используя этот механизм можно, например, реализовать систему
управления данными с разграничением прав доступа отдельных групп
пользователей.
Так же как и почтовые ящики, именованные каналы реализуют
простую архитектуру клиент-сервер.
Детали реализации именованных каналов
Для работы с файловой системой Windows именованные каналы
используют интерфейс Named Pipes File System (NPFS). Для получения и
отправки данных используются API-функции ReadFile и WriteFile.
Названия именованных каналов должны удовлетворять формату
Universal Naming Convention (UNC).
Правила именования каналов
Формат имени канала:
\\сервер\pipe\[путь]имя
Здесь \pipe – обязательная строка, уведомляющая систему, что имя
файла относится к NPFS. Примеры:
\\Testserver\pipe\Mydirectory\Subdirectory\Mypipe
\\.\pipe\Mypipe
Имя сервера может задаваться точкой, что означает имя локального
компьютера.
Режимы передачи
Именованные каналы могут передавать данные в двух режимах:
побайтовой передачи данных и в режиме сообщений.
В первом случае информация передается непрерывным потоком, ни
сервер, ни клиент не знают, сколько байтов записывается или считывается
с другой стороны. Во втором случае данные отправляются и принимаются
41
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
дискретными блоками, при этом каждое сообщение прочитывается
целиком.
Сборка приложения и коды возврата
При сборке приложений, использующих именованные каналы, в
Microsoft Visual C++ необходимо включать заголовочный файл Winbase.h
(впрочем, если включается Windows.h), то это не обязательно. Приложение
должно компоноваться с библиотекой Kernel32.lib.
Все API-функции Win32, используемые при работе с именованными
каналами, в случае неудачи возвращают 0. Исключение составляют
функции CreateFile и CreateNamedPipe, возвращающие при неудаче
INVALID_HANDLE_VALUE.
Простой сервер и клиент
Именованные каналы базируются на простой архитектуре клиентсервер с возможностью как односторонней, так и двухсторонней передачи
данных. Основным отличием сервера от клиента является то, что сервер
может создать именованный канал и принять запрос на соединение от
клиента. Клиентское приложение устанавливает связь с уже
существующим каналом. После этого оба приложения могут использовать
функции ReadFile и WriteFile.
Сервер именованного канала может работать только на компьютере с
операционной системой Windows NT и ОС, основанных на этой
технологии. Таким образом, два компьютера с ОС Windows9x не могут
обмениваться информацией посредством этого механизма, однако на них
могут работать приложения-клиенты.
Детали реализации сервера
Разработка сервера предполагает создание им каналов, к которым
подключаются клиенты. Набор API-функций, которые вызываются
сервером, включает в себя:
•
•
•
•
•
CreateNamedPipe – для создания экземпляра именованного
канала;
ConnectNamedPipe
–
для
прослушивания
клиентских
соединений;
ReadFile и WriteFile – для получения и отправки данных;
DisconnectNamedPipe – для закрытия соединения;
CloseHandle – для закрытия дескриптора экземпляра
именованного канала.
Сначала сервер должен создать экземпляр именованного канала
посредством API-функции CreateNamedPipe:
42
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
HANDLE CreateNamedPipe(
LPCTSTR lpName,
// имя канала
DWORD dwOpenMode,
// режим открытия
DWORD dwPipeMode,
// режимы ввода-вывода и ожидания
DWORD nMaxInstances, // макс. число экземпляров
DWORD nOutBufferSize, // размер выходного буфера в байтах
DWORD nInBufferSize, // размер входного буфера в байтах
DWORD nDefaultTimeOut, // тайм-аут в мс
LPSECURITY_ATTRIBUTES lpSecurityAttributes // права
);
Рассмотрим эти параметры.
Параметр lpName определяет название именованного канала в
формате:
\\.\pipe\[путь]имя
Имя сервера представлено точкой, так как канал создается на
локальном компьютере.
Параметр dwOpenMode определяет направление передачи, управление
вводом-выводом и безопасность канала. Его значение представляется в
виде комбинации флагов, задающих тот или иной режим. Для краткости
здесь мы не перечисляем все их возможные значения.
Параметр dwPipeMode представляет собой комбинацию битовых
флагов, указывающих, записываются ли данные потоком байтов
(PIPE_TYPE_BYTE) или потоком сообщений (PIPE_TYPE_MESSAGE),
каким образом они считываются (PIPE_READMODE_BYTE и
PIPE_READMODE_MESSAGE соответственно), а также включен
(PIPE_WAIT) или нет (PIPE_NOWAIT) режим блокировки. Значение
параметра должно быть представлено как комбинация указанных констант,
объединенных с помощью побитового OR (по одному флагу из каждой
категории).
Параметр nMaxInstances задает максимальное число экземпляров
канала, то есть соединений с клиентами, которые может создать сервер.
Значения параметров nOutBufferSize и nInBufferSize достаточно очевидны.
Значение nDefaultTimeOut определяет время ожидания соединения в
мс. Действие параметра распространяется только на клиентские
приложения, определяющие, можно ли установить соединение с сервером,
с помощью функции WaitNamedPipe.
Параметр lpSecurityAttributes позволяет указать дескриптор
безопасности именованного канала и определяет, сможет ли дочерний
процесс наследовать созданный описатель. Не рассматривая данный
вопрос более подробно, отметим, что использование значения NULL в
данном случае означает использование стандартного дескриптора
безопасности.
43
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Для установления соединения со стороны сервера предназначена
функция ConnectNamedPipe:
BOOL ConnectNamedPipe(
HANDLE hNamedPipe,
// описатель канала
LPOVERLAPPED lpOverlapped // для перекрытого ввода-вывода
);
Здесь hNamedPipe – дескриптор канала, возвращенный функцией
CreateNamedPipe. Если при создании канала использовался флаг
FILE_FLAG_OVERLAPPED, то параметр lpOverlapped позволяет функции
выполняться асинхронно (без блокировки). Задание значения NULL для
данного параметра означает выполнение функции в блокирующем режиме.
Ниже приводится код простого сервера именованного канала (файл
Server.cpp).
#include <windows.h>
#include <stdio.h>
void main(void)
{
HANDLE
PipeHandle;
DWORD
BytesRead;
CHAR
buffer[256];
if ((PipeHandle = CreateNamedPipe("\\\\.\\Pipe\\MyPipe",
PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE,
1, 0, 0, 1000, NULL)) == INVALID_HANDLE_VALUE)
{
printf("CreateNamedPipe failed with error %d\n",
GetLastError());
return;
}
printf("Server is now running\n");
if (ConnectNamedPipe(PipeHandle, NULL) == 0)
{
printf("ConnectNamedPipe failed with error %d\n",
GetLastError());
CloseHandle(PipeHandle);
return;
}
44
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if (ReadFile(PipeHandle, buffer, sizeof(buffer),
&BytesRead, NULL) <= 0)
{
printf("ReadFile failed with error %d\n",
GetLastError());
CloseHandle(PipeHandle);
return;
}
printf("%.*s\n", BytesRead, buffer);
if (DisconnectNamedPipe(PipeHandle) == 0)
{
printf("DisconnectNamedPipe failed with error %d\n",
GetLastError());
return;
}
CloseHandle(PipeHandle);
}
Усовершенствованный сервер каналов
В приведенном выше примере сервер обслуживает только один
экземпляр именованного канала. При этом все API-вызовы выполняются
синхронно (в блокирующем режиме).
Чтобы позволить двум и более клиентам установить соединение,
сервер должен поддерживать несколько экземпляров именованного канала
(параметр nMaxInstances). Несколько экземпляров канала можно создать
либо с помощью потоков, либо с помощью асинхронного ввода-вывода
Win32, например, перекрытого ввода-вывода и портов завершения.
Ниже приводится пример сервера именованного канала, который для
каждого соединения создает отдельный поток и может обслуживать до
пяти соединений одновременно (файл Threads.cpp).
#include <windows.h>
#include <stdio.h>
#include <conio.h>
#define NUM_PIPES 5
DWORD WINAPI PipeInstanceProc(LPVOID lpParameter);
45
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
void main(void)
{
HANDLE
ThreadHandle;
INT
i;
DWORD
ThreadId;
for(i = 0; i < NUM_PIPES; i++)
{
// Создание потока для обслуживания каждого экземпляра
if ((ThreadHandle = CreateThread(NULL, 0,
PipeInstanceProc, NULL, 0, &ThreadId)) == NULL)
{
printf("CreateThread failed with error %\n",
GetLastError());
return;
}
CloseHandle(ThreadHandle);
}
printf("Press a key to stop the server\n");
_getch();
}
//
// Функция: PipeInstanceProc
// Обрабатывает один экземпляр именованного канала
//
DWORD WINAPI PipeInstanceProc(LPVOID lpParameter)
{
HANDLE PipeHandle;
DWORD BytesRead;
DWORD BytesWritten;
CHAR Buffer[256];
if ((PipeHandle = CreateNamedPipe("\\\\.\\PIPE\\MyPipe",
PIPE_ACCESS_DUPLEX, PIPE_TYPE_BYTE | PIPE_READMODE_BYTE,
NUM_PIPES, 0, 0, 1000, NULL)) == INVALID_HANDLE_VALUE)
{
printf("CreateNamedPipe failed with error %d\n",
GetLastError());
return 0;
}
46
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Обслуживание соединений клиентов в бесконечном цикле
while(1)
{
if (ConnectNamedPipe(PipeHandle, NULL) == 0)
{
printf("ConnectNamedPipe failed with error %d\n",
GetLastError());
break;
}
// Чтение данных и отправка их обратно клиенту,
// пока он не закроет соединение
while(ReadFile(PipeHandle, Buffer, sizeof(Buffer),
&BytesRead, NULL) > 0)
{
printf("Echo %d bytes to client\n", BytesRead);
if (WriteFile(PipeHandle, Buffer, BytesRead,
&BytesWritten, NULL) == 0)
{
printf("WriteFile failed with error %d\n",
GetLastError());
break;
}
}
if (DisconnectNamedPipe(PipeHandle) == 0)
{
printf("DisconnectNamedPipe failed with error %d\n",
GetLastError());
break;
}
}
CloseHandle(PipeHandle);
return 0;
}
Еще одним вариантом организации функционирования сервера именованного канала является работа в режиме перекрытого ввода-вывода.
Перекрытый ввод-вывод – это механизм асинхронного выполнения
функций чтения и записи (ReadFile и WriteFile). Функциям необходимо
передать структуру OVERLAPPED, которая затем будет использована для
получения результатов запроса ввода-вывода с помощью API-функции
GetOverlappedResult.
47
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Далее приводится пример кода сервера, работающего в режиме
перекрытого ввода-вывода и обслуживающего до пяти соединений одновременно (файл Overlap.cpp).
#include <windows.h>
#include <stdio.h>
#define NUM_PIPES 5
#define BUFFER_SIZE 256
void main(void)
{
HANDLE
DWORD
CHAR
INT
OVERLAPPED
HANDLE
PipeHandles[NUM_PIPES];
BytesTransferred;
Buffer[NUM_PIPES][BUFFER_SIZE];
i;
Ovlap[NUM_PIPES];
Event[NUM_PIPES];
// Для каждого канала необходимо знать,
// какую операцию (чтение или запись) следует выполнить.
// Эта информация содержится в массиве DataRead.
BOOL DataRead[NUM_PIPES];
DWORD Ret;
DWORD Pipe;
for(i = 0; i < NUM_PIPES; i++)
{
// Создание экземпляра именованного канала
if ((PipeHandles[i] =
CreateNamedPipe("\\\\.\\PIPE\\MyPipe",
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, NUM_PIPES,
0, 0, 1000, NULL)) == INVALID_HANDLE_VALUE)
{
printf("CreateNamedPipe for pipe %d failed "
"with error %d\n", i, GetLastError());
return;
}
//
//
//
if
Для каждого экземпляра канала создается событие,
которое будет использоваться для определения
активности операций перекрытого ввода-вывода.
((Event[i] = CreateEvent(NULL, TRUE, FALSE, NULL))
== NULL)
{
printf("CreateEvent for pipe %d failed with error %d\n",
i, GetLastError());
continue;
48
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
}
// Инициализация флага состояния, определяющего
// писать или читать из канала
DataRead[i] = FALSE;
ZeroMemory(&Ovlap[i], sizeof(OVERLAPPED));
Ovlap[i].hEvent = Event[i];
// Прослушивание клиентских соединений
if (ConnectNamedPipe(PipeHandles[i], &Ovlap[i]) == 0)
{
if (GetLastError() != ERROR_IO_PENDING)
{
printf("ConnectNamedPipe for pipe %d failed "
"with error %d\n", i, GetLastError());
CloseHandle(PipeHandles[i]);
return;
}
}
}
printf("Server is now running\n");
// Чтение данных и отправка их обратно клиенту
// в бесконечном цикле
while(1)
{
if ((Ret = WaitForMultipleObjects(NUM_PIPES, Event,
FALSE, INFINITE)) == WAIT_FAILED)
{
printf("WaitForMultipleObjects failed with error %d\n",
GetLastError());
return;
}
Pipe = Ret - WAIT_OBJECT_0;
ResetEvent(Event[Pipe]);
// Проверка результатов. В случае ошибки соединение
// устанавливается заново,
// иначе выполняется чтение и запись
if (GetOverlappedResult(PipeHandles[Pipe],
&Ovlap[Pipe], &BytesTransferred, TRUE) == 0)
{
printf("GetOverlapped result failed %d start over\n",
GetLastError());
49
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if (DisconnectNamedPipe(PipeHandles[Pipe]) == 0)
{
printf("DisconnectNamedPipe failed with error %d\n",
GetLastError());
return;
}
if (ConnectNamedPipe(PipeHandles[Pipe],
&Ovlap[Pipe]) == 0)
{
if (GetLastError() != ERROR_IO_PENDING)
{
// Обработка ошибки канала. Закрытие описателя.
printf("ConnectNamedPipe for pipe %d failed "
"with error %d\n", i, GetLastError());
CloseHandle(PipeHandles[Pipe]);
}
}
DataRead[Pipe] = FALSE;
}
else
{
// Проверка состояния канала. Если значение
// DataRead равно FALSE, асинхронно читаем данные
// клиента, иначе отправляем их обратно клиенту.
if (DataRead[Pipe] == FALSE)
{
// Подготовка к чтению данных от клиента
// путем асинхронного вызова функции ReadFile
ZeroMemory(&Ovlap[Pipe], sizeof(OVERLAPPED));
Ovlap[Pipe].hEvent = Event[Pipe];
if (ReadFile(PipeHandles[Pipe], Buffer[Pipe],
BUFFER_SIZE, NULL, &Ovlap[Pipe]) == 0)
{
if (GetLastError() != ERROR_IO_PENDING)
{
printf("ReadFile failed with error %d\n",
GetLastError());
}
}
DataRead[Pipe] = TRUE;
}
50
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
else
{
// Отправка данных обратно клиенту путем
// асинхронного вызова функции WriteFile
printf("Received %d bytes, echo bytes back\n",
BytesTransferred);
ZeroMemory(&Ovlap[Pipe], sizeof(OVERLAPPED));
Ovlap[Pipe].hEvent = Event[Pipe];
if (WriteFile(PipeHandles[Pipe], Buffer[Pipe],
BytesTransferred, NULL, &Ovlap[Pipe]) == 0)
{
if (GetLastError() != ERROR_IO_PENDING)
{
printf("WriteFile failed with error %d\n",
GetLastError());
}
}
DataRead[Pipe] = FALSE;
}
}
}
}
Чтобы получить описатели каждого канала, приложение пять раз
вызывает функцию CreateNamedPipe. Затем для прослушивания всех их
сервер пять раз вызывает асинхронно функцию ConnectNamedPipe. Ввод и
вывод данных также происходит асинхронно. По завершении соединения с
клиентом сервер вызывает функцию DisconnectNamedPipe и повторно
инициирует прослушивание канала.
Реализация клиента
Клиенты не могут создавать экземпляры именованных каналов. Они
устанавливают соединение с каналами, уже существующими на сервере.
Простой клиент должен выполнить следующую последовательность
действий:
1.
2.
3.
4.
Для проверки наличия свободного экземпляра канала вызвать
API-функцию WaitNamedPipe.
Для установления соединения вызвать API-функцию CreateFile.
Для отправки и получения данных использовать API-функции
WriteFile и ReadFile.
Для завершения соединения вызвать API-функцию CloseHandle.
51
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Формат функции WaitNamedPipe:
BOOL WaitNamedPipe(
LPCTSTR lpNamedPipeName, // имя канала в формате UNC
DWORD nTimeOut // тайм-аут для ожидания экз. канала в мс.
);
Формат функции CreateFile:
HANDLE CreateFile(
LPCTSTR lpFileName,
// имя канала
DWORD dwDesiredAccess,
// режим доступа
DWORD dwShareMode,
// режим разделения
LPSECURITY_ATTRIBUTES lpSecurityAttributes, // права
DWORD dwCreationDisposition,
// как создавать
DWORD dwFlagsAndAttributes,
// атрибуты файла
HANDLE hTemplateFile
// временный файл
);
Смысл и возможные значения параметров
информацию можно найти в документации):
•
•
•
•
•
•
•
(более
детальную
lpFileName – имя канала в формате UNC.
dwDesiredAccess – значение равно GENERIC_READ при чтении,
GENERIC_WRITE при записи. Их можно объединить с помощью
побитовой операции OR.
dwShareMode – должен быть равен 0, так как клиент имеет
доступ только к одному экземпляру канала.
lpSecurityAttributes – NULL, если не требуется, чтобы дочерний
процесс наследовал описатель клиента.
dwCreationDisposition – значение следует определить как
OPEN_EXISTING.
dwFlagsAndAttributes – обязательно должен включать флаг
FILE_ATTRIBUTE_NORMAL. Могут быть указаны дополнительные флаги, на чем здесь подробнее не останавливаемся.
hTemplateFile – не применяется с именованными каналами и
должен быть равен NULL.
Далее приводится пример простого клиента, который после установления соединения с сервером отправляет ему сообщение "This is a test"
(файл Client.cpp).
#include <windows.h>
#include <stdio.h>
#define PIPE_NAME "\\\\.\\Pipe\\MyPipe"
void main(void) {
HANDLE PipeHandle;
DWORD BytesWritten;
52
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if (WaitNamedPipe(PIPE_NAME, NMPWAIT_WAIT_FOREVER) == 0)
{
printf("WaitNamedPipe failed with error %d\n",
GetLastError());
return;
}
// Создание описателя именованного канала
if ((PipeHandle = CreateFile(PIPE_NAME,
GENERIC_READ | GENERIC_WRITE, 0,
(LPSECURITY_ATTRIBUTES) NULL, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
(HANDLE) NULL)) == INVALID_HANDLE_VALUE)
{
printf("CreateFile failed with error %d\n",
GetLastError());
return;
}
if (WriteFile(PipeHandle, "This is a test", 14,
&BytesWritten, NULL) == 0)
{
printf("WriteFile failed with error %d\n",
GetLastError());
CloseHandle(PipeHandle);
return;
}
printf("Wrote %d bytes", BytesWritten);
CloseHandle(PipeHandle);
}
Другие API-вызовы
Функция CallNamedPipe позволяет клиенту подключиться к
именованному каналу, работающему в режиме сообщений (подождать,
пока не освободится экземпляр канала), записать и считать данные, а затем
закрыть канал.
BOOL CallNamedPipe(
LPCTSTR lpNamedPipeName, // имя канала в формате UNC
LPVOID lpInBuffer,
// буфер для записи
DWORD nInBufferSize, // размер буфера для записи в байтах
LPVOID lpOutBuffer,
// буфер для чтения
DWORD nOutBufferSize, // размер буфера для чтения в байтах
LPDWORD lpBytesRead, // [out] кол-во прочитанных байтов
DWORD nTimeOut // тайм-аут для ожидания экз. канала в мс.
);
53
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Функция TransactNamedPipe используется как серверами, так и
клиентами. Она позволяет объединить операции чтения и записи в одном
API-вызове. Это позволяет оптимизировать сетевой трафик за счет
сокращения числа транзакций, выполненных через перенаправитель
MSNP.
BOOL TransactNamedPipe(
HANDLE hNamedPipe,
// описатель именованного канала
LPVOID lpInBuffer,
// буфер для записи
DWORD nInBufferSize, // размер буфера для записи в байтах
LPVOID lpOutBuffer,
// буфер для чтения
DWORD nOutBufferSize, // размер буфера для чтения в байтах
LPDWORD lpBytesRead, // [out] кол-во прочитанных байтов
LPOVERLAPPED lpOverlapped // для перекрытого ввода-вывода
);
Функция GetNamedPipeHandleState возвращает информацию о канале,
включая режим работы, количество экземпляров канала и информацию о
состоянии буферов.
BOOL GetNamedPipeHandleState(
HANDLE hNamedPipe,
// описатель именованного канала
LPDWORD lpState,
// состояние канала
LPDWORD lpCurInstances, // [out] кол-во экземпляров
LPDWORD lpMaxCollectionCount, // макс. накапливаемое
// число байтов
LPDWORD lpCollectDataTimeout, // макс. время до получения
// данных
LPTSTR lpUserName,
// имя пользователя клиента
DWORD nMaxUserNameSize
// размер этого буфера
);
Здесь параметр lpMaxCollectionCount содержит максимальное число
байтов, которые будут накоплены на компьютере клиента перед передачей
на сервер, а lpCollectDataTimeout – максимальное время в миллисекундах,
которое может пройти до того, как удаленный клиент передаст
информацию по сети.
Функция SetNamedPipeHandleState позволяет изменить характеристики канала, возвращенные функцией GetNamedPipeHandleState.
BOOL SetNamedPipeHandleState(
HANDLE hNamedPipe,
// описатель именованного канала
LPDWORD lpMode,
// новый режим работы
LPDWORD lpMaxCollectionCount, // макс. накапливаемое
// число байтов
LPDWORD lpCollectDataTimeout, // макс. время до получения
// данных
);
54
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Функция GetNamedPipeInfo возвращает размер буферов и максимальное количество экземпляров канала.
BOOL GetNamedPipeInfo(
HANDLE hNamedPipe,
// описатель именованного канала
LPDWORD lpFlags,
// вид и режим, клиент или сервер
LPDWORD lpOutBufferSize, // размер исходящего буфера в байтах
LPDWORD lpInBufferSize, // размер входящего буфера в байтах
LPDWORD lpMaxInstances // макс. кол-во экземпляров канала
);
Функция PeekNamedPipe позволяет просмотреть находящиеся в
канале данные, не удаляя из внутреннего буфера.
BOOL PeekNamedPipe(
HANDLE hNamedPipe,
// описатель именованного канала
LPVOID lpBuffer,
// указатель на буфер
DWORD nBufferSize,
// размер буфера в байтах
LPDWORD lpBytesRead, // кол-во байтов, считанных в буфер
LPDWORD lpTotalBytesAvail, // байтов, доступных для чтения
LPDWORD lpBytesLeftThisMessage // сколько байтов осталось
);
Последний параметр относится к работе в режиме сообщений. Если
сообщение не помещается в буфер, то в lpBytesLeftThisMessage
содержится количество непрочитанных байтов в данном сообщении. При
работе в побайтовом режиме этот параметр всегда содержит 0.
55
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Сетевые протоколы
Интерфейс прикладного программирования Winsock
Winsock – сетевой интерфейс (но не протокол) доступа к разным
базовым сетевым протоколам, реализованный на всех платформах Win32.
Его можно считать развитием реализации Berkley (BSD) Sockets на
платформах UNIX. В средах Win32, начиная с версии Winsock 2, он стал
абсолютно независим от протокола.
Ниже обсуждаются основные характеристики протоколов и
интерфейса Winsock, различные аспекты работы с конкретными
протоколами.
В следующих темах будут приведены примеры простейших программ,
организующих обмен данными на основе того или иного протокола, на
основе спецификации Winsock 2.
Характеристики протоколов
Ориентированность на передачу сообщений
Протокол называют ориентированным на передачу сообщений, если
для каждой команды записи он передает байты по сети в отдельном
сообщении. Это означает, что приемник также получит данные в виде
отдельного сообщения (дейтаграммы).
Платформа Win32
(отправитель)
Сетевой стек
Сетевой стек
32 байта
128 байт
64 байта
64 байта
128 байт
32 байта
Платформа Win32
(приемник)
Сеть
На рисунке показана ситуация с отправкой трех сообщений.
Приемник выдает три команды чтения с буфером, например, в 256 байт и
получает три сообщения.
56
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Протокол, не сохраняющий границы сообщений, обычно называют
протоколом, основанным на потоке (stream based). В этом случае
потоковая служба непрерывно передает данные, а получатель считывает
столько данных, сколько имеется в наличии, независимо от границ
сообщений. При запросе на чтение система возвращает максимально
возможное количество данных, не переполняющее входной буфер. Такая
ситуация представлена на следующем рисунке.
Сетевой стек
Сетевой стек
32 байта
Платформа Win32
(отправитель)
224
байта
64 байта
------128 байт
Платформа Win32
(приемник)
Сеть
Решение объединить пакеты данных может быть, например, принято
при применении алгоритма Нейгла. В отношении TCP/IP это означает, что
узел накапливает данные перед отправкой: ждет, пока накопится
достаточно данных или истечет указанный тайм-аут. Его партнер перед
тем, как отправить уведомление, также ожидает исходящих данных в
течение указанного времени. Такое поведение вызвано неэффективностью
пересылки большого количества небольших по размеру пакетов.
Можно применять также и гибридную схему (псевдопоток), при
которой отправитель каждый пакет данных посылает по отдельности, а
получатель получает их как угодно.
Обмен данными с соединением и без него
Обычно протоколы предусматривают и ориентированные, и не
ориентированные на соединение службы.
Первые перед любым обменом данными устанавливают канал связи
между партнерами. Это гарантирует существование маршрута доставки,
однако влечет дополнительные издержки. Эти издержки увеличиваются
также за счет дополнительных действий для проверки правильности
передачи данных.
Службы, не ориентированные на соединение, не гарантируют, что
приемник на самом деле примет данные, да и дойдут ли они до него.
57
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Надежность и порядок доставки сообщений
Это, вероятно, самые важные характеристики, которые следует
принимать во внимание при проектировании своих приложений.
В большинстве случаев они неразрывно связаны с тем, ориентирован
протокол на соединение или нет. Надежность, или гарантированная
доставка, подразумевает, что каждый байт данных будет доставлен
получателю без изменений. Протокол, сохраняющий порядок данных,
гарантирует, что приемник получит эти данные именно в том порядке, в
котором они были отправлены. При установлении соединения стороны
предпринимают специальные действия, чтобы гарантировать целостность
данных и порядок доставки.
Протоколы, не ориентированные на соединение, на порядок быстрее,
поскольку проверки данных на целостность и уведомления об их
успешном приеме намного усложняют даже передачу небольших порций
данных.
Корректное завершение работы
Корректное завершение работы характерно только для протоколов,
ориентированных на соединение. При этом одна из сторон инициирует
процесс завершения сеанса, однако обе стороны должны выполнить все
необходимые
операции,
чтобы
окончательно
его
завершить.
Ориентированный на соединение протокол может и не поддерживать
корректного завершения сеанса.
Широковещание
Широковещание данных подразумевает их передачу с одной рабочей
станции всем остальным рабочим станциям ЛВС. Этой функцией
обладают только неориентированные на соединение протоколы.
Основной недостаток – широковещательные сообщения вынужден
обрабатывать каждый компьютер в сети. Следствие – высокая рабочая
нагрузка в сети, что может замедлить работу ЛВС. Маршрутизаторы не
транслируют широковещательных пакетов данных.
Многоадресное вещание
Под многоадресным вещанием понимается способность одного
процесса передавать данные одному и более получателям. Методика
присоединения процесса к многоадресному сеансу зависит от
применяемого протокола.
Например, при использовании протокола IP, необходимо, чтобы все
участвующие в обмене узлы были членами особой группы. Специальный
фильтр заставляет сетевое оборудование транслировать по сетевому стеку
только данные, предназначенные соответствующему групповому адресу.
Подобного рода передача часто используется в приложениях для
организации видеоконференций.
58
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Качество обслуживания
Управляя качеством обслуживания (Quality of Service, QoS),
приложение может зарезервировать определенную часть пропускной
способности сети для монопольного использования. Наличие такой
характеристики важно, например, для плавного воспроизведения
видеопотока, получаемого из сети.
Фрагментарные сообщения
Фрагментарные сообщения (partial message) могут передавать только
ориентированные на сообщения протоколы. Так, объемные сообщения
приходится переправлять по сети блоками. Приемник при попытке чтения
получает только фрагмент. Если протокол поддерживает фрагментарные
сообщения, читатель уведомляется, что возвращаемые данные – лишь
часть всего сообщения.
Маршрутизация
Если протокол является маршрутизируемым, то между двумя
рабочими станциями можно установить канал связи, независимо от того,
какая сетевая аппаратура их разделяет. Если, например, они расположены
в разных подсетях, пакет данных направляется маршрутизатору, который
знает,
как
их
лучше
доставить
адресату.
Единственный
немаршрутизированный протокол, поддерживаемый платформами Win32 –
NetBEUI.
Сетевые протоколы, поддерживаемые Win32
Установка
соединения
Надежность
MSAFD TCP
Поток
Да
Да
MSAFD UDP
СообНет Нет Нет Нет
щение
Да
Да
Нет
65467
RSVP TCP
Поток
Нет Нет
Да
Без
огранич.
RSVP UDP
СообНет Нет Нет Нет
щение
Да
Да
65467
IP
Да
Да
59
Да
Да
Да
Макс.
размер
пакета
Да
QoS
Порядок
пакетов
Корректное
завершение
Широковещание
Многоадресность
Имя
Тип
сообщения
Протокол
Платформы Win32 поддерживают разнообразные протоколы. Каждый
протокол обычно способен работать в нескольких режимах. Например,
протокол IP поддерживает как ориентированные на соединение поточные
службы, так и службы дейтаграмм, не ориентированные на соединение.
В следующей таблице перечисляются основные доступные протоколы
и поддерживаемые ими режимы работы.
Нет Нет Нет
Без
огранич.
Да
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Infrared
Sockets
ATM
Apple-Talk
Net- BIOS
IPX/ SPX
Да
Да
Макс.
размер
пакета
Порядок
пакетов
Корректное
завершение
Широковещание
Многоадресность
Надежность
СообНет Нет Нет Нет
щение
QoS
MSAFD
nwinkipx [IPX]
MSAFD
nwinkspx
[SPX], [SPX
псевдопоток]
MSAFD
nwinkspx
[SPXII], [SPXIIпсевдопоток]
Sequential
Packets
Установка
соединения
Имя
Тип
сообщения
Протокол
Продолжение таблицы:
Нет
576
Сообщение
Да
Да
Да
Нет Нет Нет Нет
Без
огранич.
Сообщение
Да
Да
Да
Да
Без
огранич.
Нет Нет Нет
СообДа Да Да Нет Нет Нет Нет
щение
СообDatagrams
Нет Нет Нет Нет Да Нет Нет
щение
MSAFD Apple- СообДа Да Да Да Нет Нет Нет
Talk [ADSP]
щение
64 Кб
64 Кб
64 Кб
MSAFD AppleTalk [ADSP]
[псевдопоток]
Сообщение
Да
Да
Да
Да
Нет Нет Нет
Без
огранич.
MSAFD AppleTalk [PAP]
Сообщение
Да
Да
Да
Да
Нет Нет Нет
4096
MSAFD AppleTalk [RTMP]
Поток Нет Нет Нет Нет Нет Нет Нет
Без
огранич.
MSAFD AppleTalk [ZIP]
Поток Нет Нет Нет Нет Нет Нет Нет
Без
огранич.
MSAFD ATM
AAL5
Поток
Да
Нет
Да
Нет Нет
Да
Да
Без
огранич.
Native ATM
AAL5
Сообщение
Да
Нет
Да
Нет Нет
Да
Да
Без
огранич.
MSAFD Irda
[IrDA]
Поток
Да
Да
Да
Да
Нет Нет Нет
Без
огранич.
60
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Сетевые протоколы в Windows CE
В отличие от других платформ Win32, Windows CE поддерживает
только семейство протоколов TCP/IP. Кроме того, Windows CE
поддерживает только спецификацию Winsock 1.1.
Работа с Winsock
Инициализация Winsock
Перед вызовом любой функции Winsock необходимо загрузить
правильную версию библиотеки Winsock. Функция инициализации
Winsock:
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
Функция возвращает 0 в случае успеха.
Первый параметр – номер версии Winsock (для версии Winsock 2.2
можно указать 0x0202 либо макрос MAKEWORD(2,2)).
Второй параметр – структура WSADATA, содержащей информацию о
загруженной версии Winsock:
typedef struct WSAData {
WORD wVersion;
WORD wHighVersion;
char szDescription[WSADESCRIPTION_LEN+1];
char szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR * lpVendorInfo;
} WSADATA, *LPWSADATA;
Единственная полезная информация, возвращаемая в этой структуре,
это поля wVersion (версия Winsock, которую предполагает использовать
вызов) и wHighVersion (высшая версия Winsock, которую поддерживает
загруженная библиотека). Прочую информацию, например, максимальное
количество сокетов или максимальный размер UDP, следует получать из
записи каталога для конкретного протокола.
Информация о протоколе
Winsock 2 позволяет узнать, какие протоколы и с какими характеристиками установлены на рабочей станции. Для каждого рабочего
режима протокола существует соответствующая запись каталога.
Например, после установки TCP/IP в каталог будут внесены две записи для
IP: для протокола TCP (надежный, с установкой соединения) и для
протокола UDP (ненадежный, без установки соединения).
61
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Узнать об установленных протоколах можно через вызов функции
int WSAEnumProtocols(
LPINT
lpiProtocols,
LPWSAPROTOCOL_INFO lpProtocolBuffer,
LPDWORD
lpdwBufferLength
);
Функция
WSAEnumProtocols
заполняет
массив
структур
WSAPROTOCOL_INFO информацией о поддерживаемых протоколах
(аналогом ее для Winsock 1.1 является функция EnumProtocols,
заполняющая массив структур PROTOCOL_INFO). Структура определена
WSAPROTOCOL_INFO следующим образом:
typedef struct _WSAPROTOCOL_INFO {
DWORD dwServiceFlags1;
DWORD dwServiceFlags2;
DWORD dwServiceFlags3;
DWORD dwServiceFlags4;
DWORD dwProviderFlags;
GUID ProviderId;
DWORD dwCatalogEntryId;
WSAPROTOCOLCHAIN ProtocolChain;
int iVersion;
int iAddressFamily;
int iMaxSockAddr;
int iMinSockAddr;
int iSocketType;
int iProtocol;
int iProtocolMaxOffset;
int iNetworkByteOrder;
int iSecurityScheme;
DWORD dwMessageSize;
DWORD dwProviderReserved;
WCHAR szProtocol[WSAPROTOCOL_LEN+1];
} WSAPROTOCOL_INFO, FAR * LPWSAPROTOCOL_INFO;
При первом запуске функции WSAEnumProtocols удобно задать
значения первого и третьего параметра равными нулю. Произойдет
ошибка, а через третий параметр нам будет возвращен размер буфера,
достаточный для хранения информации о протоколах. Вызвав функцию
повторно с правильным размером буфера, мы получим необходимую
информацию.
Пример программы, которая получает и распечатывает информацию о
протоколах, приведен в файле Enum.c.
Не вдаваясь подробно в рассмотрение всех заполняемых полей,
отметим, что очень много информации об атрибутах протокола
62
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
содержится в поле dwServiceFlags1. Оно представляет собой обширный
набор битовых флагов, содержащий, в частности, информацию о необходимости соединения, гарантии доставки данных и их порядка, поддержке механизма широковещания, многоадресной передачи данных и т.д.
Очень важными являются поля iProtocol, iSocketType и
iAddressFamily. Поле iProtocol определяет, к какому протоколу относится
данная запись. Поле iSocketType важно, если протокол способен работать в
разных режимах (например, в поточном и дейтаграммном). Поле
iAddressFamily позволяет выяснить структуру адресации, применяемую
данным протоколом. Эти три поля используются при создании сокета.
Сокеты Windows
Весь интерфейс Winsock основан на понятии сокета, который является
описателем поставщика транспорта. Сокет представлен специальным
типом (SOCKET) и создается одной из двух функций:
SOCKET WSASocket(
int af,
// Семейство адресов протокола
int type,
// Тип сокета для данного протокола
int protocol, // Конкретный транспорт, если их несколько
LPWSAPROTOCOL_INFO lpProtocolInfo,
// Структура с информацией о созданном сокете
GROUP g,
// Зарезервировано
DWORD dwFlags // Атрибуты сокета
);
SOCKET socket(
int af,
int type,
int protocol
);
Возможные значения параметров af и type перечислены в таблице. В
качестве третьего параметра (protocol) можно взять 0. В этом случае
система выбирает поставщика транспорта, исходя из значений af и type.
Протокол
Internet
Protocol
(IP)
Семейство
адресов (af)
AF_INET
NetBIOS AF_NETBIOS
Тип сокета
Протокол (type)
TCP
SOCK_STREAM
IPPROTO_IP
UDP
SOCK_DGRAM
IPPROTO_UDP
Простые сокеты SOCK_RAW
IPPROTO_RAW
IPPROTO_ICMP
Последовательные пакеты
SOCK_SEQPACKET Номер LANA
Дейтаграммы
SOCK_DGRAM
63
Номер LANA
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Продолжение таблицы:
Протокол
Семейство
адресов (af)
Тип сокета
Протокол (type)
MSAFD
SOCK_DGRAM
nwinkipx [IPX]
NSPROTO_IPX
MSAFD
SOCK_SEQPACKET NSPROTO_SPX
nwinkspx [SPX]
IPX/SPX
AF_NS
MSAFD
nwinkspx [SPX] SOCK_STREAM
[псевдопоток]
NSPROTO_SPX
MSAFD
SOCK_SEQPACKET NSPROTO_SPXII
nwinkspx [SPXII]
Infrared
Sockets
AppleTalk
ATM
AF_IRDA
AF_APPLETALK
AF_ATM
MSAFD
nwinkspx
[SPXII],
[псевдопоток]
SOCK_STREAM
NSPROTO_SPXII
MSAFD Irda
[IrDA]
SOCK_STREAM
IRDA_PROTO_
SOCK_STREAM
MSAFD AppleSOCK_RDM
Talk [ADSP]
ATPROTO_ADSP
MSAFD AppleTalk [ADSP] SOCK_STREAM
[псевдопоток]
ATPROTO_ADSP
MSAFD AppleSOCK_RDM
Talk [PAP]
ATPROTO_PAP
MSAFD AppleSOCK_DGRAM
Talk [RTMP]
DDPPROTO_RTMP
MSAFD AppleSOCK_DGRAM
Talk [ZIP]
DDPPROTO_ZIP
MSAFD ATM
AAL5
SOCK_RAW
ATMPROTO_AAL5
Native ATM
(AAL5)
SOCK_RAW
ATMPROTO_AAL5
При использовании для создания сокета функции WSASocket можно
использовать следующую последовательность действий:
Перечислив все протоколы посредством WSAEnumProtocols,
передадать структуру LPWSAPROTOCOL_INFO в функцию WSASocket
64
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
как параметр lpProtocolInfo, а в качестве каждого из трех параметров af ,
type и protocol указать константу FROM_PROTOCOL_INFO, которая
означает,
что
информацию
следует
взять
из
структуры
LPWSAPROTOCOL_INFO. Параметр группы можно положить равным 0,
так как настоящие версии Winsock не поддерживают групп.
Последний параметр представляет собой комбинацию из флагов:
•
•
•
•
•
WSA_FLAG_OVERLAPPED
WSA_FLAG_MULTIPOINT_C_ROOT
WSA_FLAG_MULTIPOINT_C_LEAF
WSA_FLAG_MULTIPOINT_D_ROOT
WSA_FLAG_MULTIPOINT_D_LEAF
Первый параметр (WSA_FLAG_OVERLAPPED) (при использовании
функции socket задается по умолчанию) означает, что сокет допускает
перекрытый ввод-вывод. Остальные относятся к сокетам многоадресного
вещания.
65
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Семейства адресов и разрешение имен
Для связи средствами Winsock важны механизмы адресации рабочих
станций в конкретных протоколах. В Winsock 2 реализовано несколько
новых независимых от протокола функций, которые можно использовать с
сокетами любых семейств адресов. Однако в большинстве случаев у
каждого семейства свой механизм разрешения адресов.
В рамках данной темы для каждого семейства имен будут
рассмотрены основы адресации компьютера в сети, будет показано, как
создать сокет для каждого семейства, а также описана специфика каждого
протокола в методике разрешения имен.
Протокол IP
Internet Protocol (IP) широко используется в Интернете,
поддерживается большинством ОС и применяется как в локальных (local
area networks, LAN), так и в глобальных сетях (wide area networks, WAN).
Протокол IP не требует установления соединения и не гарантирует
доставку данных. Поэтому для передачи данных поверх IP используются
два протокола более высокого уровня: TCP и UDP.
Протоколы TCP и UDP
Transmission Control Protocol (TCP) реализует связь с установлением
соединения, обеспечивает надежную безошибочную передачу данных
между двумя компьютерами. После установления связи между компьютерами возможен двунаправленный обмен данными.
User Datagram Protocol (UDP) реализует связь без установления
соединения. UDP не гарантирует надежности передачи, однако может
осуществлять широковещательную передачу данных и обеспечивает прием
данных от множества источников. Данные передаются в виде дейтаграмм.
Поскольку и TCP, и UDP являются надстройками над IP, часто
говорят об использовании TCP/IP или UDP/IP. В Winsock для IPсоединений предусмотрено семейство адресов AF_INET, определенное в
файлах Winsock.h и Winsock2.h.
Адресация
При использовании IP протоколов адрес компьютера (версия IPv4)
состоит из 32 бит. Для организации взаимодействия с сервером по TCP или
UDP клиент должен указать IP-адрес и номер порта службы. Для
прослушивания входящих запросов клиента сервер также должен указать
IP-адрес и номер порта. В Winsock IP-адрес и номер порта службы задают
в структуре SOCKADDR_IN:
66
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
struct sockaddr_in {
short
sin_family;
u_short
sin_port;
struct in_addr
sin_addr;
char
sin_zero[8];
};
Поле sin_family должно быть равно AF_NET. Поле sin_port задает
номер порта, используемого для идентификации службы. Часть номеров
портов зарезервирована центром Internet Assigned Numbers Authority
(IANA) для стандартных служб, для собственных процессов и программ
следует использовать номера с 1024 по 65535.
Поле sin_addr структуры SOCKADDR_IN представляет собой
объединение и хранит IP-адрес либо в 4-байтном виде (тип unsigned long),
либо в виде 4 отдельных байтов, либо в виде 2 двухбайтных слов. Поле
sin_zero – просто заполнитель, для того, чтобы размер структуры
SOCKADDR_IN совпадал с размером структуры SOCKADDR.
Вспомогательная функция inet_addr преобразует IP-адрес из точечной
нотации (a.b.c.d) в тип unsigned long:
unsigned long inet_addr (const char FAR * cp);
Функция возвращает IP-адрес в виде 32-битного числа с сетевым
порядком следования байтов (network-byte order).
Специальными
значениями
IP-адресов
являются
адреса
INADDR_ANY и INADDR_BROADCAST. Первый из них позволяет
серверному приложению слушать клиента через любой сетевой интерфейс.
Второй позволяет широковещательно рассылать UDP-дейтаграммы по IPсети. Для его использования необходимо в приложении задать параметр
сокета SO_BROADCAST (вопрос будет рассмотрен позже).
Порядок байтов
Порядок следования байтов при представлении многобайтных чисел
зависит от процессора. Так, процессоры Intel x86 в ячейках с меньшими
адресами хранят менее значимые байты (little-endian). В этом случае
системный порядок хранения байтов (host-byte order) не совпадает с
сетевым порядком (network-byte order), так как стандарты Интернета
требуют обратного порядка (big-endian). Есть много функций для преобразования многобайтных чисел из системного порядка в сетевой и обратно.
Четыре следующих функции преобразуют числа из системного
порядка в сетевой:
u_long htonl (u_long hostlong);
int WSAHtonl (SOCKET s,
u_long hostlong, // системный порядок байтов
u_long FAR * lpnetlong // для возврата
);
67
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
u_short htons(u_short hostshort);
int WSAHtons (SOCKET s,
u_short hostshort, // системный порядок байтов
u_short FAR * lpnetshort // для возврата
);
Еще четыре функции предназначены для преобразования числа из
сетевого порядка в системный:
u_long ntohl (u_long netlong);
int WSANtohl (SOCKET s,
u_long netlong, // сетевой порядок байтов
u_long FAR * lphostlong // для возврата
);
u_short ntohs (u_short netshort);
int WSANtohs (SOCKET s,
u_short netshort, // сетевой порядок байтов
u_short FAR * lphostshort // для возврата
);
Ниже приводится пример использования функций inet_addr и ntohs:
SOCKADDR_IN InternetAddr;
INT nPortId = 5150;
InternetAddr.sin_family = AF_INET;
// Преобразование адреса в 4-байтное число
InternetAddr.sin_addr.s_addr = inet_addr("136.149.3.29");
// Приведение nPortId к сетевому порядку
InternetAddr.sin_port = htons(nPortId);
Создание сокета
Пример вызова функции socket и WSASocket для открытия IP-сокета
при помощи протокола TCP:
s = socket (AF_INET, SOCK_STREAM, 0);
s = WSASocket (AF_INET, SOCK_STREAM, 0,
NULL, 0, WSA_FLAG_OVERLAPPED);
Для использования протокола UDP в качестве второго параметра
следует указать SOCK_DGRAM, а для связи непосредственно по IP –
SOCK_RAW.
Разрешение имен
Буквенные имена узлов запоминать легче, чем IP-адреса. В Winsock
предусмотрено две функции для преобразования (разрешения) имени в IPадрес: gethostbyname и WSAAsyncGetHostByName. Они обе возвращают
структуру HOSTENT:
68
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
struct hostent {
char FAR *
h_name;
// Имя узла
char FAR * FAR * h_aliases; // Массив дополнительных имен
short
h_addrtype;// Семейство адресов
short
h_length; // Длина в бт.эл-в h_addr_list
char FAR * FAR * h_addr_list;// Массив для IP-адресов
};
Массивы h_aliases и h_addr_list завершаются нулевым указателем.
Формат функции gethostbyname:
struct hostent FAR * gethostbyname(const char FAR * name);
При успешном завершении функция возвращает указатель на
структуру HOSTENT, которая хранится в системной памяти. Приложение
не должно ее освобождать.
WSAAsyncGetHostByName
–
асинхронная
версия
функции
gethostbyname, оповещающая приложение о завершении своего
выполнения с помощью сообщений Windows:
HANDLE WSAAsyncGetHostByName(
HWND hWnd, // Дескриптор окна-получателя сообщения
u_int wMsg, // Возвращаемое сообщение
const char FAR * name, // Имя узла
char FAR * buf, // Область памяти для размещения HOSTENT
int buflen // Размер буфера (д.б. MAXGETHOSTSTRUCT)
);
Если требуется по IP-адресу узла найти его понятное имя, можно
воспользоваться одной из двух функций: gethostbyaddr или
WSAAsyncGetHostByAddr. Функция gethostbyaddr имеет следующий
прототип:
struct hostent FAR * gethostbyaddr(
const char FAR * addr, // IP-адрес
int len, // Длина addr
int type // Д.б. AF_INET (для IP-адреса)
);
WSAAsyncGetHostByAddr – асинхронная версия функции gethostbyaddr.
Номера портов
Как упоминалось ранее, для своих приложений следует использовать
номера портов, начиная с 1024. Порты с номерами до 1023 включительно
зарезервированы для стандартных служб. При необходимости узнать
69
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
номер порта стандартной службы можно воспользоваться функцией
getservbyname:
struct servent FAR * getservbyname(
const char FAR * name, // Служба, например, "ftp"
const char FAR * proto // Протокол
);
WSAAsyncGetServByName – асинхронная версия функции getservbyname.
Инфракрасные сокеты
Сокеты инфракрасного канала (Infrared sockets, IrSock) доступны,
начиная с Windows 98. Они отличаются от традиционных сокетов тем, что
учитывают непостоянство доступности переносных устройств. В этой
технологии применена новая модель разрешения имен.
Адресация
Ввиду мобильности большинства компьютеров с устройствами
инфракрасной связи (Infrared Data Association, IrDA) статические серверы
имен бесполезны. IrSock ищет ресурсы, не применяя стандартные функции
службы имен Winsock или IP-адреса. Служба имен встроена в поток связи,
а для поддержки служб IrSock введено новое семейство адресов.
Структура адреса IrSock содержит имя службы с описанием приложения,
используемого для привязки и подключения, а также идентификатор
устройства (device identifier). Структура адреса IrSock:
typedef struct sockaddr_irda {
u_short irdaAddressFamily; // равно AF_IRDA
u_char irdaDeviceID[4];
// идентификатор устройства
char
irdaServiceName[25]; // имя службы
} SOCKADDR_IRDA;
Разрешение имен
Адресация может быть основана на селекторах (IrDA Logical Service
Access Point Selector, LSAP-SEL) или на службах, зарегистрированных
Information Access Services (IAS), позволяет IAS абстрагировать службу от
LSAP-SEL и использовать текстовые имена (по аналогии с DNS-сервером).
Некоторые устаревшие IrDA-устройства не поддерживают IASрегистрацию, в этом случае необходимо пользоваться идентификатором
LSAP-SEL. Win32 разрешает использовать идентификаторы в диапазоне от
1 до 127.
Нумерация IrDA-устройств
Поскольку ИК-устройства появляются и исчезают из радиуса связи,
необходим метод динамического перечисления соседних устройств. Их
70
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
нумерация
выполняется
функцией
getsockopt
с
параметром
IRLMP_ENUMDEVICES для задания типа сокета (параметр optname):
int getsockopt(
SOCKET s,
int level,
int optname,
char FAR * optval,
int FAR * optlen
);
Параметр optval должен указывать на структуру DEVICELIST, а
параметр optlen задавать ее длину. Структура DEVICELIST определена для
Windows следующим образом (для Windows CE структура чуть
отличается, мы для краткости ее не рассматриваем):
typedef struct _WINDOWS_DEVICELIST
{
ULONG
numDevice;
WINDOWS_IRDA_DEVICE_INFO Device[1];
} WINDOWS_DEVICELIST, *PWINDOWS_DEVICELIST,
FAR *LPWINDOWS_DEVICELIST;
Структура WINDOWS_IRDA_DEVICE_INFO содержит информацию
о найденном устройстве и определена следующим образом:
typedef struct _WINDOWS_IRDA_DEVICE_INFO
{
u_char
irdaDeviceID[4];
char
irdaDeviceName[22];
u_char
irdaDeviceHints1;
u_char
irdaDeviceHints2;
u_char
irdaCharSet;
} WINDOWS_IRDA_DEVICE_INFO, *PWINDOWS_IRDA_DEVICE_INFO,
FAR *LPWINDOWS_IRDA_DEVICE_INFO;
Ниже приводится пример кода для вызова функции getsockopt с целью
перечисления всех ИК-устройств по соседству.
DEVICELIST
devList;
SOCKET
sock;
DWORD
dwListLen = sizeof(WINDOWS_DEVICELIST),
dwRet;
sock = WSASocket(AF_IRDA, SOCK_STREAM, 0, NULL, 0,
WSA_FLAG_OVERLAPPED);
devList.numDevice = 0;
dwRet = getsockopt(sock, SOL_IRLMP, IRLMP_ENUMDEVICES,
(char *)&devList, &dwListLen);
71
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Обратите внимание на необходимость присвоения полю numDevice
нуля перед вызовом getsockopt. При успешной нумерации этому полю
будет присвоено положительное значение, равное числу структур
IRDA_DEVICE_INFO в поле Device.
В реальном приложении вызов функции getsockopt необходимо
выполнить несколько раз, чтобы отследить все устройства, попавшие в
радиус связи. Пример таких действий приводится в файле irenum.c.
Создание сервера и клиента для IrSock
Последовательность действий для создания IrSock-сервера:
•
Создать сокет с семейством адресов AF_IRDA и типом
SOCK_STREAM.
•
Записать в структуру SOCKADDR_IRDA имя службы сервера.
•
Вызвать функцию bind с описателем сокета и структурой
SOCKADDR_IRDA.
•
Вызвать функцию listen с описателем сокета и тайм-аутом.
•
Вызов блокирующей функции accept для входящих запросов
клиентов.
Последовательность действий для создания IrSock-клиента несколько
сложнее, поскольку необходимо нумеровать ИК-устройства:
•
Создать сокет с семейством адресов AF_IRDA и типом
SOCK_STREAM.
•
Нумерация доступных ИК-устройств путем вызова функции
getsockopt с параметром IRLMP_ENUMDEVICES.
•
Для каждого найденного устройства запись в структуру
SOCKADDR_IRDA идентификатора найденного устройства, а
также имени службы, к которой нужно подключиться.
•
Для каждой структуры, отобранной на шаге 3, вызвать функцию
connect с описателем сокета и структурой SOCKADDR_IRDA.
Опрос IAS
Для того чтобы определить, запущена ли данная служба на
конкретном устройстве, можно либо попытаться подключиться к ней, либо
запросить у IAS имя службы. Оба способа предполагают перечисление
всех ИК-устройств и попытку запросить каждое устройство.
Запрос
выполняется
функцией
getsockopt
с
параметром
IRLMP_IAS_QUERY. Результат будет помещен в структуру IAS_QUERY,
передаваемую через параметр optval. Описание этой структуры для
Windows выглядит следующим образом:
72
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
typedef struct _WINDOWS_IAS_QUERY {
u_char irdaDeviceID[4]; // код устройства в радиусе связи
char
irdaClassName[IAS_MAX_CLASSNAME]; // строка свойств
char
irdaAttribName[IAS_MAX_ATTRIBNAME]; // имя службы
u_long irdaAttribType;
union
{
LONG irdaAttribInt;
struct
{
u_long Len;
u_char OctetSeq[IAS_MAX_OCTET_STRING];
} irdaAttribOctetSeq;
struct
{
u_long Len;
u_long CharSet;
u_char UsrStr[IAS_MAX_USER_STRING];
} irdaAttribUsrStr;
} irdaAttribute;
} WINDOWS_IAS_QUERY, *PWINDOWS_IAS_QUERY,
FAR *LPWINDOWS_IAS_QUERY;
Создание сокета
IrSock поддерживает только потоки с установлением соединения.
Примеры создания сокетов посредством функций socket и WSASocket:
s = socket (AF_IRDA, SOCK_STREAM, 0);
s = WSASocket (AF_IRDA, SOCK_STREAM, 0,
NULL, 0, WSA_FLAG_OVERLAPPED);
Для определенности можно в качестве третьего параметра указать
значение IRDA_PROTO_SOCK_STREAM (но не обязательно).
Большинство SO_ параметров не применимы к IrDA, поддерживается
только SO_LINGER (подождать при закрытии, пока есть неотправленные
данные).
Протоколы IPX/SPX
Протокол Internetwork Packet Exchange (IPX) используется
компьютерами с клиент-серверными службами NetWare фирмы Novell.
IPX обеспечивает связь без установления соединения, следовательно,
доставка пакета не гарантируется. Если требуется обеспечить
гарантированную доставку данных по протоколу IPX, можно
73
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
задействовать протокол более высокого уровня, например, Sequence Packet
Exchange (SPX) или SPX II. Winsock поддерживает IPX под управлением
Windows 95+, но не Windows CE.
Адресация
В IPX-сетях сегменты соединяются через IPX-маршрутизаторы.
Каждому сегменту назначается уникальный четырехбайтный номер сети
(network number). Компьютер, подключенный к сети, идентифицируется
при помощи шестибайтного номера узла (node number) – обычно это
физический адрес сетевого адаптера. При необходимости запустить
несколько процессов связи, они различаются по номерам сокетов. Для
адресации используется структура SOCKADDR_IPX, ее определение
содержится в файле Wsipx.h (должен быть подключен после Winsock2.h):
typedef struct sockaddr_ipx {
short sa_family;
// AF_IPX
char sa_netnum[4];
// 4-байтный номер сети
char sa_nodenum[6]; // 6-байтный номер узла
unsigned short sa_socket; // номер сокета (или порта)
} SOCKADDR_IPX, *PSOCKADDR_IPX,FAR *LPSOCKADDR_IPX;
Создание сокета
Примеры создания IPX-сокетов посредством функций socket и
WSASocket:
s = socket (AF_IPX, SOCK_DGRAM, NSPROTO_IPX);
s = WSASocket (AF_IPX, SOCK_DGRAM, NSPROTO_IPX,
NULL, 0, WSA_FLAG_OVERLAPPED);
В этом случае третий параметр должен быть обязательно задан и не
равен 0.
При создании SPX-сокета второй параметр (тип сокета) должен быть
равен SOCK_SEQPACKET или SOCK_STREAM, а тип протокола
NSPROTO_SPX или NSPROTO_SPXII.
Если тип сокета SOCK_STREAM, данные передаются в виде
непрерывного потока байт, без разделения сообщений, если тип сокета
SOCK_SEQPACKET, данные передаются с разделителями сообщений. Так,
если передатчик отправляет 200 байт, то приемник не сможет ответить,
пока не получит их все (функции приема recv или WSARecv). Для
идентификации последнего пакета в сообщении устанавливается
специальный флаг (бит) в заголовке SPX. Для поточных сокетов
выполнение функции приема будет завершено сразу же при получении
любых данных.
74
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Привязка сокета
Привязка локального адреса к сокету осуществляется с помощью
функции bind. После успешной привязки через вызов функции getsockname
можно узнать номер локальной сети и номер своего узла. Пример кода:
SOCKET sdServer;
SOCKADDR_IPX IPXAddr;
int addrlen = sizeof(SOCKADDR_IPX);
if ((sdServer = socket(AF_IPX, SOCK_DGRAM, NSPROTO_IPX))
== INVALID_SOCKET)
{
printf ("socket failed with error %d", WSAGetLastError());
return;
}
ZeroMemory(&IPXAddr, sizeof(SOCKADDR_IPX));
IPXAddr.sa_family = AF_IPX;
IPXAddr.sa_socket = htons(5150);
if (bind(sdServer, (PSOCKADDR)&IPXAddr, sizeof(SOCKADDR_IPX))
== INVALID_SOCKET)
{
printf ("socket failed with error %d", WSAGetLastError());
return;
}
if (getsockname((unsigned)sdServer, (PSOCKADDR)&IPXAddr,
&addrlen) == INVALID_SOCKET)
{
printf ("getsockname failed with error %d",
WSAGetLastError());
return;
}
Внутренний номер сети
В приведенном примере полям sa_netnum и sa_nodenum перед
вызовом функции bind были присвоены нулевые значения. Этого
достаточно при работе в ОС Windows. В них предусмотрен внутренний
(виртуальный) номер сети, используемый для внутренней маршрутизации
и четкой идентификации компьютера при межсетевых подключениях. Это
позволяет также легко осуществлять привязку на компьютере с
несколькими сетевыми адаптерами.
75
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Установка типа пакета
Выбрать тип IPX-пакета можно при создании сокета, если при
указании типа протокола вместо NSPROTO_IPX указать значение
NSPROTO_IPX+PackType. Здесь PackType может принимать следующие
значения:
•
0x01 – Routing Information Protocol (RIP);
•
0x04 – Service Advertising Protocol (SAP);
•
0x05 – Sequenced Packet Exchange (SPX);
•
0x11 – NetWare Core Protocol (NCP);
•
0x14 – широковещательный пакет для Novell NetBIOS.
Протоколы NetBIOS
Для адресации NetBIOS из Winsock необходимо знать имена NetBIOS
и номера LANA. Ниже основное внимание уделяется особенностям
доступа к NetBIOS из Winsock. При этом семейство адресов NetBIOS
доступно только в ОС Windows NT, но не Windows 9x.
Адресация
Имя NetBIOS состоит из 16 символов, причем последний обозначает
тип службы, к которой относится это имя. Имена бывают уникальными и
групповыми. Уникальное имя может использоваться только одним
процессом в сети: он регистрируется под этим именем, и все остальные
связываются с ним, используя это имя. Под групповым именем может
зарегистрироваться группа приложений, они все будут получать
дейтаграммы, направляемые на это имя.
Структура адресации NetBIOS в Winsock определена в файле
Wsnetbs.h:
#define NETBIOS_NAME_LENGTH 16
typedef struct sockaddr_nb {
short
snb_family; // равно AF_NETBIOS
u_short snb_type; // тип имени: уникальное или групповое
char
snb_name[NETBIOS_NAME_LENGTH]; // собственно имя
} SOCKADDR_NB, *PSOCKADDR_NB,FAR *LPSOCKADDR_NB;
В качестве значения поля snb_type можно использовать одну из
следующих констант:
#define NETBIOS_UNIQUE_NAME
(0x0000)
#define NETBIOS_GROUP_NAME
(0x0001)
Для заполнения структуры SOCKADDR_NB нужными значениями
можно воспользоваться макросом
SET_NETBIOS_SOCKADDR(_snb,_type,_name,_port),
определенным в файле Wsnetbs.h.
76
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Создание сокета
При создании NetBIOS-сокета нужно правильно задать номер LANA,
а для этого требуется знать, какие номера LANA доступны приложению.
Первый способ создания сокета – вызов функции socket или
WSASocket, например:
s = WSASocket (AF_NETBIOS, SOCK_DGRAM | SOCK_SEQPACKET, - lana,
NULL, 0, WSA_FLAG_OVERLAPPED);
Проблема состоит в том, что необходимо знать доступные номера
LANA.
В Winsock не существует простого способа нумерации LANA. Можно,
например, перечислить все транспортные протоколы с помощью функции
WSAEnumProtocols. В следующем примере это и делается, а для
обнаруженных транспортов NetBIOS создаются сокеты.
dwNum = WSAEnumProtocols(NULL, lpProtocolBuf, &dwBufLen);
if (dwNum == SOCKET_ERROR)
{ /* Ошибка */ }
for (i=0; i< dwNum; i++)
{
// Поиск записей в семействе адресов AF_NETBIOS
if (lpProtocolBuf[i].iAddressFamily == AF_NETBIOS)
{
// Поиск сокетов с типом SOCK_DGRAM или SOCK_SEQPACKET
if (lpProtocolBuf[i].iSocketType == SOCK_SEQPACKET)
{
s[j++] = WSASocket(FROM_PROTOCOL_INFO,
FROM_PROTOCOL_INFO, FROM_PROTOCOL_INFO,
&lpProtocolBuf[i], 0, WSA_FLAG_OVERLAPPED);
}
}
}
Здесь находятся транспорты, относящиеся к семейству адресов
AF_NETBIOS с типом сокета SOCK_SEQPACKET. Количество найденных
транспортов содержится в переменной j. Если нужен номер LANA, его
можно взять из поля iProtocol структуры WSAPROTOCOL_INFO.
Единственное исключение: значение поля iProtocol, равное 0x80000000,
соответствует нулевому номеру LANA.
Протокол AppleTalk
Протокол AppleTalk используется для связи с компьютерами
Macintosh. Он отчасти похож на протокол NetBIOS: сервер динамически
регистрирует определенное имя, по которому с ним могут соединиться
клиенты.
77
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Адресация
Имена AppleTalk сложнее имен NetBIOS: они основаны на трех
отдельных именах: собственно имени, типе и зоне, длина каждого не более
32 символов.
Имя идентифицирует процесс и связанный с ним сокет на
компьютере. Тип – это механизм группировки для зон. Зона – сеть
компьютеров, поддерживающих AppleTalk и расположенных в одной
петле (loop). Несколько сетей можно соединить мостами. Чтобы
динамически определить маршруты к соединенным сетям, AppleTalk
использует Routing Table Maintenance Protocol (RTMP).
В основе адресации узлов AppleTalk из Winsock лежит структура
(файл ATalkwsh.h):
typedef struct sockaddr_at
{
USHORT
sat_family;
USHORT
sat_net;
UCHAR
sat_node;
UCHAR
sat_socket;
} SOCKADDR_AT, *PSOCKADDR_AT;
Структура SOCKADDR_AT содержит только символы и короткие
целые, но не дружественные имена. Эта структура передается в функциях
bind, connect, WSAConnect, однако для трансляции дружественного имени
необходимо сначала разрешить или зарегистрировать это имя функциями
getsockopt и setsockopt соответственно.
Регистрация имени AppleTalk
Зарегистрировать сервер AppleTalk под определенным именем,
которое будут использовать клиенты, позволяет функция setsockopt с
параметром SO_REGISTER_NAME. Для всех параметров сокетов,
связанных с именами AppleTalk следует использовать структуру
WSH_NBP_NAME (файл ATalkwsh.h). На этом типе основаны другие типы
(для регистрации и удаления имени): WSH_REGISTER_NAME,
WSH_DEREGISTER_NAME, WSH_REMOVE_NAME.
#define MAX_ENTITY 32
typedef struct
{
CHAR ObjectNameLen;
CHAR ObjectName[MAX_ENTITY];
CHAR TypeNameLen;
CHAR TypeName[MAX_ENTITY];
CHAR ZoneNameLen;
CHAR ZoneName[MAX_ENTITY];
} WSH_NBP_NAME, *PWSH_NBP_NAME;
78
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Примерный код для регистрации имени AppleTalk :
#define MY_ZONE
"*"
#define MY_TYPE
"Winsock-Test-App"
#define MY_OBJECT "AppleTalk-Server"
WSH_REGISTER_NAME atname;
SOCKADDR_AT
ataddr;
SOCKET
s;
// Вписать регистрируемое имя
strcpy(atname.ObjectName, MY_OBJECT);
atname.ObjectNameLen = strlen(MY_OBJECT);
strcpy(atname.TypeName, MY_TYPE);
atname.TypeNameLen = strlen(MY_TYPE);
strcpy(atname.ZoneName, MY_ZONE);
atname.ZoneNameLen = strlen(MY_ZONE);
s = socket(AF_APPLETALK, SOCK_STREAM, ATPROTO_ADSP);
if (s == INVALID_SOCKET)
{
// Ошибка
}
ataddr.sat_socket = 0;
ataddr.sat_family = AF_APPLETALK;
if (bind(s, (SOCKADDR *)&ataddr, sizeof(ataddr))
==SOCKET_ERROR)
{
// Невозможно открыть конечную точку в сети AppleTalk
}
if (setsockopt (s, SOL_APPLETALK, SO_REGISTER_NAME,
(char *)&atname, sizeof(WSH_NBP_NAME)) == SOCKET_ERROR)
{
// Ошибка при регистрации имени
}
В приведенном примере звездочка в качестве имени зоны обозначает
текущую зону (в которой находится данный компьютер). В структуре
адреса, передаваемой функции привязки bind значение поля sat_socket
обнулено. Этот вызов сам по себе еще не дает приложению возможности
принимать запросы клиентов. Для этого требуется зарегистрировать имя в
сети, что и делается вызовом функции setsockopt. Если при этом
произошла ошибка, вероятнее всего, что имя уже кем-то используется.
79
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Разрешение имен AppleTalk
Клиентское приложение обычно знает дружественное имя сервера и
должно преобразовать его в номер сети, узла и сокета, т.е. разрешить. Это
делается
посредством
вызова
getsockopt
с
параметром
SO_LOOKUP_NAME.
Для
поиска
используются
структуры
WSH_NBP_TUPLE и WSH_LOOKUP_NAME (файл ATalkwsh.h):
typedef struct
{
WSH_ATALK_ADDRESS Address;
USHORT
Enumerator;
WSH_NBP_NAME
NbpName;
} WSH_NBP_TUPLE, *PWSH_NBP_TUPLE;
typedef struct _WSH_LOOKUP_NAME
{
WSH_NBP_TUPLE LookupTuple;
ULONG
NoTuples;
} WSH_LOOKUP_NAME, *PWSH_LOOKUP_NAME;
Пример использования функции getsockopt для разрешения имен
имеется в файле Atalknm.c.
Создание сокета
Для этой цели можно использовать любую подходящую функцию
Winsock, начиная с версии 1.1.
Как и ранее, можно порекомендовать для удобства задания базового
протокола вызвать функцию WSAEnumProtocols и поместить результаты в
структуру WSAPROTOCOL_INFO.
В следующей таблице приведены параметры, необходимые для
создания сокета функциями socket или WSASocket.
Протокол
Тип сокета
Тип протокола
WSAFD AppleTalk [ADSP]
SOCK_RDM
ATPROTO_ADSP
WSAFD AppleTalk [ADSP]
[псевдопоток]
SOCK_STREAM
ATPROTO_ADSP
WSAFD AppleTalk [RAP]
SOCK_RDM
ATPROTO_RAP
WSAFD AppleTalk [RTMP]
SOCK_DGRAM
DDPROTO_RTMP
WSAFD AppleTalk [ZIP]
SOCK_DGRAM
DDPROTO_ZIP
80
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Протокол ATM
Winsock 2 в ОС Windows, начиная с Windows 98, поддерживает
протокол ATM – Asynchronous Transfer Mode. Протокол применяется как в
локальных, так и глобальных сетях для высокоскоростной передачи
данных любого типа, в том числе звука и видео. Протокол гарантирует
качество обслуживания (QoS) за счет виртуальных соединений (Virtual
Connections, VC) в сети. Типичная сеть ATM состоит из конечных точек
(компьютеров), соединенных коммутаторами.
Коммутатор
ATM
Коммутатор
ATM
Конечные
точки ATM
Коммутатор
ATM
Коммутатор
ATM
Конечные
точки ATM
ATM – это фактически не протокол, а тип носителя. Сеть ATM не
способна осуществлять управление потоками (flow control). Протокол
ATM требует предварительного установления соединения, а затем
работает в режимах сообщений или потоков. Если отправка данных
замедлится, то приложение-отправитель может переполнить локальный
буфер, а если приложение-получатель не будет считывать данные
достаточно часто, то произойдет переполнение буфера на его стороне, и
часть данных будет утеряна. Для управления потоками можно, например,
использовать протокол IP поверх ATM. В этом случае приложения будут
работать с семейством IP-адресов.
Адресация
В сети ATM два сетевых интерфейса: "пользователь - сеть" (user
network interface, UNI) и "узел - сеть" (network node interface, NNI). Первый
определяет взаимодействие компьютеров и коммутаторов ATM, второй –
взаимодействие двух коммутаторов.
81
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
При использовании сигнального протокола UNI компьютер может
общаться только с ближайшим коммутатором, через несколько коммутаторов данные не передаются. При этом Winsock использует механизм
точек доступа к службам (Service Access Point, SAP). SAP позволяет
приложениям зарегистрировать и идентифицировать интерфейс сокета для
связи по сети ATM с помощью адресной структуры SOCKADDR_ATM.
Структура SOCKADDR_ATM определена в файле Ws2atm.h:
typedef struct sockaddr_atm {
u_short
satm_family; // Должно быть AF_ATM
ATM_ADDRESS satm_number; // Фактический ATM адрес
ATM_BLLI
satm_blli;
// BLLI
ATM_BHLI
satm_bhli;
// BHLI
} sockaddr_atm, SOCKADDR_ATM, *PSOCKADDR_ATM, *LPSOCKADDR_ATM;
Адрес ATM представляется структурой ATM_ADDRESS. Он основан
на одной из двух схем адресации ATM: либо E.164, либо точках доступа к
сетевым службам (Network Service Access Points, NSAP). Поля satm_blli и
satm_bhli (Broadband Lower Layer Information, BLLI и Broadband Higher
Layer Information, BHLI) используются для идентификации стека
протокола, работающего поверх ATM.
Структура ATM_ADDRESS в файле Ws2atm.h определена так:
#define ATM_ADDR_SIZE
20
typedef struct {
DWORD AddressType; // ATM_E164 или ATM_NSAP
DWORD NumofDigits; // Должно быть ATM_ADDR_SIZE
UCHAR Addr[ATM_ADDR_SIZE]; // Адрес по схеме E164 или NSAP
} ATM_ADDRESS;
Ниже приводится пример, иллюстрирующий возможное применение
структуры SOCKADDR_ATM для настройки SAP под адрес NSAP:
SOCKADDR_ATM atm_addr;
UCHAR MyAddress[ATM_ADDR_SIZE];
atm_addr.satm_family = AF_ATM;
atm_addr.satm_number.AddressType = ATM_NSAP;
atm_addr.satm_number.NumofDigits = ATM_ADDR_SIZE;
atm_addr.satm_blli.Layer2Protocol = SAP_FIELD_ABSENT;
atm_addr.satm_blli.Layer3Protocol = SAP_FIELD_ABSENT;
atm_addr.satm_bhli.HighLayerInfoType = SAP_FIELD_ABSENT;
memcpy(&atm_addr.satm_number.Addr, MyAddress, ATM_ADDR_SIZE);
Присвоение значения SAP_FIELD_ABSENT полям Layer2Protocol и
Layer3Protocol структуры BLLI и HighLayerInfoType структуры BHLI
приводит к тому, что остальные поля данных структур не используются.
82
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
ATM-адрес – это шестнадцатеричная ASCII строка из 40 символов,
она соответствует 20 байтам, которые задают адрес в структуре
ATM_ADDRESS (в стиле NSAP или E.164). Для преобразования можно
воспользоваться функцией WSAStringToAddress, либо приведенной ниже
функцией AtoH.
// Преобразование ATM-адреса из строкового формата в 16-й
void AtoH( CHAR *szDest, CHAR *szSource, INT iCount )
{
while (iCount--)
*szDest++ =
( BtoH ( *szSource++ ) << 4 ) + BtoH ( *szSource++ );
}
// Эквивалентное двоичное значение для одного ASCII-символа
UCHAR BtoH( CHAR ch )
{
if ( ch >= '0' && ch <= '9' )
return ( ch - '0' );
if ( ch >= 'A' && ch <= 'F' )
return ( ch - 'A' + 0xA );
if (ch >= 'a' && ch <= 'f' )
return ( ch - 'a' + 0xA );
return -1; // Неверные значения не принимаются
}
Создание сокета
В ATM приложения могут создавать только ориентированные на
соединение сокеты. Данные могут быть переданы или как поток байт, или
как отдельное сообщение. Параметры для функций socket или WSASocket
приведены в примерах:
s = socket (AF_ATM, SOCK_RAW, ATMPROTO_AAL5);
s = WSASocket (AF_ATM, SOCK_RAW, ATMPROTO_AAL5,
NULL, 0, WSA_FLAG_OVERLAPPED);
По умолчанию (как в этом примере) создается потоковый сокет ATM.
Чтобы обмениваться данными в виде сообщений, требуется явное указание
поставщика ATM-протокола в структуре WSAPROTOCOL_INFO, передаваемой функции WSASocket.
В приводимом ниже примере показано, как с помощью перечисления
найти поставщика ATM, передающего данные в виде сообщений, и
открыть сокет:
83
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
dwRet = WSAEnumProtocols(NULL, lpProtocolBuf, &dwBufLen);
if (dwRet == SOCKET_ERROR)
{ /* Ошибка */ }
for (i=0; i< dwRet ; i++)
{
if ((lpProtocolBuf[i].iAddressFamily == AF_ATM) &&
(lpProtocolBuf[i].iSocketType == SOCK_RAW) &&
(lpProtocolBuf[i].iProtocol == ATMPROTO_AAL5) &&
(lpProtocolBuf[i].dwServiceFlags1 &
XP1_MESSAGE_ORIENTED)
{
s = WSASocket(FROM_PROTOCOL_INFO,
FROM_PROTOCOL_INFO, FROM_PROTOCOL_INFO,
&lpProtocolBuf[i], 0, WSA_FLAG_OVERLAPPED);
}
}
Привязка сокета к SAP
Последний байт (из 20) адреса в стиле NSAP или E.164 соответствует
номеру селектора, который уникальным образом позволяет распознать и
определить SAP на конечной точке.
Для получения всей информации об ATM-адресе и виртуальном
соединении на конечной точке можно использовать системную программу
Atmadm.exe.
Дополнительные функции Winsock 2
Функции WSAAddressToString и WSAStringToAddress обеспечивают
независимый от протокола способ преобразования структуры SOCKADDR
протокола в символьную строку и обратно. Их формат:
INT WSAAddressToString(
IN
LPSOCKADDR
lpsaAddress,
IN
DWORD
dwAddressLength,
IN
LPWSAPROTOCOL_INFO lpProtocolInfo,
IN OUT LPSTR
lpszAddressString,
IN OUT LPDWORD
lpdwAddressStringLength
);
INT WSAStringToAddress(
IN
LPSTR
IN
INT
IN
LPWSAPROTOCOL_INFO
IN OUT LPSOCKADDR
IN OUT LPINT
);
AddressString,
AddressFamily,
lpProtocolInfo,
lpAddress,
lpAddressLength
В настоящее время эти функции работают только для семейств
адресов AF_INET и AF_ATM.
84
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Основы интерфейса Winsock
В теме рассматриваются основные методики и API-вызовы,
необходимые для написания сетевых приложений. Рассматриваются
способы установления соединения между двумя компьютерами в сети и
механизмы обмена данными.
Во избежание повторов все вопросы обсуждаются на примере
протокола TCP/IP, поскольку единственная зависящая от протокола
операция – это создание сокета. В качестве примеров программ студентам
предлагаются исходные коды простых клиент-серверных приложений для
всех рассмотренных ранее протоколов.
Будут представлены разновидности API-функций для версий Winsock
1 и 2. Если в Winsock 2 обновлена или добавлена новая API-функция, то ее
имя начинается с префикса WSA (например, socket в Winsock 1 и
WSASocket в Winsock 2).
Инициализация Winsock
Любое Winsock-приложение перед вызовом функций должно
загрузить соответствующую версию библиотеки Winsock. Это выполняет
функция WSAStartup:
int WSAStartup(WORD wVersionRequested, LPWSADATA lpWSAData);
В качестве значения параметра wVersionRequested удобно
пользоваться макросом MAKEWORD (x,y), где у – номер основной версии,
а x – дополнительной. Параметр lpWSAData – указатель на структуру
WSADATA, которую функция заполняет информацией о версии
загружаемой библиотеки.
Структура WSADATA определена следующим образом:
typedef struct WSAData {
WORD
wVersion;
WORD
wHighVersion;
char
szDescription[WSADESCRIPTION_LEN+1];
char
szSystemStatus[WSASYS_STATUS_LEN+1];
unsigned short iMaxSockets;
unsigned short iMaxUdpDg;
char FAR *
lpVendorInfo;
} WSADATA, FAR * LPWSADATA;
WSAStartup присваивает параметру wVersion значение загружаемой
версии, а параметру wHighVersion номер последней доступной версии
Winsock (младший байт – основной номер, старший – дополнительный).
Поля szDescription и szSystemStatus заполняются не во всех версиях
Winsock и практически не применяются. Не следует также использовать
поля iMaxSockets и iMaxUdpDg. Предполагается, что в них заданы
максимальное количество открытых сокетов и максимальный размер
дейтаграммы. Для определения последнего следует запросить сведения о
протоколе, вызвав функцию WSAEnumProtocols, а максимальное
85
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
количество открытых сокетов зависит от свободной физической памяти.
Поле lpVendorInfo зарезервировано и не используется.
Проверка и обработка ошибок
Функции Winsock довольно часто выдают ошибки, большинство из
которых, как привило, не критические, и работу программы можно
продолжить.
Большая часть функций в качестве ошибки выдает значение
SOCKET_ERROR, определенное как (-1). Для получения более подробной
информации следует вызвать функцию WSAGetLastError:
int WSAGetLastError(void);
Функция возвращает код последней ошибки. Всем кодам ошибок,
возвращаемым функцией WSAGetLastError, соответствуют стандартные
константные значения, описанные в Winsock.h или Winsock2.h, в зависимости от версии. Эти константы обычно начинаются с префикса WSAE.
Протоколы с установлением соединения
В данном разделе рассматриваются функции Winsock для приема и
установления соединения, а также сценарий работы сервера и клиента для
установления соединения. Также будет описан процесс передачи данных в
ходе сеанса связи.
Серверные API-функции
Сервер – это процесс, ожидающий подключения клиентов для обслуживания из запросов. Он должен прослушивать соединения на стандартном имени. В TCP/IP таким именем является IP-адрес и номер порта.
Сервер должен сначала привязать сокет данного протокола к его стандартному имени функцией bind. После этого сокет переводится в режим
прослушивания функцией listen. При получении запроса клиента он может
быть принят (организовано соединение) функцией accept или WSAAccept.
Сервер Winsock
Клиент Winsock
socket / WSASocket
socket / WSASocket
bind
Разрешение адреса
listen
accept / WSAAccept
connect / WSAConnect
86
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Функция bind
После создания сокета определенного протокола он связывается со
стандартным адресом функцией bind:
int bind (SOCKET s, const struct sockaddr FAR * name,
int namelen);
Здесь s – сокет, на котором сервер ожидает соединения клиентов.
Второй параметр – буфер, в который вы помещаете адрес, соответствующий стандартам используемого протокола, поле namelen задает
размер переданной структуры адреса, зависящий от протокола. При
возникновении ошибки bind возвращает значение SOCKET_ERROR.
Пример привязки (для краткости без проверки на ошибки):
SOCKET
struct sockaddr_in
int
s;
tcpaddr;
port = 5150;
s = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
tcpaddr.sin_addr.s_addr = htonl(INADDR_ANY);
tcpaddr.sin_family = AF_INET;
tcpaddr.sin_port = htons(port);
bind (s, (struct sockaddr *)&tcpaddr, sizeof(tcpaddr));
Функция listen
После привязки сокет переводится в режим прослушивания функцией
listen:
int listen (SOCKET s, int backlog);
Здесь s – сокет, на котором сервер ожидает соединения клиентов.
Второй параметр определяет максимальную длину очереди соединений,
ожидающих обработки (остальным будет отказано). Значение backlog
зависит от поставщика протокола, недопустимое значение заменяется
ближайшим разрешенным. Стандартного способа получить действительное значение backlog нет. Можно в качестве этого параметра
воспользоваться константой SOMAXCONN, тогда будет установлено
максимально возможное значение.
Функции accept и WSAAccept
Прием соединения клиентов производится функцией accept или
WSAAccept:
SOCKET accept (SOCKET s, struct sockaddr FAR *addr,
int FAR *addrlen );
SOCKET WSAAccept (SOCKET s, struct sockaddr FAR *addr,
LPINT addrlen, LPCONDITIONPROC lpfnCondition,
DWORD dwCallbackData );
87
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Функции возвращают новый дескриптор сокета, соответствующий
принятому соединению. Их аргументы имеют следующий смысл.
Параметр s – сокет, на котором происходит прослушивание. Второй
параметр – указатель на структуру SOCKADDR_IN, а addrlen – указатель
на переменную, содержащей размер этой структуры.
По завершении функции структура addr будет содержать сведения об
IP-адресе клиента, а addrlen – размер структуры.
Параметр lpfnCondition функции WSAAccept – указатель на функцию,
вызываемую при запросе клиента (т.н. Callback-функцию), а параметр
dwCallbackData – передаваемые ей данные.
Callback-функция должна соответствовать строго определенному
прототипу (см. документацию). Здесь мы для краткости не будем
рассматривать работу с ней. Если функции нет, то в качестве
соответствующего параметра достаточно указать NULL.
API-функции клиента
Клиентская часть несколько проще, для установления соединения с
сервером требуется всего три шага:
•
Создать сокет функцией socket или WSASocket;
•
Разрешить имя сервера (в зависимости от используемого протокола);
•
Инициировать соединение функцией connect или WSAConnect.
Функции connect и WSAConnect
Формат функций connect и WSAConnect:
int connect (SOCKET s, const struct sockaddr FAR *name,
int namelen );
int WSAConnect (SOCKET s, const struct sockaddr FAR *name,
int namelen, LPWSABUF lpCallerData,
LPWSABUF lpCalleeData,
LPQOS lpSQOS, LPQOS lpGQOS );
Параметры функции connect практически очевидны: s – сокет для
установления соединения, name – структура адреса сокета, namelen –
размер этой структуры. Параметры WSASocket lpCallerData и lpCalleeData
указывают на буферы данных, используемые в ходе соединения, а lpSQOS
и lpGQOS на структуры данных, определяющих требования к пропускной
способности для сокета и группы сокетов соответственно.
В случае успешного завершения функции возвращают нулевое
значение.
88
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Передача данных: функции send и WSASend
Для пересылки данных через сокет используются функции send и
WSASend, а для приема recv и WSARecv. Следует отметить, что буферы,
используемые при приеме и отправке, состоят из элементов типа char,
следовательно, не предназначены для работы с кодировкой UNICODE.
Отправить строку данных UNICODE можно или в исходном виде,
учитывая, что каждый символ занимает два байта, либо преобразовав к
типу char *.
Все функции приема и отправки данных в случае ошибки возвращают
значение SOCKET_ERROR. Для получения более подробной информации
об ошибке можно вызвать функцию WSAGetLastError.
Формат функции send:
int send (SOCKET s, const char FAR *buf, int len, int flags);
Параметр s определяет сокет для отправки данных. Параметр buf
указывает на символьный буфер, содержащий данные, len задает
количество отправляемых символов. Последний параметр – flags может
быть равен 0, MSG_DONTROUTE, MSG_OOB или два последних,
связанных операцией побитовое ИЛИ. Флаг MSG_DONTROUTE означает,
что транспорт не будет маршрутизировать отправляемые пакеты
(исполнение остается на усмотрение базового протокола). Флаг MSG_OOB
означает срочную посылку – вне полосы (out of band).
Функция WSASend (для Winsock версии 2) имеет следующий формат:
int WSASend (SOCKET s, LPWSABUF lpBuffers,
DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent,
DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
Значения ее параметров следующие.
s – сокет, через который посылаются данные. lpBuffers – указатель на
структуру WSABUF или массив структур, dwBufferCount – количество
передаваемых структур WSABUF. Структура WSABUF включает в себя
сам символьный буфер и его длину. Параметр lpNumberOfBytesSent –
указатель на переменную, куда будет записано общее количество
переданных байтов. Параметр dwFlags имеет тот же смысл, что и для
функции
send.
Последние
два
параметра
(lpOverlapped
и
lpCompletionRoutine) используются для перекрытого ввода-вывода
(overlapped I/O) и будут рассмотрены позже.
89
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Функция WSASendDisconnect
Функция WSASendDisconnect имеет следующий формат:
int WSASendDisconnect (SOCKET s,
LPWSABUF lpOutboundDisconnectData);
Функция начинает процесс закрытия сокета и отправляет
соответствующие данные. Ни один из существующих поставщиков
транспорта не поддерживает передачу данных о закрытии соединения. Ее
действие аналогично вызову shutdown с параметром SD_SEND.
Функции recv и WSARecv
Функция recv – основной инструмент приема данных через сокет. Ее
формат:
int recv (SOCKET s, char FAR *buf, int len, int flags);
Параметр s определяет сокет для приема данных. Параметр buf
указывает на символьный буфер для приема, len задает количество
принимаемых символов или размер буфера. Последний параметр – flags
может быть равен 0, MSG_PEEK, MSG_OOB или два последних,
связанных операцией побитовое ИЛИ. 0 означает отсутствие особых
действий. Флаг MSG_PEEK означает, что доступные данные должны
копироваться в принимающий буфер и при этом оставаться в системном
буфере (не рекомендуется). Флаг MSG_OOB означает срочный характер
принимаемых данных.
По завершении функция возвращает количество полученных байтов.
Использование функции recv в сокетах, ориентированных на передачу
сообщений или дейтаграмм, имеет ряд особенностей. А именно, если
размер предоставляемого буфера меньше размера ожидающих обработки
данных, то после его заполнения возникает ошибка WSAEMSGSIZE.
С потоковыми протоколами этого не происходит.
Функция WSARecv делает то же, что и recv, однако обладает
дополнительными возможностями, в частности, поддерживает перекрытый
ввод-вывод и фрагментарные дейтаграммные уведомления. Ее формат:
int WSARecv (SOCKET s, LPWSABUF lpBuffers,
DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
Значения ее параметров следующие. s – сокет для приема данных.
lpBuffers – указатель на структуру WSABUF или массив структур,
dwBufferCount – количество передаваемых структур WSABUF. Параметр
lpNumberOfBytesRecvd – указатель на переменную, куда в случае
немедленного завершения операции получения данных будет записано
90
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
общее количество принятых байтов. Параметр lpFlags может быть равен 0,
MSG_PEEK, MSG_OOB, MSG_PARTIAL или комбинацией этих значений,
образованной операцией побитовое ИЛИ.
Значение параметра lpFlags, равное MSG_PARTIAL в зависимости от
способа использования имеет разный смысл. Оно используется только с
протоколами, ориентированными на передачу сообщений. Если оно
используется как входной параметр, то операция чтения должна
завершиться, как только будут доступны данные, даже если это только
часть сообщения. Его можно установить также, если функция возвратила
управление из-за нехватки места в буфере, и дочитать сообщение по
частям.
Последние два параметра (lpOverlapped и lpCompletionRoutine)
используются для перекрытого ввода-вывода (overlapped I/O) и будут
рассмотрены позже.
Функция WSARecvDisconnect
Функция является парной для WSASendDisconnect и имеет
следующий формат:
int WSARecvDisconnect (SOCKET s,
LPWSABUF lpOutboundDisconnectData);
Функция принимает данные о закрытии соединения, отправленные
функцией WSASendDisconnect. Сразу после этого она прекращает прием с
удаленной стороны. Ее действие аналогично вызову shutdown с
параметром SD_RECEIVE.
Функция WSARecvEx
Функция является расширением recv для Winsock, ее формат:
int PASCAL FAR WSARecvEx (
SOCKET s, char FAR *buf, int len, int *flags);
Следует обратить внимание, что параметр flags передается по ссылке.
Это позволяет базовому поставщику задавать флаг MSG_PARTIAL. Если
полученные данные не составляют полного сообщения (а протокол должен
быть ориентирован на передачу сообщений), то получатель,
проанализировав состояние flags, может диагностировать эту ситуацию.
Флаги MSG_PEEK и MSG_OOB также могут быть использованы.
Потоковые протоколы
Большинство протоколов с установлением соединения являются
потоковыми. При использовании потокового сокета нет никакой гарантии,
что при попытке отправить, скажем, 2048 байт посредством функции send
вы отправите их все сразу. Причиной неполной отправки может быть,
например, маленький приемный буфер у принимающей стороны. Функция
91
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
send возвращает количество реально отправленных данных, и этим можно
воспользоваться.
Код может выглядеть примерно так:
char sendBuff[2048];
int nBytes = 2048, nLeft = nBytes , idx = 0, ret;
// Заполнение буфера, создание сокета, установление соединения
while(nLeft > 0)
{
ret = send(sock, &sendBuff[idx], nLeft, 0);
if (ret == SOCKET_ERROR)
{
printf("send() failed: %d\n", WSAGetLastError());
break;
}
nLeft -= ret;
idx += ret;
}
Все сказанное справедливо и для принимающей стороны. Приложение
не может знать, сколько данных оно сможет прочитать в очередной раз из
потокового сокета. Пусть мы знаем, что длина сообщения равна 512 байт.
Тогда их прочитать можно, например, так:
char recvBuff[1024];
int nLeft = 512 , idx = 0, ret;
// Создание сокета, установление соединения
while(nLeft > 0)
{
ret = recv(sock, &recvBuff[idx], nLeft, 0);
if (ret == SOCKET_ERROR)
{
printf("recv() failed: %d\n", WSAGetLastError());
break;
}
nLeft += ret;
idx -= ret;
}
Чуть сложнее, если длина сообщения может варьироваться. Однако
очевидно, что не слишком сложно сначала сообщить пользователю длину
сообщения, а потом организовать его прием.
92
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Завершение сеанса: функции shutdown и closesocket
По окончании работы с сокетом необходимо закрыть соединение и
освободить все ресурсы, связанные с сокетом, вызвав функцию closesocket.
Однако перед ее вызовом нужно корректно завершить сеанс функцией
shutdown:
int shutdown (SOCKET s, int how);
Параметр how может принимать значения SD_RECEIVE, SD_SEND
или SD_BOTH. Значение SD_RECEIVE запрещает все последующие
вызовы любых функций приема данных, однако на протоколы нижнего
уровня это не действует. Если в очереди TCP-сокета есть данные, либо они
поступают позже, они сбрасываются. UDP-сокеты в аналогичной ситуации
продолжают принимать данные и ставить их в очередь. SD_SEND
запрещает все вызовы отправки данных. В случае TCP-сокетов после
приема подтверждения от получателя данных об их доставке передается
пакет FIN. Значение SD_BOTH запрещает как прием, так и отправку.
Функция closesocket закрывает сокет. Она определена так:
int closesocket (SOCKET s);
Вызов closesocket освобождает дескриптор сокета. После этого все
операции с сокетов завершатся ошибкой WSAENOTSOCK. Ожидающие
асинхронные вызовы отменяются без уведомления.
Примеры
Несмотря на огромное количество разнообразных функций отправки и
приема данных, большинству приложений для приема информации
достаточно функций recv и WSARecv, а для отправки send и WSASend.
Другие функции обеспечивают специальные возможности и используются
редко.
В качестве иллюстрации мы рассмотрим небольшой пример клиентсерверного взаимодействия с учетом рассмотренных принципов и
функций. Исходные коды содержатся в файлах Server.c и Client.c.
Первое приложение представляет собой эхо-сервер. Оно создает
сокет, привязывает его к конкретому IP-интерфейсу и порту и слушает
соединения клиентов. После приема от клиента запроса на соединение
создается новый сокет, который передается клиентскому потоку. Поток
читает данные и возвращает их клиенту.
#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#define DEFAULT_PORT
#define DEFAULT_BUFFER
5150
4096
93
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
int
BOOL
char
iPort = DEFAULT_PORT;
bInterface = FALSE,
bRecvOnly = FALSE;
szAddress[128];
//
//
//
//
Порт для прослушивания
Конкретный клиент?
Только прием данных
Интерфейс для прослушивания
void usage() // Информация о параметрах командной строки.
{
printf("usage: server [-p:x] [-i:IP] [-o]\n\n");
printf("
-p:x
Port number to listen on\n");
printf("
-i:str
Interface to listen on\n");
printf("
-o
Don't echo the data back\n\n");
ExitProcess(1);
}
// Разбор командной строки
void ValidateArgs(int argc, char **argv)
{
int i;
for(i = 1; i < argc; i++)
{
if ((argv[i][0] == '-') || (argv[i][0] == '/'))
{
switch (tolower(argv[i][1]))
{
case 'p':
iPort = atoi(&argv[i][3]);
break;
case 'i':
bInterface = TRUE;
if (strlen(argv[i]) > 3)
strcpy(szAddress, &argv[i][3]);
break;
case 'o':
bRecvOnly = TRUE;
break;
default:
usage();
break;
}
}
}
}
94
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Главная функция потока. Получает данные и отправляет обратно
DWORD WINAPI ClientThread(LPVOID lpParam)
{
SOCKET
sock=(SOCKET)lpParam;
char
szBuff[DEFAULT_BUFFER];
int
ret,
nLeft,
idx;
while(1)
{
// Блокируюший вызов recv()
ret = recv(sock, szBuff, DEFAULT_BUFFER, 0);
if (ret == 0) // Корректное завершение
break;
else if (ret == SOCKET_ERROR)
{
printf("recv() failed: %d\n", WSAGetLastError());
break;
}
szBuff[ret] = '\0';
printf("RECV: '%s'\n", szBuff);
// Если требуется, ответная отправка данных
if (!bRecvOnly)
{
nLeft = ret;
idx = 0;
// Проверка, что все данные записаны
while(nLeft > 0)
{
ret = send(sock, &szBuff[idx], nLeft, 0);
if (ret == 0)
break;
else if (ret == SOCKET_ERROR)
{
printf("send() failed: %d\n",
WSAGetLastError());
break;
}
nLeft -= ret;
idx += ret;
}
}
}
return 0;
}
95
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Главный поток. Создает сокет, ждет подключений клиентов
int main(int argc, char **argv)
{
WSADATA wsd;
SOCKET
sListen,
sClient;
int
iAddrSize;
HANDLE
hThread;
DWORD
dwThreadId;
struct sockaddr_in
local,
client;
ValidateArgs(argc, argv);
if (WSAStartup(MAKEWORD(2,2), &wsd) != 0)
{
printf("Failed to load Winsock!\n");
return 1;
}
// Создание сокета для прослушивания
//
sListen = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
if (sListen == SOCKET_ERROR)
{
printf("socket() failed: %d\n", WSAGetLastError());
return 1;
}
// Выбрать конкретный интерфейс и привязаться к нему
if (bInterface)
{
local.sin_addr.s_addr = inet_addr(szAddress);
if (local.sin_addr.s_addr == INADDR_NONE)
usage();
}
else
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_family = AF_INET;
local.sin_port = htons(iPort);
if (bind(sListen, (struct sockaddr *)&local,
sizeof(local)) == SOCKET_ERROR)
{
printf("bind() failed: %d\n", WSAGetLastError());
return 1;
}
listen(sListen, 8);
96
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Ожидание клиентов, создание потока для каждого соединения
while (1)
{
iAddrSize = sizeof(client);
sClient = accept(sListen, (struct sockaddr *)&client,
&iAddrSize);
if (sClient == INVALID_SOCKET)
{
printf("accept() failed: %d\n", WSAGetLastError());
break;
}
printf("Accepted client: %s:%d\n",
inet_ntoa(client.sin_addr), ntohs(client.sin_port));
hThread = CreateThread(NULL, 0, ClientThread,
(LPVOID)sClient, 0, &dwThreadId);
if (hThread == NULL)
{
printf("CreateThread() failed: %d\n", GetLastError());
break;
}
CloseHandle(hThread);
}
closesocket(sListen);
WSACleanup();
return 0;
}
Клиентское приложение создает сокет, разрешает переданное
приложению имя сервера и соединяется с сервером. Затем оно отправляет
несколько сообщений. После каждой отправки клиент ожидает эхо-ответа
сервера. Все данные выводятся на экран.
#include <winsock2.h>
#include <stdio.h>
#include <stdlib.h>
#define DEFAULT_COUNT
20
#define DEFAULT_PORT
5150
#define DEFAULT_BUFFER
2048
#define DEFAULT_MESSAGE
\
"This is a test of the emergency broadcasting system"
szServer[128],
// Сервер для соединения
szMessage[1024];
// Сообщение для сервера
int
iPort
= DEFAULT_PORT; // Порт сервера
DWORD dwCount
= DEFAULT_COUNT; // Сколько отправить сообщений
BOOL bSendOnly = FALSE;
// Только отправка сообщений
char
97
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
void usage() // Информация о параметрах командной строки.
{
printf("usage: client [-p:x] [-s:IP] [-n:x] [-o]\n\n");
printf("
-p:x
Remote port to send to\n");
printf("
-s:IP
Server's IP address or hostname\n");
printf("
-n:x
Number of times to send message\n");
printf("
-o
Send messages only; don't receive\n");
ExitProcess(1);
}
// Разбор командной строки
void ValidateArgs(int argc, char **argv)
{
int i;
for(i = 1; i < argc; i++)
{
if ((argv[i][0] == '-') || (argv[i][0] == '/'))
{
switch (tolower(argv[i][1]))
{
case 'p':
// Удаленный порт
if (strlen(argv[i]) > 3)
iPort = atoi(&argv[i][3]);
break;
case 's':
// Сервер
if (strlen(argv[i]) > 3)
strcpy(szServer, &argv[i][3]);
break;
case 'n':
// Сколько отправить сообщений
if (strlen(argv[i]) > 3)
dwCount = atol(&argv[i][3]);
break;
case 'o':
// Только отправка сообщений
bSendOnly = TRUE;
break;
default:
usage();
break;
}
}
}
}
98
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Главный поток. Создает сокет, соединяется с сервером,
// отправляет и принимает данные
int main(int argc, char **argv)
{
WSADATA wsd;
SOCKET
sClient;
char
szBuffer[DEFAULT_BUFFER];
int
ret,
i;
struct sockaddr_in server;
struct hostent
*host = NULL;
ValidateArgs(argc, argv);
if (WSAStartup(MAKEWORD(2,2), &wsd) != 0)
{
printf("Failed to load Winsock library!\n");
return 1;
}
strcpy(szMessage, DEFAULT_MESSAGE);
// Создание сокета
sClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (sClient == INVALID_SOCKET)
{
printf("socket() failed: %d\n", WSAGetLastError());
return 1;
}
server.sin_family = AF_INET;
server.sin_port = htons(iPort);
server.sin_addr.s_addr = inet_addr(szServer);
// Если адрес не представлен в форме "aaa.bbb.ccc.ddd",
// это имя узла. Пробуем его разрешить
if (server.sin_addr.s_addr == INADDR_NONE)
{
host = gethostbyname(szServer);
if (host == NULL)
{
printf("Unable to resolve server: %s\n", szServer);
return 1;
}
CopyMemory(&server.sin_addr, host->h_addr_list[0],
host->h_length);
}
99
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if (connect(sClient, (struct sockaddr *)&server,
sizeof(server)) == SOCKET_ERROR)
{
printf("connect() failed: %d\n", WSAGetLastError());
return 1;
}
// Посылка и получение данных
for(i = 0; i < dwCount; i++)
{
ret = send(sClient, szMessage, strlen(szMessage), 0);
if (ret == 0)
break;
else if (ret == SOCKET_ERROR)
{
printf("send() failed: %d\n", WSAGetLastError());
break;
}
printf("Send %d bytes\n", ret);
if (!bSendOnly)
{
ret = recv(sClient, szBuffer, DEFAULT_BUFFER, 0);
if (ret == 0) // Корректное завершение
break;
else if (ret == SOCKET_ERROR)
{
printf("recv() failed: %d\n", WSAGetLastError());
break;
}
szBuffer[ret] = '\0';
printf("RECV [%d bytes]: '%s'\n", ret, szBuffer);
}
}
closesocket(sClient);
WSACleanup();
return 0;
}
Если запустить приложения с параметром –o, то клиент будет только
отправлять данные, а сервер только принимать. Пример вызова:
server –p:5150 –o
client –p:5150 –s:IP –n:10 –o
В этом случае клиент совершит десять отправок, а сервер получает
все десять сообщений за один или два вызова функции recv.
100
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Протоколы, не требующие соединения
Использование таких протоколов требует несколько иных методов
отправки и приема данных.
Для начала обсудим получателя данных (пусть это сервер), поскольку
здесь сравнительно немного отличий от использования протокола,
требующего соединения.
Приемник
Сначала создаем сокет функцией socket или WSASocket. Затем
выполняем привязку сокета к интерфейсу, на котором будем принимать
данные (функция bind). Далее, в отличие от предыдущего случая, нельзя
вызывать функции listen или accept, вместо этого нужно просто ожидать
приема входящих данных. Поскольку соединения нет, компьютер может
получить дейтаграммы от любой машины в сети. Простейшая функция
приема – recvfrom:
int recvfrom (SOCKET s, char FAR* buf, int len, int flags,
struct sockaddr FAR *from, int FAR *fromlen);
Первые четыре параметра такие же, как и у функции recv. Параметр
from – указатель на структуру SOCKADDR, а fromlen на переменную для
задания ее длины. После возврата из функции from будет содержать адрес
отправителя.
В Winsock 2 применяется другая версия функции recvfrom –
WSARecvFrom
int WSARecvFrom (SOCKET s, LPWSABUF lpBuffers,
DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags, struct sockaddr FAR *lpFrom,
LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
Разница в том, что для получения данных используется структура
WSABUF. Можно выставить для приема одну или несколько таких
структур (параметр lpBuffers), указав их количество в параметре
dwBufferCount. Смысл остальных дополнительных параметров такой же,
как и у функции WSARecv.
Другой способ приема (отправки) данных в сокетах, не требующих
соединения – установление соединения. Звучит это странно, однако
реального соединения не происходит. Вызов функции connect или
WSAConnect, с переданным ему адресом удаленного компьютера, просто
позволяет ассоциировать этот адрес с сокетом. Теперь можно для приема
данных пользоваться функциями recv или WSARecv, поскольку
отправитель уже известен.
101
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Отправитель
Простейший способ отправить данные через сокет, не требующий
соединения – вызвать функцию sendto:
int sendto (SOCKET s, const char FAR *buf, int len, int flags,
const struct sockaddr FAR *to, int tolen );
Параметры у функции такие же, как и у recvfrom, с той лишь
разницей, что buf – это буфер с отправляемыми данными, len – количество
отправляемых байтов, а параметр to – указатель на структуру SOCKADDR
с адресом принимающей станции.
Вариант этой функции для Winsock 2 имеет имя WSASendTo:
int WSASendTo (SOCKET s, LPWSABUF lpBuffers,
DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent,
DWORD dwFlags, const struct sockaddr FAR *lpTo,
int iToLen, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
Параметры функции WSASendTo аналогичны параметрам WSASend с
той лишь разницей, что lpTo – указатель на структуру SOCKADDR для
данного протокола с адресом приемника, а параметр iToLen задает размер
этой структуры.
Как и при получении данных, сокет можно подключить к адресу
конечной станции вызовом connect или WSAConnect.
Протоколы, ориентированные на передачу сообщений
Большинство протоколов, требующих соединения, – потоковые, а не
требующих соединения – это протоколы, ориентированные на передачу
сообщений. Особенности работы с последними в основном объясняются
необходимостью сохранения границ сообщений. Поэтому передача не
завершится, пока не передано все сообщение, а приемник должен иметь
входной буфер достаточного объема. Исключение здесь составляют
протоколы, допускающие фрагментарный обмен данными (флаг
MSG_PARTIAL).
Поскольку соединения нет, то и его закрытия не требуется,
достаточно вызвать функцию closesocket, чтобы освободить все связанные
с сокетом ресурсы.
Пример
В качестве примера работы с протоколом, не требующим
установления соединения, на диске имеется исходный код двух программ,
обменивающихся UDP-дейтаграммами: Receiver.c и Sender.c.
Действия приемника таковы. Создается сокет и привязывается к
конкретному интерфейсу и номеру порта. После этого для чтения данных
102
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
просто вызывается функция recvfrom. Не приводя полностью исходный
код, рассмотрим основные моменты работы.
int main(int argc, char **argv)
{
// ...
// Создание сокета
s = socket(AF_INET, SOCK_DGRAM, 0);
// ...
// Разместить приемный буфер
recvbuf = GlobalAlloc(GMEM_FIXED, dwLength);
// ...
// Чтение дейтаграмм
for(i = 0; i < dwCount; i++)
{
dwSenderSize = sizeof(sender);
ret = recvfrom(s, recvbuf, dwLength, 0,
(SOCKADDR *)&sender, &dwSenderSize);
if (ret == SOCKET_ERROR)
{ /* Ошибка */}
else
if (ret == 0)
break;
else
{
recvbuf[ret] = '\0';
printf("[%s] sent me: '%s'\n",
inet_ntoa(sender.sin_addr), recvbuf);
}
}
closesocket(s);
GlobalFree(recvbuf);
WSACleanup();
return 0;
}
Отправитель требует обязательных параметров: IP-адрес приемника и
номер порта. Если дополнительно задать опцию –с, то он вызовет еще и
функцию connect, для привязки сокета к адресу (в этом случае для
отправки используется функция send, а не sendto).
int main(int argc, char **argv)
{
// ...
// Создание сокета
s = socket(AF_INET, SOCK_DGRAM, 0);
103
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// ...
// Разместить буфер отправки
sendbuf = GlobalAlloc(GMEM_FIXED, dwLength);
// ...
// Если задана опция –c, выполняется "подключение"
// и отправка с использованием функции send()
if (bConnect)
{
if (connect(s, (SOCKADDR *)&recipient,
sizeof(recipient)) == SOCKET_ERROR)
{ /* Ошибка */}
for(i = 0; i < dwCount; i++)
{
ret = send(s, sendbuf, dwLength, 0);
if (ret == SOCKET_ERROR)
{
printf("send() failed: %d\n", WSAGetLastError());
break;
}
else if (ret == 0)
break; // Отправка успешна!
}
}
else // Иначе используем функцию sendto()
{
for(i = 0; i < dwCount; i++)
{
ret = sendto(s, sendbuf, dwLength, 0,
(SOCKADDR *)&recipient, sizeof(recipient));
if (ret == SOCKET_ERROR)
{ /* Ошибка */}
else if (ret == 0)
break; // Отправка успешна!
}
}
closesocket(s);
GlobalFree(sendbuf);
WSACleanup();
return 0;
}
Дополнительные функции API
Функция getpeername возвращает информацию об адресе партнера на
подключенном сокете:
104
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
int getpeername (SOCKET s, struct sockaddr FAR *name,
int FAR *namelen);
Функция getsockname возвращает информацию о локальном адресе:
int getsockname (SOCKET s, struct sockaddr FAR *name,
int FAR *namelen);
Если нужно, чтобы другой процесс воспользовался вашим сокетом,
требуется
скопировать
информацию
о
нем
в
структуру
WSAPROTOCOL_INFO и передать этому процессу, чтобы он смог
открыть описатель того же базового сокета. Это можно сделать
посредством вызова функции WSADuplicateSocket. Для потоков в этом нет
необходимости – им можно просто передать дескриптор сокета.
int WSADuplicateSocket (SOCKET s, DWORD dwProcessId,
LPWSAPROTOCOL_INFO lpProtocolInfo);
Здесь параметр dwProcessId задает идентификатор процесса, который
будет использовать скопированный сокет, а lpProtocolInfo – указатель на
заполняемую структуру. Скопированные сокеты в конце работы
необходимо закрыть во всех процессах.
Для передачи файлов рекомендуется использовать высокоэффективную функцию TransmitFile. Все действия при этом будут
происходить в режиме ядра. Передача "руками" с использованием
функций типа send требует многократных переключений между режимами
ядра и пользовательским и потому работает намного дольше.
BOOL TransmitFile (SOCKET hSocket, HANDLE hFile,
DWORD nNumberOfBytesToWrite, DWORD nNumberOfBytesPerSend,
LPOVERLAPPED lpOverlapped,
LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers,
DWORD dwFlags );
Параметр hSocket – подключенный сокет, hFile – дескриптор
открытого файла, nNumberOfBytesToWrite – количество передаваемых
байтов
(если
он
равен
нулю,
передается
весь
файл),
nNumberOfBytesPerSend задает размер отправляемых блоков данных (если
он равен нулю, используется стандартный размер отправки). Параметр
lpOverlapped определяет структуру OVERLAPPED, применяемую в
перекрытом
вводе-выводе,
а
lpTransmitBuffers
–
структуру
TRANSMIT_FILE_BUFFERS, содержащую данные, которые нужно
отправить до и после передачи файла. Последний параметр (flags)
управляет режимами работы функции TransmitFile. Более подробную
информацию можно найти, изучив соответствующую документацию.
На диске содержатся также аналоги рассмотренных программ для
других протоколов.
105
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Ввод-вывод в Winsock
В теме рассматривается управление вводом-выводом через сокеты в
Windows-приложениях. В Winsock такое управление реализовано с
помощью режимов работы и моделей ввода-вывода.
Режим (mode) сокета определяет поведение функций, работающих с
сокетом. Модель (model) сокета описывает, как приложение производит
ввод-вывод при работе с сокетом. Модели не зависят от режима работы и
позволяют обходить их ограничения.
Winsock поддерживает два режима: блокирующий (blocking) и
неблокирующий (nonblocking). Оба эти режима поддерживаются всеми
платформами Windows.
Мы также рассмотрим ряд моделей, которые помогают приложению
управлять несколькими сокетами в асинхронном режиме: select,
WSAAsyncSelect, WSAEventSelect, перекрытый ввод-вывод (overlapped
I/O) и порты завершения (completion port). В таблице перечислены модели,
поддерживаемые различными платформами Windows.
Поддерживаемые модели ввода-вывода
Платформа
select
WSAAsync
Select
WSAEvent Перекрытый
Порт
Select
ввод-вывод завершения
Windows CE
Да
Нет
Нет
Нет
Нет
Windows 95
(Winsock 1)
Да
Да
Нет
Нет
Нет
Windows 95
(Winsock 2)
Да
Да
Да
Да
Нет
Windows 98
Да
Да
Да
Да
Нет
Windows NT
Да
Да
Да
Да
Да
Windows 2000
Да
Да
Да
Да
Да
Режимы работы сокетов
В блокирующем режиме функции ввода-вывода (например, send и
recv) не возвращают управление, пока операция не будет завершена. В
неблокирующем – возвращают управление, не дожидаясь ее завершения.
Блокирующий режим
В этом режиме любой вызов функции Winsock блокирует сокет на
некоторое время. Большинство приложений, работающих в этом режиме,
следуют модели "поставщик – потребитель", в которой программа
106
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
считывает или записывает определенное количество байтов, а затем
выполняет с ними какие-либо операции.
SOCKET sock;
char recvBuff[1024];
int done = 0, nBytes;
...
while(!done)
{
nBytes = recv(sock, recvBuff, 65, 0);
if (nBytes == SOCKET_ERROR)
{ printf("recv() failed: %d\n", WSAGetLastError());
break;
}
DoComputationOnData(recvBuff);
}
...
Проблема в том, что в таком варианте функция recv может не
завершиться никогда, так как для этого нужно считать какие-либо данные
из буфера. Первое, что приходит в голову – применить чтение без
удаления из буфера, т.е. флаг MSG_PEEK в recv или вызов функции
ioctlsocket с параметром FIONREAD. Это очень неудачное решение,
поскольку оно сопряжено со значительными системными издержками.
Более удачным решением является разделение приложения на
считывающий и вычисляющий поток, использующие общий буфер
данных. Доступ к буферу регулируется некоторым синхронизирующим
объектом, например событием или мьютексом.
Считывающий поток, поместив минимально необходимое количество
данных в буфер, инициирует сигнальное событие, уведомляющее
вычисляющий поток о возможности начать вычисления. Последний в свою
очередь удаляет часть данных из буфера и производит с ними
необходимые операции.
Ниже приводится пример кода, реализующего предложенную схему с
использованием критических секций.
CRITICAL_SECTION data;
HANDLE
hEvent;
TCHAR
buff[MAX_BUFFER_SIZE];
...
// Считывающий поток
void ReadThread (void)
{
int nTotal=0, nRead=0, nLeft=0, nBytes=0, done=0;
107
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
while (!done)
{
nTotal = 0;
nLeft = NUM_BYTES_REQUIRED;
while (nTotal != NUM_BYTES_REQUIRED)
{
EnterCriticalSection (&data);
nRead = recv(sock,
&(buff[MAX_BUFFER_SIZE - nBytes]), nLeft, 0);
if (nRead == SOCKET_ERROR)
{
printf("recv() failed: %d\n", WSAGetLastError());
ExitThread();
}
nTotal += nRead;
nLeft -= nRead;
nBytes += nRead;
LeaveCriticalSection (&data);
}
SetEvent (hEvent);
}
}
// Вычисляющий поток
void ProcessThread (void)
{
WaitForSingleObject (hEvent);
EnterCriticalSection (&data);
DoComputationOnData(buff);
// Удаление обработанных данных из массива
// и сдвиг оставшихся в начало
nBytes -= NUM_BYTES_REQUIRED;
LeaveCriticalSection (&data);
}
При работе в блокирующем режиме сложнее поддерживать связь
через несколько сокетов одновременно. Однако предложенную схему
можно изменить так, чтобы для каждого сокета создавался считывающий и
вычисляющий поток.
108
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Неблокирующий режим
Неблокирующий режим несколько сложнее в использовании, зато,
обеспечивая те же возможности, что и блокирующий, он обладает рядом
преимуществ.
Пример создания сокета и перевода его в неблокирующий режим:
SOCKET
s;
unsigned long ul = 1;
int
nRet;
...
s = socket(AF_INET, SOCK_STREAM, 0);
nRet = ioctlsocket (s, FIOBIO, (unsigned long *)&ul);
if (nRet == SOCKET_ERROR)
{
// Не удалось перевести сокет в неблокирующий режим
}
При работе с таким сокетом функции Winsock завершаются
немедленно.
Как
правило,
они
возвращают
ошибку
WSAEWOULDBLOCK, что означает, что операция не успела завершиться
за время работы функции. Вызывать функцию непрерывно вплоть до
успешного завершения при этом такое же плохое решение, что и
упомянутое ранее чтение без удаления из буфера. Рассматриваемые ниже
модели ввода-вывода снимают часть проблем, например, позволяют
приложению определить, когда сокет доступен для чтения и записи.
Модели управления вводом-выводом сокетов
Приложения Winsock могут использовать пять моделей управления
вводом-выводом: select, WSAAsyncSelect, WSAEventSelect, перекрытый
ввод-вывод и порты завершения. На диске имеются исходные коды
простейшего TCP-сервера с учетом особенностей каждой модели.
Модель select
Модель наиболее широко распространена в Winsock и основана на
использовании одноименной функции. Ее идея восходит к модели сокетов
Беркли для Unix. Модель появилась в Winsock 1.1, чтобы позволить
приложениям, избегающим блокировки, управлять несколькими сокетами
в определенном порядке.
Функцию select используют также для определения, есть ли в сокете
данные, и можно ли туда записать новые. Функция определяет статус
одного или нескольких сокетов и при необходимости ждет, пока можно
будет выполнить операции чтения и записи данных. Ее формат:
109
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
int select ( int nfds,
fd_set FAR *readfds,
fd_set FAR *writefds,
fd_set FAR *exceptfds,
const struct timeval FAR *timeout
);
Первый параметр (nfds) игнорируется, он включен лишь для
совместимости с приложениями Беркли. Следующие три параметра типа
указатель на тип fd_set предназначены для проверки возможности чтения
(readfds), возможности записи (writefds) и для срочных (out of band, OOB)
данных – exceptfds. Структура fd_set предназначена для задания набора
сокетов.
Набор readfds определяет сокеты, удовлетворяющие одному из
следующих условий:
•
данные доступны для чтения;
•
если сокет был поставлен на прослушивание функцией listen, то
уже получен запрос на соединение, то есть вызов функции accept
не заблокирует работу;
•
для ориентированных на соединение сокетов это означает, что
было получено требование разорвать соединение.
Набор writefds определяет сокеты, удовлетворяющие одному из
следующих условий:
•
•
возможна отправка данных;
если обрабатывается неблокирующий вызов соединения
(connect), то попытка соединения удалась.
Набор exceptfds определяет сокеты, удовлетворяющие одному из
следующих условий:
•
•
если обрабатывается неблокирующий вызов
(connect), то попытка соединения не удалась;
OOB-данные доступны для чтения.
соединения
Так, если требуется проверить возможность чтения из сокета, нужно
включить его в набор readfds, вызвать функцию select и проверить,
находится ли он все еще в списке readfds. После вызова функции select
любые два из трех наборов могут быть пусты (NULL), но не все три сразу.
Последний параметр (timeout) представляет собой указатель на
структуру timeval, определяющую, сколько времени select будет ждать,
пока найдет хотя бы один описатель, удовлетворяющий заданному
критерию. Структура timeval определена следующим образом:
struct timeval {
long tv_sec; // секунды
long tv_usec; // и микросекунды
};
110
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
По истечении тайм-аута функция select возвращает 0, в случае любой
неудачи – SOCKET_ERROR, в случае успешного завершения – количество
сокетов, удовлетворяющих заданным критериям отбора. Для работы с
наборами fd_set определены следующие макросы:
•
FD_CLR (s, *set) – удаляет сокет s из набора set;
•
FD_ISSET (s, *set) – проверяет, входит ли сокет s в набор set;
•
FD_SET (s, *set) – добавляет сокет s в набор set;
•
FD_ZERO (s, *set) – инициализирует набор set как пустой.
Ниже приводится пример кода с использованием функции select для
проверки возможности чтения из сокета.
SOCKET
s;
fd_set
fdread;
int
ret;
// Создание сокета и установление соединения
while (TRUE) // Управление вводом-выводом сокета
{
FD_ZERO (&fdread);
// Очистка набора fdread
FD_SET (s, &fdread); // Добавление сокета s в набор fdread
if ((ret = select (0, &fdread, NULL, NULL, NULL)
== SOCKET_ERROR)
{
// Обработка ошибок
}
if (ret > 0)
{
if (FD_ISSET (s, &fdread))
{
// Чтение через сокет s
}
}
}
Модель WSAAsyncSelect
Использование функции WSAAsyncSelect позволяет реализовать
асинхронную модель управления вводом-выводом. Приложение в этом
случае получает информацию о событиях, связанных с сокетом при
помощи сообщений Windows. Эта же модель используется объектом
CSocket из библиотеки классов MFC.
Уведомления о сообщениях
Для того, чтобы использовать модель WSAAsyncSelect, приложение
должно создать окно, используя функцию CreateWindow, и процедуру
обработки сообщений для этого окна. Можно использовать, например
диалоговое окно с диалоговой процедурой. Дескриптор этого окна вы
передадите функции WSAAsyncSelect. Ее формат:
111
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
int WSAAsyncSelect(
SOCKET s, // Сокет
HWND hWnd, // Дескриптор окна-получателя сообщения
unsigned int wMsg, // Сообщение (обычно WM_USER + ...)
long lEvent // События, которые нужно отслеживать
);
Последний параметр представляет собой битовую маску,
определяющую
комбинацию
интересующих
нас
событий.
Ее
составляющие соответствуют следующим событиям:
•
FD_READ – готовности к чтению;
•
FD_WRITE – готовности к записи;
•
FD_OOB – получению срочных данных;
•
FD_ACCEPT – наличию входящих соединений;
•
FD_CONNECT – завершению соединения или многоточечной
операции join;
•
FD_CLOSE – закрытию сокета;
•
FD_QOS – изменению QoS;
•
FD_GROUP_QOS – изменению QoS (зарезервировано для
будущего использования группами сокетов);
•
FD_ROUTING_INTERFACE_CHANGE – изменению интерфейса
маршрутизации для указанных адресов;
•
FD_ADDRESS_LIST_CHANGE – изменению списка локальных
адресов для семейства протокола сокета.
Пример вызова функции WSAAsyncSelect:
WSAAsyncSelect (s, hwnd, WM_SOCKET, FD_CONNECT | FD_READ |
FD_WRITE | FD_CLOSE);
Окно получит сообщение WM_SOCKET в случае, если произойдет
любое из указанных событий. Если требуется для разных событий
получать разные сообщения, то можно вызвать функцию несколько раз с
различными значениями третьего и четвертого параметров.
При вызове функции WSAAsyncSelect сокет автоматически переходит
в неблокирующий режим. Поэтому, например, вызов функции WSARecv
возвратит ошибку WSAEWOULDBLOCK, если в буфере нет данных.
После успешного вызова функции WSAAsyncSelect приложение будет
получать уведомления о событиях на сокете в виде сообщений Windows,
отправляемых окну, заданному параметром hWnd. Процедура обработки
этих сообщений должна иметь следующий формат:
LRESULT CALLBACK WindowProc (
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);
112
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Ее параметры имеют следующий смысл:
hWnd – дескриптор окна, вызвавшего процедуру,
uMsg – сообщение, которое нужно обработать,
wParam – сокет, на котором произошло событие,
lParam состоит из двух частей, причем младшее слово указывает
произошедшее событие, а старшее содержит код ошибки.
Для выделения кода ошибки из последнего параметра удобно
пользоваться специальным макросом WSAGETSELECTERROR.
В предлагаемом ниже примере исходного кода рассмотрены основные
шаги программирования сервера в модели WSAAsyncSelect.
#define WM_SOCKET (WM_USER + 1)
#include <windows.h>
int WINAPI WinMain (HINSTANCE hInstance,
HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
SOCKET Listen;
SOCKADDR_IN InternetAddr;
HWND Window;
// Создание окна и привязка процедуры ServerWinProc
Window = MyCreateWindow();
// Запуск Winsock и создание сокета
WSAStartup (...);
Listen = socket (...);
// Привязка сокета к адресу и порту, прослушивание соединений
InternetAddr.sin_family = AF_INET;
InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
InternetAddr.sin_port = htons(PORT);
bind (Listen, (PSOCKADDR) &InternetAddr,
sizeof(InternetAddr));
// Настройка уведомлений с сокета
WSAAsyncSelect (Listen, Window, WM_SOCKET,
FD_ACCEPT|FD_CLOSE);
listen(Listen, 5);
// Цикл для трансляции и обработки сообщений окна
}
// Функция обработки сообщений окна
LRESULT CALLBACK WindowProc (HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
SOCKET Accept;
113
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
switch (uMsg)
{
case WM_PAINT:
// Прорисовка окна
break;
case WM_SOCKET:
if (WSAGETSELECTERROR(lParam)) // Определение ошибок
{
// Вывод сообщения об ошибке
closesocket(wParam);
break;
}
switch(WSAGETSELECTEVENT(lParam))
{
case FD_ACCEPT:
Accept = accept(wParam, NULL, NULL);
WSAAsyncSelect (Accept, hwnd, WM_SOCKET,
FD_READ|FD_WRITE|FD_CLOSE);
break;
case FD_READ:
// Прием данных
break;
case FD_WRITE:
// Отправка данных
break;
case FD_CLOSE:
closesocket(wParam);
break;
}
}
return TRUE;
}
При обработке уведомления FD_WRITE нужно иметь в виду, что оно
отправляется в трех случаях:
•
•
•
после подключения к сокету функцией connect или WSAConnect;
после приема соединения функцией accept или WSAAccept;
когда функции send, WSASend, sendto или WSASendTo
возвращают ошибку WSAEWOULDBLOCK и место в буфере
освобождается.
Обычно следует полагать, что запись в сокет возможна, начиная с
первого уведомления FD_WRITE и до того момента пока send, WSASend,
sendto или WSASendTo не вернут ошибку WSAEWOULDBLOCK. После
этого следует ждать очередного уведомления FD_WRITE.
114
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Модель WSAEventSelect
Модель WSAEventSelect очень похожа на модель WSAAsyncSelect и
так же позволяет реализовать асинхронную модель управления вводомвыводом. Приложение получает информацию о событиях, связанных с
сокетом при помощи сообщений Windows. Отличие ее в том, что
сообщения направляются объекту "событие", а не окну.
Уведомления о событиях
Модель WSAAsyncSelect предполагает, что приложение должно
создать объект "событие" для каждого сокета, используя функцию
WSACreatEvent:
WSAEVENT WSACreatEvent (void);
После получения дескриптора объекта события его следует связать с
сокетом и зарегистрировать интересующие нас типы событий.
Для этого используется функция WSAEventSelect:
int WSAEventSelect(
SOCKET s, // Сокет
WSAEVENT hEventObject, // Объект "событие"
long lNetworkEvents // Битовая маска, задающая события
);
Последний параметр – битовая маска, получаемая комбинацией типов
сетевых событий, описанных при рассмотрении модели WSAAsyncSelect.
Событие имеет два рабочих состояния: свободное (signaled) и занятое
(nonsignaled), – а также два оперативных режима: ручного (manual) и
автоматического сброса (auto reset). При создании событие находится в
занятом состоянии и режиме ручного сброса, то есть приложение
ответственно за его возврат в занятое состояние после выполнения
необходимых действий по вводу-выводу. Это делает функция
WSAResetEvent:
BOOL WSAResetEvent (WSAEVENT hEvent);
Функция возвращает TRUE или FALSE в зависимости от успешности
вызова.
После завершения работы с объектом события для освобождения
системных ресурсов следует вызвать функцию
BOOL WSACloseEvent (WSAEVENT hEvent);
Функция так же, как WSAResetEvent, возвращает TRUE или FALSE в
зависимости от успешности вызова.
Функция WSAWaitForMultipleEvents позволяет дождаться сетевого
события. Она возвращает управление, когда один из заданных объектов
событий перейдет в свободное состояние либо истечет заданный тайм-аут.
115
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
DWORD WSAWaitForMultipleEvents(
DWORD cEvents,
const WSAEVENT FAR *lphEvents,
BOOL fWaitAll,
DWORD dwTimeout,
BOOL fAlertable
);
Здесь cEvents задает количество элементов в массиве событий, а
lphEvents указатель на массив. Функция WSAWaitForMultipleEvents
поддерживает не более 64 (константа WSA_MAXIMUM_WAIT_EVENTS)
событий, если их больше, то можно, например, разделить их по разным
рабочим потокам.
Если параметр fWaitAll имеет значение TRUE, то функция будет
ожидать освобождения всех объектов событий из массива, если FALSE, то
вернет управление, когда произойдет какое-нибудь из них.
Тайм-аут задается в миллисекундах. Он может быть равен нулю, тогда
функция вернет управление сразу, однако такой вариант ожидания не
рекомендуется из соображений эффективности. dwTimeout можно
положить равным WSA_INFINITE, в этом случае функция будет ждать
сколь угодно долго. Если функция WSAWaitForMultipleEvents возвратила
управление по причине истечения тайм-аута, она возвращает значение
WSA_WAIT_TIMEOUT.
Значение параметра fAlertable в данной модели можно положить
равным FALSE.
По значению, возвращаемому функцией WSAWaitForMultipleEvents
можно определить, какое именно событие произошло, для этого
достаточно вычесть из него константу WSA_WAIT_EVENT_0:
Index = WSAWaitForMultipleEvents (...);
MyEvent = EventArray [Index - WSA_WAIT_EVENT_0];
При освобождении объекта события иногда генерируется несколько
типов сетевых событий (например, FD_READ и FD_WRITE). Зная сокет,
на котором произошло событие, можно определить все события,
произошедшие
на
данном
сокете,
вызвав
функцию
WSAEnumNetworkEvents:
int WSAEnumNetworkEvents(
SOCKET s,
WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents
);
Здесь s – интересующий нас сокет, hEventObject – описатель события,
которое нужно сбросить (необязательный), lpNetworkEvents – указатель на
структуру типа WSANETWORKEVENTS, которую функция заполняет
требуемой информацией:
116
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
typedef struct _WSANETWORKEVENTS {
long lNetworkEvents;
int
iErrorCodes[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
В данной структуре массив iErrorCodes содержит информацию о
кодах ошибок, связанных с событиями из массива lNetworkEvents (поле
можно рассматривать как массив битов). Для каждого типа сетевого
события существует индекс события, обозначаемый тем же именем с
суффиксом _BIT. Его применение иллюстрирует следующий пример
анализа кода для события FD_READ:
// Обработка уведомления FD_READ
if (NetworkEvents.lNetworkEvents & FD_READ != 0)
{
if (NetworkEvents.iErrorCodes[FD_READ_BIT])
}
printf("FD_READ failed with error %d\n",
NetworkEvents.iErrorCodes[FD_READ_BIT])
}
}
В следующем листинге приводится пример программирования
сервера, способного обслуживать несколько сокетов одновременно, в
модели WSAEventSelect. Для краткости часть действий (например,
проверки на успешное завершение функций) опущена.
SOCKET Socket[WSA_MAXIMUM_WAIT_EVENTS];
WSAEVENT Event[WSA_MAXIMUM_WAIT_EVENTS];
SOCKET Accept, Listen;
DWORD EventTotal = 0;
DWORD Index;
// Настройка ТСР-сокета для прослушивания порта 5150
Listen = socket (PF_INET, SOCK_STREAM, 0);
InternetAddr.sin.family = AF_INET;
InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
InternetAddr.sin_port = htons(5150);
bind(Listen,(PSOCKADDR)&InternetAddr,sizeof(InternetAddr));
NewEvent = WSACreateEvent ();
WSAEventSelect (Listen, NewEvent, FD_ACCEPT | FD_CLOSE);
listen (Listen, 5);
Socket[EventTotal] = Listen;
117
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Event[EventTotal] = NewEvent;
EventTotal++;
while(TRUE)
{
// Ожидание события на всех сокетах
Index = WSAWaitForMultipleEvents (EventTotal, EventArray,
FALSE, WSA_INFINITE, FALSE);
WSAEnumNetworkEvents (
SocketArray[Index - WSA_WAIT_EVENT_0],
EventArray[Index - WSA_WAIT_EVENT_0],
&NetworkEvents);
// Проверка наличия сообщений FD_ACCEPT
if (NetworkEvents.lNetworkEvents & FD_ACCEPT)
{
if (NetworkEvents.iErrorCode[FD_ACCEPT_BIT] != 0)
{
printf("FD_ACCEPT failed with error %d\n",
NetworkEvents.iErrorCode[FD_ACCEPT_BIT]);
break;
}
// Прием нового соединения и добавление его
// в списки сокетов и событий
Accept = accept (SocketArray[Index - WSA_WAIT_EVENT_0],
NULL, NULL);
// Мы не можем обрабатывать более WSA_MAXIMUM_WAIT_EVENTS
// сокетов, поэтому закрываем сокет
if (EventTotal > WSA_MAXIMUM_WAIT_EVENTS)
{
printf("Too many connections");
closesocket(Accept);
break;
}
NewEvent = WSACreateEvent ();
WSAEventSelect (Accept, NewEvent,
FD_READ | FD_WRITE | FD_CLOSE);
Event[EventTotal] = NewEvent;
Socket[EventTotal] = Accept;
EventTotal++;
printf("Socket %d connected\n", Accept);
}
118
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Обработка уведомления FD_READ
if (NetworkEvents.lNetworkEvents & FD_READ)
{
if (NetworkEvents.iErrorCode[FD_READ_BIT] != 0)
{
printf("FD_READ failed with error %d\n",
NetworkEvents.iErrorCode[FD_READ_BIT]);
break;
}
// Чтение данных из сокета
recv (Socket[Index - WSA_WAIT_EVENT_0], buffer,
sizeof(buffer), 0);
}
// Обработка уведомления FD_WRITE
if (NetworkEvents.lNetworkEvents & FD_WRITE)
{
if (NetworkEvents.iErrorCode[FD_WRITE_BIT] != 0)
{
printf("FD_WRITE failed with error %d\n",
NetworkEvents.iErrorCode[FD_WRITE_BIT]);
break;
}
send (Socket[Index - WSA_WAIT_EVENT_0],
buffer, sizeof(buffer), 0);
}
// Обработка уведомления FD_CLOSE
if (NetworkEvents.lNetworkEvents & FD_CLOSE)
{
if (NetworkEvents.iErrorCode[FD_CLOSE_BIT] != 0)
{
printf("FD_CLOSE failed with error %d\n",
NetworkEvents.iErrorCode[FD_CLOSE_BIT]);
break;
}
closesocket(Socket[Index - WSA_WAIT_EVENT_0]);
// Удаление сокета и связанного события
// из массивов сокетов (Socket) и событий (Event);
// уменьшение счетчика событий EventTotal
CompressArrays (Event, Socket, &EventTotal);
}
}
119
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Модель перекрытого ввода-вывода
С помощью этой модели приложение может выдать асинхронно
несколько запросов ввода-вывода, а затем обслужить принятые запросы
после их завершения. Модель доступна на всех платформах Windows,
кроме Windows CE.
Замечание. Winsock 2 не позволяет использовать перекрытый вводвывод с использованием функций ReadFile и WriteFile в Windows 9x,
поэтому из соображений переносимости для передачи информации
предпочтительнее пользоваться функциями WSASend и WSARecv.
Для использования данной модели сокет должен создаваться с флагом
WSA_FLAG_OVERLAPPED, например:
s = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0,
WSA_FLAG_OVERLAPPED)
Если сокет создается функцией socket, то этот флаг задан по
умолчанию.
Для работы в данной модели можно использовать следующие
функции Winsock: WSASend, WSASendTo, WSARecv, WSARecvFrom,
WSAIoctl, AcceptEx, TransmitFile.
Указанные функции принимают структуру WSAOVERLAPPED в
качестве параметра. В этом случае они сразу возвращают управление, даже
если сокет работает в блокирующем режиме.
Далее есть два варианта: либо ждать уведомления от объекта
"событие" (event object notification), либо обрабатывать завершившиеся
запросы процедурами завершения (completion routines). Последний вариант
предусматривает задание при вызове упомянутых функций параметра
lpCompletionRoutine. Рассмотрим оба варианта.
Уведомление о событии
Как правило, вызовы функций ввода-вывода при использовании
данной модели возвращают ошибку SOCKET_ERROR. При этом функция
WSAGetLastError возвращает значение WSA_IO_PENDING, что означает,
что операция ввода-вывода продолжается.
Структура WSAOVERLAPPED позволяет установить связь между
началом запроса ввода-вывода и его завершением. Ее формат:
typedef struct _WSAOVERLAPPED {
DWORD
Internal;
DWORD
InternalHigh;
DWORD
Offset;
DWORD
OffsetHigh;
WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
120
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Первые четыре поля предназначены для использования системой и
заполняться не должны. Поле hEvent позволяет задать объект "событие"
для связи с сокетом. После того как объект освободится, необходимо
проверить,
успешен
ли
вызов,
посредством
функции
WSAGetOverlappedResult:
BOOL WSAGetOverlappedResult(
SOCKET s,
LPWSAOVERLAPPED lpOverlapped,
LPDWORD lpcbTransfer,
BOOL fWait,
LPDWORD lpdwFlags
);
Значения параметров s и lpOverlapped достаточно очевидны,
остальные имеют следующий смысл. В переменную, адресуемую
указателем lpcbTransfer, будет записано количество байтов, перемещенное
операцией. Если fWait равно TRUE, функция не возвратит управление,
пока операция не завершится. Если же fWait равно FALSE и операция
ввода-вывода не завершилась, функция WSAGetOverlappedResult вернет
FALSE с ошибкой WSA_IO_INCOMPLETE. В переменную, адресуемую
указателем lpdwFlags, будут записаны результирующие флаги (для
функций WSARecv и WSARecvFrom). Если операция ввода-вывода
завершилась, функция WSAGetOverlappedResult возвращает TRUE.
В предлагаемом ниже коде перечислены основные шаги, которые
должно выполнить простое серверное приложение, управляющее
перекрытым вводом-выводом.
void main(void)
{
WSABUF DataBuf;
DWORD EventTotal = 0;
WSAEVENT EventArray[WSA_MAXIMUM_WAIT_EVENTS];
WSAOVERLAPPED AcceptOverlapped;
SOCKET ListenSocket, AcceptSocket;
// Шаг 1:
// Инициализация Winsock и начало прослушивания
...
// Шаг 2:
// Прием входящего соединения
AcceptSocket = accept(ListenSocket, NULL, NULL);
121
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Шаг 3:
// Формирование структуры перекрытого ввода-вывода
EventArray[EventTotal] = WSACreateEvent();
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));
AcceptOverlapped.hEvent = EventArray[EventTotal];
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = buffer;
EventTotal++;
// Шаг 4:
// Асинхронный запрос WSARecv для приема данных на сокете
WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags,
&AcceptOverlapped, NULL);
// Обработка перекрытых запросов на сокете
while(TRUE)
{
// Шаг 5:
// Ожидание завершения запроса перекрытого ввода-вывода
Index = WSAWaitForMultipleEvents(EventTotal,
EventArray, FALSE, WSA_INFINITE, FALSE);
// Индекс должен быть равен 0, так как
// в массиве EventArray только один описатель события
// Шаг 6:
// Сброс свободного события
WSAResetEvent(EventArray[Index - WSA_WAIT_EVENT_0]);
// Шаг 7:
// Определение состояния запроса перекрытого ввода-вывода
WSAGetOverlappedResult(AcceptSocket,
&AcceptOverlapped, &BytesTransferred, FALSE,
&Flags);
// Сначала проверим, не закрыл ли партнер соединение,
// и если да, то закроем сокет
if (BytesTransferred == 0)
{
printf("Closing socket %d\n", AcceptSocket);
closesocket(AcceptSocket);
WSACloseEvent(EventArray[Index - WSA_WAIT_EVENT_0]);
return;
}
122
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Обработка полученных данных, содержащихся в DataBuf
...
// Шаг 8:
// Асинхронная отправка нового запроса WSARecv на сокет
Flags = 0;
ZeroMemory(&AcceptOverlapped, sizeof(WSAOVERLAPPED));
AcceptOverlapped.hEvent =
EventArray[Index - WSA_WAIT_EVENT_0];
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = Buffer;
WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags,
&AcceptOverlapped, NULL);
}
}
Для того чтобы этот пример расширить на случай обработки
нескольких сокетов, следует выделить в отдельный поток части кода,
которая обрабатывает перекрытый ввод-вывод, и для каждого соединения
запускать свой поток.
В Windows NT, 2000 и более новых версиях в аналогичной манере
можно также принимать соединения. Для этого можно использовать
функцию AcceptEx (определена в Mswsock.h, библиотека Mswsock.lib). Ее
формат:
BOOL AcceptEx(
SOCKET sListenSocket,
SOCKET sAcceptSocket,
PVOID lpOutputBuffer,
DWORD dwReceiveDataLength,
DWORD dwLocalAddressLength,
DWORD dwRemoteAddressLength,
LPDWORD lpdwBytesReceived,
LPOVERLAPPED lpOverlapped
);
Здесь sListenSocket – слушающий сокет, а sAcceptSocket – сокет,
принимающий соединение. lpOutputBuffer – специальный буфер,
принимающий локальный адрес сервера, удаленный адрес клиента и
первый блок данных на новом соединении, а dwReceiveDataLength –
размер
этого
буфера.
Параметры
dwLocalAddressLength
и
dwRemoteAddressLength указывают, сколько байтов в lpOutputBuffer
резервируется для упомянутых адресов, при этом размеры буферов
должны быть минимум на 16 байтов больше, чем максимальная длина
адреса в используемом транспортном протоколе. По адресу
lpdwBytesReceived
записывается
количество
принятых
байтов.
lpOverlapped адресует структуру OVERLAPPED, используемую для
перекрытого ввода-вывода.
123
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Для выделения локального и удаленного
воспользоваться функцией GetAcceptExSockaddrs:
адреса
можно
VOID GetAcceptExSockaddrs(
PVOID lpOutputBuffer,
DWORD dwReceiveDataLength,
DWORD dwLocalAddressLength,
DWORD dwRemoteAddressLength,
LPSOCKADDR *LocalSockaddr,
LPINT LocalSockaddrLength,
LPSOCKADDR *RemoteSockaddr,
LPINT RemoteSockaddrLength
);
Смысл ее аргументов достаточно ясен. Отметим только, что
dwReceiveDataLength, dwLocalAddressLength и dwRemoteAddressLength
должны иметь те же значения, что и одноименные параметры функции
AcceptEx.
Процедуры завершения
Это альтернативный способ управлять завершенными запросами
перекрытого ввода-вывода. Для использования этого варианта при вызове
операции
ввода-вывода
необходимо
указать
как
структуру
WSAOVERLAPPED,
так
и
процедуру
завершения
(параметр
lpCompletionRoutine). Последняя должна иметь следующий прототип:
void CALLBACK CompletionROUTINE(
DWORD dwError, // Статус завершения операции ввода-вывода
DWORD cbTransferred, // Количество перемещенных байтов
LPWSAOVERLAPPED lpOverlapped, // Структура WSAOVERLAPPED
DWORD dwFlags // Параметр не используется (равен 0)
);
При использовании процедуры завершения поле hEvent структуры
WSAOVERLAPPED не используется.
Далее приводится структура простого приложения-сервера, использующего процедуры завершения.
SOCKET AcceptSocket;
WSABUF DataBuf;
void main(void)
{
WSAOVERLAPPED Overlapped;
// Шаг 1:
// Запуск Winsock, настройка сокета для прослушивания
// ...
// Шаг 2:
// Прием соединения
AcceptSocket = accept(ListenSocket, NULL, NULL);
124
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Шаг 3:
// Приняв соединение, начинаем обработку перекрытого
// ввода-вывода с использованием процедур завершения.
Flags = 0;
ZeroMemory(&Overlapped, sizeof(WSAOVERLAPPED));
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = Buffer;
//
//
//
//
if
Шаг 4:
Отправка асинхронного запроса WSARecv со структурой
WSAOVERLAPPED и процедурой завершения WorkerRoutine
в качестве параметров
(WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes, &Flags,
Overlapped, WorkerRoutine) == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING)
{
printf("WSARecv failed with error %d\n",
WSAGetLastErrorO);
return;
}
}
// Так как функция WSAWaitForMultipleEvents
// ожидает один или несколько объектов "событие",
// нужно создать фиктивный объект.
EventArray[0] = WSACreateEvent();
while(TRUE)
{
// Шаг 5:
Index = WSAWaitForMultipleEvents(1, EventArray,
FALSE, WSA_INFINITE, TRUE);
// Шаг 6:
if (Index == WAIT_IO_COMPLETI0N)
{
// Процедура завершения запроса перекрытого
// ввода-вывода закончила работу.
break;
}
else
{
// Произошла ошибка, нужно остановить обработку!
return;
}
}
}
125
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
void CALLBACK WorkerRoutine (DWORD Error,
DWORD BytesTransferred,
LPWSAOVERLAPPED Overlapped,
DWORD InFlags)
{
DWORD SendBytes, RecvBytes;
DWORD Flags;
if (Error != 0 || BytesTransferred == 0)
{
// Или на сокете произошла ошибка,
// или сокет закрыт партнером
closesocket(AcceptSocket);
return;
}
// Запрос перекрытого ввода-вывода WSARecv завершился
// успешно. Обработка данных, содержащихся в DataBuf.
// Отправка еще одного запроса WSARecv.
Flags = 0;
ZeroMemory(&Overlapped, sizeof(WSAOVERLAPPED));
DataBuf.len = DATA_BUFSIZE;
DataBuf.buf = Buffer;
if (WSARecv(AcceptSocket, &DataBuf, 1, &RecvBytes,
&Flags, &Overlapped, WorkerRoutine) == SOCKET_ERROR)
{
if (WSAGetLastError() != WSA_IO_PENDING )
{
printf("WSARecv() failed with error %d\n",
WSAGetLastError());
return;
}
}
}
Модель портов завершения
Это наиболее сложная модель ввода-вывода. Она доступна только для
Windows NT, Windows 2000 и более свежих версий. Ее разумно
использовать, только если приложению требуется одновременно управлять
сотнями и тысячами сокетов одновременно.
Для использования данной модели требуется создать Win32-объект
порта завершения, который будет управлять запросами перекрытого вводавывода для любого количества сокетов. Это делается через вызов функции
CreateIoCompletionPort:
126
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
HANDLE CreateIoCompletionPort (
HANDLE FileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads
);
Данная функция используется в двух совершенно разных целях:
создания порта завершения и привязки к нему описателя сокета. При
создании
порта
используется
только
один
параметр
–
NumberOfConcurrentThreads, остальные игнорируются. Этот параметр
задает количество потоков, которые одновременно могут обрабатываться
на порте завершения. Значение, равное нулю, разрешает выделить число
потоков, равное числу процессоров в системе. Пример вызова:
CompetionPort = CreateIoCompletionPort
(INVALID_HANDLE_VALUE, NULL, 0, 0);
При этом возвращается дескриптор порта завершения.
Рабочие потоки и порты завершения
Следующий этап – это связь с портом завершения описателей сокетов.
Однако до этого необходимо создать один или несколько рабочих
потоков для обслуживания порта, когда на него направляются запросы
ввода-вывода сокетов. Заранее трудно сказать, сколько именно потоков
понадобится. Например, их можно создать больше, чем указано в
параметре NumberOfConcurrentThreads функции CreateIoCompletionPort.
Это может оказаться полезным, если часть рабочих потоков на время
приостанавливается, тогда могут работать остальные.
Собственно привязка сокета осуществляется путем вызова функции
CreateIoCompletionPort с передачей ему в первых трех параметрах:
FileHandle, ExistingCompletionPort и CompletionKey – информации о
сокете. Здесь FileHandle – дескриптор сокета, ExistingCompletionPort
определяет порт завершения, а CompletionKey – данные описателя
(per-handle data), которые можно связать с конкретным описателем сокета
(обычно это указатель на структуру, содержащую необходимую
информацию).
Ниже приводится пример исходного кода простого эхо-сервера,
использующего модель портов завершения.
StartWinsock();
// Шаг 1:
// Создается порт завершения ввода-вывода
CompletionPort = CreateIoCompletionPort
(INVALID_HANDLE_VALUE,NULL, 0, 0);
127
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Шаг 2:
// Выяснение количества процессоров в системе
GetSystemInfo(&SystemInfo);
// Шаг 3:
// Создание рабочих потоков для доступных процессоров в системе.
// В данном простейшем случае, мы создадим
// один рабочий поток для каждого процессора.
for(i = 0; i < Systemlnfo.dwNumberOfProcessors; i++)
{
HANDLE ThreadHandle;
// Создание рабочего потока сервера и передача
// порта завершения в качестве параметра.
// Примечание: процедура ServerWorkerThread
// не определена в этом листинге.
ThreadHandle = CreateThread(NULL, 0, ServerWorkerThread,
CompletionPort, 0, &ThreadID);
// Закрытие описателя потока
CloseHandle(ThreadHandle);
}
// Шаг 4:
// Создание слушающего сокета
Listen = WSASocket(AF_INET, SOCK.STREAM, 0, NULL, 0,
WSA_FLAG_OVERLAPPED);
InternetAddr.sin_family = AF_INET;
InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY);
InternetAddr.sin_port = htons(5150);
bind(Listen, (PSOCKADDR) &InternetAddr,
sizeof(InternetAddr));
// Подготовка сокета для прослушивания
listen(Listen, 5);
while(TRUE)
{
// Шаг 5:
// Прием соединений и их привязка к порту завершения
Accept = WSAAccept(Listen, NULL, NULL, NULL, 0);
128
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Шаг 6:
// Создание структуры данных описателя,
// которая будет связана с сокетом
PerHandleData = (LPPER_HANDLE_DATA)
GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));
printf("Socket number %d connected\n", Accept);
PerHandleData->Socket = Accept;
// Шаг 7:
// Привязка сокета к порту завершения
CreateIoCompletionPort((HANDLE) Accept, CompletionPort,
(DWORD) PerHandleData, 0);
// Шаг 8:
// Начало обработки ввода-вывода на сокете.
// Отправка одного или нескольких запросов WSASend() или
// WSARecv() на сокет, используя перекрытый ввод-вывод.
WSARecv(...);
}
Порты завершения и перекрытый ввод-вывод
Процесс обработки запросов ввода-вывода в данной модели опирается
на механизм перекрытого ввода-вывода. Вызовы WSASend() и WSARecv()
возвращают управление немедленно, а далее приложение должно извлечь
результаты из структуры OVERLAPPED. В данном случае это достигается
постановкой в очередь ожидания на порте завершения одного или
нескольких
рабочих
потоков
с
помощью
функции
GetQueuedCompletionStatus:
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
// описатель порта завершения
LPDWORD lpNumberOfBytes,
// кол-во перемещенных байтов
PULONG_PTR lpCompletionKey, // данные описателя сокета
LPOVERLAPPED *lpOverlapped, // данные операции ввода-вывода
DWORD dwMilliseconds
// тайм-аут в мс.(или INFINITE)
);
Данные описателя и операции
Параметр lpCompletionKey содержит данные, переданные в параметре
CompletionKey при вызове функции CreateIoCompletionPort. Приложение
может передать через него любой тип информации, связанной с сокетом.
Параметр lpOverlapped содержит структуру OVERLAPPED, за
которой следуют так называемые данные операции (в любом количестве).
Чтобы ими пользоваться, проще всего определить свою структуру данных,
129
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
в которую структура OVERLAPPED входила бы в качестве первого поля.
Например:
typedef struct
{
OVERLAPPED Overlapped;
WSABUF
DataBuf;
CHAR
Buffer[DATA_BUFSIZE];
BOOL
OperationType;
} PER_IO_OPERATION_DATA, *LPPER_IO_OPERATION_DATA;
При вызове функции Winsock, принимающей в качестве параметра
указатель на структуру OVERLAPPED можно или привести к
соответствующему типу указатель:
PER_IO_OPERATION_DATA PerIoData;
...
WSARecv (socket, ..., (OVERLAPPED *)&PerIoData, ...);
или передать ссылку на первое поле:
PER_IO_OPERATION_DATA PerIoData;
...
WSARecv (socket, ..., &PerIoData.Overlapped, ...);
Для доступа к полям структуры PER_IO_OPERATION_DATA, нужно,
естественно, произвести обратное преобразование типа.
Ниже предложен пример кода основной процедуры рабочего потока,
использующей данные операции и данные описателя для обслуживания
запросов ввода-вывода.
DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)
{
HANDLE CompletionPort = (HANDLE) CompletionPortID;
DWORD BytesTransferred;
LPOVERLAPPED Overlapped;
LPPER_HANDLE_DATA PerHandleData;
LPPER_IO_OPERATION_DATA PerIoData;
DWORD SendBytes, RecvBytes;
DWORD Flags;
while(TRUE)
{
// Ожидание завершения ввода-вывода на любом
// из сокетов, связанных с портом завершения
GetQueuedCompletionStatus(CompletionPort,
&BytesTransferred,(LPDWORD)&PerHandleData,
(LPOVERLAPPED *) &PerIoData, INFINITE);
130
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
//
//
//
if
Сначала проверим, не было ли ошибки на сокете;
если была, закрываем сокет и очищаем
данные описателя и данные операции
(BytesTransferred == 0 &&
(PerIoData->OperationType == RECV_POSTED
|| PerIoData->OperationType == SEND_POSTED))
{
// Отсутствие перемещенных байт (BytesTransferred)
// означает, что сокет закрыт партнером по соединению
// и нам тоже нужно закрыть сокет. Примечание:
// для ссылки на сокет, связанный с операцией
// ввода-вывода,использовались данные описателя.
closesocket(PerHandleData->Socket);
GlobalFree(PerHandleData);
GlobalFree(PerIoData);
continue;
}
//
//
//
if
{
Обслуживание завершившегося запроса ввода-вывода.
Чтобы определить, какой запрос завершился, нужно
просмотреть в данных операции поле OperationType.
(PerIoData->OperationType == RECV_POSTED)
// Обработка принятых данных
// в буфере PerIoData->Buffer
}
// Отправка нового запроса ввода-вывода. В качестве
// примера отправим еще один асинхронный запрос WSARecv()
Flags = 0;
// Настройка данных операции
// для следующего запроса перекрытого ввода-вывода
ZeroMemory(&(PerIoData->Overlapped), sizeof(OVERLAPPED));
PerIoData->DataBuf.len = DATA_BUFSIZE;
PerIoData->DataBuf.buf = PerIoData->Buffer;
PerIoData->OperationType = RECV_POSTED;
WSARecv(PerHandleData->Socket, &(PerIoData->DataBuf), 1,
&RecvBytes, &Flags, &(PerIoData->Overlapped), NULL);
}
}
131
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Корректное закрытие порта завершения
Корректное закрытие порта завершения особенно важно, если один
или несколько выполняющихся потоков ведут ввод-вывод на нескольких
сокетах.
Главное – не освобождать структуру OVERLAPPED, пока
выполняется перекрытый запрос ввода-вывода. Лучше всего вызвать
функцию closesocket для каждого описателя сокета. После закрытия всех
сокетов нужно завершить все рабочие потоки порта завершения. Для этого
можно отправить каждому потоку специальный завершающий пакет через
вызов функции PostQueuedCompletionStatus:
BOOL PostQueuedCompletionStatus(
HANDLE CompletionPort,
DWORD dwNumberOfBytesTransferred,
ULONG_PTR dwCompletionKey,
LPOVERLAPPED lpOverlapped
);
Здесь первый параметр – дескриптор порта завершения, которому
необходимо отправить пакет, а оставшиеся три позволяют задать значения,
которые должны быть записаны в соответствующие параметры функции
GetQueuedCompletionStatus.
После закрытия всех потоков следует закрыть порт завершения путем
вызова функции CloseHandle.
132
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Параметры сокета и
команды управления вводом-выводом
После создания сокета его свойствами можно манипулировать через
параметры и команды управления вводом-выводом (ioctl-команды).
Упоминающиеся ниже команды и параметры описаны в файлах
Winsock.h или Winsock2.h. Расширения для определенных транспортных
протоколов описываются в собственных заголовочных файлах
поставщиков. Например, для расширений Microsoft это файл Mswsock.h, а
соответствующая библиотека – Mswsock.lib.
Параметры сокета
Для получения информации о данном сокете обычно используется
функция getsockopt:
int getsockopt(
SOCKET s,
int level,
int optname,
char FAR *optval,
int FAR *optlen
);
Здесь s задает сокет. Второй параметр (level) задает уровень, к
которому относится опция. Например, уровень SOL_SOCKET
соответствует уровню опций, характерных для сокета и не зависящих для
протокола, уровень SOL_APPLETALK – опциям, специфичным для
протокола AppleTalk, и т.д.
Ниже мы для краткости рассмотрим только уровень SOL_SOCKET, а
также уровни IPPROTO_IP и IPPROTO_TCP, соответствующие протоколам на базе протокола IP.
Последние два параметра функции getsockopt указывают на
переменные, предназначенные для получения информации (чаще всего это
целое значение).
Для задания параметров сокета используется функция setsockopt:
int setsockopt(
SOCKET s,
int level,
int optname,
const char FAR *optval,
int optlen
);
В случае успешного выполнения функция возвращает нулевое
значение. Типичной ошибкой при использовании этих двух функций
133
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
является попытка получить или установить значение параметра, который
не поддерживает данную характеристику. Например, сокет типа
SOCK_STREAM не поддерживает широковещание, поэтому попытка
задать или получить значение параметра SO_BROADCAST вызовет
ошибку WSAENOPROTOOPT.
Уровень SOL_SOCKET
Параметр SO_ACCEPTCONN
Тип optval – BOOL, версия Winsock 1+.
Параметр можно только получить. Возвращается TRUE, если сокет
находится в режиме прослушивания. Сокеты типа SOCK_DGRAM не
поддерживают этот параметр.
Параметр SO_BROADCAST
Тип optval – BOOL, версия Winsock 1+.
Параметр можно и получить, и задать. Он равен TRUE, если сокет
сконфигурирован для отправки и получения широковещательных
сообщений. Сокеты типа SOCK_STREAM не поддерживают этот
параметр.
Пример отправки широковещательного сообщения по UDP:
SOCKET
s;
BOOL
bBroadcast;
char
*sMsg == "This is a test"
SOCKADDR_IN
to;
s = WSASocket(AF_INET, SOCK_DGRAM, 0, NULL, 0,
WSA_FLAG_OVERLAPPED);
bBroadcast = TRUE;
setsockopt(s, SOL_SOCKET, SO_BROADCAST,
(char *)&bBroadcast, sizeof(bBroadcast));
to.sin_family = AF_INET;
to.sin_addr.s_addr = htonl(INADDR_BROADCAST);
to.sin_port = htons(5150);
sendto (s, sMsg, strlen(sMsg), 0, (SOCKADDR *)&to,
sizeof(to));
Параметр SO_CONNECT_TIME
Тип optval – int, версия Winsock 1+.
Параметр можно только получить. Он задает длительность
соединения на сокете в секундах. Если сокет не соединен в данный
момент, возвращается значение 0xFFFFFFFF.
134
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Параметр SO_DEBUG
Тип optval – BOOL, версия Winsock 1+.
Параметр можно и получить, и задать. Если он равен TRUE, вывод
отладочной информации включен. Способ представления этой
информации зависит от реализации, предоставляемой поставщиком услуг.
Параметр SO_DONTLINGER
Тип optval – BOOL, версия Winsock 1+.
Параметр можно и получить, и задать. Если он равен TRUE,
SO_LINGER отключен. Смысл параметра SO_LINGER объясняется позже.
Параметр не поддерживается на сокетах типа SOCK_DGRAM.
Параметр SO_DONTROUTE
Тип optval – BOOL, версия Winsock 1+.
Параметр можно и получить, и задать. Если он равен TRUE,
маршрутизация отключена, то есть сообщения отправляются прямо
сетевому интерфейсу в обход таблицы маршрутов. Поставщик Microsoft
игнорирует данный параметр и всегда использует маршрутизацию.
Параметр SO_ERROR
Тип optval – int, версия Winsock 1+.
Параметр можно только получить. Возвращается и сбрасывается код
ошибки для сокета, причем он отличается от кода ошибки для потока,
получаемого через функцию WSAGetLastError. Обычно для работы с
ошибками лучше вызывать WSAGetLastError.
Параметр SO_EXCLUSIVEADDRUSE
Тип optval – BOOL, версия Winsock 1+.
Параметр можно и получить, и задать. Если он равен TRUE,
локальный порт, с которым связан сокет, нельзя повторно использовать в
другом процессе. Этот параметр является парным с параметром
SO_REUSEADDR. Он доступен только в Windows 2000, и для его
использования нужны полномочия администратора.
Параметр SO_KEEPALIVE
Тип optval – BOOL, версия Winsock 1+.
Параметр можно и получить, и задать. Если он равен TRUE, сокет
сконфигурирован для отправки сообщений об активности соединения.
Сообщения выдаются через промежуток времени, указанный в
реестре, но не реже, чем через два часа. Параметр не поддерживается на
сокетах типа SOCK_DGRAM.
135
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Параметр SO_LINGER
Тип optval – struct linger, версия Winsock 1+.
Параметр можно и получить, и задать. С его помощью определяют
текущие значения задержки.
Данный параметр задает действие, предпринимаемое после вызова
функции closesocket, если на сокете еще есть не отправленные данные.
Информация передается в структуре linger:
struct linger {
u_short
l_onoff;
u_short
l_linger;
};
Ненулевое значение поля l_onoff означает, что задержка включена, а в
поле l_linger хранится значение тайм-аута в секундах. По истечении этого
времени все данные ожидающие отправки или приема отбрасываются, и
соединение разрывается. Параметр не поддерживается на сокетах типа
SOCK_DGRAM.
Параметр SO_MAX_MSG_SIZE
Тип optval – unsigned int, версия Winsock 1+.
Параметр можно только получить. Задает максимальный размер
исходящего сообщения. Не применим к поточным сокетам.
Параметр SO_OOBINLINE
Тип optval – BOOL, версия Winsock 1+.
Параметр можно и получить, и задать. Если он равен TRUE, срочные
данные (out-of-bans, OOB) передаются в основном потоке данных, а не в
отдельном сообщении (значение по умолчанию). Параметр не
поддерживается на сокетах типа SOCK_DGRAM. Кроме того, он
неустойчиво работает в настоящих реализациях Win32.
Параметр SO_PROTOCOL_INFO
Тип optval – WSAPROTOCOL_INFO, версия Winsock 2+.
Параметр можно только получить. Определяет характеристики
протокола, связанного с сокетом.
Параметр SO_RCVBUF
Тип optval – int, версия Winsock 1+.
Параметр можно и получить, и задать. Определяет размер буфера
приема данных, связанного с данным сокетом.
136
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Параметр SO_REUSEADDR
Тип optval – BOOL, версия Winsock 1+.
Параметр можно и получить, и задать. Если он равен TRUE, вы вправе
связать сокет только с адресом, уже используемым другим сокетом, или с
адресом в состоянии ожидания.
Параметр SO_SNDBUF
Тип optval – int, версия Winsock 1+.
Параметр можно и получить, и задать. Определяет размер буфера
отправки данных, связанного с данным сокетом.
Параметр SO_TYPE
Тип optval – int, версия Winsock 1+.
Параметр можно только получить. Возвращает тип сокета:
SOCK_DGRAM, SOCK_STREAM, SOCK_SEQPACKET, SOCK_RDM или
SOCK_RAW.
Параметр SO_SNDTIMEIO
Тип optval – int, версия Winsock 1+.
Параметр можно и получить, и задать. Определяет тайм-аут (в мс) на
блокирующем сокете при отправке данных.
Параметр SO_RCVTIMEIO
Тип optval – int, версия Winsock 1+.
Параметр можно и получить, и задать. Определяет тайм-аут (в мс) на
блокирующем сокете при приеме данных.
Параметр SO_UPDATE_ACCEPT_CONTEXT
Тип optval – SOCKET, версия Winsock 1+.
Параметр можно и получить, и задать. Определяет тайм-аут (в мс) при
приеме соединения. Чаще всего используется с функцией AcceptEx.
Функции AcceptEx в качестве параметра передается прослушивающий
сокет, а также описатель сокета, который должен быть принят клиентом. С
помощью параметра SO_UPDATE_ACCEPT_CONTEXT клиентскому
сокету назначают характеристики прослушивающего сокета.
Параметр обязателен для прослушивающего сокета QoS-приложения.
Применяется только в Windows NT и 2000.
Уровень параметров IPPROTO_IP
Параметры этого уровня относятся к атрибутам, специфичным для
протокола IP. При загрузке Winsock 1 следует использовать заголовочный
файл Winsock.h и библиотеку Wsock32.lib, а при использовании Winsock 2
соответственно Winsock2.h и Ws2_32.lib.
137
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Параметр IP_OPTIONS
Тип optval – char[], версия Winsock 1+.
Параметр можно и получить, и задать. Позволяет задавать различные
параметры IP внутри заголовка IP, например:
•
ограничения обработки и защиты;
•
запись маршрута – каждый маршрутизатор добавляет свой IPадрес к заголовку;
•
штамп времени – каждый маршрутизатор добавляет свой IPадрес и время;
•
нестрогая маршрутизация источника – пакет должен посетить
каждый из перечисленных в заголовке IP-адресов;
•
строгая маршрутизация источника – пакет должен посетить
только перечисленные в заголовке IP-адреса.
Заголовок параметра IP имеет следующий формат. Сначала идет поле
кода (1 байт), которое указывает тип параметра, например 0x7 означает
тип "запись маршрута". Второй байт задает длину заголовка, а третий –
смещение в заголовке на фрагмент данных. Далее следуют данные, причем
длина всего заголовка не должна превышать 40 байтов.
Пример вызова функции для задания параметра "запись маршрута":
struct ip_option_hdr
{
unsigned char code;
unsigned char length;
unsigned char offset;
unsigned long addrs[9]
} opthdr;
ZeroMemory ((char *)&opthdr, sizeof(opthdr));
opthdr.code = 0x7;
opthdr.length = 39;
opthdr.offset = 4;
ret = setsockopt (s, IPPROTO_IP, IP_OPTIONS,
(char *)&opthdr, sizeof(opthdr));
Фрагмент данных не будет возвращен при вызове getsockopt с
IP_OPTIONS – возвращаются только значения параметров. Если
необходимо получить эти данные, следует или использовать простые
сокеты (SOCK_RAW), либо параметр IP_HDRINCL.
138
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Параметр IP_HDRINCL
Тип optval – BOOL, версия Winsock 2+.
Параметр можно и получить, и задать. Если он равен TRUE, то
функция отправки включит заголовок IP перед посылаемыми данными, а
функция приема вернет его вместе с данными. Параметр доступен только в
Windows 2000+.
Формат заголовка IP:
Версия Длина заго- Тип службы
(TOS) –
ловка
IP
8 бит
(4 бита) (4 бита)
Полная длина в байтах –
16 бит
Идентификатор – 16 бит
13-битное
Три односмещение
битных
фрагмента данных
флага
Время жизни
(TTL) – 8 бит
Тип протокола –
8 бит
Контрольная сумма заголовка
–
16 бит
IP-адрес отправителя – 32 бита
IP-адрес получателя – 32 бита
Параметры IP (если есть)
Данные
Комментарии к некоторым полям. Версия IP в настоящий момент
равна 4. Длина заголовка задает количество 32-битных слов в заголовке
(длина заголовка должна быть кратна 32 битам). Поля флагов и смещения
используются для разбиения IP-пакетов на пакеты меньшего размера.
Время жизни ограничивает количество маршрутизаторов, через которые
может пройти пакет (декрементируется при прохождении очередного
маршрутизатора). Смысл остальных полей достаточно ясен. Отметим
также, что поле параметров IP имеет переменную длину.
Параметр IP_TOS
Тип optval – int, версия Winsock 1+.
Параметр можно и получить, и задать. Параметр задает тип службы
(type of service, TOS) – соответствующее поле есть в заголовке IP.
Используется только 8 бит.
139
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Параметр IP_TTL
Тип optval – int, версия Winsock 1+.
Параметр можно и получить, и задать. Параметр задает время жизни
(time-to-live, TTL) – соответствующее поле есть в заголовке IP, смысл
объяснен выше.
Параметр IP_MULTICAST_IF
Тип optval – unsigned long, версия Winsock 1+.
Параметр можно и получить, и задать. Используется на компьютере с
несколькими сетевыми интерфейсами (сетевыми адаптерами, модемами и
т.п.), задает используемый локальный интерфейс. Пример использования:
DWORD mcastIF = inet_addr ("129.113.43.120");
ret = setsockopt (s, IPPROTO_IP, IP_ MULTICAST_IF,
(char *)&mcastIF, sizeof(mcastIF));
Параметр IP_MULTICAST_TTL
Тип optval – int, версия Winsock 1+.
Параметр можно и получить, и задать. Определяет время жизни на
пакетах многоадресной рассылки для данного сокета. Позволяет кроме
прочего сузить область распространения данных. Стандартное значение
для групповых дейтаграмм – 1.
Параметр IP_MULTICAST_LOOP
Тип optval – BOOL, версия Winsock 1+.
Параметр можно и получить, и задать. Если он равен TRUE (значение
по умолчанию), то данные, отправленные на групповой адрес, попадут и во
входящий буфер данного сокета.
Параметр IP_ADD_MEMBERSHIP
Тип optval – struct ip_mreq, версия Winsock 1+.
Параметр можно только задать. Это способ добавить сокет к
многоадресной группе IP в Winsock.
Сначала следует создать сокет с семейством адресов AF_INET и
типом SOCK_DGRAM. Для добавления сокета в многоадресную группу
используется структура
struct ip_mreq {
struct in_addr imr_multiaddr;
struct in_addr imr_interface;
};
Здесь поле imr_multiaddr – двоичный групповой адрес, а imr_interface
– локальный интерфейс (либо INADDR_ANY для выбора интерфейса по
умолчанию), на котором следует отправлять и получать групповые
данные.
140
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Параметр IP_DROP_MEMBERSHIP
Тип optval – struct ip_mreq, версия Winsock 1+.
Параметр можно только задать. Противоположен по значению
параметру IP_ADD_MEMBERSHIP (удаляет сокет из группы).
Параметр IP_DONTFRAGMENT
Тип optval – BOOL, версия Winsock 1+.
Параметр можно и получить, и задать. Если он равен TRUE, сети
запрещается фрагментировать дейтаграмму в ходе передачи. Если при
этом размер дейтаграммы больше максимального блока передачи данных
(maximum transmission unit, MTU), отправка будет приостановлена, и
появится сообщение об ошибке.
Уровень параметров IPPROTO_TCP
К этому уровню относится только один параметр, применимый к
потоковым сокетам (SOCK_STREAM) семейства адресов AF_INET.
Параметр TCP_NODELAY
Тип optval – BOOL, версия Winsock 1+.
Параметр можно и получить, и задать. Если он равен TRUE,
отключается алгоритм Nagle. Этот алгоритм предназначен для
"укрупнения" пакетов путем объединения нескольких маленьких
сообщений в один пакет. Это позволяет уменьшить накладные расходы на
передачу, однако в ряде случаев существенно замедляет работу.
Функции ioctlsocket и WSAIoctl
Функции используются для управления режимом ввода-вывода, а
также для получения сведений об ожидающем вводе-выводе. Функция
ioctlsocket появилась в спецификации Winsock 1. Ее формат:
int ioctlsocket(
SOCKET s,
long cmd,
u_long FAR *argp
);
Здесь s – описатель сокета, cmd – команда для выполнения, argp –
указатель на переменную, зависящий от команды.
В Winsock 2 появилась функция, у которой появилось несколько
новых параметров, а вместо одного параметра argp используется набор
входных и набор выходных параметров:
141
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
int WSAIoctl(
SOCKET s,
DWORD dwIoControlCode,
LPVOID lpvInBuffer,
DWORD cbInBuffer,
LPVOID lpvOutBuffer,
DWORD cbOutBuffer,
LPDWORD lpcbBytesReturned,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
Здесь s – описатель сокета, dwIoControlCode – команда для
выполнения. Параметры lpvInBuffer и lpvOutBuffer задают буферы для
входной и выходной информации, а cbInBuffer и cbOutBuffer – размеры
этих буферов. По адресу lpcbBytesReturned записывается количество
реально переданных байтов. Последние два параметра задают структуру
для перекрытого ввода-вывода и процедуру завершения.
Стандартные ioctl-команды
Эти команды доступны на всех платформах Win32 и могут быть
вызваны как посредством функции ioctlsocket, так и функции WSAIoctl.
Команда FIONBIO
Версия Winsock 1+, тип входного значения: unsigned.
Включает или отключает неблокирующий режим на сокете. Значение
0 переводит сокет в блокирующий режим, ненулевое значение – в
неблокирующий.
Команда FIONREAD
Версия Winsock 1+, тип возвращаемого значения: unsigned long, на
входе ничего указывать не требуется. Возвращает количество данных,
которые могут быть считаны с сокета. При использовании на
дейтаграммном сокете возвращаемое значение задает размер первого
сообщения, которое может быть прочитано.
Команда SIOCATMARK
Версия Winsock 1+, тип возвращаемого значения: BOOL, на входе
ничего указывать не требуется. Возвращает значение TRUE, если при
следующем чтении будут прочитаны OOB-данные.
142
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Другие ioctl-команды
Ниже разбираются команды, специфичные для Winsock 2,
соответственно доступные только через вызов функции WSAIoctl. Для
краткости опущены SSL-команды, специфичные для Windows CE и
управляющие команды для семейства протоколов ATM.
Команда SIO_ENABLE_CIRCULAR_QUEUEING
Версия Winsock 2+. Тип входного значения: BOOL, тип
возвращаемого значения: BOOL. Если параметр равен TRUE, старые
сообщения из очереди удаляются, чтобы освободить место для вновь
приходящих сообщений. Команда допустима только для сокетов,
связанных с ненадежными, ориентированными на сообщения протоколами. Поддерживается в Windows NT и Windows 2000+.
Чтобы задать значение, нужен только входной параметр, чтобы
получить – только выходной.
Команда SIO_FIND_ROUTE
Версия Winsock 2+. Тип входного значения: SOCKADDR, тип
возвращаемого значения: BOOL. Команда используется для проверки,
можно ли связаться с определенным адресом в сети. Поставщик Microsoft
для текущих платформ Win32 не реализует эту команду.
Команда SIO_FLUSH
Версия Winsock 2+. Не требует входных и выходных параметров.
Отбрасывает текущее содержание очереди отправки. Поддерживается в
Windows NT Service Pack 4 и Windows 2000+.
Команда SIO_GET_BROADCAST_ADDRESS
Версия Winsock 2+. Не требует входных параметров. Возвращается
структура SOCKADDR, содержащая широковещательный адрес для
семейства адресов из сокета s, который может быть использован для
посылки данных.
Команда SIO_GET_EXTENSION_FUNCTION_POINTER
Версия Winsock 2+. Тип входного значения: GUID (globally unique
identifier). Возвращается указатель на функцию. Используется для доступа
к функциям, специфичным для поставщика, но не входящим в
спецификацию Winsock.
Команда SIO_CHK_QOS
Версия Winsock 2+. Тип входного и выходного значения: DWORD.
Поддерживается в Windows 2000+. Позволяет задать атрибуты QoS или
проверить состояние QoS.
143
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Команда SIO_GET_QOS
Версия Winsock 2+. Возвращает структуру QOS, связанную с сокетом.
Поддерживается в Windows 98+.
Команда SIO_SET_QOS
Версия Winsock 2+. Парная команда для SIO_GET_QOS. Принимает
структуру QOS, определяющую требования к пропускной способности для
данного сокета. Поддерживается в Windows 98+.
Команда SIO_MULTIPOINT_LOOPBACK
Версия Winsock 2+. Тип входного и выходного значения: BOOL.
Определяет, будут ли многоадресные данные возвращены на сокет. Чтобы
установить значение, его нужно передать через входной буфер, чтобы
получить, необходимо в качестве входного буфера задать NULL.
Команда SIO_MULTIPOINT_SCOPE
Версия Winsock 2+. Тип входного и выходного значения: int. Контролирует время жизни (TTL), фактически область распространения многоадресных данных. Чтобы установить значение, его нужно передать через
входной буфер, чтобы получить, необходимо в качестве входного буфера
задать NULL.
Команда SIO_KEEPALIVE_VALS
Версия Winsock 2+. Тип входного и выходного значения: структура
tcp_keepalive. Позволяет включить передачу сообщений о сохранении
соединения для TCP и задать интервал между их отправкой.
struct tcp_keepalive
{
u_long onoff;
u_long keepalivetime;
u_long keepaliveinterval;
}
Значения параметров keepalivetime и keepaliveinterval сохраняются в
реестре и задают в миллисекундах соответственно частоту отправки
сообщений и интервал между повторными отправками, если не получен
отклик. Команда поддерживается в Windows 2000+.
Команда SIO_RCVALL
Версия Winsock 2+. Тип входного значения: unsigned int,
возвращаемого значения нет.
Использование команды с параметром TRUE позволяет данному
сокету получать все IP-пакеты в сети. Сокет должен относиться к
семейству адресов AF_INET, иметь тип SOCK_RAW и протокол
IPPROTO_IP. Сокет должен быть приязан к конкретному интерфейсу, то
есть нельзя создать привязку INADDR_ANY. Команда поддерживается в
Windows 2000+. Требуются привилегии администратора.
144
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Команда SIO_RCVALL_MCAST
Версия Winsock 2+. Тип входного значения: unsigned int, возвращаемого значения нет.
Аналогична команде SIO_RCVALL с той лишь разницей, что
принимаются только многоадресные IP-пакеты (с адресов 224.0.0.0 –
239.255.255.255). Сокет должен быть создан под протокол
IPPROTO_IGMP.
Команда SIO_RCVALL_IGMPMCAST
Версия Winsock 2+. Тип входного значения: unsigned int, возвращаемого значения нет.
Аналогична команде SIO_RCVALL с той лишь разницей, что
принимаются все IGMP –пакеты (Internet Group Management Protocol).
Сокет должен быть создан под протокол IPPROTO_IGMP.
Команда SIO_ROUTING_INTERFACE_QUERY
Версия Winsock 2+. Тип входного значения: SOCKADDR.
Команда позволяет найти интерфейс, который следует использовать
при отправке данных на удаленный компьютер. Адрес этого компьютера
предоставляют в виде структуры SOCKADDR и передают через входной
буфер. Через выходной буфер (он должен быть достаточно большим)
возвращается массив из одной или нескольких структур SOCKADDR,
описывающих доступные интерфейсы.
Команда поддерживается в Windows 2000+.
Команда SIO_ROUTING_INTERFACE_CHANGE
Команда отправляет уведомление, когда изменился интерфейс для
конечной точки. Более подробно она описана в документации.
Команда SIO_ADDRESS_LIST_QUERY
Версия Winsock 2+. Тип выходного значения: SOCKET_ADDRESS_LIST.
Входного значения нет.
Команда позволяет получить список адресов локального транспорта,
соответствующих семейству протоколов данного сокета. Описание
структуры SOCKET_ADDRESS_LIST:
typedef struct _SOCKET_ADDRESS_LIST {
INT iAddressCount; // Количество структур SOCKET_ADDRESS
SOCKET_ADDRESS Address[1]; // Массив адресов
} SOCKET_ADDRESS_LIST, FAR * LPSOCKET_ADDRESS_LIST;
typedef struct _SOCKET_ADDRESS {
LPSOCKADDR
lpSockaddr; // Адрес
INT
iSockaddrLength; // Длина адреса
} SOCKET_ADDRESS, *PSOCKET_ADDRESS;
145
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Команда SIO_ADDRESS_LIST_CHANGE
Команда используется для получения уведомления об изменениях в
списке адресов локального транспорта, соответствующих семейству
протоколов данного сокета. За более полным описанием следует
обратиться к документации.
Команда SIO_GET_INTERFACE_LIST
Версия Winsock 2+. Входного значения нет. Выходное значение:
массив структур INTERFACE_INFO.
Команда используется для получения информации о каждом
интерфейсе на локальном компьютере. Определена в файле Ws2tcpip.h.
typedef union sockaddr_gen{
struct sockaddr Address;
struct sockaddr_in AddressIn;
struct sockaddr_in6 AddressIn6;
} sockaddr_gen;
/* Структура для хранения информации об интерфейсе */
typedef struct _INTERFACE_INFO
{
u_long
iiFlags;
// Флаги интерфейса
sockaddr_gen
iiAddress;
// Адрес интерфейса
sockaddr_gen
iiBroadcastAddress; // Широковещательный
// адрес
sockaddr_gen
iiNetmask;
// Сетевая маска
} INTERFACE_INFO, FAR * LPINTERFACE_INFO;
/* Возможные флаги для поля iiFlags */
#define IFF_UP
#define IFF_BROADCAST
0x00000001
0x00000002
#define IFF_LOOPBACK
0x00000004
#define IFF_POINTTOPOINT 0x00000008
#define IFF_MULTICAST
0x00000010
146
//
//
//
//
//
//
//
//
Интерфейс работает
Широковещание
поддерживается
Допускаются петли
Интерфейс
"точка-точка"
Поддерживается
многоадр. рассылка
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Ioctl-команды для ATM
Команда SIO_GET_NUMBER_OF_ATM_DEVICES
Версия Winsock 2+. Входного значения нет. Тип выходного значения:
DWORD. В выходной буфер записывается количество ATM-устройств в
системе. Если их количество равно n , то им присвоены номера от 0 до n-1,
играющие роль идентификаторов.
Команда SIO_GET_ATM_ADDRESS
Версия Winsock 2+. Тип входного значения: DWORD. Тип выходного
значения: структура ATM_ADDRESS. Идентификатор устройства задается
во входном буфере. В выходной буфер записывается структура
ATM_ADDRESS, содержащая локальный адрес, пригодный для
использования в функции bind.
Команда SIO_ASSOCIATE_PVC
Версия Winsock 2+. Команда связывает сокет с постоянным
виртуальным каналом связи (permanent virtual circuit, PVC), указанным в
буфере ввода в виде структуры ATM_PVC_PARAMS. Сокет должен быть
из семейства адресов AF_ATM. Выходного значения нет. После успешного
завершения данной команды приложение может отправлять и принимать
данные, как если бы было установлено соединение. Определение
структуры ATM_PVC_PARAMS:
typedef struct {
DWORD DeviceNumber;
DWORD VPI;
DWORD VCI;
} ATM_CONNECTION_ID;
typedef struct {
ATM_CONNECTION_ID
QOS
} ATM_PVC_PARAMS;
PvcConnectionId;
PvcQos;
Команда SIO_GET_ATM_CONNECTION_ID
Версия Winsock 2+. Команда получает идентификатор ATMсоединения, связанного с сокетом. После успешного выполнения команды
в выходном буфере содержится структура ATM_CONNECTION_ID,
содержащая номер устройства и значения VPI и VCI, ранее заданные
командой SIO_ASSOCIATE_PVC.
147
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Регистрация и разрешение имен
Под регистрацией имени понимается сопоставление дружественного
имени с адресом рабочей станции (зависящим от протокола). Например,
IP-адрес 193.233.51.120 сложнее запомнить, чем имя univ.uniyar.ac.ru.
В протоколе IP соответствие IP-адресов именам обеспечивает служба
Domain Name System (DNS). Другие протоколы предоставляют свои
способы привязки используемых ими адресов к дружественным именам.
Однако регистрация и разрешение имен – это еще не все, что может
потребоваться. Если необходим динамичный сервер, способный
выполняться на разных компьютерах, то для качественного обслуживания
требуется иметь возможность динамически обновлять зарегистрированную
службу и ее адреса.
В этой теме обсуждаются независимая от протокола модель
регистрации и разрешения имен, реализованная в Winsock 2, и
предоставляемые для этого функции.
Модели пространства имен
Пространство имен позволяет связать протокол и его атрибуты
адресации с дружественным именем. К наиболее распространенным
пространствам имен относятся DNS (используется для протокола IP) и
NDS (NetWare Directory Services, используется протоколом IPX). Разные
пространства имен очень различаются и по организации, и по реализации.
Существует три типа пространства имен: динамическое, статичное и
постоянное.
Динамическое пространство имен позволяет регистрировать сведения
о службе на лету. Кроме того, клиенты могут вести поиск службы в период
выполнения. Обычно в таком пространстве осуществляется периодическая
широковещательная рассылка сведений о доступности службы. Пример
динамических пространств имен: Service Advertising Protocol (SAP),
использующийся в сетях NetWare, и Name Binding Protocol (NBP),
применяемый протоколом AppleTalk.
Статичное пространство имен – наименее гибкий тип.
Регистрировать службу требуется вручную. С помощью функций Winsock
сделать это невозможно, Winsock предоставляет только функции для
разрешения имен. Примером служит DNS, где IP-адреса и имена
компьютеров вручную вносятся в файл, который используется системой
для обработки запросов на разрешение имени.
Постоянное пространство имен, как и динамическое, позволяет
службе регистрировать сведения о себе на лету. Отличие состоит в том,
что информация в этом случае хранится на энергонезависимом носителе,
например, на диске, что позволяет исключить постоянную
широковещательную передачу информации о службах. Запись о службе
148
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
удаляется только при получении от нее соответствующего запроса. При
этом возможна ситуация с устареванием информации о доступности
службы. Пример постоянного пространства имен – NDS.
Перечень пространств имен
Большинство предопределенных пространств имен объявлено в заголовочном файле Nspapi.h (каждому присвоен целочисленный
идентификатор). Наиболее распространенными на платформах Win32
являются следующие:
#define NS_SAP (1)
// Пространство SAP для сетей IPX
#define NS_NDS (2)
// Пространство NDS для сетей IPX
#define NS_DNS (12) // Пространство DNS для сетей TCP/IP
#define NS_NTDS (32) // Пространство домена Windows NT
Последнее из перечисленных пространств не зависит от протокола и
поддерживается в Windows 2000.
При перечислении пространств имен возвращаемый список зависит от
установленных на рабочей станции протоколов. Например, если не
установлен протокол IPX/SPX, то в списке не будет присутствовать
пространство SAP. Для получения списка пространств имен используется
функция WSAEnumNameSpaceProviders:
INT WSAAPI WSAEnumNameSpaceProviders(
IN OUT LPDWORD lpdwBufferLength,
OUT LPWSANAMESPACE_INFO lpnspBuffer
);
Параметры функции задают буфер для получения результирующего
списка и его размер. Если размер буфера недостаточен, произойдет сбой, а
через первый аргумент будет возвращен требуемый размер буфера. При
успешном завершении функция возвратит число структур типа
WSANAMESPACE_INFO, записанных в выходной буфер. Эта структура
описывает пространство имен следующим образом:
typedef struct _WSANAMESPACE_INFO {
GUID
NSProviderId;
DWORD
dwNameSpace;
BOOL
fActive;
DWORD
dwVersion;
LPTSTR
lpszIdentifier;
} WSANAMESPACE_INFO, *PWSANAMESPACE_INFO;
Поле NSProviderId содержит глобально уникальный идентификатор
(globally unique identifier, GUID) для описания конкретного пространства
имен, dwNameSpace – соответствующую ему целочисленную константу
(например, NS_DNS). fActive задает доступность пространства имен,
lpszIdentifier содержит описание поставщика, а dwVersion – его версию.
149
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Регистрация службы
Зарегистрировать службу – значит предоставить сведения о ней
другим компьютерам сети и сделать службу доступной для них.
Сначала требуется зарегистрировать класс службы. Под этим
понимается указание ее характеристик. После этого можно
зарегистрировать реальный экземпляр службы. Затем клиенты с помощью
запросов узнают, где выполняется требуемый экземпляр службы, и
устанавливают соединение с ним.
Определение класса службы
Для регистрации класса службы используется функция
WSAInstallServiceClass:
INT WSAInstallServiceClass(
LPWSASERVICECLASSINFO lpServiceClassInfo
);
Структура WSASERVICECLASSINFO, определяющая
класса службы, имеет следующее определение:
атрибуты
typedef struct _WSAServiceClassInfo {
LPGUID
lpServiceClassId;
LPTSTR
lpszServiceClassName;
DWORD
dwCount;
LPWSANSCLASSINFOW
lpClassInfos;
} WSASERVICECLASSINFO, *PWSASERVICECLASSINFOW;
Первое поле задает GUID для идентификации данного класса службы.
Для создания этого уникального идентификатора можно либо
воспользоваться утилитой Uuidgen.exe, либо одним из макросов,
определенных в заголовочном файле Svcguid.h. Эти макросы генерируют
GUID, используя:
•
•
•
•
SVCID_TCP (Port) – номер порта протокола TCP;
SVCID_DNS (RecordType) – тип записи DNS;
SVCID_UDP (Port) – номер порта протокола UDP;
SVCID_NETWARE (SapId) – идентификатор SAP;
Второе поле структуры (lpszServiceClassName) задает имя данного
класса службы. Параметр dwCount определяет количество структур типа
WSANSCLASSINFOW, в буфере с адресом lpClassInfos.
Структура WSANSCLASSINFOW определена следующим образом:
typedef struct _WSANSClassInfo
{
LPSTR
lpszName;
DWORD
dwNameSpace;
DWORD
dwValueType;
DWORD
dwValueSize;
LPVOID lpValue;
} WSANSCLASSINFO, *PWSANSCLASSINFO, *LPWSANSCLASSINFO;
150
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Первое поле (lpszName) определяет строковое значение атрибута,
принадлежащего классу службы, второе (dwNameSpace) – пространство
имен, на которое распространяется данный атрибут. В следующей таблице
перечислены возможные значения этих двух параметров, определенные в
файле Nspapi.h.
Строковое
значение
lpszName
Соответствующая Пространконстанта
ство имен
Описание
SapId
SERVICE_TYPE_
VALUE_SAPID
NS_SAP
ConnectionOriented
SERVICE_TYPE_
VALUE_CONN
Любое
TcpPort
SERVICE_TYPE_
VALUE_TCPPORT
NS_DNS
Порт TCP
NS_NTDS
UdpPort
SERVICE_TYPE_
VALUE_UDPPORT
NS_DNS
Порт UDP
NS_NTDS
Идентификатор SAP
Указывает, требует ли
служба соединения
Поле dwValueType определяет тип данных, связанный с этой записью,
и может принимать одно из значений, используемых в реестре. Например,
если тип данных DWORD, то значение этого поля равно REG_DWORD.
Поле dwValueSize содержит размер данных, а lpValue – указатель на
данные.
Ниже приводится пример исходного кода для регистрации класса
службы с именем My Server Class.
WSASERVICECLASSINFO
sci;
WSANSCLASSINFO
aNameSpaceClassInfo[4];
DWORD
dwSapId = 200,
dwUdpPort = 5150,
dwZero = 0;
int
ret;
memset(&sci, 0, sizeof(sci));
SET_NETWARE_SVCID(&sci.lpServiceClassId, dwSapId);
sci.lpszServiceClassName = (LPSTR)"My Server Class";
sci.dwCount = 4;
sci.lpClassInfos = aNameSpaceClassInfo;
memset(aNameSpaceClassInfo, 0, sizeof(WSANSCLASSINFO) * 4);
151
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Настройка пространства имен NTDS
aNameSpaceClassInfo[0].lpszName = SERVICE_TYPE_VALUE_CONN;
aNameSpaceClassInfo[0].dwNameSpace = NS_NTDS;
aNameSpaceClassInfo[0].dwValueType = REG_DWORD;
aNameSpaceClassInfo[0].dwValueSize = sizeof(DWORD);
aNameSpaceClassInfo[0].lpValue = &dwZero;
aNameSpaceClassInfo[1].lpszName = SERVICE_TYPE_VALUE_UDPPORT;
aNameSpaceClassInfo[1].dwNameSpace = NS_NTDS;
aNameSpaceClassInfo[1].dwValueType = REG_DWORD;
aNameSpaceClassInfo[1].dwValueSize = sizeof(DWORD);
aNameSpaceClassInfo[1].lpValue = &dwUdpPort;
// Настройка пространства имен SAP
aNameSpaceClassInfo[2].lpszName = SERVICE_TYPE_VALUE_CONN;
aNameSpaceClassInfo[2].dwNameSpace = NS_SAP;
aNameSpaceClassInfo[2].dwValueType = REG_DWORD;
aNameSpaceClassInfo[2].dwValueSize = sizeof(DWORD);
aNameSpaceClassInfo[2].lpValue = &dwZero;
aNameSpaceClassInfo[3].lpszName = SERVICE_TYPE_VALUE_SAPID;
aNameSpaceClassInfo[3].dwNameSpace = NS_SAP;
aNameSpaceClassInfo[3].dwValueType = REG_DWORD;
aNameSpaceClassInfo[3].dwValueSize = sizeof(DWORD);
aNameSpaceClassInfo[3].lpValue = &dwSapId;
ret = WSAInstallServiceClass(&sci);
if (ret == SOCKET_ERROR)
{
printf("WSAInstallServiceClass() failed %d\n",
WSAGetLastError());
}
Последовательность действий, которая должна быть произведена,
такова:
1. Выбирается GUID, под которым будет зарегистрирован класс.
Здесь для краткости мы взяли идентификатор NetWare SAP,
равный 200, однако могли бы взять произвольный GUID или
GUID, основанный на номере порта UDP. Кроме того, наша
служба может работать по протоколу UDP на порте 5150.
2. Полю dwCount присваивается значение 4. Хотя мы регистрируем
нашу службу только в двух пространствах имен (в пространстве
SAP и пространстве домена Windows NT), используются четыре
структуры WSANSCLASSINFO. Причина в том, что для каждого
пространства имен определены два атрибута, которым нужны
152
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
отдельные
структуры.
В
данном
примере
полю
SERVICE_TYPE_VALUE_CONN присваивается нулевое значение,
что означает, что установление логического соединения не
требуется. Кроме того, для пространства домена Windows NT
задается номер порта, а для пространства SAP – идентификатор
SAP.
3. На последнем этапе вызывается функция WSAInstallServiceClass,
как параметр ей передается структура WSASERVICECLASSINFO.
При успешном вызове функция вернет 0, в случае ошибки –
SOCKET_ERROR. Если такой класс службы уже существует,
функция WSAGetLastError вернет значение WSAEALREADY.
Для
удаления
класса
службы
используется
функция
WSARemoveServiceClass с указателем на GUID в качестве аргумента:
INT WSARemoveServiceClass (LPGUID lpServiceClassId);
Регистрация экземпляра службы
Для регистрации экземпляра службы используется функция
WSASetService:
INT WSASetService(
LPWSAQUERYSET lpqsRegInfo,
WSAESETSERVICEOP essOperation,
DWORD dwControlFlags
);
Первый аргумент (lpqsRegInfo) задает указатель на структуру
WSAQUERYSET, определяющую конкретную службу.
Параметр
essOperation
определяет
выполняемое
действие.
Допустимые значения:
• RNRSERVICE_REGISTER – регистрирует службу;
• RNRSERVICE_DEREGISTER – удаляет все сведения о службе из
реестра;
• RNRSERVICE_DELETE – удаляет данный экземпляр службы из
пространства имен.
Третий
параметр
(dwControlFlags)
равен
нулю
или
SERVICE_MULTIPLE. Последнее значение означает, что данный
экземпляр службы регистрируется с несколькими адресами.
Структура WSAQUERYSET, которую необходимо заполнить и
передать функции, определена следующим образом:
typedef struct _WSAQuerySet {
DWORD
dwSize;
LPTSTR
lpszServiceInstanceName;
LPGUID
lpServiceClassId;
LPWSAVERSION
lpVersion;
153
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
LPTSTR
lpszComment;
DWORD
dwNameSpace;
LPGUID
lpNSProviderId;
LPTSTR
lpszContext;
DWORD
dwNumberOfProtocols;
LPAFPROTOCOLS
lpafpProtocols;
LPTSTR
lpszQueryString;
DWORD
dwNumberOfCsAddrs;
LPCSADDR_INFO
lpcsaBuffer;
DWORD
dwOutputFlags;
LPBLOB
lpBlob;
} WSAQUERYSET, *PWSAQUERYSETW;
Поля структуры имеют следующий смысл:
•
dwSize – размер структуры WSAQUERYSET;
•
lpszServiceInstanceName – имя данного экземпляра сервера
(строка);
•
lpServiceClassId – GUID класса службы;
•
lpVersion – необязательные сведения о версии;
•
lpszComment – необязательный строковый комментарий;
•
dwNameSpace – конкретное пространство имен для службы, либо
NS_ALL,
если
использования
любого
пространства,
необязательно, если указано поле lpNSProviderId;
•
lpNSProviderId
–
GUID,
представляющий
поставщика
пространства имен (необязательное поле, если указано
dwNameSpace);
•
lpszContext – начальная точка запроса в иерархическом
пространстве имен, например, DNS (необязательное поле);
•
dwNumberOfProtocols – число структур в массиве lpafpProtocols,
можно 0;
•
lpafpProtocols – массив структур AFPROTOCOLS для сужения
поиска (необязательное поле);
typedef struct _AFPROTOCOLS {
INT iAddressFamily; // Например, AF_INET
INT iProtocol;
// Например, IPPROTO_TCP
} AFPROTOCOLS, *PAFPROTOCOLS, *LPAFPROTOCOLS;
•
•
•
lpszQueryString – необязательное поле для строки расширенного
SQL-запроса;
dwNumberOfCsAddrs – число структур CSADDR_INFO,
переданных в параметре lpcsaBuffer (при запросе службы
игнорируется, при регистрации обязательное);
lpcsaBuffer – массив структур CSADDR_INFO (при запросе
службы игнорируется, при регистрации поле обязательно);
154
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
•
•
dwOutputFlags – набор флагов, при регистрации службы не
требуется, может применяться в запросе к экземпляру службы;
lpBlob – необязательное поле для информации поставщика пространства имен (может возвращаться запросом).
Ниже приводится пример регистрации службы в пространствах SAP и
NTDS одновременно. Отметим также, что пространство домена Windows
NT (требуется Windows 2000) может регистрировать адреса сокетов из
любого семейства протоколов, то есть все IP- и IPX-службы можно
зарегистрировать в одном пространстве имен, а удаление и добавление IPадресов осуществлять динамически.
SOCKET
socks[2];
WSAQUERYSET
qs;
CSADDR_INFO
lpCSAddr[2];
SOCKADDR_IN
sa_in;
SOCKADDR_IPX
sa_ipx;
IPX_ADDRESS_DATA
ipx_data;
GUID
guid = SVCID_NETWARE(200);
int
ret, cb;
memset(&qs, 0, sizeof(WSAQUERYSET));
qs.dwSize = sizeof(WSAQUERYSET);
qs.lpszServicelnstanceName = (LPSTR)"My Server";
qs.lpServiceClassId = &guid;
qs.dwNameSpace = NS_ALL;
qs.lpNSProviderId = NULL;
qs.lpcsaBuffer = lpCSAddr;
qs.lpBlob = NULL;
//
// Задаем IP-адрес нашей службы
//
memset(&sa_in, 0, sizeof(sa_in));
sa_in.sin_family = AF_INET;
sa_in.sin_addr.s_addr = htonl(INADDR_ANY);
sa_in.sin_port = 5150;
socks[0] = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
ret = bind(socks[0], (S0CKADDR *)&sa_in, sizeof(sa_in));
cb = sizeof(sa_in);
getsockname(socks[0], (SOCKADDR *)&sa_in, &cb);
lpCSAddr[0].iSocketType = SOCK_DGRAM;
lpCSAddr[0].iProtocol = IPPROTO_UDP;
lpCSAddr[0].LocalAddr.lpSockaddr = (SOCKADDR *)&sa_in;
155
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
lpCSAddr[0].LocalAddr.iSockaddrLength = sizeof(sa_in);
lpCSAddr[0].RemoteAddr.lpSockaddr = (SOCKADDR *)&sa_in;
lpCSAddr[0].RemoteAddr.iSockaddrLength = sizeof(sa_in);
//
// Задаем IPX-адрес нашей службы
//
memset(sa_ipx.sa_netnum, 0, sizeof(sa_ipx.sa_netnum));
memset(sa_ipx.sa_nodenum, 0, sizeof(sa_ipx.sa_nodenum));
sa_ipx.sa_family = AF_IPX;
sa_ipx.sa_socket = 0;
socks[1] = socket(AF_IPX, SOCK_DGRAM, NSPROTO_IPX);
ret = bind(socks[1], (SOCKADDR *)&sa_ipx, sizeof(sa_ipx));
cb = sizeof(IPX_ADDRESS_DATA);
memset (&ipx_data, 0, cb);
ipx_data.adapternum = 0;
ret = getsockopt(socks[1], NSPROTO_IPX, IPX_ADDRESS,
(char *)&ipx_data, &cb);
cb = sizeof(SOCKADDR_IPX);
getsockname(socks[1], (SOCKADDR *)sa_ipx, &cb);
memcpy(sa_ipx.sa_netnum, ipx_data.netnum,
sizeof(sa_ipx.sa_netnum));
memcpy(sa_ipx.sa_nodenum, ipx_data.nodenum,
sizeof(sa_ipx.sa_nodenum));
lpCSAddr[1].iSocketType = SOCK_DGRAM;
lpCSAddr[1].iProtocol = NSPROTO_IPX;
lpCSAddr[1].LocalAddr.lpSockaddr = (struct sockaddr *)&sa_ipx;
lpCSAddr[1].LocalAddr.iSockaddrLength = sizeof(sa_ipx);
lpCSAddr[1].RemoteAddr.lpSockaddr = (struct sockaddr *)&sa_ipx;
lpCSAddr[1].RemoteAddr.iSockaddrLength = sizeof(sa_ipx);
qs.dwNumberOfCsAddrs = 2;
ret = WSASetService(&qs, RNRSERVICE_REGISTER, 0L);
Как можно видеть, последовательность действий для настройки
экземпляра службы так, чтобы ее клиент мог найти адреса для
взаимодействия с ней, такова:
•
Инициализировать структуру WSAQUERYSET, задав в ней имя
экземпляра службы, GUID класса службы и необходимые
пространства имен (здесь NS_ALL).
156
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
•
•
Настроить структуры SOCKADDR в массиве CSADDR_INFO,
которые функция WSASetService передает через поле lpcsaBuffer
структуры WSAQUERYSET. Для этого мы создаем сокеты и
связываем их с конкретным адресом. В нашем примере это
делается отдельно для работы с протоколом IP и протоколом IPX.
Присваиваем полю dwNumberOfCsAddrs структуры WSAQUERYSET
значение 2 (мы можем работать с двумя адресами: IPX и UDP).
Вызываем WSASetService с флагом RNRSERVICE_REGISTER.
Запрос к службе
Прежде, чем связаться со службой, клиент может получить
необходимую для этого информацию, запросив пространство имен для
интересующей его службы. Для этой цели используются функции
WSALookupServiceBegin, WSALookupServiceNext и WSALookupServiceEnd.
INT WSALookupServiceBegin(
LPWSAQUERYSET lpqsRestrictions,
DWORD dwControlFlags,
LPHANDLE lphLookup
);
Первые два параметра функции WSALookupServiceBegin позволяют
установить ряд ограничений на поиск и определить его глубину. Для
краткости мы не перечисляем все возможные значения этих параметров –
они описаны в справочной системе. Через последний параметр
возвращается дескриптор, используемый в серии итераций поиска,
осуществляемых через вызов функции WSALookupServiceNext:
INT WSALookupServiceNext(
HANDLE hLookup,
DWORD dwControlFlags,
LPDWORD lpdwBufferLength, // размер выходного буфера
LPWSAQUERYSET lpqsResults // выходной буфер
);
Вызывать эту функцию следует до тех пор, пока система не выдаст
сообщение об ошибке WSA_E_NO_MORE. Результат поиска содержится в
выходном буфере в виде структуры WSAQUERYSET.
Для
завершения
работы
следует
вызвать
функцию
WSALookupServiceEnd:
INT WSALookupServiceEnd(HANDLE hLookup);
Создание запроса
Рассмотрим этот процесс поэтапно.
Прежде всего следует настроить структуру WSAQUERYSET,
определяющую запрос. Пример соответствующего кода:
157
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
WSAQUERYSET
GUID
AFPROTOCOLS
HANDLE
int
qs;
guid = SVCID_NETWARE(200);
afp[2] =
{{AF_IPX, NSPROTO_IPX},{AF_INET, IPPROTO_UDP}};
hLookup;
ret;
memset(&qs, 0, sizeof(WSAQUERYSET));
qs.dwSize = sizeof(WSAQUERYSET);
qs.lpszServicelnstanceName = (LPSTR)"My Server";
qs.lpServiceClassId = &guid;
qs.dwNameSpace = NS_ALL;
qs.dwNumbersOfProtocols = 2;
qs.lpafpProtocols = afp;
ret = WSALookupServiceBegin(&qs,
LUP_RETURN_ADDR | LUP_RETURN_NAME, &hLookup);
if (ret == SOCKET_ERROR)
// Ошибка
Отметим здесь, что флаг LUP_RETURN_NAME в данном случае
является излишним. Его есть смысл использовать, если в качестве имени
службы вы указали "*", то есть потребовали найти все службы.
Следующий этап, получение информации обо всех службах,
иллюстрируется следующим примером:
char
buff[sizeof(WSAQUERYSET) + 2000];
DWORD
dwLength, dwErr;
WSAQUERYSET *pqs = NULL;
SOCKADDR
*addr;
int
i;
pqs = (WSAQUERYSET *)buff;
dwLength = sizeof(WSAQUERYSET) + 2000;
while (1)
{
ret = WSALookupServiceNext(hLookup, 0, &dwLength, pqs);
if (ret == SOCKET_ERROR)
{
if ((dwErr = WSAGetLastError()) == WSAEFAULT)
{
printf("Buffer too small; required size is: &d\n",
dwLength);
break;
}
158
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
else
if ((dwErr == WSA_E_NO_MORE)||(dwErr = WSAENOMORE))
break;
else
{
printf("Failed with error: %d\n", dwErr);
break;
}
}
for (i = 0; i < pqs->dwNumberOfCsAddrs; i++)
{
addr = (SOCKADDR *)pqs->
lpcsaBuffer[i].RemoteAddr.lpSockaddr;
if (addr->sa_family == AF_INET)
{
SOCKADDR_IN *ipaddr = (SOCKADDR_IN *)addr;
printf("IP address:port = %s:%d\n",
inet_ntoa(addr->sin_addr), addr->sin_port);
}
else if (addr->sa_family == AF_IPX)
{
SOCKADDR_IPX *ipxaddr = (SOCKADDR_IPX *)addr;
printf("%02Х%02Х%02Х%02Х. "
"%02Х%02Х%02Х%02Х%02Х%02Х:%04Х ",
(unsigned char)ipxaddr->sa_netnum[0],
(unsigned char)ipxaddr->sa_netnum[1],
(unsigned char)ipxaddr->sa_netnum[2],
(unsigned char)ipxaddr->sa_netnum[3],
(unsigned char)ipxaddr->sa_nodenum[0],
(unsigned char)ipxaddr->sa_nodenum[1],
(unsigned char)ipxaddr->sa_nodenum[2],
(unsigned char)ipxaddr->sa_nodenum[3],
(unsigned char)ipxaddr->sa_nodenum[4],
(unsigned char)ipxaddr->sa_nodenum[5],
ntohs(ipxaddr->sa_socket));
}
}
}
Этот пример несколько упрощен, в основном, за счет отсутствия
проверок на ряд возможных ошибок, в частности на недостаток места в
буфере.
После успешного вызова функции WSALookupServiceNext в буфер
WSAQUERYSET помещается структура, содержащая результаты.
Например, поле lpszServiceInstanceName содержит имя службы, а поле
lpcsaBuffer представляет массив структур CSADDR_INFO с ее адресами.
Поле dwNumberOfCsAddrs содержит количество возвращенных адресов.
159
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Запрос к DNS
Пространство имен DNS статично, то есть не позволяет динамически
зарегистрировать собственную службу. Однако для разрешения имен DNS
можно воспользоваться соответствующими функциями Winsock.
Выполнение DNS-запросов осложняется тем, что поставщик
пространства имен DNS возвращает информацию в форме BLOB (Binary
Large Object). Этот формат плохо документирован, поэтому приходится
изобретать обходные пути.
Рассмотрим основные этапы для выполнения DNS-запроса. Пример
инициализации запроса:
WSAQUERYSET qs;
AFPROTOCOLS afp[2] =
{{AF_IPX, NSPROTO_IPX},{AF_INET, IPPROTO_UDP}};
GUID
HostnameGuid = SVCID_INET_HOSTADDRBYNAME;
DWORD
dwLength = sizeof(WSAQUERYSET) +
sizeof(HOSTENT) + 2048;
HANDLE
hQuery;
int
ret;
memset(&qs, 0, sizeof(WSAQUERYSET));
qs.dwSize = sizeof(WSAQUERYSET);
qs.lpszServicelnstanceName = argv[1];
qs.lpServiceClassId = &HostnameGuid;
qs.dwNameSpace = NS_DNS;
qs.dwNumbersOfProtocols = 2;
qs.lpafpProtocols = afp;
ret = WSALookupServiceBegin(&qs,
LUP_RETURN_NAME | LUP_RETURN_BLOB, &hQuery);
if (ret == SOCKET_ERROR)
// Ошибка
В примере используется предопределенный идентификатор
SVCID_INET_HOSTADDRBYNAME, он используется для запросов имен
компьютеров.
После
создания
запроса
следует
вызвать
функцию
WSALookupServiceNext:
char
DWORD
buff[sizeof(WSAQUERYSET) + sizeof(HOSTENT) + 2048];
dwLength = sizeof(WSAQUERYSET) +
sizeof(HOSTENT) + 2048;;
WSAQUERYSET *pqs;
HOSTENT
*hostent;
160
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
pqs = (WSAQUERYSET *)buff;
pqs->dwSize = sizeof(WSAQUERYSET);
ret = WSALookupServiceNext(hQuery, 0, &dwLength, pqs);
if (ret == SOCKET_ERROR)
// Ошибка
WSALookupServiceEnd(hQuery);
hostent = pqs->lpBlob->pBlobData;
Обратите внимание, с каким запасом выделен приемный буфер
(sizeof(WSAQUERYSET) + sizeof(HOSTENT) + 2048). Если и этого
окажется недостаточно, произойдет сбой, и будет возвращено значение
WSAEFAULT. В DNS-запросе все данные о компьютере возвращаются в
виде структуры HOSTENT, даже если имя компьютера связано с
несколькими IP-адресами. Поэтому не требуется вызывать функцию
WSALookupServiceNext несколько раз. Напомним определение структуры
HOSTENT:
struct hostent {
char FAR *
h_name;
char FAR * FAR * h_aliases;
short
h_addrtype;
short
h_length;
char FAR * FAR * h_addr_list;
} HOSTENT;
В случае, если структура HOSTENT возвращается в виде BLOBданных, указатели внутри нее представляют собой смещения по адресам
памяти, где хранятся данные. Для последующей работы с данными
необходимо скорректировать эти значения так, чтобы они указывали на
реальные адреса. Вот как, например, сделать это для поля h_name:
hostent->h_name =
(PCHAR)((DWORD_PTR)hostent->h_name + (PCHAR)hostent);
Чтобы изменить массив указателей (здесь h_aliases и h_addr_list),
необходимо организовать цикл для обработки всего массива. Для этой
цели можно, например, использовать такую функцию:
VOID FixList(PCHAR ** List, PCHAR Base)
{
if (*List)
{
PCHAR * Addr;
Addr = *List = (PCHAR *)( ((DWORD)*List + Base) );
161
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
while(*Addr)
{
*Addr = (PCHAR)(((DWORD)*Addr + Base));
Addr++;
}
}
}
А это пример ее вызова:
PCHAR pch = (PCHAR)hostent;
FixList(&hostent->h_aliases, pch);
FixList(&hostent->h_addr_list, pch);
После корректировки смещений со структурой HOSTENT можно
работать как обычно.
162
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Многоадресная рассылка
Технология многоадресной рассылки (multicasting) позволяет
отправлять данные от одной рабочей станции в сети, а затем тиражировать
их для остальных, не создавая большой нагрузки на сеть. Она является
альтернативой широковещанию (broadcasting). При многоадресной
рассылке данные передаются в сеть, только если их запрашивает
получатель.
На платформе Win32 протоколов, поддерживающих многоадресную
рассылку и доступных из Winsock, только два: IP и ATM. Следует также
отметить, что некоторые устаревшие сетевые адаптеры не способны
принимать и отправлять информацию по адресам многоадресной IPрассылки.
Основные понятия
Многоадресная рассылка обладает двумя важными характеристиками:
плоскостью управления (control plane) и плоскостью данных (data plane).
Первая определяет способ организации членства в группах, вторая
отражает способ распространения данных среди членов группы.
Любая из этих плоскостей может быть корневой или равноправной.
На следующем рисунке представлены корневая и равноправная плоскость
управления.
163
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
В корневой плоскости управления есть особый участник
многоадресной группы – корень c_root. Остальные участники называются
листами – c_leaf. Корневой узел в группе может быть только один. Именно
он инициирует соединение с листьевыми узлами, хотя те могут запросить
членства в группе и позже. Пример корневой плоскости управления –
протокол ATM.
Равноправная плоскость управления позволяет соединиться с группой
любому участнику. Собственно, протокол членства зависит от
разработчика. Можно, например, на базе равноправного членства
фактически реализовать корневую схему. Пример равноправной плоскости
управления – многоадресная IP-рассылка.
Плоскость данных также может быть маршрутизируемой и
немаршрутизируемой – см. рисунок.
В маршрутизируемой плоскости данных есть участник, называемый
d_root. Передача данных происходит только между ним и остальными
участниками (d_leaf). Трафик при этом может быть и одно-, и
двунаправленным. Пример маршрутизируемой плоскости данных – ATM.
В немаршрутизируемой плоскости данных любой член группы вправе
передавать данные любому другому члену.
Многоадресная рассылка в сетях ATM маршрутизируется и в
плоскости управления, и в плоскости данных, в то время как в сетях IP она
не маршрутизируется ни в одной из плоскостей.
164
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
При перечислении записей протоколов информацию о поддержке
многоадресной рассылки можно извлечь из поля dwServiceFlags1 структуры WSAPROTOCOL_INFO (установлен бит XP1_SUPPORT_MULTIPOINT).
Установка бита XP1_MULTIPOINT_CONTROL_PLANE означает поддержку маршрутизируемой плоскости управления, а установка бита
XP1_MULTIPOINT_DATA_PLANE – маршрутизируемой плоскости данных.
Многоадресная рассылка в сетях IP
Для многоадресной рассылки в сетях IP используются групповые
адреса (multicast address). Они должны лежать в диапазоне 224.0.0.0 –
239.255.255.255. При этом часть этих адресов зарезервирована для
специальных целей. Так, например, адрес 224.0.0.0 –базовый, 224.0.0.1
используется как групповой адрес всех систем данной подсети, 224.0.0.2 –
как групповой адрес всех маршрутизаторов данной подсети, 224.0.1.24 –
как групповой адрес WINS-сервера. IP требует, чтобы специальные адреса
использовались только для многоадресного трафика.
Для управления клиентами многоадресной рассылки и их членством в
группах был разработан специальный протокол IGMP (Internet Group
Management Protocol). Он позволяет осуществлять действия, для которых
нельзя воспользоваться средствами IP, например, присоединить рабочие
станции из разных подсетей к одной многоадресной группе.
Протокол IGMP
Узлы многоадресной рассылки используют IGMP для сообщения
маршрутизаторам, что компьютер маршрутизируемой подсети хочет
присоединиться к определенной многоадресной группе. Необходимо,
чтобы протокол IGMP поддерживался всеми маршрутизаторами на пути
между узлами, обменивающимися информацией, в противном случае
данные многоадресной рассылки будут ими просто отбрасываться.
Для того, чтобы присоединить приложение к многоадресной группе,
необходимо отправить в адрес всех маршрутизаторов данной подсети
(224.0.0.2) IGMP-команду join. При этом следует указать параметр время
жизни (time-to-live, TTL) – сколько маршрутизаторов может пересекать
сообщение на пути к точке назначения. Далее маршрутизаторы сами
транслируют данную команду, уменьшая TTL на единицу.
Для сообщения о выходе из группы клиент отправляет
маршрутизатору сообщение leave (поддерживается версией 2 протокола
IGMP). Это позволяет избежать пересылки ненужной информации, как
может быть при использовании первой версии протокола. Версия 2
протокола IGMP поддерживается, начиная с Windows 98, для Windows 95
требуется специальное обновление.
165
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Листовые узлы IP
Поскольку любой участник многоадресной рассылки в сети IP
является листовым (leaf) узлом, процесс присоединения к группе для всех
одинаков. Он несколько различается при использовании Winsock первой
или второй версии.
При использовании Winsock 1 следует выполнить следующие шаги:
1. Посредством функции socket создать сокет семейства адресов
AF_INET типа SOCK_DGRAM.
2. Связать сокет с локальным портом.
3. Вызвать функцию setsockopt с параметром IP_ADD_MEMBERSHIP
и указанием адресной структуры для группы, к которой нужно
присоединиться.
При использовании Winsock 2 на третьем шаге вместо функции
setsockopt используется вызов WSAJoinLeaf для добавления своего адреса
в группу.
Если приложение только отправляет данные, то присоединяться к
группе не требуется – достаточно просто отправлять UDP-пакеты по
групповому адресу. Многоадресная рассылка в сетях IP, как и обычный
протокол UDP, не требует логического соединения и так же ненадежна.
Многоадресная рассылка в сетях ATM
ATM также поддерживает многоадресную рассылку, причем предоставляет существенно большие возможности по сравнению с IP. ATM
поддерживает маршрутизируемые плоскости управления и данных. При
этом узел c_root фактически исполняет роль многоадресного сервера,
который контролирует состав групп, а также маршруты передачи внутри
группы.
В ATM-сетях IP-трафик может передаваться поверх ATM (эмуляция
IP-сети путем сопоставления IP-адресам собственных ATM-адресов). При
этом можно выбрать, применять ли многоадресную рассылку в сетях IP,
которая будет транслироваться на уровень ATM, или использовать для
рассылки собственные возможности ATM.
В ATM-сети вообще можно сконфигурировать несколько
эмулирующих сетей (LANE) с тем, чтобы использовать, как обычно,
различные протоколы: IPX/SPX, NetBEUI, TCP/IP, IGMP, ICMP и другие.
При создании многоадресной группы в сети ATM образуется
корневой узел, который приглашает листовые. В настоящее время в
Windows 2000 поддерживается только соединение, инициированное
корнем, то есть лист не может попросить о включении в группу. Особые
адреса при этом не требуются – достаточно только, чтобы корень знал
адреса всех листьев, которые он будет приглашать. В многоадресной
группе может быть только один корень, если какой-нибудь листовой узел
начинает приглашать другие листья, он отделяется от старой группы.
166
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Листовые узлы ATM
Для создания листового узла и присоединения к многоадресной
группе в сети ATM необходимо выполнить следующие действия:
1. Используя функцию WSASocket, создать сокет семейства адресов
AF_ATM с флагами WSA_FLAG_MULTIPOINT_C_LEAF и
WSA_FLAG_MULTIPOINT_D_LEAF.
2. Связать сокет с локальным ATM-адресом и портом функцией bind.
3. Вызвать функцию listen.
4. Дождаться приглашения, используя функции accept или
WSAAccept в зависимости от применяемой модели ввода-вывода.
После этого листовой узел может получать данные от корня (при
рассылке в ATM данные передаются только в одном направлении).
Замечание. В настоящее время Windows 98 и Windows 2000 способны
поддерживать только один листовой узел одновременно.
Корневые узлы ATM
Последовательность шагов для создания корневого узла:
1. Используя функцию WSASocket, создать сокет семейства адресов
AF_ATM
с
флагами
_FLAG_MULTIPOINT_C_ROOT
и
WSA_FLAG_MULTIPOINT_D_ROOT.
2. Вызвать функцию WSAJoinLeaf и передать ей в качестве параметра
ATM-адрес каждой конечной точку, которую следует пригласить.
Для каждой точки нужен отдельный вызов WSAJoinLeaf.
Многоадресная рассылка с использованием Winsock
Как упоминалось выше, организация многоадресной рассылки в IPсетях несколько отличается при использовании различных версий Winsock.
В первой версии для присоединения к группе используется изменение
параметров сокета, а во второй – специальная функция присоединения
WSAJoinLeaf.
Метод с использованием Winsock 1 широко распространен, так как
унаследован от сокетов Беркли.
Рассылка средствами Winsock 1
В Winsock 1 для присоединения или выхода из многоадресной группы
используется функция setsockopt с параметрами IP_ADD_MEMBERSHIP и
IP_DROP_MEMBERSHIP. Функции при вызове передается структура
ip_mreq:
struct ip_mreq {
struct in_addr imr_multiaddr;
struct in_addr imr_interface;
};
167
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Поле imr_multiaddr задает IP-адрес группы, а поле imr_interface –
интерфейс для отправки данных (если указать INADDR_ANY, будет
использован интерфейс по умолчанию). Пример иллюстрирует присоединение к группе 234.5.6.7:
SOCKET
s;
struct ip_mreq ipmr;
SOCKADDR_IN
local
int
len = sizeof (ipmr);
s = socket(AF_INET, SOCK_DGRAM, 0);
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_port
= htons(10000);
ipmr.imr_multiaddr.s_addr = inet_addr("234.5.6.7");
ipmr.imr_interface.s_addr = htonl(INADDR_ANY);
bind(s, (SOCKADDR *)&local, sizeof(local));
setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, (char *)&ipmr, &len);
Для выхода из группы достаточно вызвать функцию setsockopt,
передав ей структуру ip_mreq с теми же значениями полей, что и ранее:
setsockopt(s, IPPROTO_IP, IP_DROP_MEMBERSHIP, (char *)&ipmr, &len);
Пример многоадресной рассылки в IP-сети средствами Winsock 1
Приведенный ниже пример кода содержится в файле Mcastws1.c.
Программа присоединяется к заданной группе, а затем (в зависимости от
аргументов командной строки) действует как отправитель или получатель.
В принципе возможна ситуация, когда узел, действующий как отправитель
и как получатель, принимает собственные данные (петля). Этот вопрос
будет рассмотрен ниже.
#include <windows.h>
#include <winsock.h>
#include <stdio.h>
#include <stdlib.h>
#define
#define
#define
#define
MCASTADDR
"234.5.6.7"
MCASTPORT
25000
BUFSIZE
1024
DEFAULT_COUNT 500
168
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
BOOL
bSender = FALSE, // Действовать как отправитель?
bLoopBack = FALSE; // Запретить петли?
DWORD dwInterface,
// Интерфейс
dwMulticastGroup, // Многоадресная группа
dwCount;
// Кол-во сообщ. для отправки/приема
short iPort;
// Номер порта
//
// Функция: usage
// Вывод информации об использовании и выход
//
void usage(char *progname)
{
printf("usage: %s -s -m:str -p:int -i:str -l -n:int\n",
progname);
printf(" -s
Act as server (send data); otherwise\n");
printf("
receive data.\n");
printf(" -m:str Dotted decimal multicast IP addres to join\n");
printf("
The default group is: %s\n", MCASTADDR);
printf(" -p:int Port number to use\n");
printf("
The default port is: %d\n", MCASTPORT);
printf(" -i:str Local interface to bind to; by default \n");
printf("
use INADDRY_ANY\n");
printf(" -l
Disable loopback\n");
printf(" -n:int Number of messages to send/receive\n");
ExitProcess(-1);
}
//
// Функция: ValidateArgs
// Разбор аргументов командной строки и установка
// некоторых глобальных флагов
//
void ValidateArgs(int argc, char **argv)
{
int
i;
dwInterface = INADDR_ANY;
dwMulticastGroup = inet_addr(MCASTADDR);
iPort = MCASTPORT;
dwCount = DEFAULT_COUNT;
for(i = 1; i < argc; i++)
{
169
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if ((argv[i][0] == '-') || (argv[i][0] == '/'))
{
switch (tolower(argv[i][1]))
{
case 's':
// Отправитель
bSender = TRUE;
break;
case 'm':
// Многоадресная группа
if (strlen(argv[i]) > 3)
dwMulticastGroup = inet_addr(&argv[i][3]);
break;
case 'i':
// Интерфейс
if (strlen(argv[i]) > 3)
dwInterface = inet_addr(&argv[i][3]);
break;
case 'p':
// Номер порта
if (strlen(argv[i]) > 3)
iPort = atoi(&argv[i][3]);
break;
case 'l':
// Запретить образование петли?
bLoopBack = TRUE;
break;
case 'n':
// Количество сообщений
dwCount = atoi(&argv[i][3]);
break;
default:
usage(argv[0]);
break;
}
}
}
return;
}
//
// Функция: main
// Анализ командной строки, загрузка библиотеки Winsock,
// создание сокета, присоединение к многоадресной группе.
// Если запущен, как отправитель, то отправка сообщений,
// иначе их чтение.
//
int main(int argc, char **argv)
{
WSADATA
wsd;
struct sockaddr_in local,
remote,
from;
struct ip_mreq
mcast;
170
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
SOCKET
TCHAR
int
DWORD
sockM;
recvbuf[BUFSIZE],
sendbuf[BUFSIZE];
len = sizeof(struct sockaddr_in),
optval,
ret;
i=0;
// Анализ командной строки, загрузка библиотеки Winsock
ValidateArgs(argc, argv);
if (WSAStartup(MAKEWORD(1, 1), &wsd) != 0)
{
printf("WSAStartup failed\n");
return -1;
}
// Создание сокета
if ((sockM = socket(AF_INET, SOCK_DGRAM, 0)) ==
INVALID_SOCKET)
{
printf("socket failed with: %d\n", WSAGetLastError());
WSACleanup();
return -1;
}
// Привязка к интерфейсу
local.sin_family = AF_INET;
local.sin_port
= htons(iPort);
local.sin_addr.s_addr = dwInterface;
if (bind(sockM, (struct sockaddr *)&local,
sizeof(local)) == SOCKET_ERROR)
{
printf("bind failed with: %d\n", WSAGetLastError());
closesocket(sockM);
WSACleanup();
return -1;
}
// Настройка структуры im_req и интерфейса
remote.sin_family
= AF_INET;
remote.sin_port
= htons(iPort);
remote.sin_addr.s_addr = dwMulticastGroup;
mcast.imr_multiaddr.s_addr = dwMulticastGroup;
mcast.imr_interface.s_addr = dwInterface;
171
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if (setsockopt(sockM, IPPROTO_IP, IP_ADD_MEMBERSHIP,
(char *)&mcast, sizeof(mcast)) == SOCKET_ERROR)
{
printf("setsockopt(IP_ADD_MEMBERSHIP) failed: %d\n",
WSAGetLastError());
closesocket(sockM);
WSACleanup();
return -1;
}
// Настройка значения TTL, по умолчанию 1.
optval = 8;
if (setsockopt(sockM, IPPROTO_IP, IP_MULTICAST_TTL,
(char *)&optval, sizeof(int)) == SOCKET_ERROR)
{
printf("setsockopt(IP_MULTICAST_TTL) failed: %d\n",
WSAGetLastError());
closesocket(sockM);
WSACleanup();
return -1;
}
// Запрет петли, если требуется.
// В Windows NT4 и Windows 95 петлю запретить нельзя.
if (bLoopBack)
{
optval = 0;
if (setsockopt(sockM, IPPROTO_IP, IP_MULTICAST_LOOP,
(char *)&optval, sizeof(optval)) == SOCKET_ERROR)
{
printf("setsockopt(IP_MULTICAST_LOOP) failed: %d\n",
WSAGetLastError());
closesocket(sockM);
WSACleanup();
return -1;
}
}
if (!bSender)
// Получатель
{
// Прием порции данных
for(i = 0; i < dwCount; i++)
{
172
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if ((ret = recvfrom(sockM, recvbuf, BUFSIZE, 0,
(struct sockaddr *)&from, &len)) ==
SOCKET_ERROR)
{
printf("recvfrom failed with: %d\n",
WSAGetLastError());
closesocket(sockM);
WSACleanup();
return -1;
}
recvbuf[ret] = 0;
printf("RECV: '%s' from <%s>\n", recvbuf,
inet_ntoa(from.sin_addr));
}
}
else
// Отправитель
{
// Отправка порции данных
for(i = 0; i < dwCount; i++)
{
sprintf(sendbuf, "server 1: This is a test: %d", i);
if (sendto(sockM, (char *)sendbuf, strlen(sendbuf),
0, (struct sockaddr *)&remote,
sizeof(remote)) == SOCKET_ERROR)
{
printf("sendto failed with: %d\n",
WSAGetLastError());
closesocket(sockM);
WSACleanup();
return -1;
}
Sleep(500);
}
}
// Выход из группы
if (setsockopt(sockM, IPPROTO_IP, IP_DROP_MEMBERSHIP,
(char *)&mcast, sizeof(mcast)) == SOCKET_ERROR)
{
printf("setsockopt(IP_DROP_MEMBERSHIP) failed: %d\n",
WSAGetLastError());
}
closesocket(sockM);
WSACleanup();
return 0;
}
173
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Замечание. При компиляции и компоновке обязательно нужно
использовать
версию
заголовочного
файла
и
библиотеки,
соответствующие версии Winsock: для Winsock 1.1 – Winsock.h и
Wsock32.lib, для Winsock 2 –Winsock2.h и Ws2_32.lib.
Рассылка средствами Winsock 2
Многоадресная рассылка в Winsock 2 хотя и чуть сложнее, зато
поддерживает разные протоколы и предоставляет дополнительные
возможности. Так, например, можно использовать Quality of Service (QoS),
а также применять протоколы, поддерживающие маршрутизируемые
схемы.
Для инициализации членства в группе вместо задания параметров
сокета используется функция WSAJoinLeaf:
SOCKET WSAJoinLeaf(
SOCKET s,
const struct sockaddr FAR *name,
int namelen,
LPWSABUF lpCallerData,
LPWSABUF lpCalleeData,
LPQOS lpSQOS,
LPQOS lpGQOS,
DWORD dwFlags
);
Здесь s – дескриптор сокета, возвращенный функцией WSASocket.
Необходимо при создании сокета задать флаги для плоскости управления
(WSA_FLAG_MULTIPOINT_C_LEAF или WSA_FLAG_MULTIPOINT_С_ROOT)
и для плоскости данных (WSA_FLAG_MULTIPOINT_D_LEAF или
WSA_FLAG_MULTIPOINT_D_ ROOT).
Второй параметр задает адрес в виде структуры SOCKADDR. Для
маршрутизируемых схем управления это адрес приглашаемого клиента,
для немаршрутизируемых – адрес группы, к которой присоединяется
клиент. Параметр namelen задает размер этого адреса.
Параметры lpCallerData и lpCalleeData предназначены для задания
буферов. В настоящее время они не используются и должны быть равны
NULL. Параметр lpSQOS задает структуру FLOWCPEC, указывающую
требуемую пропускную способность для приложения, параметр lpGQOS
пока игнорируется.
Последний параметр (dwFlags) указывает функции узла: отправка или
прием данных, либо и то, и другое. Возможные значения:
JL_SENDER_ONLY, JL_RECEIVER_ONLY, JL_BOTH.
Вызов WSAJoinLeaf может быть осуществлен как синхронно, так и
асинхронно. Функция возвращает дескриптор для сокета, связанного с
174
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
многоадресной группой, причем возвращаемое значение зависит от того,
является ли сокет маршрутизируемым или листовым узлом.
В случае маршрутизируемого узла функция WSAJoinLeaf возвращает
новый описатель сокета для приглашенного листа. Этот сокет имеет те же
свойства, что и корневой сокет, использованный для приглашения.
Впрочем, этот новый сокет обычно применяется только для получения
уведомления FD_CLOSE от листа. Для отправки многоадресных данных
следует использовать сокет c_root, в противном случае данные получит
только один лист. Для удаления узла из группы достаточно вызвать
функцию closesocket, передав ей описатель сокета для соответствующего
листа.
В случае вызова функции WSAJoinLeaf листовым узлом возвращается
тот же описатель сокета, который был передан через параметр s. Здесь
возможны два варианта. Первый – присоединение к многоадресной группе,
что в настоящее время не поддерживается (такая возможность
предполагается в спецификации ATM UNI 4.0). Второй вариант – рассылка
в IP.
Если вызов WSAJoinLeaf служит для присоединения со стороны
листа, корневой узел должен слушать входные соединения с
использованием функций listen и accept/WSAAccept, как это обычно делает
сервер. Аналогично, если используется схема с приглашениями со стороны
корня, соединения ожидает клиент, используя для этого функции listen и
accept/WSAAccept.
Пример многоадресной рассылки в IP-сети средствами Winsock 2
Данный пример очень похож на пример, базирующийся на средствах
Winsock 1. Поэтому ниже приводится код только модифицированной
функции main. Полный вариант исходного кода содержится в файле
Mcastws2.c.
// Функция: main
// Анализ командной строки, загрузка библиотеки Winsock,
// создание сокета, присоединение к многоадресной группе.
// Если запущен, как отправитель, то отправка сообщений,
// иначе их чтение.
//
int main(int argc, char **argv)
{
WSADATA
wsd;
struct sockaddr_in local,
remote,
from;
SOCKET
sock, sockM;
TCHAR
recvbuf[BUFSIZE],
sendbuf[BUFSIZE];
175
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
int
DWORD
len = sizeof(struct sockaddr_in),
optval,
ret;
i=0;
// Анализ командной строки, загрузка библиотеки Winsock
ValidateArgs(argc, argv);
if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0)
{
printf("WSAStartup() failed\n");
return -1;
}
// Создание сокета
if ((sock = WSASocket(AF_INET, SOCK_DGRAM, 0, NULL, 0,
WSA_FLAG_MULTIPOINT_C_LEAF
| WSA_FLAG_MULTIPOINT_D_LEAF
| WSA_FLAG_OVERLAPPED)) == INVALID_SOCKET)
{
printf("socket failed with: %d\n", WSAGetLastError());
WSACleanup();
return -1;
}
// Привязка к интерфейсу. Нужно для приема данных
local.sin_family = AF_INET;
local.sin_port
= htons(iPort);
local.sin_addr.s_addr = dwInterface;
if (bind(sock, (struct sockaddr *)&local,
sizeof(local)) == SOCKET_ERROR)
{
printf("bind failed with: %d\n", WSAGetLastError());
closesocket(sock);
WSACleanup();
return -1;
}
// Настройка структуры SOCKADDR_IN описывающей
// многоадресную группу, к которой мы хотим присоединиться
//
remote.sin_family
= AF_INET;
remote.sin_port
= htons(iPort);
remote.sin_addr.s_addr = dwMulticastGroup;
176
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
//
// Настройка значения TTL, по умолчанию 1
//
optval = 8;
if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL,
(char *)&optval, sizeof(int)) == SOCKET_ERROR)
{
printf("setsockopt(IP_MULTICAST_TTL) failed: %d\n",
WSAGetLastError());
closesocket(sock);
WSACleanup();
return -1;
}
// Запрет петли, если требуется
//
if (bLoopBack)
{
optval = 0;
if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP,
(char *)&optval, sizeof(optval)) == SOCKET_ERROR)
{
printf("setsockopt(IP_MULTICAST_LOOP) failed: %d\n",
WSAGetLastError());
closesocket(sock);
WSACleanup();
return -1;
}
}
//
//
//
//
//
if
Присоединение к многоадресной группе.
Замечание: sockM используется не для отправки и получения
данных, а только если требуется выйти из группы.
Тогда просто вызывается closesocket().
((sockM = WSAJoinLeaf(sock, (SOCKADDR *)&remote,
sizeof(remote), NULL, NULL, NULL, NULL,
JL_BOTH)) == INVALID_SOCKET)
{
printf("WSAJoinLeaf() failed: %d\n", WSAGetLastError());
closesocket(sock);
WSACleanup();
return -1;
}
177
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if (!bSender)
// Получатель
{
// Прием данных
//
for(i = 0; i < dwCount; i++)
{
if ((ret = recvfrom(sock, recvbuf, BUFSIZE, 0,
(struct sockaddr *)&from, &len)) ==
SOCKET_ERROR)
{
printf("recvfrom failed with: %d\n",
WSAGetLastError());
closesocket(sockM);
closesocket(sock);
WSACleanup();
return -1;
}
recvbuf[ret] = 0;
printf("RECV: '%s' from <%s>\n", recvbuf,
inet_ntoa(from.sin_addr));
}
}
else
// Отправитель
{
// Отправка данных
//
for(i = 0; i < dwCount; i++)
{
sprintf(sendbuf, "server 1: This is a test: %d", i);
if (sendto(sock, (char *)sendbuf, strlen(sendbuf),
0, (struct sockaddr *)&remote,
sizeof(remote)) == SOCKET_ERROR)
{
printf("sendto failed with: %d\n",
WSAGetLastError());
closesocket(sockM);
closesocket(sock);
WSACleanup();
return -1;
}
Sleep(500);
}
}
178
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Выход из группы путем закрытия sock. При использовании
// немаршрутизируемых плоскостей управления и данных
// WSAJoinLeaf возвращает переданный ей описатель сокета.
//
closesocket(sock);
WSACleanup();
return -1;
}
Пример многоадресной рассылки в ATM-сети средствами Winsock 2
Полный текст программы имеется на диске и включает в себя три
файла: Mcastatm.c, Support.h и Support.c. Здесь мы для краткости
ограничимся текстом основных функций программы (Server и Client).
Функция реализует функциональность многоадресного корня. Она в
цикле вызывает WSAJoinLeaf для каждого клиента, указанного в
командной строке. Сервер хранит массив сокетов для каждого
присоединяемого клиента, однако в вызовах WSASend используется
главный сокет.
Здесь использованы вызовы следующих функций из файла Support.c:
•
GetNumATMInterfaces – возвращает количество ATM-интерфейсов на локальном компьютере;
•
GetATMAddress – возвращает ATM-адрес для данного интерфейса;
•
AtoH – преобразует ATM-адрес из строкового формата (ascii) в
шестнадцатеричный;
•
BtoH – возвращает шестнадцатеричное значение, соответствующее заданному символьному значению.
// Функция: Server
// Осуществляет привязку к локальному интерфейсу,
// приглашает каждый лист, указанный в командной строке,
// затем передает порцию данных.
void Server(SOCKET s, WSAPROTOCOL_INFO *lpSocketProtocol)
{
SOCKADDR_ATM
atmleaf, atmroot;
WSABUF
wsasend;
char
sendbuf[BUFSIZE],
szAddr[BUFSIZE];
DWORD
dwBytesSent,
dwAddrLen=BUFSIZE,
dwNumInterfaces,
i;
int
ret;
179
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
//
// Если не указан локальный интерфейс,
// выбираем первый найденный
memset(&atmroot, 0, sizeof(SOCKADDR_ATM));
if (!bLocalAddress)
{
dwNumInterfaces = GetNumATMInterfaces(s);
GetATMAddress(s, 0, &atmroot.satm_number);
}
else
AtoH(&atmroot.satm_number.Addr[0], szLocalAddress,
ATM_ADDR_SIZE - 1);
//
// Заполнение структуры SOCKADDR_ATM
//
AtoH(&atmroot.satm_number.Addr[ATM_ADDR_SIZE - 1], szPort, 1);
atmroot.satm_family
= AF_ATM;
atmroot.satm_number.AddressType
= ATM_NSAP;
atmroot.satm_number.NumofDigits
= ATM_ADDR_SIZE;
atmroot.satm_blli.Layer2Protocol = SAP_FIELD_ANY;
atmroot.satm_blli.Layer3Protocol = SAP_FIELD_ABSENT;
atmroot.satm_bhli.HighLayerInfoType = SAP_FIELD_ABSENT;
//
// Вывод информации о привязке и собственно привязка
//
if (WSAAddressToString((LPSOCKADDR)&atmroot, sizeof(atmroot),
lpSocketProtocol, szAddr, &dwAddrLen))
{
printf("WSAAddressToString failed: %d\n",
WSAGetLastError());
}
printf("Binding to: <%s>\n", szAddr);
if (bind(s, (SOCKADDR *)&atmroot, sizeof(SOCKADDR_ATM))
== SOCKET_ERROR)
{
printf("bind() failed: %d\n", WSAGetLastError());
return;
}
180
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
//
// Приглашение каждого листа
//
for(i = 0; i < dwAddrCount; i++)
{
//
// Заполнение структуры SOCKADDR_ATM для каждого листа
//
memset(&atmleaf, 0, sizeof(SOCKADDR_ATM));
AtoH(&atmleaf.satm_number.Addr[0], szLeafAddresses[i],
ATM_ADDR_SIZE - 1);
AtoH(&atmleaf.satm_number.Addr[ATM_ADDR_SIZE - 1],
szPort, 1);
atmleaf.satm_family
= AF_ATM;
atmleaf.satm_number.AddressType = ATM_NSAP;
atmleaf.satm_number.NumofDigits = ATM_ADDR_SIZE;
atmleaf.satm_blli.Layer2Protocol = SAP_FIELD_ANY;
atmleaf.satm_blli.Layer3Protocol = SAP_FIELD_ABSENT;
atmleaf.satm_bhli.HighLayerInfoType = SAP_FIELD_ABSENT;
//
// Вывод информации о клиенте, его приглашение
//
if (WSAAddressToString((LPSOCKADDR)&atmleaf,
sizeof(atmleaf), lpSocketProtocol, szAddr,
&dwAddrLen))
{
printf("WSAAddressToString failed: %d\n",
WSAGetLastError());
}
printf("[%02d] Inviting: <%s>\n", i, szAddr);
if ((sLeafSock[i] = WSAJoinLeaf(s, (SOCKADDR *)&atmleaf,
sizeof(SOCKADDR_ATM), NULL, NULL, NULL, NULL,
JL_SENDER_ONLY)) == INVALID_SOCKET)
{
printf("WSAJoinLeaf() failed: %d\n",
WSAGetLastError());
WSACleanup();
return;
}
}
181
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
//
// Замечание: ATM протокол отличается от TCP.
// Когда завершается вызов WSAJoinLeaf пользователь еще
// не обязательно принял соединение, поэтому прежде чем
// отправить данные, ждем некоторое время.
//
printf("Press a key to start sending.");
getchar();
printf("\n");
//
// Отправка данных на групповой адрес.
// Их должны получить все клиенты
//
wsasend.buf = sendbuf;
wsasend.len = 128;
for(i = 0; i < dwDataCount; i++)
{
memset(sendbuf, 'a' + (i%26), 128);
ret = WSASend(s, &wsasend, 1, &dwBytesSent, 0, NULL,
NULL);
if (ret == SOCKET_ERROR)
{
printf("WSASend() failed: %d\n", WSAGetLastError());
break;
}
printf("[%02d] Wrote: %d bytes\n", i, dwBytesSent);
Sleep(500);
}
for(i = 0; i < dwAddrCount; i++)
closesocket(sLeafSock[i]);
return;
}
Функция Client создает привязку к локальному интерфейсу и ожидает
приглашения от сервера. После приема приглашения (accept или
WSAAccept) новый сокет используется для получения данных от корня.
// Функция: Client
// Привязывается к локальному интерфейсу (указанному в
// командной строке или к первому локальному ATM-адресу).
// Ожидает приглашения, затем принимает данные
//
void Client(SOCKET s, WSAPROTOCOL_INFO *lpSocketProtocol)
{
SOCKET
sl;
182
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
SOCKADDR_ATM
DWORD
WSABUF
char
int
atm_leaf,
atm_root;
dwNumInterfaces,
dwBytesRead,
dwAddrLen = BUFSIZE,
dwFlags,
i;
wsarecv;
recvbuf[BUFSIZE],
szAddr[BUFSIZE];
iLen = sizeof(SOCKADDR_ATM),
ret;
// Настройка локального интерфейса
//
memset(&atm_leaf, 0, sizeof(SOCKADDR_ATM));
if (!bLocalAddress)
{
dwNumInterfaces = GetNumATMInterfaces(s);
GetATMAddress(s, 0, &atm_leaf.satm_number);
}
else
AtoH(&atm_leaf.satm_number.Addr[0], szLocalAddress,
ATM_ADDR_SIZE - 1);
AtoH(&atm_leaf.satm_number.Addr[ATM_ADDR_SIZE - 1],
szPort, 1);
//
// Заполнение структуры SOCKADDR_ATM
//
atm_leaf.satm_family
= AF_ATM;
atm_leaf.satm_number.AddressType = ATM_NSAP;
atm_leaf.satm_number.NumofDigits = ATM_ADDR_SIZE;
atm_leaf.satm_blli.Layer2Protocol = SAP_FIELD_ANY;
atm_leaf.satm_blli.Layer3Protocol = SAP_FIELD_ABSENT;
atm_leaf.satm_bhli.HighLayerInfoType = SAP_FIELD_ABSENT;
//
// Вывод информации о привязке и собственно привязка
//
if (WSAAddressToString((LPSOCKADDR)&atm_leaf,
sizeof(atm_leaf), lpSocketProtocol, szAddr, &dwAddrLen))
{
printf("WSAAddressToString failed: %d\n",
WSAGetLastError());
}
printf("Binding to: <%s>\n", szAddr);
183
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
if (bind(s, (SOCKADDR *)&atm_leaf, sizeof(SOCKADDR_ATM))
== SOCKET_ERROR)
{
printf("bind() failed: %d\n", WSAGetLastError());
return;
}
listen(s, 1);
// Ожидание приглашения
memset(&atm_root, 0, sizeof(SOCKADDR_ATM));
if ((sl = WSAAccept(s, (SOCKADDR *)&atm_root, &iLen, NULL, 0))
== INVALID_SOCKET)
{
printf("WSAAccept() failed: %d\n", WSAGetLastError());
return;
}
printf("Received a connection!\n");
// Прием данных
wsarecv.buf = recvbuf;
for(i = 0; i < dwDataCount; i++)
{
dwFlags = 0;
wsarecv.len = BUFSIZE;
ret = WSARecv(sl, &wsarecv, 1, &dwBytesRead, &dwFlags,
NULL, NULL);
if (ret == SOCKET_ERROR)
{
printf("WSARecv() failed: %d\n", WSAGetLastError());
break;
}
if (dwBytesRead == 0)
break;
recvbuf[dwBytesRead] = 0;
printf("[%02d] READ %d bytes: '%s'\n", i, dwBytesRead,
recvbuf);
}
closesocket(sl);
return;
}
184
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Общие параметры Winsock
Три параметра сокета применяются как в Winsock 1, так и в Winsock
2: IP_MULTICAST_TTL, IP_MULTICAST_IF и IP_MULTICAST_LOOP.
Все три параметра относятся только к IP-рассылке.
Параметр IP_MULTICAST_TTL
Задает значение TTL для данных рассылки. По умолчанию TTL равно
1, то есть данные отбрасываются первым же маршрутизатором и
доставляются только в пределах локальной сети. Маршрутизатор не
пересылает дейтаграммы с адресами назначения 224.0.0.0 – 224.0.0.255.
Эти адреса зарезервированы для протоколов маршрутизации и других
служебных протоколов.
Пример, иллюстрирующий настройку TTL:
int optval;
optval = 8;
if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL,
(char *)&optval, sizeof(int)) == SOCKET_ERROR)
{
// Ошибка
}
Параметр TTL не обязателен для многоадресной рассылки в ATM,
поскольку отправка производится только в одном направлении, и все
получатели известны.
Параметр IP_MULTICAST_IF
Этот параметр задает IP-интерфейс, с которого рассылаются данные.
Он необходим в случае, если компьютер, с которого рассылаются данные,
подключен к нескольким сетям через несколько сетевых интерфейсов.
Ниже приводится пример кода для назначения адреса 129.121.32.19 в
качестве локального интерфейса. Любые данные, отправленные на сокет
sock, будут передаваться с сетевого интерфейса, которому назначен этот
IP-адрес.
DWORD dwInterface;
dwInterface = inet_addr("129.121.32.19");
if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF,
(char *)&dwInterface, sizeof(DWORD)) == SOCKET_ERROR)
{
// Ошибка
}
ATM не требует задания интерфейса через параметры сокета, так как
можно явно привязать c_root к локальному интерфейсу перед вызовом
WSAJoinLeaf.
185
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Параметр IP_MULTICAST_LOOP
Параметр определяет, будет ли приложение получать данные
собственной рассылки.
Для отправки данных по групповому адресу вовсе не требуется быть
членом группы. Однако, если приложение является членом группы, то оно
по умолчанию будет получать данные собственной рассылки. Параметр
сокета IP_MULTICAST_LOOP предназначен для отключения этого эха на
локальный интерфейс. Пример его установки в значение "отключено":
int optval;
optval = 0; // Отключение "петли"
if (setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP,
(char *)&optval, sizeof(int)) == SOCKET_ERROR)
{
// Ошибка
}
Этот параметр сокета не реализован в Windows 95, Windows 98 и
Windows NT-4.
При использовании ATM-рассылки петля не образуется, поскольку
рассылка ведется корневым узлом, а получают данные только листовые.
186
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Простые сокеты
Простые сокеты обеспечивают доступ к базовому протоколу
передачи. В данной теме мы рассмотрим их использование для создания
утилит Traceroute и Ping на базе протокола IP. Простые сокеты можно
также применять для манипулирования информацией в заголовке IP.
Кроме протокола IP еще только протокол ATM позволяет работать с
простыми сокетами. Они создаются с использованием типа сокета
SOCK_RAW и поддерживаются Winsock начиная со второй версии.
Наши примеры будут использовать следующие протоколы:
•
Internet Control Message Protocol (ICMP);
•
Internet Group Management Protocol (IGMP);
•
User Datagram Protocol (UDP).
Создание простого сокета
Для создания простого сокета можно воспользоваться как функцией
socket, так и WSASocket. Пример вызова для создания простого сокета с
использованием ICMP в качестве базового IP-протокола:
s = socket (AF_INET, SOCK_RAW, IPPROTO_ICMP);
или
s = WSASocket (AF_INET, SOCK_RAW, IPPROTO_ICMP, NULL, 0,
WSA_FLAG_OVERLAPPED);
Поскольку простые сокеты могут быть использования для
осуществления вредоносных действий, в Windows NT их вправе создавать
только члены группы Administrators (в Windows 9x никаких ограничений
нет). Если есть необходимость обойти это ограничение, можно создать
следующую переменную системного реестра и присвоить ей 1 как
значение типа DWORD.
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services
\Afd\Parameters\DisableRawSecurity
После этого необходимо перезагрузить компьютер.
В приведенных примерах в качестве базового протокола был
использован ICMP. Допустимо также использование IGMP (параметр
IPPROTO_IGMP), UDP (IPPROTO_UDP), IP (IPPROTO_IP) или упрощенный IP (IPPROTO_RAW). Последние три варианта не допускаются в
Windows NT и Windows 9x, поскольку они требуют использования
параметра сокета IP_HDRINCL, не поддерживаемого этими платформами.
Впрочем, независимо от задания этого параметра для простых сокетов
заголовок IP будет добавляться в данные, возвращаемые после приема
каждой порции информации.
187
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Протокол ICMP
Этот протокол представляет собой инструмент для передачи
сообщений между узлами. Большинство из этих сообщений касается
ошибок взаимодействия узлов, остальные применяются для опроса узлов.
Протокол применяет IP-адресацию, поскольку его сообщения
встраиваются в дейтаграммы IP. Сообщение ICMP, встроенное в заголовок
IP, имеет следующую структуру:
8-битный тип
ICMP
8-битный код
ICMP
16-битная контрольная
сумма ICMP
Содержание ICMP (в зависимости от типа и кода)
Возможные значения типов сообщений и кодов можно найти в
соответствующей документации. Сгенерированное сообщение ICMP об
ошибке всегда содержит заголовок IP и первые 8 байт дейтаграммы IP,
ставшей причиной ошибки. Это позволяет узлу, принявшему сообщение,
связать его с конкретным протоколом и процессом.
Приводимый ниже пример полагается не на сообщение об ошибке, а
на эхо-запрос и эхо-ответ ICMP.
Пример: программа Ping
Утилиту Ping обычно используют, чтобы определить, доступен ли
конкретный узел по сети. Программа создает эхо-запрос ICMP (тип 8,
код 0) и по наличию эхо-ответа (тип 0, код 0) определяет, доступен ли
узел. Это, конечно, не гарантирует пользователю возможность связаться с
конкретным процессом на этом узле, однако означает, что сетевой уровень
удаленного узла реагирует на сетевые события.
Программа Ping выполняет следующие шаги:
1. Создает сокет типа SOCK_RAW под протокол IPPROTO_ICMP.
2. Создает и инициализирует заголовок ICMP.
3. Вызывает sendto или WSASendTo, чтобы отправить ICMP запрос
на удаленный узел.
4. Вызывает recvfrom или WSARecvFrom для приема любых ICMPоткликов.
При получении эхо-запроса ICMP удаленный компьютер создает
сообщение эхо-ответа. Если по каким-то причинам узел не доступен,
соответствующее ICMP-сообщение об ошибке будет возвращено
маршрутизатором где-нибудь на пути к месту назначения. Если узел не
доступен лишь временно, следует задать некоторое время ожидания, чтобы
определить это.
Ниже приводится исходный текст соответствующей программы (файл
Ping.c).
188
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
#define WIN32_LEAN_AND_MEAN
#include <winsock2.h>
#include <ws2tcpip.h>
#include <stdio.h>
#include <stdlib.h>
#define IP_RECORD_ROUTE 0x7
// Структура заголовка IP
typedef struct _iphdr
{
unsigned int
h_len:4;
//
unsigned int
version:4;
//
unsigned char tos;
//
unsigned short total_len;
//
unsigned short ident;
//
unsigned short frag_and_flags;
unsigned char ttl;
//
unsigned char proto;
//
unsigned short checksum;
//
unsigned int
unsigned int
} IpHeader;
Длина заголовка
Версия IP
Тип службы
Полный размер пакета
Уникальный идентификатор
// Флаги
Время жизни
Протокол (TCP, UDP и т.п.)
Контрольная сумма IP
sourceIP;
destIP;
#define ICMP_ECHO 8
#define ICMP_ECHOREPLY 0
#define ICMP_MIN 8 // Пакет ICMP не меньше 8 байт (заголовок)
//
// Структура заголовка ICMP
//
typedef struct _icmphdr
{
BYTE
i_type;
BYTE
i_code; // Тип субкода
USHORT i_cksum;
USHORT i_id;
USHORT i_seq;
// Это нестандартный заголовок,
// но мы резервируем место для времени
ULONG timestamp;
} IcmpHeader;
189
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
//
// Расширенный заголовок IP –
// используется с параметром IP_OPTIONS
//
typedef struct _ipoptionhdr
{
unsigned char code;
// Тип параметра
unsigned char len;
// Длина расширенного заголовка
unsigned char ptr;
// Смещение первого адреса
unsigned long addr[9]; // Перечень IP адресов
} IpOptionHeader;
#define DEF_PACKET_SIZE 32 // Стандартный размер пакета
#define MAX_PACKET 1024 // Максимальный размер ICMP-пакета
#define MAX_IP_HDR_SIZE 60 // Максимальный размер
// IP-заголовка с параметрами
BOOL bRecordRoute;
int
datasize;
char *lpdest;
//
// Функция: usage
// выводит информацию об использовании
//
void usage(char *progname)
{
printf("usage: ping -r <host> [data size]\n");
printf("
-r
record route\n");
printf("
host
remote machine to ping\n");
printf("
datasize can be up to 1KB\n");
ExitProcess(-1);
}
//
// Функция: FillICMPData
// Вспомогательная функция заполнения полей ICMP-запроса
//
void FillICMPData(char *icmp_data, int datasize)
{
IcmpHeader *icmp_hdr = NULL;
char
*datapart = NULL;
icmp_hdr = (IcmpHeader*)icmp_data;
icmp_hdr->i_type = ICMP_ECHO;
// Эхо-запрос ICMP
190
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
icmp_hdr->i_code = 0;
icmp_hdr->i_id = (USHORT)GetCurrentProcessId();
icmp_hdr->i_cksum = 0;
icmp_hdr->i_seq = 0;
datapart = icmp_data + sizeof(IcmpHeader);
// Поместим какие-нибудь данные в буфер
memset(datapart,'E', datasize - sizeof(IcmpHeader));
}
//
// Функция: checksum
// Вычисляет 16-битную комплементарную сумму
// для указанного буфера с заголовком
//
USHORT checksum(USHORT *buffer, int size)
{
unsigned long cksum=0;
while (size > 1)
{
cksum += *buffer++;
size -= sizeof(USHORT);
}
if (size)
{
cksum += *(UCHAR*)buffer;
}
cksum = (cksum >> 16) + (cksum & 0xffff);
cksum += (cksum >>16);
return (USHORT)(~cksum);
}
//
// Функция: DecodeIPOptions
// Если есть расширенный заголовок, находит в нем
// параметры IP и выводит значения параметра записи маршрута
//
void DecodeIPOptions(char *buf, int bytes)
{
IpOptionHeader *ipopt = NULL;
IN_ADDR
inaddr;
int
i;
HOSTENT
*host = NULL;
191
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
ipopt = (IpOptionHeader *)(buf + 20);
printf("RR:
");
for(i = 0; i < (ipopt->ptr / 4) - 1; i++)
{
inaddr.S_un.S_addr = ipopt->addr[i];
if (i != 0)
printf("
");
host = gethostbyaddr((char *)&inaddr.S_un.S_addr,
sizeof(inaddr.S_un.S_addr), AF_INET);
if (host)
printf("(%-15s) %s\n",inet_ntoa(inaddr),host->h_name);
else
printf("(%-15s)\n", inet_ntoa(inaddr));
}
return;
}
//
// Функция: DecodeICMPHeader
// Декодирует IP-заголовок ответного пакета
// для нахождения данных ICMP.
//
void DecodeICMPHeader(char *buf, int bytes,
struct sockaddr_in *from)
{
IpHeader
*iphdr = NULL;
IcmpHeader
*icmphdr = NULL;
unsigned short iphdrlen;
DWORD
tick;
static int
icmpcount = 0;
iphdr = (IpHeader *)buf;
// Количество 32-битных слов * 4 = число байт
iphdrlen = iphdr->h_len * 4;
tick = GetTickCount();
if ((iphdrlen == MAX_IP_HDR_SIZE) && (!icmpcount))
DecodeIPOptions(buf, bytes);
if (bytes < iphdrlen + ICMP_MIN)
{
printf("Too few bytes from %s\n",
inet_ntoa(from->sin_addr));
}
192
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
icmphdr = (IcmpHeader*)(buf + iphdrlen);
if (icmphdr->i_type != ICMP_ECHOREPLY)
{
printf("nonecho type %d recvd\n", icmphdr->i_type);
return;
}
//
// Проверка, что это ICMP-ответ на наше сообщение
//
if (icmphdr->i_id != (USHORT)GetCurrentProcessId())
{
printf("someone else's packet!\n");
return ;
}
printf("%d bytes from %s:",bytes,inet_ntoa(from->sin_addr));
printf(" icmp_seq = %d. ", icmphdr->i_seq);
printf(" time: %d ms", tick - icmphdr->timestamp);
printf("\n");
icmpcount++;
return;
}
void ValidateArgs(int argc, char **argv)
{
int
i;
bRecordRoute = FALSE;
lpdest = NULL;
datasize = DEF_PACKET_SIZE;
for(i = 1; i < argc; i++)
{
if ((argv[i][0] == '-') || (argv[i][0] == '/'))
{
switch (tolower(argv[i][1]))
{
case 'r':
// Параметр записи маршрута
bRecordRoute = TRUE;
break;
default:
usage(argv[0]);
break;
}
}
193
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
else
if (isdigit(argv[i][0]))
datasize = atoi(argv[i]);
else
lpdest = argv[i];
}
}
//
// Функция: main
// Настраивает простой сокет ICMP, создает заголовок ICMP.
// Добавляет расширенный заголовок IP, рассылает ICMP эхо// запросы по конечным точкам. Задает тайм-аут во избежание
// простоя. Принятый пакет декодируется.
//
int main(int argc, char **argv)
{
WSADATA
wsaData;
SOCKET
sockRaw = INVALID_SOCKET;
struct sockaddr_in
dest,
from;
int
bread,
fromlen = sizeof(from),
timeout = 1000,
ret;
char
*icmp_data = NULL,
*recvbuf = NULL;
unsigned int
addr = 0;
USHORT
seq_no = 0;
struct hostent *hp = NULL;
IpOptionHeader ipopt;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("WSAStartup() failed: %d\n", GetLastError());
return -1;
}
ValidateArgs(argc, argv);
194
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Параметрам SO_RCVTIMEO и SO_SNDTIMEO требуется флаг
// WSA_FLAG_OVERLAPPED. В противном случае
// (NULL в качестве последнего параметра WSASocket) весь
// ввод-вывод на сокете будет синхронным, код ожидания
// пользовательского режима никогда не будет выполнен
// и навсегда блокируется ввод-вывод в режиме ядра.
// Если требуется использовать тайм-аут с синхронным
// неперекрытым сокетом, его следует задавать функцией
// select или использовать WSAEventSelect и задать
// тайм-аут в функции WSAWaitForMultipleEvents.
//
sockRaw = WSASocket(AF_INET,SOCK_RAW,IPPROTO_ICMP,NULL,
0,WSA_FLAG_OVERLAPPED);
if (sockRaw == INVALID_SOCKET)
{
printf("WSASocket() failed: %d\n", WSAGetLastError());
return -1;
}
if (bRecordRoute)
{
// Настройка отправки расширенного заголовка IP
// с каждым ICMP пакетом
ZeroMemory(&ipopt, sizeof(ipopt));
ipopt.code = IP_RECORD_ROUTE; // Запись маршрута
ipopt.ptr = 4;
// Смещение первого адреса
ipopt.len = 39; // Длина расширенного заголовка
ret = setsockopt(sockRaw, IPPROTO_IP, IP_OPTIONS,
(char *)&ipopt, sizeof(ipopt));
if (ret == SOCKET_ERROR)
{
printf("setsockopt(IP_OPTIONS) failed: %d\n",
WSAGetLastError());
}
}
// Настройка тайм-аутов отправки и приема
//
bread = setsockopt(sockRaw, SOL_SOCKET, SO_RCVTIMEO,
(char*)&timeout, sizeof(timeout));
if(bread == SOCKET_ERROR)
{
printf("setsockopt(SO_RCVTIMEO) failed: %d\n",
WSAGetLastError());
return -1;
}
195
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
timeout = 1000;
bread = setsockopt(sockRaw, SOL_SOCKET, SO_SNDTIMEO,
(char*)&timeout, sizeof(timeout));
if (bread == SOCKET_ERROR)
{
printf("setsockopt(SO_SNDTIMEO) failed: %d\n",
WSAGetLastError());
return -1;
}
memset(&dest, 0, sizeof(dest));
//
// Если надо, разрешить имя конечной точки
//
dest.sin_family = AF_INET;
if ((dest.sin_addr.s_addr=inet_addr(lpdest))==INADDR_NONE)
{
if ((hp = gethostbyname(lpdest)) != NULL)
{
memcpy(&(dest.sin_addr), hp->h_addr, hp->h_length);
dest.sin_family = hp->h_addrtype;
printf("dest.sin_addr = %s\n",
inet_ntoa(dest.sin_addr));
}
else
{
printf("gethostbyname() failed: %d\n",
WSAGetLastError());
return -1;
}
}
//
// Создание ICMP-пакета
//
datasize += sizeof(IcmpHeader);
icmp_data = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
MAX_PACKET);
recvbuf = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY,
MAX_PACKET);
if (!icmp_data)
{
printf("HeapAlloc() failed: %d\n", GetLastError());
return -1;
}
196
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
memset(icmp_data,0,MAX_PACKET);
FillICMPData(icmp_data,datasize);
//
// Начало отправки/приема ICMP-пакетов
//
while(1)
{
static int nCount = 0;
int
bwrote;
if (nCount++ == 4)
break;
((IcmpHeader*)icmp_data)->i_cksum = 0;
((IcmpHeader*)icmp_data)->timestamp = GetTickCount();
((IcmpHeader*)icmp_data)->i_seq = seq_no++;
((IcmpHeader*)icmp_data)->i_cksum =
checksum((USHORT*)icmp_data, datasize);
bwrote = sendto(sockRaw, icmp_data, datasize, 0,
(struct sockaddr*)&dest, sizeof(dest));
if (bwrote == SOCKET_ERROR)
{
if (WSAGetLastError() == WSAETIMEDOUT)
{
printf("timed out\n");
continue;
}
printf("sendto() failed: %d\n", WSAGetLastError());
return -1;
}
if (bwrote < datasize)
{
printf("Wrote %d bytes\n", bwrote);
}
bread = recvfrom(sockRaw, recvbuf, MAX_PACKET, 0,
(struct sockaddr*)&from, &fromlen);
if (bread == SOCKET_ERROR)
{
if (WSAGetLastError() == WSAETIMEDOUT)
{
printf("timed out\n");
continue;
}
printf("recvfrom() failed: %d\n", WSAGetLastError());
return -1;
}
197
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
DecodeICMPHeader(recvbuf, bread, &from);
Sleep(1000);
}
//
// Очистка
//
if (sockRaw != INVALID_SOCKET)
closesocket(sockRaw);
HeapFree(GetProcessHeap(), 0, recvbuf);
HeapFree(GetProcessHeap(), 0, icmp_data);
WSACleanup();
return 0;
}
В рассмотренной программе используется параметр сокета
IP_RECORD_ROUTE, что позволяет добавлять адреса пройденных
маршрутизаторов в IP-заголовок в месте, определяемом заданным
смещением (у нас на каждый адрес отводится 4 байта). При декодировании
ответного пакета на экран выводятся IP-адреса и имена пройденных
маршрутизаторов.
Программа Traceroute
В качестве другого примера использования простых сокетов можно
привести программу Traceroute, которая позволяет определить IP-адреса
маршрутизаторов, проходимых запросом на пути к определенному узлу.
Ее идея состоит в отправке серии UDP-пакетов с постепенным
изменением времени жизни (TTL). Первоначально TTL устанавливается
равным 1, и когда пакет достигает первого маршрутизатора, тот
генерирует ICMP-пакет с сообщением о превышении лимита времени.
Далее каждый раз значение TTL увеличивается на 1 до тех пор, пока UDPпакет не достигнет точки назначения. Поскольку там ни один процесс не
ждет этого сообщения, отправителю будет отправлено ICMP-сообщение о
недостижимости порта, что и является признаком того, что цель
достигнута.
При разработке такой программы возможны два варианта. Во-первых,
для отправки можно использовать UDP-пакеты и посылать дейтаграммы,
постепенно изменяя TTL. В этом случае потребуется два сокета. Первый –
это обычный сокет протокола UDP, второй – сокет типа SOCK_RAW под
протокол IPPROTO_ICMP для чтения возвращенных сообщений. Значение
TTL для UDP-сокета можно контролировать через параметр сокета
IP_TTL.
Другой вариант – просто отправлять ICMP-пакеты адресату с
постепенным изменением TTL. В результате возвращаются такие же
ICMP-пакеты с сообщениями об ошибках. Именно этот вариант
реализован программно в файле Traceroute.c. Для краткости мы не
приводим здесь текст этой программы – читателям предлагается изучить
его самостоятельно.
198
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Служба удаленного доступа
Служба удаленного доступа (Remote Access Service, RAS) позволяет
пользователям подключаться к локальной сети с удаленного компьютера и
использовать сетевые функции, как если бы они были подключены к сети
напрямую.
Все платформы Windows используют клиент службы RAS,
позволяющий подключаться к удаленному компьютеру, если тот является
сервером удаленного доступа. Поскольку клиент RAS обычно задействует
модем, его иногда называют клиентом удаленного доступа (dial-up
networking, DUN).
Клиент RAS может устанавливать соединение с несколькими типами
серверов удаленного доступа. Для этого он пользуется стандартными
кадрирующими (framing) протоколами:
•
•
•
Point-to-Point Protocol – может передавать протоколы IP, IPX и
NetBEUI;
Serial Line Internet Protocol (SLIP) – может передавать протокол
IP;
Asynchronous NetBEUI (Windows 3.11, Windows NT 3.1) –
только протокол NetBEUI.
Кадрирующие протоколы описывают процесс передачи данных через
RAS-соединение и указывают, какой сетевой протокол может
устанавливать связь через соединение. В Windows 9x, NT и 2000
компонент сервера RAS поддерживает все перечисленные кадрирующие
протоколы. После установления соединения между RAS-клиентом и
сервером стеки сетевого протокола могут взаимодействовать так, как если
бы компьютеры были соединены в рамках ЛВС.
Когда сервер RAS принимает соединение по телефонной линии, он
сначала устанавливает соединение с клиентом с помощью одного из
кадрирующих протоколов. После определения кадрирующего протокола
он делает попытку аутентифицировать пользователя. API-функции RAS
позволяют клиенту задать имя пользователя, пароль и домен,
проверяющий учетные реквизиты на сервере. При этом сервер RAS не
подключает клиента к домену, а только проверяет его права на
подключение. Процесс подключения к домену нами здесь не
рассматривается.
Для управления устройствами связи, такими, как модемы,
используется программный интерфейс компьютерной телефонии
(Telephony Application Programming Interface, TAPI). RAS рассматривает
модемы просто как порты TAPI-интерфейса, которые могут набирать
номер и устанавливать телефонное соединение с удаленным сервером.
Ниже в основном рассматриваются вопросы использования RAS для
установления соединения с удаленной сетью.
199
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Компиляция и компоновка
Необходимые для сборки приложений RAS файлы заголовков и
библиотеки:
•
Ras.h – содержит прототипы функций и структуры данных,
используемые API-функциями RAS;
•
Raserror.h – содержит коды ошибок, возвращаемые APIфункциями при сбое;
•
Rasapi32.lib – библиотека всех API-функций RAS.
В файле Raserror.h имеются строки с описанием ошибки для каждого
кода, используемого в RAS. Для доступа к ним можно использовать
функцию
DWORD RasGetErrorString(
UINT uErrorValue,
// Код ошибки RAS
LPTSTR lpszErrorString,// Строка для описания ошибки
DWORD cBufSize
// Размер буфера
);
Здесь буфер для получения строки должен быть размером не менее
256 символов.
Структуры данных и совместимость платформ
Для различных платформ Windows используется разный набор полей
структур, передаваемых RAS-функциям. Некоторые поля могут быть
включены или выключены в зависимости от значения WINVER. Кроме
того, в структуре данных RAS должно быть корректно заполнено поле
dwSize – размер структуры в байтах.
Значение WINVER определяет минимальные требования к
операционной системе, а именно:
•
•
•
•
•
WINVER = 0x0400 – требуется Windows 95 или Windows NT 4;
WINVER = 0x0410 – требуется Windows 98;
WINVER = 0x0500 – требуется Windows ME или Windows 2000;
WINVER = 0x0501 – требуется Windows XP;
WINVER = 0x0502 – требуется Windows Server 2003.
Не
рекомендуется
создавать
приложения
RAS,
которые
предназначены для работы на любой платформе (хотя теоретически это и
возможно) – лучше конструировать приложение, рассчитанное на
конкретную версию ОС.
Функция RasDial
Эта функция используется приложением-клиентом для подключения к
удаленному компьютеру. Она определяется следующим образом:
200
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
DWORD RasDial(
LPRASDIALEXTENSIONS lpRasDialExtensions,
LPCTSTR lpszPhonebook,
LPRASDIALPARAMS lpRasDialParams,
DWORD dwNotifierType,
LPVOID lpvNotifier,
LPHRASCONN lphRasConn
);
Здесь параметр lpRasDialExtensions – необязательный указатель на
структуру RASDIALEXTENSIONS. В Windows 9x и Windows CE он
игнорируется. На остальных платформах он позволяет активизировать
расширенные возможности функции RasDial, например, задать некоторые
настройки модема, параметры сжатия и т.п. Мы не рассматриваем ее более
подробно.
Параметр lpszPhonebook задает путь к файлу телефонного
справочника. В Windows 9x и Windows CE он должен быть равен NULL,
так как телефонный справочник хранится в реестре. Телефонный
справочник – это совокупность свойств набора номера RAS,
определяющих способ установки RAS-соединения.
Параметр
lpRasDialParams
–
указатель
на
структуру
RASDIALPARAMS, которая определяет параметры набора номера и
аутентификации пользователя. Для краткости мы не описываем данную
структуру, эту информацию можно получить из справочной системы.
Параметры dwNotifierType и lpvNotifier определяют режим работы
RAS (синхронно или асинхронно она вызывается). Последнему параметру
(lphRasConn) перед вызовом нужно присвоить NULL, после вызова он
будет указывать на структуру типа HRASCONN, содержащую описание
RAS-соединения.
Синхронный режим
Если параметр lpvNotifier функции RasDial равен NULL, функция
будет работать синхронно, при этом значение параметра dwNotifierType
игнорируется. Это наиболее простой способ соединения, однако в данном
случае отсутствует возможность отслеживать процесс соединения, как это
можно сделать в асинхронном режиме. Основные действия программы
ясны из следующего примера.
RASDIALPARAMS RasDialParams;
HRASCONN
hRasConn;
DWORD
Ret;
// Нужно обязательно задать размер структуры RASDIALPARAMS
RasDialParams.dwSize = sizeof(RASDIALPARAMS);
hRasConn = NULL;
201
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Если записать в этом поле пустую строку, RasDial будет
// использовать стандартные свойства установки связи.
lstrcpy (RasDialParams.szEntryName, "");
lstrcpy (RasDialParams.szPhoneNumber, "867-5309");
lstrcpy (RasDialParams.szUserName, "User");
lstrcpy (RasDialParams.szPassword, "mypassword");
lstrcpy (RasDialParams.szDomain, "mydomain");
// Вызов RasDial в синхронном режиме
// (пятый параметр равен NULL)
Ret=RasDial(NULL, NULL, &RasDialParams, 0, NULL, &hRasConn);
if (Ret != 0)
{
printf("RasDial failed: Error = %d\n", Ret);
}
Асинхронный режим
Если параметр lpvNotifier функции RasDial не равен NULL, функция
будет работать в асинхронном режиме, то есть возвратит управление до
окончательного установления соединения. Параметр lpvNotifier в этом
случае может быть указателем на функцию, вызываемую по завершении
соединения, либо дескриптором окна, получающего уведомления через
сообщеня Windows.
Возможные значения параметра dwNotifierType в этом случае:
•
0 – для управления событиями подключения RasDial будет
использовать указатель на функцию RasDialFunc;
•
1 – для управления событиями подключения RasDial будет
использовать указатель на функцию RasDialFunc1;
•
2 – для управления событиями подключения RasDial будет
использовать указатель на функцию RasDialFunc2;
•
0xFFFFFFFF – RasDial должна отправлять оконные сообщения.
Есть три прототипа функций, на которые может указывать параметр
lpvNotifier. Первый из них описан следующим образом:
void CALLBACK RasDialFunc(
UINT unMsg,
RASCONNSTATE rasconnstate,
DWORD dwError
);
В качестве первого параметра (unMsg) функции передается тип
произошедшего события. Поскольку событие может быть только одно
(WM_RASDIALEVENT), то этот параметр практически бесполезен. Через
параметр dwError передается код ошибки RAS. Через параметр rasconnstate
202
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
функция получает уведомление о текущем действии или событии. Список
возможных значений его весьма обширен, а потому предлагаем читателю
обратиться за полной информацией к соответствующей документации,
ограничившись здесь некоторыми замечаниями.
Уведомления,
передаваемые
через
параметр
rasconnstate,
соответствуют трем возможным группам состояний: выполнение действия,
пауза и завершение. Пауза означает, что функции RasDial требуется
дополнительная информация для установления соединения. По умолчанию
режим паузы отключен. Для его включения следует задать флаг
RDEOPT_PausedStates в структуре RASDIALEXTENSIONS. Если система
находится в режиме паузы, то это может означать, что пользователь
должен:
•
Ввести новые учетные реквизиты, так как аутентификация не
удалась;
•
Ввести новый пароль, так как текущий устарел;
•
Ввести номер обратного вызова.
Когда возникает пауза, RasDial уведомит об этом клиентскую
функцию обратного вызова или оконную процедуру. Если при этом режим
паузы отключен, то будет отправлено сообщение об ошибке, а если
включен – RasDial останется в состоянии, позволяющем приложению
вводить новую информацию через структуру RASDIALPARAMS. Для
возобновления работы RasDial ее следует вызвать заново с описателем
соединения исходного вызова (только это нельзя делать непосредственно
из функции обработчика уведомления, например, RasDialFunc). Для
завершения работы в режиме паузы нужно вызвать функцию RasHangUp.
Ниже приводится примерный вид исходного кода программы,
асинхронно вызывающей RasDial. Более полный вариант исходного кода
содержится в файле RasDial.cpp.
void main(void)
{
DWORD Ret;
RASDIALPARAMS RasDialParams;
HRASCONN hRasConn;
// Задайте в структуре RASDIALPARAMS параметры вызова,
// как это было сделано в примере синхронного вызова
if ((Ret = RasDial(NULL, NULL, &RasDialParams, 0,
&RasDialFunc, &hRasConn)) != 0)
{
printf(RasDial failed with error %d\n", Ret);
return;
}
// Выполните другие задачи, пока работает RasDial
...
}
203
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
// Функция обратного вызова RasDialFunc()
void WINAPI RasDialFunc(UINT unMsg, RASCONNSTATE,
rasconnstate,DWORD dwError)
{
char szRasString[256]; // Буфер для строки с ошибкой
if (dwError)
{
RasGetErrorString((UINT)dwError, szRasString, 256);
printf("Error: %d - %s\n",dwError, szRasString);
return;
}
// Привязка каждого из режимов RasDial и вывод на экран
// сведений о состоянии, в которое переходит RasDial
switch (rasconnstate)
{
case RASCS_ConnectDevice:
printf ("Connecting device...\n");
break;
case RASCS_DeviceConnected:
printf ("Device connected.\n");
break;
// Здесь можно добавить другие действия подключения
...
default:
printf ("Unmonitored RAS activity.\n");
break;
}
}
В этих примерах в качестве функции обратного вызова
использовалась RasDialFunc. Другие два варианта: RasDialFunc1 и
RasDialFunc2 – имеют следующий формат:
void CALLBACK RasDialFunc1(
HRASCONN hrasconn,
UINT unMsg,
RASCONNSTATE rascs,
DWORD dwError,
DWORD dwExtendedError
);
204
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
DWORD CALLBACK RasDialFunc2(
DWORD dwCallbackId,
DWORD dwSubEntry,
HRASCONN hrasconn,
UINT unMsg,
RASCONNSTATE rascs,
DWORD dwError,
DWORD dwExtendedError
);
У функции RasDialFunc1 два дополнительных парметра: hrasconn и
dwExtendedError. Первый – это описатель возвращаемого соединения,
второй позволяет получить расширенную информацию об ошибке.
Функция RasDialFunc2 по сравнению с RasDialFunc1 имеет еще два
параметра. Параметр dwCallbackId содержит значение, которое
первоначально было в поле dwCallbackId структуры RASDIALPARAMS,
переданной в вызов RasDial. Параметр dwSubEntry содержит индекс
подзаписи телефонного справочника, по которому RasDialFunc2
осуществила обратный вызов.
Уведомление о состоянии
Функция RasConnectionNotification позволяет определить объект
события, которое срабатывает во время создания или закрытия
асинхронного соединения:
DWORD RasConnectionNotification(
HRASCONN hrasconn,
HANDLE hEvent,
DWORD dwFlags
);
Здесь hrasconn – описатель соединения, возвращенный функцией
RasDial, hEvent – описатель события, созданного функцией CreateEvent. В
качестве параметра dwFlags может быть использована комбинация
следующих флагов:
•
•
•
•
RASCN_Connection – уведомляет о создании RAS-соединения;
если hrasconn равен INVALID_HANDLE_VALUE, событие
освобождается при каждом RAS-соединении;
RASCN_Disconnection – уведомляет о закрытии RASсоединения; если hrasconn равен INVALID_HANDLE_VALUE,
событие освобождается при каждом завершении RASсоединения;
RASCN_BandwidthAdded – при многоканальном соединении
событие освобождается, когда присоединяется подзапись
(Windows NT);
RASCN_BandwidthRemoved – при многоканальном соединении
событие освобождается, когда отсоединяется подзапись (Windows
NT);
205
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Для ожидания освобождения событий, как обычно, следует воспользоваться одной из функций ожидания, например, WaitForSingleObject.
Завершение соединения
Для завершения установленного соединения достаточно вызвать
функцию RasHangUp:
DWORD RasHangUp(
HRASCONN hrasconn
);
Параметр hrasconn – описатель соединения, возвращенный функцией
RasDial.
Следует иметь в виду, что соединение не может закрыться мгновенно,
и нужно подождать окончательного закрытия. Для того, чтобы узнать
состояние сброшенного соединения, можно вызвать функцию
RasGetConnectStatus:
DWORD RasGetConnectStatus(
HRASCONN hrasconn,
LPRASCONNSTATUS lprasconnstatus
);
Здесь, как обычно, hrasconn – описатель соединения. Параметр
lprasconnstatus – указатель на структуру RASCONNSTATUS, заполняемую
информацией о состоянии текущего соединения:
typedef struct _RASCONNSTATUS {
DWORD
dwSize;
RASCONNSTATE rasconnstate;
DWORD
dwError;
TCHAR
szDeviceType[RAS_MaxDeviceType + 1];
TCHAR
szDeviceName[RAS_MaxDeviceName + 1];
#if (WINVER >= 0x401)
TCHAR
szPhoneNumber[RAS_MaxPhoneNumber + 1];
#endif // (WINVER >= 0x401)
} RASCONNSTATUS;
Здесь dwSize – размер структуры RASCONNSTATUS, rasconnstate –
один из параметров активности соединения. При ожидании закрытия
соединения функцию придется вызывать несколько раз, пока значение
этого параметра не станет равным RASCS_Disconnected. Параметр dwError
содержит код ошибки, если RasGetConnectStatus возвратила не 0,
szDeviceType – тип устройства, используемого при соединении,
szDeviceName – имя текущего устройства, szPhoneNumber – строку с
телефонным номером текущего соединения.
206
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Литература
1.
2.
3.
4.
5.
Джонс Э., Оланд Дж. Программирование в сетях Microsoft Windows.
Мастер-класс. - СПб: Питер, 2002.
Снейдер Йон. Эффективное программирование TCP/IP. СПб:
Питер, 2002.
Олафсен Юджин, Скрайбер Кенн, Уайт К.Дэвид и др. MFC и
Visual C++ 6. Энциклопедия программиста. - СПб: ООО "ДиаСофтЮП",
2004.
Круглински Д., Уингоу С., Шеферд Дж. Программирование на
Microsoft Visual C++ 6.0 для профессионалов. - СПб: Питер, 2001.
Рихтер Дж. Windows для профессионалов: создание эффективных
Win32-приложений с учетом специфики 64-разрядной версии
Windows. - СПб: Питер, 2001.
207
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Оглавление
Введение .............................................................................................................. 3
Интерфейс NetBIOS .......................................................................................... 4
NetBIOS и сетевая модель OSI ..................................................................... 4
Интерфейс Microsoft NetBIOS ........................................................................ 5
Номера LANA ................................................................................................ 5
Имена NetBIOS .............................................................................................. 6
Основы программирования NetBIOS .............................................................. 6
Синхронный и асинхронный вызов ............................................................. 8
Типовые процедуры NetBIOS ........................................................................... 8
Общие функции приложений NetBIOS ....................................................... 8
Сервер сеансов: модель асинхронного обратного вызова ...................... 13
Пример сервера, основанный на модели событий ................................... 17
Клиент сеанса NetBIOS ............................................................................... 22
Дейтаграммные операции ............................................................................. 25
Дополнительные команды NetBIOS.............................................................. 27
Проверка состояния адаптера (команда NCBASTAT) ............................ 27
Команда поиска имени (NCBFINDNAME)............................................... 28
Сопоставление протоколов номерам LANA............................................. 29
Перенаправитель ............................................................................................. 30
Универсальные правила именования............................................................. 30
Поставщик нескольких UNC ......................................................................... 31
Компоненты сетевого доступа ................................................................... 31
Перенаправитель ............................................................................................ 31
Протокол SMB ................................................................................................ 32
Пример ............................................................................................................. 32
Почтовые ящики ............................................................................................. 34
Имена почтовых ящиков ............................................................................. 34
Размеры сообщений..................................................................................... 34
Сборка приложения и коды возврата ........................................................ 35
Использование архитектуры клиент-сервер .............................................. 35
Сервер почтовых ящиков ............................................................................ 35
Клиент почтовых ящиков ........................................................................... 37
Дополнительные API-функции почтовых ящиков ...................................... 39
Особенности работы в Windows 9x ............................................................. 40
Правила наименования ............................................................................... 40
Неспособность отменить блокирующие запросы ввода-вывода ........... 40
Утечки памяти .............................................................................................. 40
Именованные каналы .................................................................................... 41
Детали реализации именованных каналов ................................................... 41
Правила именования каналов ..................................................................... 41
Режимы передачи ........................................................................................ 41
Сборка приложения и коды возврата ........................................................ 42
208
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Простой сервер и клиент .............................................................................. 42
Детали реализации сервера ........................................................................ 42
Усовершенствованный сервер каналов ........................................................ 45
Реализация клиента ..................................................................................... 51
Другие API-вызовы ......................................................................................... 53
Сетевые протоколы ........................................................................................ 56
Интерфейс прикладного программирования Winsock ............................ 56
Характеристики протоколов ....................................................................... 56
Ориентированность на передачу сообщений ........................................... 56
Обмен данными с соединением и без него ............................................... 57
Надежность и порядок доставки сообщений ............................................ 58
Корректное завершение работы ................................................................. 58
Широковещание........................................................................................... 58
Многоадресное вещание ............................................................................. 58
Качество обслуживания .............................................................................. 59
Фрагментарные сообщения ........................................................................ 59
Маршрутизация ........................................................................................... 59
Сетевые протоколы, поддерживаемые Win32 ........................................... 59
Сетевые протоколы в Windows CE ............................................................ 61
Работа с Winsock ........................................................................................... 61
Инициализация Winsock ............................................................................. 61
Информация о протоколе ........................................................................... 61
Сокеты Windows .......................................................................................... 63
Семейства адресов и разрешение имен....................................................... 66
Протокол IP .................................................................................................... 66
Протоколы TCP и UDP ............................................................................... 66
Адресация ..................................................................................................... 66
Порядок байтов ............................................................................................ 67
Создание сокета ........................................................................................... 68
Разрешение имен ......................................................................................... 68
Номера портов ............................................................................................. 69
Инфракрасные сокеты .................................................................................. 70
Адресация ..................................................................................................... 70
Разрешение имен ......................................................................................... 70
Нумерация IrDA-устройств ........................................................................ 70
Создание сервера и клиента для IrSock ..................................................... 72
Опрос IAS ..................................................................................................... 72
Создание сокета ........................................................................................... 73
Протоколы IPX/SPX ....................................................................................... 73
Адресация ..................................................................................................... 74
Создание сокета ........................................................................................... 74
Привязка сокета ............................................................................................. 75
Внутренний номер сети .............................................................................. 75
Установка типа пакета ................................................................................ 76
209
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Протоколы NetBIOS ....................................................................................... 76
Адресация ..................................................................................................... 76
Создание сокета ........................................................................................... 77
Протокол AppleTalk ....................................................................................... 77
Адресация ..................................................................................................... 78
Регистрация имени AppleTalk .................................................................... 78
Разрешение имен AppleTalk ....................................................................... 80
Создание сокета ........................................................................................... 80
Протокол ATM ................................................................................................ 81
Адресация ..................................................................................................... 81
Создание сокета ........................................................................................... 83
Привязка сокета к SAP ................................................................................ 84
Дополнительные функции Winsock 2 ............................................................ 84
Основы интерфейса Winsock ........................................................................ 85
Инициализация Winsock ................................................................................. 85
Проверка и обработка ошибок ..................................................................... 86
Протоколы с установлением соединения .................................................... 86
Серверные API-функции............................................................................. 86
Функция bind ................................................................................................ 87
Функция listen .............................................................................................. 87
Функции accept и WSAAccept .................................................................... 87
API-функции клиента .................................................................................. 88
Функции connect и WSAConnect ................................................................ 88
Передача данных: функции send и WSASend........................................... 89
Функция WSASendDisconnect .................................................................... 90
Функции recv и WSARecv .......................................................................... 90
Функция WSARecvDisconnect.................................................................... 91
Функция WSARecvEx ................................................................................. 91
Потоковые протоколы ................................................................................. 91
Завершение сеанса: функции shutdown и closesocket .............................. 93
Примеры ....................................................................................................... 93
Протоколы, не требующие соединения ..................................................... 101
Приемник .................................................................................................... 101
Отправитель ............................................................................................... 102
Протоколы, ориентированные на передачу сообщений ........................ 102
Пример ........................................................................................................ 102
Дополнительные функции API .................................................................... 104
Ввод-вывод в Winsock .................................................................................. 106
Поддерживаемые модели ввода-вывода ................................................. 106
Режимы работы сокетов ........................................................................... 106
Блокирующий режим ................................................................................ 106
Неблокирующий режим ............................................................................ 109
Модели управления вводом-выводом сокетов ........................................... 109
Модель select .............................................................................................. 109
210
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Модель WSAAsyncSelect .......................................................................... 111
Модель WSAEventSelect ........................................................................... 115
Модель перекрытого ввода-вывода ......................................................... 120
Модель портов завершения ...................................................................... 126
Параметры сокета и команды управления вводом-выводом ............. 133
Параметры сокета ...................................................................................... 133
Уровень SOL_SOCKET............................................................................. 134
Уровень параметров IPPROTO_IP ........................................................... 137
Уровень параметров IPPROTO_TCP ....................................................... 141
Функции ioctlsocket и WSAIoctl .................................................................... 141
Стандартные ioctl-команды ...................................................................... 142
Другие ioctl-команды ................................................................................ 143
Ioctl-команды для ATM............................................................................. 147
Регистрация и разрешение имен ................................................................ 148
Модели пространства имен ........................................................................ 148
Перечень пространств имен ....................................................................... 149
Регистрация службы ................................................................................... 150
Определение класса службы .................................................................... 150
Регистрация экземпляра службы ............................................................. 153
Запрос к службе ............................................................................................ 157
Создание запроса ....................................................................................... 157
Запрос к DNS .............................................................................................. 160
Многоадресная рассылка ............................................................................ 163
Основные понятия........................................................................................ 163
Многоадресная рассылка в сетях IP .......................................................... 165
Протокол IGMP .......................................................................................... 165
Листовые узлы IP ....................................................................................... 166
Многоадресная рассылка в сетях ATM ...................................................... 166
Листовые узлы ATM ................................................................................. 167
Корневые узлы ATM ................................................................................. 167
Многоадресная рассылка с использованием Winsock ............................... 167
Рассылка средствами Winsock 1 .............................................................. 167
Рассылка средствами Winsock 2 .............................................................. 174
Общие параметры Winsock....................................................................... 185
Простые сокеты ............................................................................................. 187
Создание простого сокета ......................................................................... 187
Протокол ICMP ............................................................................................ 188
Пример: программа Ping ........................................................................... 188
Программа Traceroute ................................................................................ 198
Служба удаленного доступа ........................................................................ 199
Компиляция и компоновка ........................................................................... 200
Структуры данных и совместимость платформ ................................... 200
Функция RasDial ........................................................................................... 200
Синхронный режим ................................................................................... 201
211
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Асинхронный режим ................................................................................. 202
Уведомление о состоянии ......................................................................... 205
Завершение соединения ............................................................................ 206
Литература...................................................................................................... 207
Учебное издание
Васильчиков Владимир Васильевич
Основы разработки сетевых
Windows-приложений
Учебное пособие
Редактор, корректор А.А. Аладьева
Подписано в печать 22.03.2007 г. Формат 60х84/16.
Бумага тип. Усл. печ. л. 12,32. Уч.-изд. л. 8,12.
Тираж 100 экз. Заказ
Оригинал-макет подготовлен
в редакционно-издательском отделе ЯрГУ.
Ярославский государственный университет.
150000 Ярославль, ул. Советская, 14.
Отпечатано
ООО «Ремдер» ЛР ИД № 06151 от 26.10.2001.
г. Ярославль, пр. Октября, 94, оф. 37
тел. (4852) 73-35-03, 58-03-48, факс 58-03-49.
212
Документ
Категория
Информатика
Просмотров
113
Размер файла
1 219 Кб
Теги
1791, разработка, основы, сетевые, windows, приложение
1/--страниц
Пожаловаться на содержимое документа