Версия для печати темы
Нажмите сюда для просмотра этой темы в оригинальном формате
Форум программистов > Delphi: WinAPI и системное программирование > Подскажите как правильно сделать отложенную обрабо


Автор: vogel 11.7.2008, 18:14
Уважаемые коллеги. 
Возникла у меня проблема при обработке большго потока данных. Описываю ситуацию :

1. Есть DLL-ка, которая внедряется в чужой процесс, хукает send и recv и передаёт перехваченный трафик главному приложению при помощи WM_COPYDATA  вот как-то так :

Код

function ProcessPacket(msg : TIPCMessage) : boolean;
var
  copyDataStruct : TCopyDataStruct;
begin
  // Locate parent window
  parentWindowHandle := FindWindow(nil, PARENT_WINDOW_CAPTION);
  if parentWindowHandle <> 0 then
  begin
    msg.Pid  := GetCurrentProcessID;
    // Send WM_COPYDATA message
    copyDataStruct.dwData := NOTIFY_API_CALL; //use it to identify the message contents
    copyDataStruct.cbData := sizeOf(msg);
    copyDataStruct.lpData := @msg;
    //
    if SendMessage(parentWindowHandle, WM_COPYDATA, MAGIC_NUMBER, Integer(@copyDataStruct)) = 1
    then result := true else result := false;
  end;
end;

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~ Наша замена recv'у
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
function recvHookProc(s: TSocket; var Buf; len, flags: Integer): Integer; stdcall;
var
  msg     : TIPCMessage;
  rawData : array [0..$FFFF] of byte;
  i, packetLen : word;
begin
  // Вызываем оригинальный приёмщик, но данные пишем к себе в буфер
  Result := recvNextHook(s, rawData[0], Len, Flags);

  // Общее для данной посылки
  msg.Operation := OT_RECV;
  msg.Sock      := s;
  msg.TimeStamp := now();

  // Разбираем входной поток на пакеты
  i := 0;
  while i < Result do
  begin
    packetLen := rawData[i] + rawData[i+1]*$100;
    // Готовим к отправке наш пакетик
    msg.Size := packetLen;
    Move(rawData[i], msg.Data[0], packetLen);
    // Засылаем собранную инфу в наше приложение
    ProcessPacket(msg);
    // TODO: обрабатываем блокировку и фильтрацию пакетов
    // Переходим к следующему пакету
    i := i + packetLen;
  end;

  // Передаём данные дальше по цепочке
  Move(rawData[0], Buf, Result);
end;



2. Есть основное приложение, которое получает это сообщение и обрабатывает полученный пакет. ВОт примерно так :
Код

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//~~~ Обслуживаем сообщение от DLL-ки
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
procedure TfmMain.WMCOPYDATA(var Msg: TWMCopyData);
var
  ipcMessage : TIPCMessage;
  packetBuffer : string;
begin
  // Проверяем, что нам пришло наше сообщение
  if (Msg.From <> MAGIC_NUMBER) then
  begin
    ReplyMessage(1);
    exit;
  end;

  // Выделяем значимую часть
  ipcMessage := PIPCMEssage(Msg.CopyDataStruct.lpData)^;

  // Что будем делать с пришедшими данными ?
  case Msg.CopyDataStruct.dwData of
    NOTIFY_API_CALL: // Уведомление о вызове функции
    begin
      // Если работать с данными пакета как со строкой
      SetLength(packetBuffer, ipcMessage.Size);
      Move(ipcMessage.Data[0], packetBuffer[1], ipcMessage.Size);
      self.LogPacket(ipcMessage.Operation, packetBuffer);
    end;
  end;
  // reply to sender
  ReplyMessage(1);
  application.ProcessMessages();
end;



И всё это работает прекрасно, если приложение-жертва посылает пакеты достаточно редко. Как только приложение-жертва начинает активный обмен по сети, причём достаточно большими объёмами данных - возникают проблемы :

self.LogPacket(ipcMessage.Operation, packetBuffer), которая занимается идентификацией и разбором пакетов отрабатывает долго и приложение падает с 'Stack Overflow' в TfmMain.WMCOPYDATA(var Msg: TWMCopyData);

