Модераторы: Poseidon, Snowy, bems, MetalFan
  

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Использование потоков данных (TStream) 
:(
    Опции темы
Alexeis
Дата 18.5.2008, 16:14 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Амеба
Group Icon


Профиль
Группа: Админ
Сообщений: 11743
Регистрация: 12.10.2005
Где: Зеленоград

Репутация: 109
Всего: 459



Использование потоков данных (TStream)

Для чего эта статья.

Статья для тех кто до сих пор игнорирует этот весьма эффективный и быстрый метод обработки данных.

Чтобы легче себе представить что такое поток можно представить себе магнитную ленту. Чтобы прочитать что-то с нее мы опускаем считывающую головку и читаем данные определенного объема или пока не дойдем до конца ленты. Тоже самое при записи. Если запись производить несколько раз (блок за блоком), то информация будет ложиться на ленту последовательно блок за блоком. После записи или чтения позиция считывающей головки останется там же где она осталось после предыдущей операции. Вот и все ограничения! Внутри это может быть реализовано совершенно по разному. В перерывах между чтением данных данных хранилище может шириться (например подгружаться из интернета или из диска), можно сделать бесконечную ленту, т.е. при переходе в конец потока подставить ему начало хранилища, можно реализовать принцип конвейера, т.е. хранилище маленькое, но в него постоянно подкладывают в конец (очередь), а забирают всегда поочереди. Это весьма удобный механизм при работе с одиночными объектами, коллекциями, массивами объектов и даже деревьями.

Все начинается именно с TStream. В делфи есть много классов его наследников таких как TMemoryStreamTFileStreamTResourceStreamTCompressionStreamTStringStream и другие. Их объединяют механизмы  удобной быстрой загрузки, сохранения, добавления и обрезки данных. Но их объединяет не только логика но и реализация, реализация класса TStream. Забегая наперед скажу что сам класс "недореализован", т.е. нельзя создать объект такого класса (так как он абстрактный), но кой чего он умеет.

Как это ни странно, но начнем именно с того чего он не умеет. Не умеет он то что касается низкоуровневого доступа к самим данным. Оно и понятно, наследников много всем не угодишь, но при этом он "знает" как это должны делать его наследники. Под знает я имею ввиду что часть его методов виртуальные и даже абстрактные (без реализации). 

 Работает это так. Пусть мы создаем собственный класс. Наш класс занимается обработкой данных. Может получать их из некоторого источника и после обработки сохранять или передавать дальше по цепочке. Однако источников данных может быть много, например из файла, из ресурса, или просто из буфера в памяти. Если решать это задачу в лоб, то нам понадобилось бы создавать 3 разных метода, для получения данных из перечисленных источников. Используя класс TStream, нам досрочного сделать всего один. Часто его объявляют так

Код

Procedure TMyClass.LoadFromStream(AStrem : TStream);

Это совсем не значит, что мы ожидаем что нам в функцию передадут объект класса TStream (Это невозможно! Такой класс создать нельзя в принципе). Вместо  TStream, нам подойдет любой полноценный класс где реализованы все возможности ввода и вывода (например TFileStreamTResourceStream и т.д.). Это возможно потому, что все эти классы являются наследниками TStream. А мы у себя считая что работаем  TStream, на самом деле будем вызывать методы того класса который в действительности был создан (это то что я имел ввиду когда писал, что TStream "знает" как это должны делать его наследники). Это и есть полиморфизм на практике т.е. наследник и предок реагируют похоже на одинаковые события. Чтобы такое было возможно метод должен объявлен как виртуальный или абстрактный. Если метод был объявлен как виртуальный, то  производимое методом действие будет заменено действием того объекта, который был реально создан, если же он объявлен как абстрактный, то изначально предок ничего и не делал, но по его названию можно догадаться что бы он хотел сделать. Мы уже говорили, что механизм виртуальных методов позволяет подменять действие, но бывает так что класс предназначается только для подмены своего наследника (именно такой TStream), тогда метод предка никогда не будет вызываться, вот как раз в таком случае виртуальные методы делают абстрактными (т.е. без своей реализации). 

После прочтения предыдущего абзаца у вас уже достачно знаний, чтобы разобрать структуру класса 

Код

TStream = class(TObject)
  private
    function GetPosition: Int64;
    procedure SetPosition(const Pos: Int64);
    procedure SetSize64(const NewSize: Int64);
  protected
    function GetSize: Int64; virtual;
    procedure SetSize(NewSize: Longint); overload; virtual;
    procedure SetSize(const NewSize: Int64); overload; virtual;
  public
    function Read(var Buffer; Count: Longint): Longint; virtual; abstract;
    function Write(const Buffer; Count: Longint): Longint; virtual; abstract;
    function Seek(Offset: Longint; Origin: Word): Longint; overload; virtual;
    function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; overload; virtual;
    procedure ReadBuffer(var Buffer; Count: Longint);
    procedure WriteBuffer(const Buffer; Count: Longint);
    function CopyFrom(Source: TStream; Count: Int64): Int64;
    function ReadComponent(Instance: TComponent): TComponent;
    function ReadComponentRes(Instance: TComponent): TComponent;
    procedure WriteComponent(Instance: TComponent);
    procedure WriteComponentRes(const ResName: string; Instance: TComponent);
    procedure WriteDescendent(Instance, Ancestor: TComponent);
    procedure WriteDescendentRes(const ResName: string; Instance, Ancestor: TComponent);
    procedure WriteResourceHeader(const ResName: string; out FixupInfo: Integer);
    procedure FixupResourceHeader(FixupInfo: Integer);
    procedure ReadResHeader;
    property Position: Int64 read GetPosition write SetPosition;
    property Size: Int64 read GetSize write SetSize64;
  end;

Начнем анализ класса с методов Read и Write. Они абстрактные, значит реализованы в наследнике. Служат они для чтения данных из текущей позиции потока наследника TStream и записи в него соответственно. Первый параметр это сам буфер данных (внимание не указатель, а сама переменная буфера!). Второй размер буфера в байтах. Результат число переданных данных. Это базовые методы ввода/вывода. На них построена вся передача данных. Их удобство заключается в том, что при правильном использовании вы никогда не прочитаете данных больше чем вам может предоставит источник и не запишите в никуда в случае если приемник данных не готов в данный момент их принять.  Если что-то не так то результат функция всегда вернет нуль или значение отличное от того что вы ей передали в поле Count.

На этом удобства не заканчиваются. Если вы уже умеете использовать такой гибкий механизм как исключения, то вы должны оценить методы ReadBufferWriteBuffer, они вызывают Read и Write для передачи данных, но в случае неудачи вызывают исключения EReadError и EWriteError соответственно. Это нам дает возможность грамотно выйти из непредвиденной ситуации и объяснить пользователю, почему его действия не увенчались успехом. 

Пример: Создаем пустой текстовый файл на диске C: с именем "1.txt".

Код

procedure SeadFromStream(AStream : TStream);
var
  i          : Integer;
Begin
  try
    AStream.ReadBuffer(i, sizeof(i));
  except
    on EReadError do ShowMessage('Ошибка чтения данных');
    else ShowMessage('Неизвестная ошибка ');
  end;
end;

procedure TForm1.FormCreate(Sender: TObject);
var
  F : TStream;
  i : Integer;
begin
  Try
    F := TFileStream.Create('C:\1.txt', fmOpenRead);
    SeadFromStream(F);
    F.Free;
  finally end;
end;

Под отладкой все равно вылетают исключения, но так  бывает только под отладкой... 

Кто повнимательнее заметил заметил что открытие файла тоже в защищенной секции. Что поделаешь, отстуствие файла это тоже ошибка, но тут я ее не стал отдельно отрабатывать, так как для исследуемой функции LoadFromStream не имеет значения к какому из потоков принадлежит переданный ей объект. 

Уточню, что методы ReadBuffer и WriteBuffer определенны уже в TStream. Т.е. если вы вдруг захотите написать своего наследника TStream, для своего специфического источника данных, то вам не нужно будет их переопределять самому(ой). 

Говоря об операциях чтения и записи следует уточнить, что не все наследники TStream позволят вам писать/читать данные одновременно. Например, в ресурс нельзя ничего писать, а из файла открытого для записи бесполезно читать. Это значит что если класс имеет метод WriteToStream, то  передавать ему объект класса TResourceStream будет неправильно. 

 Метод Seek устанавливает позицию чтения/записи в потоке, хотя он и реализован в TStream, но его ОБЯЗАНЫ перекрывать все наследники. Он существует в 2х вариациях 

Код

function TStream.Seek(Offset: Longint; Origin: Word): Longint;
function TStream.Seek(const Offset: Int64; Origin: TSeekOrigin): Int64;

Перекрыть нужно хотя бы один. Если один из них не будет реализован, то при необходимости всегда будет вызван другой. Если не будет ни одного, то при попытке изменить позицию чтения произойдет исключение.

Метод  Seek позволяет производить установку позиции от начала потока (параметр Origin = soBeginning), от текущей позиции (Origin = soCurrent), или от конца(Origin = soEnd). 

Для получения позиции от начала хранилища удобно пользовать свойством Position.

Еще один метод, который часто нужно перекрывать в наследнике это SetSize

Код

procedure TStream.SetSize(NewSize: Longint);

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

В простых случаях линейных хранилищ Функцию GetSize можно не переопределять. Она самостоятельно использует метод Seek для получения размеров хранилища. 

При использовании объектов класса следует использовать публичное свойство  Size.

Теперь о реализованных высокоуровневых возможностях. 

Копирование больших фрагментов (более 100кб) 

Код

function CopyFrom(Source: TStream; Count: Int64): Int64;

Замечательная функция, она нам позволит скопировать большой блок данных из чужого потока, начиная с текущей позиции в потоке Source, при этом начнет писать данные, начиная с текущей позиции в нашем потоке. Для копирования будет выделен внутренний буфер размером в 60 кб, позволяющий сравнительно большими блоками копировать данные, при этом не занимая надолго сам источник данных (источник может оказаться неспособным к  работе с несколькими процессами). И еще если в качестве Count указать 0, то функция поймет что нужно скопировать весь поток от начала (Внимание! Не от текущей позиции) до конца. Весьма удобная функция, при перекачке данных из медленного источника в быстрый или из ReadOnly потока в поток доступный для записи.

Сериализация объектов. 

Сериализация это перевод структур данных (в данном случае классов наследников TComponent) в бинарное представление. В таком виде данные объектов можно передать из программы или сохранить на диске. Тут же уточняю сохраняться лишь published свойства. Потому если вы хотите использовать такой механизм в своей программе, то в published секцию следует перенести все свойства определяющие состояние объекта. 

Код

  function ReadComponent(Instance: TComponent): TComponent;
  procedure WriteComponent(Instance: TComponent);

Можно записывать несколько объектов подряд, сохранять картинки, тесктовые файлы и много чего другого. Кстати, таким же образом создается любая форма, только источником для нее становиться TResourceStream. Файл DFM сохраняется в ресурс, на лету конвертируется из текстового ресурса в бинарный и становиться доступным для загрузки функцией ReadComponent. Однако перед использованием этих функций нестандартные компоненты следует зарегистрировать при помощи функции RegisterClass. (Подробный пример см. в справке по Delphi "TStream.ReadComponent Method") 

Дополнительные возможности.

 

 Реализация интерфейса IStream. Связь с COM

Наверное некоторые уже заметили созвучность имени класса TStream и COM интерфейса IStreamIStream - это универсальный интерфейс приема-передачи данных в Windows. Если бы нам удалось подружить  TStream и  IStream, то задачу передачи данных между двумя сторонними объектами можно было бы считать решенной. Например, передать картинку из потока TFileStream в объект класса Bitmap библиотеки GDI+. Аналогичная задача стоит при обмене данными между приложением и своей собственной Dll. Как известно в Dll нельзя передавать объекты классов, а интерфейсы очень даже можно, при этом идеально сохраняется принцип инкапсуляции класса и объектный подход к разарботке приложений. 

Ближе к делу. Для того чтобы получить интерфейс IStream имея объект наследник TStream необходимо дополнительно воспользоваться классом TStreamAdapter, который и реализует IStream, но не сам, а с помощью класса наследника TStream. Вот его кноструктор. 

Код

constructor Create(Stream: TStream; Ownership : TStreamOwnership = soReference);

Видно, что при создании объект должен принять экземпляр класса наследника TStream, но кроме того у него есть еще такой замечательный параметр Ownership, который указывает нужно ли уничтожать сам объект потока (тот который передали ему в конструкторе Stream: TStream). Напомню, что после того как интерфейс освобождается (переменной интерфейса присваивают nil) и больше не остается ссылок на этот объект (ссылока значит то, что  у этого объекта получали интерфейс), сам объект, который релизовывал интерфейс уничтожается. Уничтожается, ну и пусть, но ведь после уничтожения у нас останеться неуничтоженным сам объект потока (наследник TStream, который и реализовывал передачу данных). Уничтожить самостоятельно мы его не можем, ведь мы не знаем, когда клиент прекратит с ним работать. Вот для это и нужно в качестве Ownership передвать значение soOwned, т.е. адптер владеет экземпляром потока и удалит его, перед тем как самоуничтожиться. 

Звучит это все страшно, а используется очень просто. Вот пример.

Код

var
  strm : IStream; 
  mem : TMemorySteam;
Begin
  mem := TMemorySteam.Create;
  strm := TStreamAdapter.Create(mem, soOwned) as IStream;

Все теперь передаем интерфейс strm в Dll ил вообще куда угодно и не думаем о уничтожении ни TStreamAdapter ни TMemorySteam, в нужное время они сами уничтожаться, тогда когда клиент закончит с ними работать. 

Теперь рассмотрим случай, когда программа на делфи является не сервером, а клиентов, т.е. ей передают интерфейс IStream, объекта потока созданного неизвестно где. Идеально было бы если бы мы получили не IStream, а TStream, ведь все объекты VCL умеют работать именно с TStream, а работать с IStream они не умеют. Решение в лоб состоит в том, чтобы банально скопировать все данные из IStream например в TMemorySteam. Однако, таким образом мы получаем лишнее копирование. Это ничего, когда данных на 20кб, другое дело если их 1Гб, тогда дополнительное копирование выйдет боком. Для таких случаев предлагаю написать свой собственный адаптер, но уже не адаптер сервера, а клиента. Класс назвал TExternStream (от слова External т.е. внешний). Заодно это будет пример написания своего класса наследника TStream

Основная задача этого класса переадресовать все запросы интерфейсу  внешнего объекта IStream. Сам интерфейс нужно передать в конструктор. В деструкторе интерфейс освобождается FSource := nil; в классе перекрыты все абстрактные методы Read, Write, виртуальный Seek, GetSize и SetSize.

Код

type
  TExternStream = class(TStream)
  protected
    FSource : IStream;
    procedure SetSize(const NewSize: Int64); override;
  public
    constructor Create(Source : IStream);
    destructor Destroy; override;
    function Read(var Buffer; Count: Longint): Longint; override;
    function Write(const Buffer; Count: Longint): Longint; override;
    function Seek(const Offset: Int64; Origin: TSeekOrigin): Int64; override;
  end;

procedure TForm1.FormCreate(Sender: TObject);
var
  T : TStreamAdapter;
begin
  RegisterComponents
end;

{ TExternStream }


constructor TExternStream.Create(Source: IStream);
begin
  inherited Create;
  FSource := Source;
end;

destructor TExternStream.Destroy;
begin
  FSource := nil;
  inherited;
end;

function TExternStream.Read(var Buffer; Count: Integer): Longint;
begin
  if FSource.Read(@Buffer, Count, @Result) <> S_OK
  then
    Result := 0;
end;

function TExternStream.Seek(const Offset: Int64; Origin: TSeekOrigin): Int64;
begin
  FSource.Seek(Offset, byte(Origin), Result);
end;

procedure TExternStream.SetSize(const NewSize: Int64);
begin
  FSource.SetSize(NewSize);
end;

function TExternStream.Write(const Buffer; Count: Integer): Longint;
begin
  if FSource.Write(@Buffer, Count, @Result) <> S_OK
  then
    Result := 0;
end;

Используется все это счастье следующим образм. Пусть в клиенсткую часть передали интерфейс strm : IStream, тогда создаем свой экземпляр класса TExternStream на базе этого strm и используем как любой другой объект наследник TStream. Напрмер вызываем Image1.Picture.Bitmap.LoadFromStream(aOut); 


Код

var
  aOut : TExternStream;
try
  aOut := TExternStream.Create(strm);
  Image1.Picture.Bitmap.LoadFromStream(aOut);
....
  aOut.Free; //Тут уничтожиться поток созданный в Dll, так как указатель на aStream занулиться. 


Чистого кода!


--------------------
Vit вечная память.

Обсуждение действий администрации форума производятся только в этом форуме

гениальность идеи состоит в том, что ее невозможно придумать
PM ICQ Skype   Вверх
THandle
Дата 18.5.2008, 16:25 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Хранитель Клуба
Group Icon
Награды: 1



Профиль
Группа: Админ
Сообщений: 3639
Регистрация: 31.7.2007
Где: Moscow, Dubai

Репутация: 65
Всего: 372



Alexeis, хорошая статья smile 
PM   Вверх
EvilsInterrupt
Дата 23.5.2008, 16:04 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Executables research
***


Профиль
Группа: Завсегдатай
Сообщений: 1019
Регистрация: 14.7.2007
Где: Железнодорожный, МО, Россия

Репутация: 3
Всего: 9



Спасибо автору! Столько всего открыл для себя, что просто жесть. Но у меня закралось сомнение в необходимости написания TExternalStream, может все-таки разработчики VCL предусмотрели и этот случай ?
PM MAIL WWW ICQ Jabber   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Правила форума "Delphi: Общие вопросы"
SnowyMetalFan
bemsPoseidon
Rrader

Запрещается!

1. Публиковать ссылки на вскрытые компоненты

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

  • Литературу по Дельфи обсуждаем здесь
  • Действия модераторов можно обсудить здесь
  • С просьбами о написании курсовой, реферата и т.п. обращаться сюда
  • Вопросы по реализации алгоритмов рассматриваются здесь
  • 90% ответов на свои вопросы можно найти в DRKB (Delphi Russian Knowledge Base) - крупнейшем в рунете сборнике материалов по Дельфи


Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, Snowy, MetalFan, bems, Poseidon, Rrader.

 
1 Пользователей читают эту тему (1 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | Delphi: Общие вопросы | Следующая тема »


 




[ Время генерации скрипта: 0.0977 ]   [ Использовано запросов: 22 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.