(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

Как научить программу общаться

Источник: welikeit

Предисловие
В этой статье я расскажу о том, как мне пришлось решать достаточно простую, но вместе с тем интересную задачу. Смысл её заключался в том, чтобы обеспечить передачу файла по локальной сети на несколько машин по списку. При этом на всех компьютерах файл может располагаться в различных каталогах и пользователь компьютера, принимающего файл, не должен никоим образом участвовать в процессе. Практически стояла задача обеспечить автоматизированную рассылку обновления софта и антивирусных баз.
Решать задачу пришлось в спешке, поэтому был избран самый простой путь - использовать "троянообразный" сервер на машинах пользователей и клиент с простенькой базой данных, который по очереди соединяется с серверами и выполняет необходимые действия.
Результатом работы стало реальное клиент-серверное приложение, программа "Админ-рассылка". В качестве платформы для разработки был использован Borland Delphi 7, база данных - локальная с использованием библиотеки firebird - gds32.dll.

Часть 1: Троян на службе сисадмина.
Как уже отмечалось, пользователь в результате работы программы должен принимать файл, не подозревая об этом. Следовательно, на его машине должно скрытно выполняться приложение-сервер, открывающее определённый порт для приёма. То есть, придется в наших (исключительно мирных) целях использовать трояна.
Описание технологии построения троянского приложения я пропущу, так как это подробно описано на www.miterx.users.kemcity.ru. Отмечу лишь то, что я добавил от себя. В статье, о которой я упоминал, троян тихий и послушный: он всего лишь выполняет команду. Мне же пришлось учить серверную и клиентскую часть приложения полноценному общению.
В своей работе для связи приложений я использовал компоненты ServerSocket и ClientSocket, которые реализуют асинхронный обмен данными через порт. Не слишком углубляясь в теорию протоколов, обрисую этот процесс таким образом. Приложения посылают пакет данных, и продолжают работать, независимо от того, получен их пакет или нет. При этом, даже если пакет был принят благополучно (а в нормально работающей сети это бывает ;)) нельзя быть уверенным, КОГДА его приняли. Такая особенность обмена данными требует подтверждения готовности к приёму сообщений, вроде того, как принято общаться по рации. Но обо всём по порядку.
В серверной части на форму кладём компонент ServerSocket и прописываем ему порт, который он будет слушать. Свойство Active ставим false. Обрабатываем события:

procedure TForm1.ServerSocket1ClientConnect(Sender: TObject; Socket: TCustomWinSocket); begin ServerSocket1.Socket.Connections[0].SendText(version);
old:=false;
end;


(При подключении клиента сервер шлёт ему свою версию и устанавливает глобальную переменную old:=false) Версию нужно знать для того, чтобы можно было организовать обновление серверной части на машинах пользователей. Переменная old в дальнейшем будет использоваться, чтобы решить, как относиться к файлу с тем же именем, если он уже лежит в месте назначения. Если old = false, то файл заменяем, если true, значит дописываем.

procedure TForm1.FormCreate(Sender: TObject);
begin
regwrite (Application.ExeName); // Простенькая процедура работы с реестром моего производства. ServerSocket1.Active:=true;
rez:=\'диалог\';
end;


(При создании формы приложения прописываемся в автозапуске, открываем порт и устанавливаем глобальную переменную rez). Об этой переменной расскажу подробнее. Она определяет режим, в котором работает наш сервер. В зависимости от заранее установленного режима сервер по разному относится к порции принимаемых данных. В режиме "диалог" данные - это команды, в режиме "файл" - порция данных файла и т.п. Теперь самое главное событие - получение данных от клиента. Рассмотрим эту процедуру по частям в зависимости от режима:

procedure TForm1.ServerSocket1ClientRead(Sender: TObject; Socket: TCustomWinSocket); .... if rez=\'диалог\' then begin
clTMsg:=ServerSocket1.Socket.Connections[0].ReceiveText;
if not (clTMsg=\'\') then clMsg:=StrToInt(clTMsg);
case clMsg of
1: begin
rez:=\'путь\';
ServerSocket1.Socket.Connections[0].SendText(\'1\');
end;
3: begin
rez:=\'размер\';
ServerSocket1.Socket.Connections[0].SendText(\'3\');
end;
5: begin
rez:=\'файл\';
ServerSocket1.Socket.Connections[0].SendText(\'5\');
end;
end;
end;
end;
....
end;


Эта часть процедуры выполняется тогда, когда сервер работает в режиме диалога. В строковую переменную clTMsg заносится строка, полученная от клиента. Если строка не пустая, она превращается в число (я использовал числовые команды для общения клиента с сервером). Затем сервер переключается в требуемый режим и отвечает клиенту.

....
if rez=\'путь\' then begin
path:=ServerSocket1.Socket.Connections[0].ReceiveText;
ServerSocket1.Socket.Connections[0].SendText(\'2\');
if not DirectoryExists (ExtractFilePath(path)) then ForceDirectories(ExtractFilePath(path));
rez:=\'диалог\';
end;
....


В режиме "путь" сервер присваивает полученную от клиента строку переменной path, затем отвечает клиенту, что путь получен успешно. Далее выполняется проверка на наличие каталога, в который нужно будет положить файл, и если каталога нет, он тут же создаётся. И, наконец, сервер переключает себя в режим диалога.

....
if rez=\'размер\' then begin
sz:=StrToInt(ServerSocket1.Socket.Connections[0].ReceiveText);
ServerSocket1.Socket.Connections[0].SendText(\'4\');
rez:=\'диалог\';
end;
....


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

....
if rez=\'файл\' then begin
lr:=0;
while (lr < sz) do begin
l:=Socket.ReceiveLength;
GetMem(buf,l+1);
Socket.ReceiveBuf(buf^,l);
try
if (FileExists(path) and old) then begin
src:=TFileStream.Create(path,fmOpenReadWrite);
src.Seek(0,soFromEnd);
end
else begin
src:=TFileStream.Create(path,fmCreate);
old:=true;
end;
src.WriteBuffer(buf^,l);
lr:=lr+l;
except
FreeMem(buf);
src.Free;
sz:=0;
rez:=\'диалог\';
ServerSocket1.Socket.Connections[0].SendText(\'9\');
end;
FreeMem(buf);
src.Free;
end;
sz:=0;
ServerSocket1.Socket.Connections[0].SendText(\'6\');
rez:=\'диалог\';
end;
....

В режиме "файл" принимаемые сервером данные рассматриваются как части файла. Они через буфер дописываются к файлу, до тех пор, пока суммарная длина принятых данных не сравняется с ранее установленной длиной файла. Практически это реализовано следующим образом:
- переменной lr, которая хранит размер уже принятых данных, присваивается нулевое значение.
- выполняем цикл до тех пор пока lr меньше размера файла
- занимаем память под буфер, для чего получаем длину текущей порции данных (переменная l)
- читаем порцию данных из сокета в буфер (переменная buf)
Дальше пытаемся выполнить запись:
- если файл уже имеется и получаемый файл не должен его заменять (вот и пригодилась глобальная переменная old) то открываем файл для записи и ставим указатель в конец файла
- если файла ещё нет или его нужно переписать (old=false) создаём файл
- так или иначе, в полученный файловый поток пишем содержимое буфера, прибавляем размер текущего блока данных к общей длине принятого файла и повторяем цикл со следующей порцией данных.
Если запись не удалась (блок except), освобождаем память, переключаемся в режим диалога и отправляем клиенту сообщение "9" ("найн" то есть ничего не вышло;))
Если всё прошло успешно, говорим клиенту "6".
Вот такой общительный троян получился! Я не стал описывать второстепенные вещи, типа обработки выхода, разрыва соединения и т.п. Вместо этого лучше давайте рассмотрим второго участника "сокетной беседы" - программу-клиента.

Часть 2: Файловый почтальон
Данные о пользователях, которые должны получить файл я решил хранить в базе данных. База состоит из единственной таблицы USERS с полями, в которых хранится по порядку ip-адрес, имя хоста, версия сервера, локальный путь на машине пользователя, поле "отправка" и примечание. О том, как подключать локальную базу к программе писать не буду: это легко найти в других источниках. Базу я создавал с помощью прекрасной утилиты IBExpert, используя библиотеку gds32.dll из инсталляции firebird, для работы с полученной базой использовались компоненты из вкладки interbase. Такая схема не требует установки на компьютере сервера баз данных, что, согласитесь, очень удобно. База данных заполняется перед началом работы программы вручную. В поле "отправка" 1 означает, что файл успешно передан, 0 - требуется передать файл.
Первую в работе кнопочку "Выбрать файл для рассылки" первой и обрабатываем:

procedure TForm1.SelectFileButtonClick(Sender: TObject);
begin
if OpenDialog1.Execute then begin
filename := OpenDialog1.FileName;
SendButton.Enabled:=true;
StatusBar1.Panels.Items[0].Text:=\'Отправляем файл \'+filename;
end;
end;


Из OpenDialog-а получаем в переменную filename полное имя файла, который нужно отправлять. Включаем (выключенную по умолчанию) кнопку "Выполнить рассылку" и обеспечиваем интерфейс с пользователем ;) выводя в StatusBar строку с именем отправляемого файла.
Кнопочки "Все отправлять" и "Все не отправлять" в принципе обрабатываем одинаково:

procedure TForm1.Oll_0_ButtonClick(Sender: TObject);
begin
IBTable1.First;
while not IBTable1.EOF do begin
IBTable1.Edit;
IBTable1.FieldByName(\'SEND\').AsInteger:=0;
IBTable1.Post;
IBTable1.Next;
end;
end;
Проходим по таблице и в поле "отправка" проставляем 0 или 1 в зависимости от нажатой кнопки.
Кнопка "Установить пути по умолчанию" тоже не блещет интеллектом: Также происходит проход по всем записям таблицы и редактирование поля с путём размещения файла. Только строка с путём берётся из InputBox-а. Понятно, что если в вашей сети три компьютера, эта кнопка бесполезна. Но если их, как у меня, 140…
Итак, теперь, отбрасывая всё незначительное, мы вплотную приближаемся к "главной кнопке".
Её код:

procedure TForm1.SendButtonClick(Sender: TObject);
begin
IBTable1.First;
rez:=\'диалог\';
NextServ();
end;


Становимся в начало таблицы, устанавливаем уже знакомую нам переменную rez, и...

procedure NextServ();
label nx;
begin
srcfile := TFileStream.Create(filename,fmOpenRead);
nx:
if Form1.IBTable1.FieldByName(\'SEND\').AsInteger=0 then begin
Form1.ClientSocket1.Address:=Form1.IBTable1.FieldByName(\'IP\').AsString;
path:=Form1.IBTable1.FieldByName(\'PATH\').AsString;
Form1.StatusBar1.Panels.Items[0].Text:=\'Обрабатываем сервер \' + Form1.ClientSocket1.Address;
Form1.ClientSocket1.Open;
Form1.ClientSocket1.Socket.SendText(\'0\');
rez:=\'версия\';
end
else begin
Form1.IBTable1.Next;
if not Form1.IBTable1.Eof then goto nx;
end;
end;


Да, я знаю, что использовать goto это дурной тон. Но если надо быстро, и вообще…
Итак: читаем файл в поток, затем если в поле "отправка" стоит 0, устанавливаем сокету ip из базы, читаем в переменную path путь из базы, пишем в StatusBar, какой сервер в данный момент обрабатываем, подключаемся и шлём нашему дорогому троянчику нолик. Мы уже знаем, что в ответ на наш запрос сервер пошлёт свою версию, поэтому и переходим в соответствующий режим. Если сервер обработан, идём дальше.
Тут я хочу обратить внимание на одну пикантную особенность. В этой процедуре мы только запускаем обмен данными с серверами, а сам обмен будет реализован совсем в другом месте.
Вот в этом:

procedure TForm1.ClientSocket1Read(Sender: TObject; Socket: TCustomWinSocket); begin if rez=\'диалог\' then begin
servMsg:=StrToInt(ClientSocket1.Socket.ReceiveText);
case servMsg of
1: begin
StatusBar1.Panels.Items[0].Text:=ClientSocket1.Address+\' готов принимать путь...\';
sleep(800);
ClientSocket1.Socket.SendText(path+ \'\\' +ExtractFileName(filename));
end;
2: begin
StatusBar1.Panels.Items[0].Text:=ClientSocket1.Address+ \' путь получил!\';
sleep(800);
ClientSocket1.Socket.SendText(\'3\');
end;
3: begin
StatusBar1.Panels.Items[0].Text:=ClientSocket1.Address+\' готов принимать размер...\';
sleep(800);
ClientSocket1.Socket.SendText(IntToStr(srcfile.Size));
end;
4: begin
StatusBar1.Panels.Items[0].Text:=ClientSocket1.Address+ \' размер получил!\';
sleep(800);
ClientSocket1.Socket.SendText(\'5\');
end;
5: begin
StatusBar1.Panels.Items[0].Text:=ClientSocket1.Address+ \' готов принимать файл...\';
sleep(800);
ClientSocket1.Socket.SendStream(srcfile);
end;
6: begin
StatusBar1.Panels.Items[0].Text:=ClientSocket1.Address+ \' файл получил!\';
sleep(800);
ClientSocket1.Active:=false;
IBTable1.Edit;
IBTable1.FieldByName(\'SEND\').AsInteger:=1;
IBTable1.Post;
IBTable1.Next;
NextServ();
end;
9: begin
StatusBar1.Panels.Items[0].Text:=ClientSocket1.Address+ \' сообщил об ошибке!\';
sleep(800);
ClientSocket1.Active:=false;
IBTable1.Edit;
IBTable1.FieldByName(\'SEND\').AsInteger:=9;
IBTable1.Post;
IBTable1.Next;
NextServ();
end;
end;
end;
....

Это обработка получения сообщения от сервера в случае, если клиент находится в режиме диалога. Как и в случае с трояном, разбираем сообщение через case, пишем в StatusBar перевод сообщения на человеческий язык, и если сообщение означало готовность сервера что-то принять, тут же ему это и отправляем. Если сервер своим сообщением подтверждает получение, мы не даём ему расслабляться, и шлём команду приготовиться (т.е. перейти в соответствующий режим) к приёму следующей информации. Из листинга вполне понятно, что за чем отправляем. А sleep(800) нужно, как вы уже, наверное догадались, для того, чтобы пользователь успевал читать в StatusBar-е.
Особое внимание уделяется двум сообщениям сервера: 6 и 9. При получении шестёрки отключаемся от сервера, пишем в базу единичку (типа файл отправлен), и запускаем уже знакомую нам процедуру NextServ уже для следующего сервера в списке. Почти также реагируем на девятку, только в базе возникнет не благополучная "1" а тревожная "9", сообщающая о том, что с сервером что-то не то.
Есть у нас ещё один режим. В нём мы принимаем версию сервера и заносим её в базу:

....
if rez=\'версия\' then begin
serVer:=ClientSocket1.Socket.ReceiveText;
StatusBar1.Panels.Items[0].Text:=\'Получен ответ от \'+ClientSocket1.Address+\'; версия сервера \'+ serVer;
IBTable1.Edit;
IBTable1.FieldByName(\'VERSION\').AsString:=serVer;
IBTable1.Post;
sleep(800);
ClientSocket1.Socket.SendText(\'1\');
rez:=\'диалог\';
end;


Тут, как можно без труда догадаться, переменная serVer хранит эту самую версию. После сообщения о версии и записывания её в базе, отсылаем серверу единицу и переходим в режим диалога, который мы уже разобрали выше.
И последнее: ошибка может возникнуть, если сервер вообще недоступен по какой-либо причине. Обрабатываем это так:

procedure TForm1.ClientSocket1Error(Sender: TObject; Socket: TCustomWinSocket;
ErrorEvent: TErrorEvent; var Errorpre: Integer);
begin
Errorpre:= 0; Form1.StatusBar1.Panels.Items[0].Text:=\'Подключиться к \'+Form1.ClientSocket1.Address+\' не удалось!\'; Form1.IBTable1.Edit; Form1.IBTable1.FieldByName(\'SEND\').AsInteger:=0;
Form1.IBTable1.Post;
Form1.IBTable1.Next;
NextServ();
end;


Тут избавляемся от неприятных сообщений об ошибке (Errorpre:= 0) и заменяем их культурной записью в StatusBar. Потом устанавливаем ноль в таблицу и переходим к обработке следующего сервера.
Вот, собственно, и вся программа. За скобками оставим подключение к базе при запуске и отключение при выходе, прочие мелочи. Главное, что я хотел проиллюстрировать на этом примере - использовать асинхронные сокеты можно не только для посылки односторонних команд. Достаточно простой приём, использованный мной, можно существенно оптимизировать, выделить и сделать методом класса. Я уже молчу о том, что обрабатывать сервера один за другим совсем не обязательно: стоит только разнести сервера на разных машинах по разным номерам портов - и вот вам одновременная беседа со всеми компьютерами в списке.

Ссылки по теме


 Распечатать »
 Правила публикации »
  Написать редактору 
 Рекомендовать » Дата публикации: 27.12.2006 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
SmartBear LoadComplete - Node-Locked License Subscription w/ 250 Virtual Users (includes 1 year of Maintenance)
TeeChart for .NET Standard Business Edition 2017 single license
Microsoft 365 Business Standard (corporate)
Nero 2018 Platinum ESD
Microsoft Windows Professional 10, Электронный ключ
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Безопасность компьютерных сетей и защита информации
Новости ITShop.ru - ПО, книги, документация, курсы обучения
Программирование на Microsoft Access
CASE-технологии
Компьютерный дизайн - Все графические редакторы
СУБД Oracle "с нуля"
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
 



    
rambler's top100 Rambler's Top100