Внимание вопрос : Каким образом реализовать такую обработку событий, чтобы пришедшие данный быстро-быстро копировались в какое-либо временное хранилище и мы быстро-быстро выходили из  TfmMain.WMCOPYDATA. В то время как некий тред в фоновом режими уже занимался разгребанием и логированием пакетов.

Очень расчитываю на помощь знающих людей.


Автор: ne0n 12.7.2008, 01:46
ну собственно что первое приходит в голову это реализовать подобие клиент-сервера.
Т.е. отправила dll данные-> ждем-> приоложение получает данные, обрабатывает их-> отправляет потвержение того что все обработано нашей dll библиотеке-> при получение подтверждения обработки, dll отправляет следующию порцию данных итд. Вроде так никаких переполнений возникнуть не должно...

Автор: vogel 13.7.2008, 14:05
Спасибо, но ткаой вариант не пойдёть. Ибо в этом случа мы спровоцируем лаги в приложении-жертвне, в котором захуканы send и recv.
Попробую переформклирвать вопрос : Каким образом я могу вызывать процедуру асинхронно ? То есть вызывать self.LogPacket(ipcMessage.Operation, packetBuffer) таким образом, чтобы не ждать окончания выполнения, а переходить сразу дальше. 
Чувствую, что это можно сделать через потоки, но пока не понимаю как...

Автор: CodeMonkey 14.7.2008, 10:13
Я бы сделал так: выделил бы буфер в разделяемой памяти (CreateFileMapping). При получении данных писал бы в этот буфер, затем - отправлял бы уведомление другой программе (например через SetEvent) и сразу же выходил бы из функции. 
Другая программа имела бы один или несколько потоков, которые бы ждали наступления события (WaitForSingleObject) и как только оно получено - начинали обработку блока. После того, как блок обработан - пометить его свободным и отправлять уведомление первой программе (наверное, опять через SetEvent).
Нужно только отслеживать, какая часть общего буфера свободна, а какая - занята (проще всего делать это циклически, т.е. ввести маркер начала необработанных данных и конца, при достижении конца буфера - начинать запись с его начала). Если данные прибывают чаще, чем вторая программа успевает их обработать, то надо, либо отбрасывать часть данных, либо выделять дополнительный буфер, либо приостанавливать приём-передачу, пока вторая программа не обработает данные. Для последнего и нужен был второй SetEvent - в случае, если места в буфере мало, то первая программа может ждать на этом событии, пока вторая её не уведомит, что в буфере появилось новое свободное место.

Автор: Riply 14.7.2008, 10:46
Цитата(vogel @  13.7.2008,  14:05 Найти цитируемый пост)
Каким образом я могу вызывать процедуру асинхронно ? То есть вызывать self.LogPacket(ipcMessage.Operation, packetBuffer) таким образом, чтобы не ждать окончания выполнения, а переходить сразу дальше. 


Я делала подобное (данные надо было получать сразу от нескольких процессов) используя Pipe - ы.  
А как только ты определился с "каналом связи" далее все будет ограничиваеться только воображением.
Вариантов масса : от ReadFileEx (например, с очередью)  до пула потоков.  smile

P.S. 
Слишком обще сформулирована задача, вот и получаются общие ответы.  smile 

Автор: vogel 14.7.2008, 13:33
Хммм, почему слишком обще ??
Я же в первом посте написал по поводу "канала связи" - это WM_COPYDATA.
Вопрос в том, что обработка принятых данных (на некоторых этапах) идёт медленне, чем эти данные поступают. Из-за этого stack overflow.

Мне самому видится такое решение :

1. Обработчик WM_COPYDATA складывает полученные строки, например, в TStringList и тутже возвращает управление - этим мы достигаем быстроту ответов и избавляемся от stack ovwrflow (как мне кажется).

2. При этом отдельный TThread мониторит этот самый TStringList на предмет count > 0 и елси оно так - выгребает строку и обрабатывает её.

главный вопрос теперь - критические секции - как бы не потерять время на них...

Автор: Rennigth 14.7.2008, 14:15
vogel, Логичнее будет использовать пул потоков.

