Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методическое пособие по СПр.doc
Скачиваний:
7
Добавлен:
16.12.2018
Размер:
690.69 Кб
Скачать

1.4.2 Ход выполнения работы

  1. Ознакомиться с краткими теоретическими сведениями, текстом учебного проекта и материалами к данной лабораторной работе.

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

  3. Разработать клиентское приложение, в котором будет осуществляться прием и отображение данных, получаемых по запросу от сервера.

  4. Организовать обмен данными, возвращаемыми Вашими функциями Windows API, между серверным и клиентским приложениями с использованием технологии сокетов.

1.4.3 Контрольные вопросы к лабораторной работе 4

  1. На каком уровне семиуровневой архитектуры OSI находятся сокеты и между какими другими уровнями? Какие в связи с этим у них преимущества?

  2. Что такое сокет по своей сущности и с точки зрения системного программиста? Какой API сокетов поддерживается в настоящий момент в Windows?

  3. В чем разница между функционированием серверного сокета и клиентского сокета? Возможна ли ситуация асинхронного ввода-вывода при работе с сокетами?

  4. Какое имя надо дать хосту, что иметь возможность отладить межпрограммное взаимодействие на одной машине (дайте четыре варианта).

  5. Какие возможности предоставляет разработчику компоненты Delphi TServerSocket и TClientSocket?

  6. Какие возможности предоставляет разработчику свойство Socket: TServerWinSocket компонента Delphi TServerSocket и свойство Socket: TClientWinSocket компонента Delphi TClientSocket?

  7. В чем смысл задания блокирующего и неблокирующего (асинхронного) типа сокетов? В каком случае необходимо создание отдельного потока для работы с сокетом?

  8. Чем является хост при сетевом обмене данными?

  9. Может ли в рамках одного хоста для сетевого обмена данными между приложениями использоваться несколько портов?

  10. Каким способом можно связать сокеты двух обменивающихся данными приложений при их отладке на одном компьютере?

  11. При реализации технологии обмена с помощью сокетов для чего предназначен класс TServerWinSocket в Delphi.

  12. При реализации технологии обмена с помощью сокетов для чего предназначен класс TServerSocket в Delphi.

  13. Какое наименование у методов классов 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.