
- •5. Управление коммуникациями в ос
- •5.1. Общая характеристика коммуникаций
- •5.2. Концепции технологии Клиент-Сервер
- •5.3. Внутренние коммуникации между процессами
- •5.3.1. Неименованные каналы
- •5.3.2. Обмен сообщениями
- •5.3.3. Обмен данными через Clipboard
- •1. Подготовка данных для передачи в Clipboard;
- •1. Открытие Clipboard
- •2. Проверка наличия требуемого формата данных в Clipboard
- •3. Чтение данных из Clipboard
- •4. Закрытие Clipboard
- •5.3.4. Обмен данными по технологии dde
- •5.3.5. Обмен данными по технологии ole
- •5.3.5.1. Введение
- •5.3.5.2. Понятие документ-ориентированной среды
- •5.3.5.3. Принципы ole
- •5.3.5.4. Характеристика технологии ole
- •5.3.5.4.1. Ole реализует концепцию визуального редактирования - редактирования на месте.
- •5.3.5.4.2. Другой особенностью ole является возможность приложения предоставить ряд своих функций (например, проверку орфографии) для доступа из других приложений - ole-Automation.
- •5.3.5.4.3. Ядром ole стал способ хранения данных в составном документе.
- •5.3.5.4.4. Дополнительные возможности ole:
- •5.3.5.4.5. Иерархия средств ole
- •5.3.5.4.6. Недостатки ole
- •5.3.5.4.7. Трехуровневая адресация ole-объекта
- •5.4. Внешние коммуникации
- •5.4.1. Протоколы тср/ip
- •5.4.1.1. Определение и достоинства протокола tcp/ip
- •5.4.1.2. Архитектура tcp/ip
- •5.4.1.3. Соответствие между моделями tcp/ip и iso osi
- •5.4.2. Протокол ip
- •5.4.2.1. Структура заголовка ip-пакета
- •5.4.3. Протокол udp
- •5.4.4. Протокол tcp
- •5.4.4.1. Структура tcp-сегмента
- •5.4.4.2. Этапы tcp-сеанса
- •5.4.5. Программные средства поддержки сетей
- •5.4.5.1. Состав программных средств поддержки сетей
- •1. Почтовые ящики
- •2. Именованные каналы
- •3. Удаленные вызовы процедур
- •4. Протокол NetBios
- •5.4.5.2. Программные средства работы с сокетами
- •5.4.6. Сетевое программное обеспечение уровня приложений
- •5.4.6.1. Протокол ftp
- •5.4.6.2. Протокол smtp
2. Именованные каналы
Именованные каналы предоставляют приложениям надежную двухстороннюю связь.
Вопрос (
Именованный канал создается вызовом:
NP = CreateNamedPipe("\\\\.\\pipe\\mypipe",
PIPE_ACCESS_DUPLEX,
PIPE_TYPE_BYTE|PIPE_READMODE_BYTE|PIPE_WAIT,
1,
0,
0,
INFINITE,
NULL);
Приложение, создавшее такой канал, становится сервером.
)Вопрос
Вопрос (
Сервер переходит в режим ожидания запросов клиента с помощью вызова:
ConnectNamedPipe(NP, NULL);
Поскольку вызов функции ConnectNamedPipe() приводит к ожиданию процесса, то лучше ее вызывать в отдельном потоке.
Клиент подключается к каналу с помощью вызова:
NP = CreateFile("\\\\pc_name\\pipe\\mypipe",
GENERIC_READ|GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL);
)Вопрос
Вопрос (
После того, как соединение между сервером и клиентом будет установлено, они могут обмениваться данными с помощью вызовов ReadFile() и WriteFile(), аналогично предыдущему случаю.
Аналогичным же образом (CloseHandle()) канал закрывается.
)Вопрос
Здесь уместно рассказать более подробно об асинхронном вводе-выводе.
Основы асинхронного ввода-вывода
Вопрос (
Существует два варианта ввода-вывода:
Синхронный ввод-вывод, при котором поток, вызвавший эту операцию, блокируется до завершения ввода/вывода.
Асинхронный ввод-вывод, при котором поток вызывает операцию ввода-вывода (ReadFile() или WriteFile()) и сразу же получает управление обратно. Операция ввода-вывода выполняется не зависимо от потока, но когда она завершается, поток узнает об этом с помощью дополнительных средств.
)Вопрос
Вопрос (
Существует три способа асинхронного ввода-вывода.
Синхронный ввод-вывод в отдельном потоке. Самый простой и многими рекомендуемый способ. Программа продолжает выполняться в своем потоке, а ожидание завершения ввода-вывода происходит в дополнительном потоке.
Ввод-вывод с перекрытием. В этом случае к операции ввода-вывода “привязывается” событие. Когда операция ввода-вывода начинается, процесс переходит к ожиданию этого события. При этом ожидание может выполняться без блокировки. Когда операция ввода-вывода завершается, событие взводится и сигнализирует процессу о своем завершении.
Ввод-вывод с процедурой завершения. К операции ввода-“привязывается” специальная процедура, которая вызывается операционной системой, когда операция завершается. В начале работы операционной системе надо передать адрес этой процедуры. Внутри процедуры можно взводить, например, некоторый флаг, установка которого говорит о том, что операция завершилась.
)Вопрос
Про первый вариант говорить не будем, т. к. он использует традиционный вариант вызовов ReadFile() и WriteFile().
Про третий вариант поговорим, когда будем рассматривать протокол NetBIOS.
На данном этапе рассмотрим второй вариант, при этом для конкретности рассмотрим его применительно к именованным каналам.
Первое, что нужно сделать, это создавать объект (в данном случае, именованный канал) с флагом FILE_FLAG_OVERLAPPED.
NP = CreateNamedPipe("\\\\.\\pipe\\mypipe",
PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,...
В операции ввода-вывода последним параметром передается указатель на специальную структуру данных, которая называется структурой перекрытия.
OVERLAPPED OverLap;
Обычно в качестве последнего параметра передается NULL, что указывает на синхронный ввод-вывод.
ReadFile(hPipe, buffer, BUF_SIZE, &NumberOfBytesRead, &OverLap);
Если такая структура передана в вызов, то операция ReadFile() завершается сразу же. Т. е. как бы дается команда “начать ввод-вывод”. За состоянием ввода-вывода можно следить теперь, получая информацию из этой структуры.
Как выглядит структура перекрытия:
typedef struct _OVERLAPPED
DWORD Internal; //резервируется ОС
DWORD InternalHigh; //резервируется ОС
DWORD Offset; //используется при вводе-выводе с дисковыми файлами
DWORD OffsetHigh; //используется при вводе-выводе с дисковыми файлами
HANDLE hEvent; //ссылка на событие
} OVERLAPPED
Для коммуникационных устройств типа COM-портов и именованных каналов большинство параметров не используются.
Для нас важно, что в эту структуру входит событие hEvent.
Событие надо создать вызовом:
OverLap.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
HANDLE CreateEvent(
LPSECURITY_ATTRIBUTES lpEventAttributes, //указатель на атрибуты безопасности
BOOL bManualReset, // флаг события ручного сброса
BOOL bInitialState, // флаг начального состояния
LPCTSTR lpName // указатель на имя объекта-события
);
После выполнения операции ReadFile() необходимо перейти к ожиданию события OverLap.hEvent, сигнализирующего о завершении операции ввода-вывода.
WaitForSingleObject(OverLap.hEvent, TIME_OUT);
DWORD WaitForSingleObject(
HANDLE hHandle, // ссылка на объект ожидания
DWORD dwMilliseconds // интервал тайм-аута в ms
);
Значение параметра TIME_OUT может варьироваться от 0 до INFINITE. При значении 0 функция сразу же завершится.
DWORD result;
while (1) {
result = WaitForSingleObject(OverLap.hEvent, TIME_OUT);
if (WAIT_TIMEOUT == result){
// делать что-то
continue;
}else if (WAIT_OBJECT_0 == result) {
break;
}
}
Когда операция ввода-вывода завершится, процесс выйдет из цикла по break-у, и тогда можно узнать количество реально переданных данных.
Это делается операцией:
DWORD bytesTrans;
GetOverlappedResult(hPipe, &OverLap, &bytesTrans, FALSE);
BOOL GetOverlappedResult(
HANDLE hFile, //ссылка на файл, канал или com-устройство
LPOVERLAPPED lpOverlapped, //указатель на структуру перекрытия
LPDWORD lpNumberOfBytesTransferred, //указатель на число пртнятых байтов
BOOL bWait //флаг ожидания
);
Т.е. в буфере buffer находится bytesTrans вновь полученных байтов.
Пример потока, выполняющего операцию чтения с перекрытием, представлен ниже:
UINT ReadThread(LPVOID pParam)
{
char buffer[IN_BUF_SIZE];
DWORD NumberOfBytesRead;
DWORD bytesTrans;
OVERLAPPED OverLapRd;
memset(&OverLapRd,0,sizeof(OVERLAPPED));
OverLapRd.hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
while (1) {
memset(buffer,0,IN_BUF_SIZE);
if (!ReadFile(hPipe, buffer, IN_BUF_SIZE, &NumberOfBytesRead, &OverLapRd)) {
if (GetLastError() == ERROR_IO_PENDING) {//операция не завершена
WaitForSingleObject(OverLapRd.hEvent, INFINITE);
}else{
AfxMessageBox("ReadFile error!");
return 0;
}
}
GetOverlappedResult(hPipe, &OverLapRd, &bytesTrans, FALSE);
pReadEdit->SetWindowText(buffer);
}
return 0;
}
Вопрос (
Подводя итог, перечислим этапы ввода-вывода с перекрытием:
Создать объект с флагом: FILE_FLAG_OVERLAPPED;
Создать структуру перекрытия: OVERLAPPED OverLap;
Создать событие в этой структуре: OverLap.hEvent = CreateEvent(…);
Вызвать функцию ввода-вывода: ReadFile(…, &OverLap);
Перейти к ожиданию события: WaitForSingleObject(OverLap.hEvent);
При наступлении события узнать количество принятых байтов вызовом GetOverlappedResult()
Вопрос (
В заключении материала об асинхронном вводе-выводе следует отметить сложность реализации такого способа ввода-вывода.
Основы безопасности объектов
Еще одна проблема обременяет использование именованных каналов, а именно, проблема безопасности.
Если создать именованный канал вызовом:
CreateNamedPipe(..., NULL);
Т.е. если последним параметром этой функции передать NULL, то обмен данными между сервером и клиентом, расположенными на разных машинах, производится не будет.
Дело в том, что последний параметр этой функции (как и почти всех функций, начинающихся с префикса “Create”) представляет собой указатель на атрибут безопасности, на структуру данных, которая содержит информацию, позволяющую регламентировать доступ к объекту.
Атрибут безопасности содержит три параметра, важнейшим из которых является указатель на дескриптор безопасности. Именно эта структура и содержит права доступа к объекту.
Дескриптор безопасности содержит информацию о следующих компонентах безопасности объекта:
Идентификатор безопасности владельца (owner sid)
Идентификатор безопасности группы (group sid)
Собственный список доступа (DACL)
Системный список доступа (SACL)
Списки доступа могут быть проинициализированы вызовом, затем в них могут быть добавлены элементы (sid и права (разрешающие и запрещающие)).
Затем список может быть добавлен либо к SACL либо к DACL.
С другой стороны каждый процесс привязан к т. н. маркеру доступа, который описывает контекст безопасности процесса. Система использует этот маркер, когда процесс обращается к объекту со специфицированными атрибутами безопасности.
Система в этом случае ищет SID, содержащийся в маркере доступа, среди SID списков доступа объекта, и действует соответственно правам, привязанным к данному SID, - разрешает заявленную операцию или отказывает в ней.
Чтобы использовать дескриптор безопасности объекта, надо выделить под него память, а затем проинициализировать его. Пример вызова представлен ниже:
PSECURITY_DESCRIPTOR pSD = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR,SECURITY_DESCRIPTOR_MIN_LENGTH);
InitializeSecurityDescriptor(pSD, SECURITY_DESCRIPTOR_REVISION);
Где PSECURITY_DESCRIPTOR pSD; - указатель на дескриптор безопасности.
/************************************************************************************
Затем необходимо найти идентификатор безопасности пользователя, который будет владеть объектом.
Это делается по имени пользователя следующим образом:
LookupAccountName(NULL, buffer, pSid, &cbSid, pDomainName, &cbDomainName, &eUse)
Где buffer содержит имя пользователя;
pSid – это указатель на идентификатор беопасности;
cbSid – размер идентификатора;
pDomainName – домен, в котором находится имя пользователя;
cbDomainName – размер строки, описывающей домен;
eUse – тип идентификатора.
Затем идентификатор безопасности привязывается к дескриптору безопасности. Это делается вызовом:
BOOL SetSecurityDescriptorOwner(
PSECURITY_DESCRIPTOR pSecurityDescriptor, // адрес дескриптора безопасности
PSID pOwner, // адрес SID владельца
BOOL bOwnerDefaulted // флаг умолчания
);
То же самое делается и для группы, владеющей объектом.
Затем создается список контроля доступа:
BOOL InitializeAcl(
PACL pAcl, // адрес списка контроля доступа
DWORD nAclLength, // размер списка контроля доступа
DWORD dwAclRevision // уровень редакции списка контроля доступа
);
и в него вносятся элементы, либо позволяющие операции определенному пользователю (через его sid, найденный функцией LookupAccountName), либо запрещающие какие-либо операции:
BOOL AddAccessAllowedAce(
PACL pAcl, // адрес списка контроля доступа
DWORD dwAceRevision, // уровень редакции списка контроля доступа
DWORD AccessMask, // маска доступа
PSID pSid // адрес идентификатора безопасности
);
BOOL AddAccessDeniedAce(
PACL pAcl, // адрес списка контроля доступа
DWORD dwAceRevision, // уровень редакции списка контроля доступа
DWORD AccessMask, // маска доступа
PSID pSid // адрес идентификатора безопасности
);
На последнем этапе заполненный список заносится в дескриптор безопасности:
BOOL SetSecurityDescriptorDacl(
PSECURITY_DESCRIPTOR pSecurityDescriptor, // адрес дескриптора безопасности
BOOL bDaclPresent, // флаг присутствия собственного списка ACL
PACL pDacl, // адрес собственного списка ACL
BOOL bDaclDefaulted // флаг собственного списка ACL по умолчанию
);
********************************************************************************************/
Если указатель на список контроля доступа будет равен NULL, то будет использован список (NULL discretionary ACL), позволяющий полный доступ к объекту:
SetSecurityDescriptorDacl(pSD, TRUE, (PACL)NULL, FALSE);
Т.е. этапы, отмеченные /*** ... ***/, могут быть опущены, если нет необходимости регламентировать для различных пользователей доступ к объекту.
Дескриптор безопасности вносится в атрибут безопасности, а указатель на атрибут безопасности передается в качестве параметра в функцию создания объекта.
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = pSD;
sa.bInheritHandle = TRUE;
hPipe = CreateNamedPipe ( ..., &sa);
Важно отметить, что отсутствие атрибута безопасности (NULL вместо &sa) и использование NULL-списка контроля доступа приводят к противоположным результатам.
В первом случае доступ полностью отсутствует, а во втором случае предоставляется полный доступ.