Автор: vogel 14.7.2008, 14:47
Цитата(Rennigth @ 14.7.2008,  14:15)
vogel, Логичнее будет использовать пул потоков.

Спасибо за идею, мне б примерчик.

А вообще по теме вопроса - проблему решил :
1. Надо было сделать все переменные в обработчике WM_COPYDATA глобальными - это сняло проблему stack overflow
2. Данные заносятся в TStringList и обрабатываются отдельным потоком - это снялдо проблему производительности.

Спасибо всем участникам за обсуждение, однако пока не закрываю - жду интересных решений. Желательно с примерами.
Вот пул потоков очень даже заинтересмоало.

Автор: CodeMonkey 14.7.2008, 14:52
Цитата(vogel @  14.7.2008,  13:33 Найти цитируемый пост)
избавляемся от stack ovwrflow

Причём тут stack overflow вообще? smile Уберите Application.ProcessMessages из обработчика сообщения.

Цитата(vogel @  14.7.2008,  13:33 Найти цитируемый пост)
Обработчик WM_COPYDATA складывает полученные строки, например, в TStringList

У вас получается, что вы будете гонять данные впустую между различными буферами. Не самое изящное решение. Для межпроцессного взаимодействия лучше использовать специально предназначенные для этого IPC функции, а вовсе не сообщения Windows, предназначенные (в первую очередь, конечно) для визуального интерфейса.

Добавлено через 48 секунд
Цитата(vogel @  14.7.2008,  14:47 Найти цитируемый пост)
1. Надо было сделать все переменные в обработчике WM_COPYDATA глобальными - это сняло проблему stack overflow

Не сняло, а скрыло ;)

Автор: vogel 14.7.2008, 16:22
Цитата
Причём тут stack overflow вообще? smile 

Ну Вы внимательно прочитайте первое сообщение топика - Вам сразу станет понятно причём оно тут.

Цитата
У вас получается, что вы будете гонять данные впустую между различными буферами. Не самое изящное решение. Для межпроцессного взаимодействия лучше использовать специально предназначенные для этого IPC функции, а вовсе не сообщения Windows, предназначенные (в первую очередь, конечно) для визуального интерфейса.


Я весь внимание. Очень хочется посмотреть, что же именно вы понимаете под IPC ?? MMF, NamedPipes, UDP ??
Кстати, WM_COPYDATA в частности относится к механизмам IPC.


Цитата
Не сняло, а скрыло ;)

Сняло. Ибо память под локальные переменные внутри процедур отводится из стека.
А скрыть эту проблему нельзя - у вас либо переполняется стек либо нет.

Автор: Rennigth 14.7.2008, 16:31
Цитата(vogel @  14.7.2008,  14:47 Найти цитируемый пост)
Вот пул потоков очень даже заинтересмоало. 

Вот http://wm-help.net/books-online/book/59464/59464-4.html#h11 можно почитать про встроеммые механизмы винды.
Если не подойдет, надо самому организовывать, и затачивать под Ваши задачи.

Автор: CodeMonkey 14.7.2008, 17:44
Что ж вы такой непонятливый.
Ладно, не будем намёками. Будем правду резать smile 
Смотрите:

Цитата(vogel @  11.7.2008,  18:14 Найти цитируемый пост)
Код
procedure TfmMain.WMCOPYDATA(var Msg: TWMCopyData);
var
  ipcMessage : TIPCMessage;
  packetBuffer : string;
begin
  ...
  Application.ProcessMessages;
end;


Как это работает: приходит сообщение WM_COPYDATA, вы запускаете обработку данных, затем вызывается Application.ProcessMessages, который просматривает накопившиеся в очереди сообщения и запускает их обработчики. Если вам сообщения WM_COPYDATA поступают быстрее, чем вы их обрабатываете, то это значит, что на момент вызова Application.ProcessMessages в очереди окна уже есть одно или более сообщений WM_COPYDATA. Что значит, что обработчик WMCOPYDATA будет запускаться из Application.ProcessMessages, который в свою очередь вызывается из... правильно, WMCOPYDATA. Видите рекурсию?
У вас получается такое дерево вызовов:
Код
WMCOPYDATA
  Application.ProcessMessages
    WMCOPYDATA
      Application.ProcessMessages
        WMCOPYDATA
          ...

