- •Содержание
- •2 Вопросы для текущего контроля знаний по темам 70
- •Введение
- •1 Разработка приложений с элементами системного программирования на основе использования технологий межпрограммного обмена данными
- •1.1 Лабораторная работа №1. Обмен данными между потоками с использованием сообщения wm_copydata
- •Краткие теоретические сведения
- •1.1.2 Ход выполнения работы
- •1.1.3 Задание на лабораторную работу 1
- •Варианты индивидуального задания
- •1.1.4 Контрольные вопросы к лабораторной работе 1
- •1.2 Лабораторная работа №2. Обмен данными между приложениями с использованием технологии динамического обмена данными – Dynamic Data Exchange (dde)
- •1.2.1 Краткие теоретические сведения
- •Глобальные данные
- •Посылка и прием данных
- •Завершение сеанса
- •Компонент tddeServerConv
- •Компонент tddeServerItem
- •Компонент tddeClientItem
- •1.2.2. Ход выполнения работы
- •1.2.3 Задание на лабораторную работу 2
- •1.2.4 Контрольные вопросы к лабораторной работе 2
- •1.3 Лабораторная работа №3. Обмен данными между приложениями с использованием технологии динамического обмена данными с помощью файлов, отображаемых в память
- •1.3.1 Краткие теоретические сведения Создание объекта файлового отображения
- •Совместное использование отображаемых данных
- •1.3.2 Ход выполнения работы
- •1.3.3 Задание на лабораторную работу 3
- •1.3.4 Контрольные вопросы к лабораторной работе 3
- •1.4 Лабораторная работа №4. Обмен данными между приложениями с использованием технологии динамического обмена данными с помощью сокетов
- •1.4.1 Краткие теоретические сведения Сокеты
- •1.4.2 Ход выполнения работы
- •1.4.3 Контрольные вопросы к лабораторной работе 4
- •1.5 Лабораторная работа №5. Обмен данными между приложениями с использованием технологии динамического обмена данными с помощью именованных каналов
- •1.5.1 Краткие теоретические сведения Каналы
- •1.5.2 Ход выполнения работы
- •1.5.3 Контрольные вопросы к лабораторной работе 5
- •1.6 Лабораторная работа №6. Использование технологии сом при разработке приложений в Delphi. Создание и использование внутреннего сервера
- •1.6.1 Краткие теоретические сведения
- •1.6.2 Ход выполнения работы
- •1.6.3 Контрольные вопросы к лабораторной работе 6
- •2 Вопросы для текущего контроля знаний по темам
- •Список литературы
- •107/2009. Підп. До друку 25.12.09. Формат 60 х 84/8.
- •84313, М. Краматорськ, вул. Шкадінова, 72.
1.4.2 Ход выполнения работы
-
Ознакомиться с краткими теоретическими сведениями, текстом учебного проекта и материалами к данной лабораторной работе.
-
Разработать серверное приложение, в котором осуществляется вызов изученных в соответствии с индивидуальным заданием к первой лабораторной работе функций, обработка и представление в понятном для пользователя виде возвращаемых ними результатов или достигаемых с их помощью эффектов (изменений режимов работы объектов операционной системы или пользовательского интерфейса).
-
Разработать клиентское приложение, в котором будет осуществляться прием и отображение данных, получаемых по запросу от сервера.
-
Организовать обмен данными, возвращаемыми Вашими функциями Windows API, между серверным и клиентским приложениями с использованием технологии сокетов.
1.4.3 Контрольные вопросы к лабораторной работе 4
-
На каком уровне семиуровневой архитектуры OSI находятся сокеты и между какими другими уровнями? Какие в связи с этим у них преимущества?
-
Что такое сокет по своей сущности и с точки зрения системного программиста? Какой API сокетов поддерживается в настоящий момент в Windows?
-
В чем разница между функционированием серверного сокета и клиентского сокета? Возможна ли ситуация асинхронного ввода-вывода при работе с сокетами?
-
Какое имя надо дать хосту, что иметь возможность отладить межпрограммное взаимодействие на одной машине (дайте четыре варианта).
-
Какие возможности предоставляет разработчику компоненты Delphi TServerSocket и TClientSocket?
-
Какие возможности предоставляет разработчику свойство Socket: TServerWinSocket компонента Delphi TServerSocket и свойство Socket: TClientWinSocket компонента Delphi TClientSocket?
-
В чем смысл задания блокирующего и неблокирующего (асинхронного) типа сокетов? В каком случае необходимо создание отдельного потока для работы с сокетом?
-
Чем является хост при сетевом обмене данными?
-
Может ли в рамках одного хоста для сетевого обмена данными между приложениями использоваться несколько портов?
-
Каким способом можно связать сокеты двух обменивающихся данными приложений при их отладке на одном компьютере?
-
При реализации технологии обмена с помощью сокетов для чего предназначен класс TServerWinSocket в Delphi.
-
При реализации технологии обмена с помощью сокетов для чего предназначен класс TServerSocket в Delphi.
-
Какое наименование у методов классов Delphi, обеспечивающих запись в сокет и чтение из него?
1.5 Лабораторная работа №5. Обмен данными между приложениями с использованием технологии динамического обмена данными с помощью именованных каналов
Цель работы: приобрести навыки создания клиентских и пользовательских приложений, осуществляющих обмен различными данными между параллельно работающими приложениями при помощи технологии именованных каналов.
1.5.1 Краткие теоретические сведения Каналы
Каналы бывают двух видов — именованные (named pipes) и безымянные (anonymous pipes). Вторые не предназначены для связи между самостоятельными приложениями и представляют собой, так сказать, рудимент Win32 API.
Каналы рассматривались Microsoft как протокол для организации клиент-серверных приложений. Поэтому их серверная часть реализована только в среде Windows NT и выше и не поддерживается в Windows 95/98; клиенты могут быть созданы во всех этих ОС. Каналы - один из основных протоколов работы такого продукта, как Microsoft SQL Server.
Канал можно представить себе как среду, через которую могут обмениваться данными два приложения. Обмен данными может быть как односторонним, так и двухсторонним. Тем не менее, одно из приложений играет роль сервера (оно создает канал), другое (или другие) лишь подключается к нему. Противоречия с вышесказанной фразой о двух приложениях здесь нет: на сервере канал виден как ресурс с единственным уникальным именем. Когда к серверу подключается очередной клиент, для него создается уникальный экземпляр канала со своими дескриптором, буфером и т. п. Но имя канала для всех клиентов одинаково.
Имя канала записывается в соответствии с так называемым соглашением UNC (Universal Naming Convention). Выглядеть оно должно так: \\<servername>\pipe\<pipename>
где servername — сетевое имя компьютера — сервера, pipename — имя канала. В случае, если клиенту нужно подключиться к серверу на том же компьютере, его сетевое имя заменяется точкой: \\.\pipe\<pipename>.
Максимальная длина имени канала ограничена 256 символами; строчные и прописные буквы не отличаются.
Для программиста алгоритмы создания серверного и клиентского "концов" канала отличаются. Для создания сервера применяются специальные функции Win 32 API; клиентский же конец открывается как обычный файл и работа с ним также напоминает работу с файлом. В дальнейшем эти функции будут инкапсулированы в класс Delphi — потомок THandleStream, ближайшего родственника файлового потока TFileStream. Пока же рассмотрим необходимые функции API. Главная из них для нас следующая:
function CreateNamedPipe (lpName: PChar;
dwOpenMode, dwPipeMode, nMaxInstances, nOutBufferSize,
nInBufferSize, nDefaultTimeOut: DWORD;
lpSecurityAttributes: PSecurityAttributes): THandle;
Она создает серверный конец канала с именем lpName. Остальные параметры перечислены в таблице.
Таблица 1 – Параметры функции CreateNamedPipe
Параметр |
Назначение |
dwOpenMode |
Режим открытия. Флаги: PIPE_ACCESS_DUPLEX — двунаправленный обмен данными PIPE_ACCESS_INBOUND — только от клиента к серверу PIPE_ACCESS_OUTBOUND — только от сервера к клиенту FILE_FLAG_WRITE_THROUGH — запись данных, минуя кэш FILE_FLAG_OVERLAPPED — режим отложенной операции ввода/вывода |
dwPipeMode |
Режим работы канала. Флаги: PIPE_TYPE_BYTE — запись в режиме потока байт PIPE_TYPE_MESSAGE — запись в режиме потока сообщений PIPE_READMODE_BYTE — чтение в режиме потока байт PIPE_READMODE_MESSAGE — чтение в режиме потока сообщений PIPE_WAIT — функции ввода/вывода не будут возвращать управление до завершения операции PIPE_NOWAIT — функции ввода/вывода возвращают управление немедленно |
nMaxInstances |
Максимальное количество открываемых экземпляров канала, от 1 до PIPE_UNLIMITED_INSTANCES |
nOutBufferSize |
Размер буфера для записи |
nInBufferSize |
Размер буфера для чтения |
nDefaultTimeOut |
Задает время ожидания конца операции ввода/вывода в канале (в мс) |
lpSecurityAttributes |
Указатель на структуру Windows NT, содержащую информацию о правах доступа к каналу |
Режимы потока байт и потока сообщений не слишком отличаются друг от друга — в первом случае система смешивает данные от различных операций чтения/записи в единый поток, во втором — разделяет их на отдельные порции. Канал в режиме PIPE_TYPE_BYTE может работать на чтение только в режиме чтения PIPE_READMODE_BYTE, канал в режиме PIPE_TYPE_MESSAGE — в обоих режимах чтения.
Функция:
function ConnectNamedPipe(hNamedPipe: THandle; lpOverlapped: POverlapped): BOOL;
позволяет подключиться к уже созданному каналу hNamedPipe, а функция:
function DisconnectNamedPipe(hNamedPipe: THandle): BOOL;
позволяет отключить клиента — она разрывает связь клиентского и серверного концов канала hNamedPipe.
Чтобы собрать информацию о состоянии канала hNamedPipe, нужно вызвать функцию:
function GetNamedPipeInfо(hNamedPipe: THandle; var lpFlags: DWORD; lpOutBufferSize, lpInBufferSize, lpMaxInstances: Pointer): BOOL;
Указатели lpOutBufferSize, lpInBufferSize, lpMaxInstances должны указывать на переменные, куда будут записаны размеры буферов и число открытых экземпляров канала. Параметр lpFlags указывает на переменную, в которую будут записаны флаги состояния канала. Среди них уже знакомый флаг PIPE_TYPE_MESSAGE, а также флаг PIPE_SERVER_END. Он установлен, если hNamedPipe — серверный конец канала.
Функция:
function PeekNamedPipe(hNamedPipe: THandle; lpBuffer: Pointer; nBufferSize: DWORD; lpBytesRead, lpTotalBytesAvail,
lpBytesLeftThisMessage: Pointer): BOOL;
позволяет прочитать данные из буфера канала, оставив их там (доступными для последующего чтения). Кроме того, она возвращает дополнительную полезную информацию о состоянии канала, в частности по адресу, указанному в параметре lpTotalBytesAvail, будет записано число еще не прочитанных байт в канале.
Функция:
function WaitNamedPipe(lpNamedPipeName: PChar; nTimeOut: DWORD): BOOL;
позволяет организовать ожидание окончания операции в канале. Параметр nTimeOut задает время ожидания в миллисекундах; возможны еще два особых значения — NMPWAIT_USE_DEFAULT_WAIT (ожидать в течение времени, указанного при создании канала) и NMPWAIT_WAIT_FOREVER (ждать бесконечно). Осталось добавить, что чтение и запись в канал осуществляется так же, как и в обычный файл — функциями ReadFile и WriteFile, после чего можно перейти к рассмотрению класса TServerPipestream, соответствующего серверному концу канала.
Классы, инкапсулирующие функции для работы с каналами
Реализация класса TserverPipestream
const BUF_SIZE = 65535;
TIME_OUT = 15000;
Security_Descriptor_REVISION = 1;
type
TServerPipestream = class(THandleStream)
private
FHandle : THandle; Overlap : TOverlapped;
public
constructor Create(Name:String;OpenMode:Longint);
destructor Destroy;
function Read(var Buffer; Count: Longint): Longint; override;
function Write(const Buffer; Count: Longint): Longint; override;
function Connect(Timeout:Integer): boolean;
end;
constructor TServerPipestream.Create;
var sd : TSecurityDescriptor;
sa : TSecurityAttributes;
pn : array[0..MAX_PATH-l] of char;
begin
if not InitializeSecurityDescriptor(@sd,
Security_Descriptor_REVISION) or
not SetSecurityDescriptorDACL(@sd, TRUE, pAcl(nil), False) then
raise ENamedPipeError.CreateFmt(' Cannot set pipe security: %d',[GetLastError]);
sa.nLength := SizeOf(sa);
sa.lpSecurityDescriptor := @sd;
sa.bInheritHandle := True;
FillChar (Overlap, 0, SizeOf(TOverlapped));
Overlap.hEvent := CreateEvent (nil, True, FALSE, nil);
StrFmt(pn,'\\.\PIPE\%s', [Name]);
FHandle := CreateNamedPipe(pn, OpenMode or FILE_FLAG_OVERLAPPED,
PIPE_WAIT or PIPE_READMODE_MESSAGE or PIPE_TYPE_MESSAGE, PIPE_UNLIMITED_INSTANCES, BUF_SIZE, BUF_SIZE, TIME_OUT, @sa);
if (FHandle = INVALID_HANDLE_VALUE) or (FHandle = 0) then
raise ENamedPipeError.CreateFmt('Create Pipe Error %d',
[GetLastError]);
end;
destructor TServerPipeStream.Destroy;
begin
FlushFileBuffers(FHandle) ;
DisconnectNamedPipe(FHandle) ;
CloseHandle(Overlap.hEvent) ;
CloseHandle(FHandle);
end;
function TServerPipeStream.Read(var Buffer; Count: Longint): Longint;
var LastError : Longint;NB : Cardinal;
begin
if not ReadFile(FHandle, Buffer, Count, NB, @OverLap) then
begin
LastError := GetlastError;
case LastError of
ERROR_IO_PENDING: WaitForSingleObject(OverLap.hEvent, INFINITE);
ERROR_BROKEN_PIPE: raise EPipeBroken.Create('Pipe broken') else
raise ENamedPipeError.CreateFmt(' Read failed, error %d', [LastError]);
end; {case}
end; {not read file}
GetOverlappedResult(FHandle, Overlap, nb, FALSE); Result := nb; end;
function TServerPipeStream.Write(const Buffer; Count: Longint): Longint; var LastError : Longint;NB : Cardinal;
begin
if not WriteFile(FHandle, Buffer, Count, NB, @Overlap) then
begin
LastError:=GetLastError;
if (lastError = ERROR_IO_PENDING) then
WaitForSingleObject (Overlap.hEvent, INFINITE)
else
if (LastError <> ERROR_NO_DATA) then
raise ENamedPipeError.CreateFmt(' Sending data
error %d', [LastError]);
end; (not write to file)
GetOverlappedResult(FHandle, Overlap, NB, FALSE); Result := nb;
end;
function TServerPipeStream.Connect;
var LastError : Longint;
begin
Result := ConnectNamedPipe(FHandle,@Overlap);
if not result then
begin
LastError := GetLastError;
case LastError of
ERROR_PIPE_CONNECTED: Result := true;
ERROR_IO_PENDING: begin
LastError := WaitForSingleObject(Overlap.hEvent,
TimeOut);
if LastError = WAIT_OBJECT_0 then Result := True;
end;
end; (case)
end;
end;
Этот пример может послужить также иллюстрацией использования отложенного (overlapped) ввода/вывода. Для этого в конструкторе инициализируется дескриптор объекта типа события (функция CreateEvent) и инициализируется структура TOverlapped.
Функции ReadFile, WriteFile и многие другие поддерживают отложенный ввод/вывод. Они начинают операцию (ее выполняет отдельный программный поток, скрытый от программиста) и немедленно возвращают управление. Признак того, что операция началась и продолжается — возврат кода возврата ERROR_IO_PENDING. Пусть вас не пугает слово "error" в названии — это совершенно нормально. Если операция продолжается долго (а чтение и запись именованных каналов можно отнести к "длинным" операциям), то программа может спокойно выполнять последующие операторы. Когда, по мнению программиста, ввод/вывод должен быть завершен, можно использовать функцию WaitForSingleObject. Объект ожидания в этом случае — тот самый, который создан нами, указан в структуре TOverlapped и передан в качестве параметра в функцию ReadFile или WriteFile. Можно указать любое время ожидания, в том числе бесконечное (параметр Timeout при этом равен константе infinite). Признаком нормального завершения служит получение кода возврата WAIT_OBJECT_0.
Обратите внимание на инициализацию дескриптора доступа (TSecurityDescriptor). Многие, имевшие дело с функциями Win32 API, которые ссылаются на дескриптор доступа, привыкли, что этот параметр равен нулю. В таком случае создаваемый ресурс по умолчанию имеет те же права, что и создавший его процесс, и этого обычно достаточно. Но в данном случае канал создается на сервере, а обращаются к нему с клиентской машины. Если канал создал администратор, то обычный пользователь доступа к нему не получит. Поэтому в данном случае дескриптор явно описан как общедоступный.
По завершении сеанса на серверном конце нужно очистить буферы (функция FlushFileBuffers), отсоединить клиента (DisconnectNamedPipe) и закрыть дескриптор канала, созданного CreateNamedPipe при помощи функции CloseHandle.
Клиентская часть реализуется примерно так же. Но, в отличие от серверной, подключение к экземпляру канала происходит при помощи широко распространенной и универсальной функции CreateFile.
constructor TClientPipeStream.Create(ShareName: String;
AccessMode, ShareMode: integer);
var sn : array[0..MAX_PATH-1] of char;
LastError : Longint;
begin
FHandle := CreateFile( pChar(ShareName),
AccessMode, ShareMode, nil, {security}
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, 0);
if FHandle = INVALID_HANDLE_VALUE then
begin
LastError := GetLastError;
if (LastError = ERROR_SEEK_ON_DEVICE) or (LastError = ERROR_FILE_NOT_FOUND) then
raise ENamedPipeError.Create('Pipe not Found') else
raise ENamedPipeError.Create(Format('Pipe opening Error %d',[LastError]) ) ;
end;
FillChar(OverlapRd, SizeOf(OverlapRd), 0);
FillChar(OverlapWrt, SizeOf(OverlapWrt), 0);
OverlapRd.hEvent := CreateEvent(nil, true, False, nil);
OverlapWrt.hEvent := CreateEvent(nil, true, False, nil) ;
end;
Методы, осуществляющие чтение и запись, полностью схожи с применяемыми на серверном конце и здесь не приводятся.
Если общение с сервером планируется по схеме "запрос-ответ", то удобной для организации работы клиента представляется еще одна функция API:
function TransactNamedPipe(hNamedPipe: THandle; lpInBuffer: Pointer; nInBufferSize: DWORD; lpOutBuffer: Pointer;
nOutBufferSize: DWORD; var lpBytesRead: DWORD; lpOverlapped: POverlapped): BOOL;
Она объединяет "в одном флаконе" сразу и чтение и запись, в том числе и отложенную. Смысл ее параметров предельно ясен: для запроса предоставляется буфер lpInBuffer длиной nInBufferSize байт, а для ответа — lpOutBuffer длиной nOutBufferSize байт. Число фактически прочитанных байт возвращается в параметре lpBytesRead.
В примере учебного проекта, прилагаемом к данной лабораторной работе, имеется вариант реализации клиент/серверной связки на базе каналов. Она усложнена по сравнению с рассмотренными выше вариантами, и вот почему. Для полноценной многозадачности каждым экземпляром канала нужно управлять в отдельном потоке, причем один созданный серверный конец все время должен находиться в состоянии готовности к установлению связи.
Реализовано это на базе класса TServerThread, соответствующего программному потоку. Первый поток создается вместе и одновременно с главным приложением и "слушает эфир". Как только в канал записаны данные, это означает, что кто-то подключился со стороны клиента. Поток переключается на обмен данными с этим клиентом, но при этом запускает новый поток, готовый к общению с новым клиентом, и так далее. Поток выполняется до тех пор, пока либо не будет разорван канал (по инициативе клиента), либо не поступит сигнал завершения от породившего процесса (свойство потока Terminated указывает, что "пора закругляться").
Реализация потока TServerThread
TCliRec = class
Name: string;
DataPipe: TServerPipeStream;
class procedure SyncClients(Sender: TObject);
end;
var ClientsList : TStringList;
class procedure TCliRec.SyncClients(Sender: TObject);
begin
MainForm.ClientsListBox.Items := ClientsList;
end;
procedure RemoveClient(Client: TCliRec);
var i: Integer;
begin
with ClientsList do begin
i := IndexOfObject(Client);
if i <> -1 then
Delete(i);
Client.DataPipe.Free;
Client.Free;
end;
end;
procedure TServerThread.Execute;
var InBuf: array [0..1023] of char;
i, BytesRead : Integer;
ThisClient : TCliRec;
begin
FreeOnTerminate := True;
ThisClient := TCliRec.Create;
ThisClient.DataPipe := TServerPipeStream. Create ('data' ,PIPE_ACCESS_DUPLEX);
if not ThisClient.DataPipe.Connect(INFINITE) then
begin
ShowMessage('Pipe connect error');
Exit;
end;
try
repeat BytesRead := ThisClient.DataPipe.Read(inBuf, SizeOf(InBuf)-1);
if BytesReadoO then begin
InBuf[BytesRead] := #0;
HandleRequest(ThisClient,InBuf);
end;
until Terminated;
except
on EPipeBroken do begin
ShowMessage(Format ('Pipe with %s is
broken',[ThisClient.Name])]
end;
on E:ENamedPipeError do begin
ShowMessage(Format('Pipe with %s error: #13#10'%s',
[ThisClient.Name,E.Message])) ;
end;
end;
RemoveClient(ThisClient);
end;
procedure TMainForm.FormCreate(Sender: TObject);
begin
TServerThread.Create(False);
end;
initialization
ClientsList := TStringList.Create;
(ClientsList as TStringList).OnChange := TCliRec.SyncClients;
finalization
ClientsList.Free;
end.
В цикле обработки данных, поступивших от клиента, упоминается, но не описана функция HandleRequest. Она собственно и должна распознавать и обрабатывать поступающие от клиента сообщения. Вы можете приспособить ее для своих нужд. Единственное, что нужно иметь в виду: в ответ на самое первое сообщение нужно обязательно породить новый поток. Такой пример имеется в дополнительных справочных материалах.
Каналы хорошо приспособлены для обмена данными в сети. Но все же их применение сдерживается наличием необходимых протоколов и поддержкой UNC.