Причём чем больше разница в скорости прибытия/обработки сообщений, тем быстрее растёт вложенность вызовов. Вот вам и причина переполнения стека - рано или поздно стек закончится из-за большой рекурсии вызовов. Разумеется, если в потоке входящих данных появляется окно (замедление скорости данных), то начинается процесс выхода из дерева вызовов, и стек освобождается.
И то, что вы вынесли переменные за пределы процедуры принципиально не меняет ситуации - это лишь меняет скорость заполнения стека (в стек при каждом вызове попадает меньше информации, но, тем не менее, всё ещё попадает). Да, это уменьшает вероятность переполнения стека, т.к. теперь стек заполняется дольше, а значит больше вероятность появления окна во входящих данных, чтобы стек успел освободиться. Тем не менее проблему вы не сняли, она всё ещё тут - достаточно поддерживать высокий темп входящих данных и стек рано или поздно снова переполнится. Именно поэтому я сказал, что проблему вы скрыли, а не решили.
Теперь ситуация ясна? Убедите Application.ProcessMessages (зачем вы его вообще туда всунули?) и проблема решена. Поэтому я и сказал: причём тут переполнение стека, когда проблемы нет вообще - вы её просто специально создали.

Цитата(vogel @  14.7.2008,  16:22 Найти цитируемый пост)
Я весь внимание. Очень хочется посмотреть, что же именно вы понимаете под IPC ?? MMF, NamedPipes, UDP ??Кстати, WM_COPYDATA в частности относится к механизмам IPC.

Посмотрите, вам уже предлагали воспользоваться MMF или пайпами. Формально, да, WM_COPYDATA относится к IPC. Проблема в том, что оно ориентировано на простую пересылку данных между двумя GUI-приложениями. Поскольку фактически это сообщение является обёрткой вокруг MMF, то для вашего сценария нет смысла нагружать приложение дополнительными накладными расходами. Впрочем, это ваш выбор.

Автор: vogel 15.7.2008, 11:52
CodeMonkey, Огромнейшее спасибо за подробное объяснение такому вот непонятливому. 
Проблема действительно решилась. А я был неправ. 
Теперь "раскуриваю" MMF, но оно меня как-то пугает... Через WM_COPYDATA всё происходит достаточно лего и просто

Автор: CodeMonkey 15.7.2008, 13:35
Если вас устроит ваша реализация с WM_COPYDATA - ради бога, используйте её. Просто она будет не самой оптимальной (в плане быстродействия). Зато (наверное) наиболее просто реализуемой.

И, кстати, стек потоков - правильность этого решения зависит от того, какие именно действия вы выполняете в процедуре обработки. Например, если у вас поток занимается тем, что производит какие-либо вычисления по полученным данным (т.е. работа идёт в основном процессором), то выделение дополнительных потоков просто ничего не даст, а наоборот - ухудшит ситуацию. 
Если же в обработке вы, скажем, просто копируете данные в файл, то тут процессор почти не участвует, и использование нескольких потоков (может) существенно увеличит пропускную способность обработки. Впрочем, даже в этом случае наиболее быстродействующим решением был бы один поток с использованием асинхронного ввода-вывода, но такого монстра вы замучаетесь писать (но если решитесь, то начать стоит http://www.google.com/search?&q=completion+port). Это так, информация к размышлению.

Автор: vogel 15.7.2008, 14:55
Реализация с WM_COPYDATA оказалась наиболее простой и понятной, но и у неё есть свои недостсаки - как-то передача блоков только фиксированного рамера. В итоге, зная, что данные у меня длиногй максимум word - пришлось делать массив [0..$FFFF]. Это работает, но приносит дополнительные расходы, ибо размер передаваемых данных всегда разный и в-основном значительно меньше FFFF.

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

Цитата

Например, если у вас поток занимается тем, что производит какие-либо вычисления по полученным данным (т.е. работа идёт в основном процессором), то выделение дополнительных потоков просто ничего не даст, а наоборот - ухудшит ситуацию. 


Именно это и происходит. Поэтому от потока я уже оказался. Ещё раз спасибо Вам за то, что разъяснили такую грубую ошибку.

Автор: CodeMonkey 15.7.2008, 15:28
Цитата(vogel @  15.7.2008,  14:55 Найти цитируемый пост)
Реализация с WM_COPYDATA оказалась наиболее простой и понятной, но и у неё есть свои недостсаки - как-то передача блоков только фиксированного рамера. В итоге, зная, что данные у меня длиногй максимум word - пришлось делать массив [0..$FFFF]. Это работает, но приносит дополнительные расходы, ибо размер передаваемых данных всегда разный и в-основном значительно меньше FFFF.

Непонятно, откуда вы это взяли?
В вашем же примере:
Цитата(vogel @  11.7.2008,  18:14 Найти цитируемый пост)
Код
    copyDataStruct.cbData := sizeOf(msg); // Явно указываете размер данных, т.е. он может быть произвольным
    copyDataStruct.lpData := @msg; // Любые данные

Т.е. у вас уже написан код по передаче данных любого размера. Правда конкретно сейчас вы передаёте только одну запись фиксированного размера (TIPCMessage). Или вы говорите, что не знаете, как подготовить (упаковать в запись или что-то такое) данные произвольной длины для отправки?

Цитата(vogel @  15.7.2008,  14:55 Найти цитируемый пост)
Однако, я не уверен, что через MMF можно будет просто кидать динамические массивы или строки.

Точно так же, как любые другие данные. При передаче вы все данные всё равно рассматриваете как массив (набор) байт, без разницы: строка это, запись или массив.

Автор: vogel 22.7.2008, 16:00
Конкретно, если мне надо передать запись где есть строки, объявленные как string, делаем это, напрмер, так :

Код

TSomeRecord = record
  ...
  ID : integer;
  Size : word;
  Data : string;
   ...
end;


и дальше в коде :

Код

var
  Message : TSomeRecord;
begin
...
Message.Size := len;
setlength(Message.Data, Message.Size);
move(Buffer[0], Message.Data[1], Message.Size);
...
end;


получаем, что sizeOf(TSomeRecord) совсем не равен реальному размеру записи. И припопытке вычитать такую строку получаем AV.

Однако, если я использую вместо string - array[0..$FFFF] of char, то всё работает прекрасно, хоть и значительно перерасходуется память.

Автор: Snowy 22.7.2008, 16:29
Разумеется.
Ведь string - это указатель.
Передавать такую структуру через WM_COPYDATA бесполезно.
Нужно передавать фиксированный блок.
Либо использовать записи строгого размера, либо записать все данные в единый буфер/стрим.
Записать данные можно вручную, либо через сериализацию.
Ситуация подобна передаче данных через сокеты. Нужно передавать сплошной блок/поток данных, а не указатели на них.

Автор: CodeMonkey 22.7.2008, 17:36
Понятно, короче говоря, вы не знаете, как динамические данные упаковать в одну запись.

Грубо говоря, вы должны разделить данные на данные фиксированного размера и динамические. Закиньте все фиксированные данные в структуру:

Код
TSomeRecord = record
  ...
  ID : integer;
  Size : word;
   ...
end;


Далее, вы должны подготовить блок к передаче, например, так:

Код
var
  P: TSomeRecord;
  MS: TMemoryStream;
  X: Integer;
...
// Готовите фиксированную часть
P.ID := 3;
...

// Далее, собираем всё в кучу
MS := TMemoryStream.Create;
try
  // Динамические данные пишутся как: [длина данных] [сами данные]
  X := Length(S);
  MS.WriteBuffer(X, SizeOf(X));
  if X > 0 then
    MS.WriteBuffer(Pointer(S)^, X);

  // Теперь здесь MS.Memory - указатель на данные для пересылки. Все данные оказались упакованными в один блок.

finally
  FreeAndNil(MS);
end;  


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

Powered by Invision Power Board (http://www.invisionboard.com)
© Invision Power Services (http://www.invisionpower.com)