Практикум прикладного программирования на C# в среде VS.NET 2008

Работа с потоками данных

Разбить на страницы
Показывать лекцию целиком

Полезные ссылки

Все необходимые для выполнения данной работы программы можно найти в http://old.intuit.ru/department/se/prcsharp08/19/Setup.zipприлагаемом каталоге.

  • Работа с потоками слегка описана в intuit:
  • http://www.intuit.ru/department/se/csharpdevtools/11/
  • Работа с альтернативными потоками:
  • http://habrahabr.ru/blogs/windows/46990/
  • http://wladm.narod.ru/C_Sharp/filesstreams.html#top
  • Динамическое создание классов в модели CodeDOM:
  • http://msdn.microsoft.com/ru-ru/library/ms404245.aspx
  • Динамическое создание классов в модели System.Reflection.Emit:
  • http://www.codeproject.com/KB/cs/introreflection.aspx
  • Внедрение WPF в Windows Forms
  • http://msdn.microsoft.com/ru-ru/library/ms745781.aspx
  • Как в WPF управлять проигрыванием видео с помощью MediaElement
  • http://msdn.microsoft.com/ru-ru/library/system.windows.controls.mediaelement.aspx
  • Пример по криптографии
  • http://www.codeproject.com/KB/security/crypto_net.aspx
  • Класс Type
  • http://msdn.microsoft.com/ru-ru/library/system.type.aspx
  • Введение

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

    Под Stream понимают логическую абстракцию, с помощью которой можно подключаться к различным типам источников данных одинаковым образом. Stream - это модель, через которую последовательно считываются или записываются элементы данных. Для исключения разночтения некоторые авторы предлагают такие термины: Thread - нить (или поток выполнения ), Stream - поток (или поток данных ), чем мы с удовольствием и воспользуемся.

    Абстракция Stream создает канал связи между приложением и устройством хранения информации (резервным хранилищем), а также предоставляет необходимые для манипулирования данными инструменты. Для потока характерно направление: входной поток - данные передаются от устройства в приложение, выходной поток - от приложения к устройству. Есть однонаправленные потоки - например, файловый поток может открываться только для чтения или только для записи, с клавиатуры символы можно только считывать, и т.д. Есть и двунаправленные потоки - например, сессия по протоколу telnet или обмен данными между компьютером и модемом, которые представляют собой чередование однонаправленных потоков. Файл тоже может быть открыт для чтения и записи одновременно.

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

    Потоки, которые непосредственно связываются с устройствами, можно назвать рабочими (основными). На самом низком уровне система ввода-вывода .NET оперирует байтами, поэтому все рабочие потоки являются байтовыми. Однако есть несколько классов-надстроек (оболочек) к рабочим потокам, которые устанавливают свой формат данных при чтении/записи или облегчают пересылку данных определенного типа.

    Создав основной поток и погрузив его в надстройку, можно значительно облегчить решение задачи пересылки некоторых типов данных. Так, для упрощения пересылки текстов имеются специализированные классы, автоматически преобразующие байтовые потоки в символьные и наоборот. Чтение информации с клавиатуры и вывод на консоль обеспечивают стандартные потоки, которые являются одновременно и символьными.

    Базовым классом большей части потоков является класс Stream, который в свою очередь наследует от класса System.MarshalByRefObject

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

    Члены класса Stream
    Член Описание
    BeginRead() Начинает операцию асинхронного чтения, возможно, в отдельно созданной ните Thread. Основная нить действий продолжает выполняться
    BeginWrite() Начинает операцию асинхронной записи, возможно, в отдельно созданной ните Thread. Основная нить действий продолжает выполняться
    Close() Закрывает поток, освобождая связанные с ним системные ресурсы
    EndRead() Переводит систему в режим ожидания, пока не будет завершено чтение данных или не произойдет ошибка
    EndWrite() Переводит систему в режим ожидания, пока не будет завершена запись данных или не произойдет ошибка
    Flush() Сбрасывает (выталкивает, сливает) кэш буфера записи
    Read() Читает в массив с нужного места потока заданное количество байт. Возвращает число фактически прочитанных байт
    ReadByte() Читает очередной байт потока
    Seek() Смещяет курсор потока на заданное количество байт относительно заданного начала отсчета (начала, конца или текущей позиции) для последующей работы
    Write() Записывает в поток фрагмент данных из массива, характеризуемых начальным индексом и количеством элементов
    WriteByte() Записывает в выходной поток один байт
    CanRead CanSeek CanTimeout CanWrite Проверяются в экземплярах потомков для определения поддержки соответствующей операции
    Length Длина потока в байтах. Не во всех потомках реализована и может вызвать исключение!
    Position Определяет или устанавливает текущую позицию (курсор) потока
    ReadTimeout Определяет или устанавливает время в миллисекундах, отпущенное для попыток чтения ожидаемых данных, по безрезультатному истечению которого нить Thread будет синхронизирована с основной нитью приложения
    WriteTimeout Определяет или устанавливает время в миллисекундах, отпущенное для попыток записи, по безрезультатному истечению которого асинхронная нить Thread будет синхронизирована с основной нитью приложения

    Упражнение 1. Работа с FileStream как с чисто байтовым потоком

    Знакомство с потоками начнем с файлового потока FileStream как инструмента для чтения/записи информации в файлы. Нужно понимать, что объектная модель FileStream является высокоуровневой оболочкой API -функций операционной системы по работе с файлами. Но в то же время сам FileStream имеет несколько специализированных высокоуровневых оболочек для облегчения чтения/записи в файл данных различных форматов.

    Вначале рассмотрим использование только одного FileStream в чистом виде. Начнем с упражнения, иллюстрирующего пример из MSDN, который попутно немного 'подкрутим'.

  • Создайте новый проект App1 консольного типа в решении Stream
  • Стартовым его назначать нет смысла, поскольку в решении он пока будет единственным.

  • Заполните файл Program.cs следующим кодом
  • using System;
    using System.Text;
        
    using System.IO;
        
    namespace App1
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.ForegroundColor = ConsoleColor.White;
                string newLine = System.Environment.NewLine;// Пара "\r\n"
        
                string path = @"e:\temp1\MyTest.txt";// Задаем полное имя файла
                string dir = Path.GetDirectoryName(path);   // Выделяем каталог
        
                // Если каталог не существует, создаем его
                if (!Directory.Exists(dir))
                {
                    Directory.CreateDirectory(dir);
                }
        
                // Создаем новый или усекаем существующий файл
                // и сразу открываем его для записи
                using (FileStream fs = File.Create(path))
                {
                    AddText(fs, "Буря мглою ");
                    AddText(fs, "небо кроет,");
                    AddText(fs, "\r\nВихри снежные крутя.");
                    AddText(fs, "\r\nТо как зверь она завоет,\r\nТо заплачет как дитя.\r\n");
        
                    AddText(fs, newLine + "Сгенерированные символы:" + newLine);
                    for (int i = 32; i < 123; i++)  // Начинаем с пробела ASCII=32
                    {
                        // Добавляем символ в поток
                        AddText(fs, Convert.ToChar(i).ToString());
        
                        // Через каждые десять символов вставляем новую строку,
                        // когда i делится без остатка
                        //if (i % 10 == 0)  // Вариант
                        if (Math.IEEERemainder(Convert.ToDouble(i), 10) == 0)
                        {
                            // Вариант с библиотечным свойством
                            //AddText(fs, newLine);
                            // Вариант в управляющих символах
                            //AddText(fs, "\r\n");  
                            // Вариант в шестнадцатиричных кодах ASCII
                            //AddText(fs, Convert.ToChar(0x0D).ToString() +
                            //    Convert.ToChar(0x0A).ToString());
                            // Вариант в целых кодах ASCII
                            AddText(fs, 
                                Convert.ToChar(13).ToString() + // Попробуйте закомментировать эту строку!!!
                                Convert.ToChar(10).ToString());
                        }
                    }
                } // Здесь, по окончании инструкции using поток автоматически закрывается
        
                // Открываем файловый поток для чтения и обрабатываем его
                //using (FileStream fs = File.OpenRead(path))   // Вариант
                using(Stream fs = new FileInfo(path).OpenRead())    // Вариант для разнообразия
                {
                    byte[] bytes = new byte[fs.Length]; // Заготавливаем байтовый массив
                    int count = fs.Read(bytes, 0, bytes.Length);// Заполняем массив из файла
        
                    UTF8Encoding tmp = new UTF8Encoding(true);// Объект кодировки символов
                    String str = tmp.GetString(bytes);// Преобразуем массив в строку
                    Console.WriteLine(str);// Печатаем на консоль
        
                    Console.WriteLine("\nВсего прочитано байт: {0}", count);
                    Console.WriteLine("Из них отображаемых символов: {0}", str.Length);
                } // Здесь, по окончании инструкции using поток автоматически закрывается
        
                // Печатаем ASCII 'перевод строки' - 'возврат каретки'
                int a = '\r', b = '\n';
                String ascii = String.Format("Управляющие символы: ");
                ascii += String.Format("\\r={0}, \\n={1}", a, b);
                Console.WriteLine(ascii);
        
                // Проверяем строку NewLine
                byte[] bts = new ASCIIEncoding().GetBytes(newLine);
                a = bts[0]; b = bts[1];
                ascii = String.Format("Управляющие символы NewLine: ");
                ascii += String.Format("\\r={0}, \\n={1}", a, b);
                Console.WriteLine(ascii);
        
                Console.ReadLine();
            }
        
            private static void AddText(FileStream fs, string value)
            {
                // Преобразуем строку в массив байтов через объект кодировки символов
                byte[] bytes = new UTF8Encoding(true).GetBytes(value);
                // Записываем в открытый поток
                fs.Write(bytes, 0, bytes.Length);
            }
        }
    }
  • Запустите приложение - результат должен быть таким
  • Разберитесь с кодом
  • Интересно отметить следующее... Если не записывать в файл управляющий символ ' \r ' - возврат каретки, а только ' \n ' - новая строка, то при чтении файла на консоль функция Console.WriteLine() этого не заметит, а текстовый редактор Notepad заметит (попробуйте). Это значит, что при побайтовой записи нужно полностью обеспечивать документ всей необходимой информацией, не надеясь на умные устройства вывода. Альтернативной заменой строки " \r\n " может служить статическое свойство System.Environment.NewLine, которое означает то же самое и в приведенном коде частично используется.

    Всякий раз, когда мы открываем файл через объект FileStream, следует помнить, что по завершении работы этот файл следует закрыть, чтобы освободить дескриптор файла и вернуть его операционной системе. В противном случае файл будет считаться занятым и будет освобожден только после завершения приложения. Закрыть файл можно методом Close() объекта FileStream, либо открывать в инструкции using(), по завершению работы которой файл автоматически будет закрыт. Последний прием и был использован нами в приведенном выше коде.

    Упражнение 2. Подробности использования байтового потока FileStream

    В данном упражнении рассмотрим файловый поток более подробно. Класс FileStream обеспечивает практически все мыслимые потребности программного управления чтением/записью информации в файлы. После создания экземпляра потока (объекта) связанный с ним файл будет представлен этим объектом и все дальнейшие манипуляции с файлом будут выполняться через этот объект.

    Для создания экземпляра класс FileStream имеется множество перегрузок его конструктора, с которыми можно ознакомиться в MSDN. Наиболее типовая из них следующая:

    FileStream(string path, System.IO.FileMode mode, 
       System.IO.FileAccess access, System.IO.FileShare share)
  • path - относительный или абсолютный путь для файла, который будет представлен потоком
  • mode - значение перечисления FileMode, устанавливающее режим открытия или создания файла
  • Append - открывает файл, если он существует, и переходит в его конец, либо создает новый файл. Этот режим может быть использован только в сочетании с доступом FileAccess.Write. Попытка чтения или перемещения указателя на другую позицию генерирует исключение
  • Create - создает новый файл и открывает его для записи. Если файл уже существует, то он будет перезаписан на пустой
  • CreateNew - создает новый файл и открывает его для записи. Если файл уже существует, генерируется исключение IOException
  • Open - открывает существующий файл в режиме, определяемом перечислением FileAccess. Если указанный файл не существует, генерируется исключение FileNotFoundException
  • OpenOrCreate - открывает существующий или создает новый файл с режимом доступа, определяемым перечислением FileAccess
  • Truncate - открывает существующий файл для записи и усекает его длину до нуля байт. При попытке чтения генерируется исключение
  • access - значение перечисления FileAccess, устанавливающее режим доступа к файлу. Оно влияет на значения флагов CanRead - возможно чтение, CanWrite - возможна запись, CanSeek - возможен параллельный доступ к дисковому файлу
  • Read - только для чтения
  • ReadWrite - для чтения и записи
  • Write - только для записи
  • share (разделять, делить, совместно использовать) - значение перечисления FileShare, устанавливающее режим доступа к тому же самому файлу других процессов или объектов FileStream
  • Delete - разрешает последующее удаление файла
  • Inheritable - делает дескриптор файла наследуемым дочерними процессами. Этот режим непосредственно не поддерживается Win32
  • None - отключает режим разделения и переводит файл в монопольный доступ только текущего процесса
  • Read - открывает совместный доступ только для чтения
  • ReadWrite - открывает совместный доступ для чтения и записи
  • Write - открывает совместный доступ только для записи
  • Не стоит забывать, что класс FileStream имеет в своем распоряжении и весь набор членов, наследуемых от базового класса Stream.

  • В панели Solution Explorer вызовите контекстное меню для узла решения (не проекта!), командой Add/New Project добавьте новый проект с именем App2 типа консольного приложения и назначьте его стартовым
  • Заполните файл Program.cs следующим кодом, иллюстрирующим работу с байтовым потоком FileStream
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен
    using System.IO;// Для потоков
        
    namespace App2
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.ForegroundColor = ConsoleColor.White;
                Console.Title = "Упражнение 2. Файловые потоки";
        
                new MyClass().Foo();
        
                Console.ReadLine();// Задержка консоли
            }
        }
    }
        
    namespace App2
    {
        class MyClass
        {
            public void Foo()
            {
                byte[] bytes;   // Только объявили
        
                // Сохраняемые данные
                string sData = "Привет всем!";  // В Unicode займет 12*2 байт
                int iData1 = (int)12345;        // 4 байта
                int iData2 = int.MaxValue;      // 4 байта
                float fData = 67F;              // 4 байта
                double dData = 89D;             // 8 байт
        
                // Сохраняем значения в файле
                String fileName = "Data.dat";// Без пути - будет создан в каталоге сборки
                FileStream fileStream = null;
                try
                {
                    // Создаем или усекаем
                    fileStream = new FileStream(fileName, FileMode.Create);
        
                    // Сохраняем строку в универсальной кодировке, 
                    // занимающей 2 байта на каждый символ
                    bytes = new UnicodeEncoding().GetBytes(sData);  // Кодировка UTF-16
                    for (int i = 0; i < bytes.Length; i++)
                        fileStream.WriteByte(bytes[i]); // Побайтно
        
                    // Сохраняем первое целое число
                    bytes = new byte[sizeof(int)];
                    for (int i = 0; i < sizeof(int); i++)
                        bytes[i] = (byte)(iData1 >> 8 * i);  // Делим на байты, начиная с младшего
                    fileStream.Write(bytes, 0, sizeof(int));// Нулевой сдвиг относительно текущего
        
                    // Сохраняем второе целое число
                    bytes = new byte[sizeof(int)];
                    bytes = BitConverter.GetBytes(iData2);
                    fileStream.Write(bytes, 0, sizeof(int));// Нулевой сдвиг относительно текущего
        
                    // Сохраняем внутренне представление вещественного float (Single)
                    bytes = BitConverter.GetBytes(fData);// Массив той же длины int
                    fileStream.Write(bytes, 0, sizeof(float));// Нулевой сдвиг относительно текущего
        
                    // Сохраняем внутренне представление удвоенного double
                    bytes = new byte[sizeof(double)];
                    bytes = BitConverter.GetBytes(dData);
                    fileStream.Write(bytes, 0, sizeof(double));// Нулевой сдвиг относительно текущего
        
                    fileStream.Flush(); // Слить кэш (необязательно!)
                }
                finally
                {
                    if (fileStream != null)
                        fileStream.Close();
                }
        
                // Читаем значения из файла и печатаем их на консоль
                try
                {
                    // Открываем только для чтения
                    fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
                    if (fileStream.CanSeek) // Поддерживает ли такую возможность
                        fileStream.Seek(0L, SeekOrigin.Begin);  // Необязательно!
        
                    // Читаем строку, зная, сколько нужно прочитать
                    bytes = new byte[sData.Length * 2];// Для UTF-16 это 2 байта на каждый символ
                    fileStream.Read(bytes, 0, sData.Length * 2);
                    Console.WriteLine((new UnicodeEncoding()).GetString(bytes));
        
                    // Далее идут байты для представления первого целого числа
                    bytes = new byte[sizeof(int)];
                    fileStream.Read(bytes, 0, sizeof(int));
                    int a = 0;
                    for (int i = 0; i < sizeof(int); i++)
                        a |= bytes[i] << 8 * i; // Заполняем все байты, начиная с младшего
                    Console.WriteLine(a);
        
                    // Далее идут байты для представления второго целого числа
                    fileStream.Read(bytes, 0, sizeof(int));
                    a = BitConverter.ToInt32(bytes, 0);
                    Console.WriteLine(a);
        
                    // Считываем и печатаем вещественное float 
                    // Побитовые операции к типам с плавающей точкой применять нельзя
                    bytes = new byte[sizeof(float)];
                    fileStream.Read(bytes, 0, sizeof(float));
                    float b = BitConverter.ToSingle(bytes, 0);
                    Console.WriteLine(b);
        
                    // Считываем и печатаем double 
                    bytes = new byte[sizeof(Double)];
                    fileStream.Read(bytes, 0, sizeof(double));
                    double c = BitConverter.ToDouble(bytes, 0);
                    Console.WriteLine(c);
        
                    // Измеряем длину потока, пока открыт
                    Console.WriteLine("==============\nВсего байт: {0}", fileStream.Length);
                }
                finally
                {
                    fileStream.Close();
                }
        
                // Измеряем длину файла
                Console.WriteLine("Длина файла: {0}", new FileInfo(fileName).Length);
            }
        }
    }

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

  • Запустите приложение App2 - результат будет таким
  • Упражнение 3. Сериализация данных в потоке FileStream

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

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

    Для поддержки сериализации объекту необходимо следующее:

  • Наличие атрибута Serializable перед объявлением класса (типа)
  • Поля класса должны быть сериализуемы, что для значимых (в отличие от ссылочных!) типов всегда выполнимо
  • Если сериализуется производный класс, то вся предшествующая цепочка наследования тоже должна состоять из сериализуемых классов
  • Для выполнения сериализации библиотека .NET Framework имеет три класса:

  • BinaryFormatter - сериализует объект в компактном двоичном представлении
  • SoapFormatter - сериализует объект в текстовое представление на основе XML
  • XmlSerializer - сериализует объект в текстовое представление на основе 'чистого' XML
  • Рассмотрим эти способы на предыдущем примере, чуток изменив сохраняемые данные.

  • Командой File/Add/New Project добавьте к решение новый проект согласно следующему снимку мастера
  • В панели Solution Explorer вызовите контекстное меню для узла App3 и выполните команду Set as StartUp Project, чтобы назначить проект стартовым
  • В панели Solution Explorer выделите узел App3 и щелкните на пиктограмме Properties, чтобы открыть панель конструктора проектов
  • На вкладке Application установите выпадающий список Output type в значение Console Application, включив параллельно окну формы еще и консоль для вывода информации
  • В режиме Design конструктора форм поместите на форму из панели Toolbox интерфейсные элементы управления и настройте их в соответствии с таблицей свойств
  • Элемент Свойство Значение
    Form Text Сериализация объектов
    LinkLabel (Name) saveBinary
    Text Serialize BinaryFormatter and save to file
    LinkLabel (Name) loadBinary
    Text Load file and deserialize BinaryFormatter
    LinkLabel (Name) saveSoap
    Text Serialize SoapFormatter and save to file
    LinkLabel (Name) loadSoap
    Text Load file and deserialize SoapFormatter
    LinkLabel (Name) saveXml
    Text Serialize XmlSerializer and save to file
    LinkLabel (Name) loadXml
    Text Load file and deserialize XmlSerializer

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

    Мы подготовили интерфейс пользователя, который больше менять не будем. Поэтому разумно будет его замкнуть, чтобы защитить от случайных изменений.

  • В режиме Design конструктора форм выполните команду Edit/Select All, а затем - команду Format/Lock Controls, элементы интерфейса станут недоступными для редактирования в графическом режиме проектирования
  • Последовательно двойным щелчком на каждом экземпляре объекта LinkLabel создайте заготовки обработчиков и их регистрацию для событий LinkClicked
  • Для тренировки мы уберем регистрацию обработчиков, созданную автоматически в файле Form1.Designer.cs, и перенесем ее в конструктор класса формы. Для этого

  • Внесите в заготовки обработчиков хотя бы комментарии, чтобы при выполнении следующего действия оболочка их не удалила
  • В режиме Design выделите на форме последовательно объекты LinkLabel, в панели Properties щелкните на пиктограмме Events и очистите поля события LinkClicked
  • Библиотечные классы BinaryFormatter, SoapFormatter, XmlSerializer находятся в отдельных сборках: mscorlib.dll, System.Runtime.Serialization.Formatters.Soap.dll и System.Xml.dll соответственно. Они сгруппированы в пространствах имен System.Runtime.Serialization.Formatters.Binary, System.Runtime.Serialization.Formatters.Soap и System.Xml.Serialization. Библиотека mscorlib.dll является основной и всегда подключена к проектам оболочки. Библиотека System.Xml.dll тоже подключается при создании проекта, а вот System.Runtime.Serialization.Formatters.Soap.dll нужно подключить самостоятельно.

  • В панели Solution Explorer вызовите контекстное меню для узла References проекта App3, командой Add Reference вызовите одноименное окно и подключите библиотеку System.Runtime.Serialization.Formatters.Soap.dll
  • В панели Solution Explorer вызовите контекстное меню для узла App3 и командой Add/Class добавьте к проекту новый файл с именем DataObject.cs, который заполните так
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    namespace App3
    {
        [Serializable()]    // Атрибут
        public class DataObject
        {
            // Сохраняемые данные
            string stringData = "Привет всем!";
            int integerData = (int)12345;       
            float floatData = 67F; 
            double doubleData = 89D;
            DateTime date;  // Инициализируем в конструкторе
        
            // Конструкторы
            public DataObject(string message)
            {
                this.stringData = message;
                this.date = DateTime.Now;
            }
            public DataObject()
            {
                this.date = DateTime.Now;
            }
        
            // Свойства доступа к полям
            public String StringData
            {
                get { return stringData; }
                set { stringData = value; }
            }
            public Int32 IntegerData
            {
                get { return integerData; }
                set { integerData = value; }
            }
            public Single FloatData
            {
                get { return floatData; }
                set { floatData = value; }
            }
            public Double DoubleData
            {
                get { return doubleData; }
                set { doubleData = value; }
            }
            public DateTime Date
            {
                get { return date; }
                set { date = value; }
            }
        }
    }
  • Модифицируйте файл Form1.cs, чтобы он стал таким
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Runtime.Serialization.Formatters.Binary;   // Для класса BinaryFormatter
    using System.Runtime.Serialization.Formatters.Soap;     // Для класса SoapFormatter
    using System.Xml.Serialization;                         // Для класса XmlSerializer
        
    namespace App3
    {
        public partial class Form1 : Form
        {
            String fileNameBinary = "DataBinary.dat";   // Без пути - будет создан в каталоге сборки
            String fileNameSoap = "DataSoap.dat";       // Без пути - будет создан в каталоге сборки
            String fileNameXml = "DataXml.dat";         // Без пути - будет создан в каталоге сборки
        
            public Form1()
            {
                InitializeComponent();
        
                // Настраиваем консоль
                Console.ForegroundColor = ConsoleColor.White;
                Console.Title = "Упражнение 3. Сериализация объектов";
        
                // Регистрируем обработчики вручную
                saveBinary.LinkClicked += saveBinary_LinkClicked;
                loadBinary.LinkClicked += loadBinary_LinkClicked;
                saveSoap.LinkClicked += saveSoap_LinkClicked;
                loadSoap.LinkClicked += loadSoap_LinkClicked;
                saveXml.LinkClicked += saveXml_LinkClicked;
                loadXml.LinkClicked += loadXml_LinkClicked;
            }
        
            private void saveBinary_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                FileMode mode = FileMode.Create;
                if (new FileInfo(fileNameBinary).Exists)
                    mode = FileMode.Append; // Будем добавлять!
        
                using (FileStream fs = new FileStream(fileNameBinary, mode))
                {
                    // Создаем объект с данными
                    DataObject data = new DataObject();
                    // Создаем форматировщик
                    BinaryFormatter formatter = new BinaryFormatter();
                    // Сериализуем объект в файл
                    formatter.Serialize(fs, data);
                }
            }
        
            private void loadBinary_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                if (!File.Exists(fileNameBinary))
                {
                    MessageBox.Show("Нет данных для отображения",
                        "Предупреждение", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                    return;
                }
        
                using (FileStream fs = new FileStream(fileNameBinary, FileMode.Open))
                {
                    // Создать форматировщик
                    BinaryFormatter formatter = new BinaryFormatter();
                    // Прочитать и десериализовать все экземпляры объекта в файле
                    Console.Clear();
                    while (fs.Position < fs.Length)
                    {
                        // Прочитать, десериализовать и переместить указатель на следующий объект
                        DataObject data = (DataObject)formatter.Deserialize(fs);
        
                        // Распечатать
                        Console.WriteLine(data.StringData);
                        Console.WriteLine(data.IntegerData);
                        Console.WriteLine(data.FloatData);
                        Console.WriteLine(data.DoubleData);
                        Console.WriteLine(data.Date.ToString());
                    }
                }
            }
        
            private void saveSoap_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                FileMode mode = FileMode.Create;
                if (new FileInfo(fileNameSoap).Exists)
                    mode = FileMode.Append; // Будем добавлять!
        
                using (FileStream fs = new FileStream(fileNameSoap, mode))
                {
                    // Создаем объект с данными
                    DataObject data = new DataObject();
                    // Создаем форматировщик
                    SoapFormatter formatter = new SoapFormatter();
                    // Сериализуем объект в файл
                    formatter.Serialize(fs, data);
                }
            }
        
            private void loadSoap_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                if (!File.Exists(fileNameSoap))
                {
                    MessageBox.Show("Нет данных для отображения",
                        "Предупреждение", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                    return;
                }
        
                using (FileStream fs = new FileStream(fileNameSoap, FileMode.Open))
                {
                    // Создать форматировщик
                    SoapFormatter formatter = new SoapFormatter();
                    // Прочитать все сериализованные объекты
                    Console.Clear();
                    while (fs.Position < fs.Length)
                    {
                        // Десериализовать
                        DataObject data = (DataObject)formatter.Deserialize(fs);
        
                        // Распечатать
                        Console.WriteLine(data.StringData);
                        Console.WriteLine(data.IntegerData);
                        Console.WriteLine(data.FloatData);
                        Console.WriteLine(data.DoubleData);
                        Console.WriteLine(data.Date.ToString());
                    }
                }
            }
        
            private void saveXml_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                // Создаем или усекаем файл 
                using (FileStream fs = new FileStream(fileNameXml, FileMode.Create))
                {
                    // Создаем объект с данными
                    DataObject data = new DataObject();
                    // Создаем форматировщик
                    XmlSerializer formatter = new XmlSerializer(typeof(DataObject));
                    // Сериализуем объект в файл
                    formatter.Serialize(fs, data);
                }
            }
        
            private void loadXml_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                if (!File.Exists(fileNameXml))
                {
                    MessageBox.Show("Нет данных для отображения",
                        "Предупреждение", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                    return;
                }
        
                using (FileStream fs = new FileStream(fileNameXml, FileMode.Open))
                {
                    // Создать форматировщик
                    XmlSerializer formatter = new XmlSerializer(typeof(DataObject));
                    Console.Clear();
                    // Прочитать и десериализовать
                    DataObject data = (DataObject)formatter.Deserialize(fs);
        
                    // Распечатать
                    Console.WriteLine(data.StringData);
                    Console.WriteLine(data.IntegerData);
                    Console.WriteLine(data.FloatData);
                    Console.WriteLine(data.DoubleData);
                    Console.WriteLine(data.Date.ToString());
                }
            }
        }
    }
  • Запустите приложение и испытайте все три способа - объект с данными сохраняется в файле и полностью восстанавливается
  • Обратите внимание на важную особенность приведенного вывода: показано время 92729 - симметричное число, хороший знак свыше ('верной дорогой идете товарищи')!

  • В панели Solution Explorer выделите узел App3, щелкните на пиктограмме Show All Files, выделите файлы DataBinary.dat, DataSoap.dat и DataXml.dat (если они созданы)
  • Командой Open контекстного меню панели Solution Explorer откройте эти файлы и ознакомтесь с форматом хранения сериализованной информации
  • Файловый поток FileStream все равно сам связывается с файлом, только он используется другим классом, выполняющим передачу данных в специфическом формате (сериализованных).

  • Разберитесь с кодом
  • Упражнение 4. Сжатие данных в потоке FileStream

    В данном упражнении познакомимся с техникой чтения/записи сжатых данных в файловом потоке FileStream, хотя то же самое будет справедливо и для некоторых других рабочих потоков. Так же как и ранее, с файлом будет связываться поток FileStream, но формат данных в потоке будут обеспечивать специализированные классы сборки System.dll:

  • System.IO.Compression.DeflateStream
  • System.IO.Compression. GZipStream
  • Оба класса-надстройки реализуют эффективные алгоритмы сжатия без потерь информации.

  • Командой File/Add/New Project добавьте к решение новый проект согласно следующему снимку мастера
  • В панели Solution Explorer вызовите контекстное меню для узла App4 и выполните команду Set as StartUp Project, чтобы назначить проект стартовым
  • В панели Solution Explorer выделите узел App4 и щелкните на пиктограмме Properties, чтобы открыть панель конструктора проектов
  • На вкладке Application установите выпадающий список Output type в значение Console Application, включив параллельно окну формы еще и консоль для вывода информации
  • В режиме Design конструктора форм поместите на форму из панели Toolbox четыре экземпляра кнопки LinkLabel, две текстовые метки Label и настройте их в соответствии с таблицей свойств
  • Элемент Свойство Значение
    Form Text Сжимающие потоки
    LinkLabel (Name) saveDeflate
    Text Compression DeflateStream and save to file
    LinkLabel (Name) loadDeflate
    Text Load file and decompression DeflateStream
    LinkLabel (Name) saveGZip
    Text Compression GZipStream and save to file
    LinkLabel (Name) loadGZip
    Text Load file and decompression GZipStream

    Интерфейс формы должен выглядеть так

    Мы подготовили интерфейс пользователя, который больше менять не будем и его следует замкнуть.

  • В режиме Design конструктора форм выполните команду Edit/Select All, а затем - команду Format/Lock Controls, элементы интерфейса станут недоступными для редактирования в графическом режиме проектирования
  • Последовательно двойным щелчком на каждом экземпляре объекта LinkLabel создайте заготовки обработчиков и их регистрацию для событий LinkClicked
  • Теперь осталось заполнить обработчики кодом, реализующим сжатие и декомпрессию данных при файловых операциях записи/чтения.

  • Щелкните правой кнопкой мыши в любом месте формы Form1 (или вне формы) и выполните команду View Code, чтобы вызвать редактор процедурного кода
  • Заполните файл Form1.cs следующим образом (код файла приведен полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.IO.Compression;
    using System.IO;
        
    namespace App4
    {
        public partial class Form1 : Form
        {
            String fileNameDeflate = "DataDeflate.dat";
            String fileNameGZip = "DataGZip.dat";
            String messageDeflate = "Привет всем от DeflateStream!";
            String messageGZip = "Привет всем от GZipStream!";
            const Int32 lenBuf = 4;
        
            public Form1()
            {
                InitializeComponent();
        
                // Настраиваем консоль
                Console.ForegroundColor = ConsoleColor.White;
                Console.Title = "Упражнение 4. Сжатие данных в потоке FileStream";
                Console.CursorVisible = false;
            }
        
            private void saveDeflate_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                using (FileStream fs = new FileStream(fileNameDeflate, FileMode.Create))
                {
                    // Погружаем файловый поток в оболочку сжимающего потока
                    DeflateStream compress = new DeflateStream(fs, CompressionMode.Compress);
                    // Преобразуем строковые данные в массив байт кодировки UTF-16
                    byte[] bytes = new UnicodeEncoding().GetBytes(messageDeflate);
                    // Записываем данные в файл через сжимающий поток
                    compress.Write(bytes, 0, bytes.Length);
        
                    // Сливаем кэш и закрываем
                    compress.Flush();
                    compress.Close();
                }
            }
        
            private void loadDeflate_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                if (!new FileInfo(fileNameDeflate).Exists)
                {
                    MessageBox.Show("Нет данных для отображения",
                        "Предупреждение", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                    return;
                }
        
                using (FileStream fs = new FileStream(fileNameDeflate, FileMode.Open, FileAccess.Read))
                {
                    // Создаем буфер нужной длины, обычно 1024b (1Кb)
                    byte[] buffer = new byte[lenBuf / 2 * 2];// Округляем до четного
                    // Погружаем файловый поток в оболочку декомпрессионного потока
                    DeflateStream decompress = new DeflateStream(fs, CompressionMode.Decompress);
        
                    int count = 0;
                    String msg = String.Empty;
                    // Читаем все данные через декомпрессионный поток
                    while ((count = decompress.Read(buffer, 0, buffer.Length)) != 0)
                        msg += new UnicodeEncoding().GetString(buffer, 0, count);
        
                    // Сливаем кэш и закрываем
                    decompress.Flush();
                    decompress.Close();
        
                    // Печатаем прочитанное 
                    Console.Clear();
                    Console.WriteLine(msg);
                }
            }
        
            private void saveGZip_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                using (FileStream fs = new FileStream(fileNameGZip, FileMode.Create))
                {
                    // Погружаем файловый поток в оболочку сжимающего потока
                    GZipStream compress = new GZipStream(fs, CompressionMode.Compress);
                    // Преобразуем строковые данные в массив байт кодировки UTF-16
                    byte[] bytes = new UnicodeEncoding().GetBytes(messageGZip);
                    // Записываем данные в файл через сжимающий поток
                    compress.Write(bytes, 0, bytes.Length);
        
                    // Сливаем кэш и закрываем
                    compress.Flush();
                    compress.Close();
                }
            }
        
            private void loadGZip_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                if (!new FileInfo(fileNameGZip).Exists)
                {
                    MessageBox.Show("Нет данных для отображения",
                        "Предупреждение", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                    return;
                }
        
                using (FileStream fs = new FileStream(fileNameGZip, FileMode.Open, FileAccess.Read))
                {
                    // Создаем буфер нужной длины, обычно 1024b (1Кb)
                    byte[] buffer = new byte[lenBuf / 2 * 2];// Округляем до четного
                    // Погружаем файловый поток в оболочку декомпрессионного потока
                    GZipStream decompress = new GZipStream(fs, CompressionMode.Decompress);
        
                    int count = 0;
                    String msg = String.Empty;
                    // Читаем все данные через декомпрессионный поток
                    while ((count = decompress.Read(buffer, 0, buffer.Length)) != 0)
                        msg += new UnicodeEncoding().GetString(buffer, 0, count);
        
                    // Сливаем кэш и закрываем
                    decompress.Flush();
                    decompress.Close();
        
                    // Печатаем прочитанное 
                    Console.Clear();
                    Console.WriteLine(msg);
                }
            }
        }
    }
  • Испытайте работу приложения и разберитесь с кодом
  • Один из результатов будет таким

    Упражнение 5. Буферизованная обертка BufferedStream

    Когда создается рабочий поток, связанный с каким-либо устройством, он работает синхронно с кодом приложения и для медленных устройств приложение будет ждать завершения соответствующих операций ввода/вывода данных. Буферизация таких операций в кэше памяти, где данные могут группироваться в большие порции, может существенно ускорить работу приложения. Для этого и существует обертка BufferedStream, в которую можно погрузить любой основной поток. Тогда все манипуляции с данными будут выполняться с помощью этого вспомогательного инструмента.

    Рассмотрим работу с буферизованным потоком на примере его совместного использования с файловым потоком FileStream.

  • Командой File/Add/New Project создайте новый проект App5 типа консольного приложения и назначьте его стартовым
  • Заполните файл Program.cs следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен
    using System.IO;
        
    namespace App5
    {
        class Program
        {
            static void Main(string[] args)
            {
                // Настраиваем консоль
                Console.ForegroundColor = ConsoleColor.White;
                Console.Title = "Упражнение 5. Буферизованная обертка BufferedStream";
        
                String fileName = "DataBuffered.dat";
                String message = "Привет всем от BufferedStream!";
        
                FileMode mode = FileMode.Create;
                if (File.Exists(fileName))
                    mode = FileMode.Append;
        
                // Пишем
                using (FileStream fs = new FileStream(fileName, mode))
                {
                    using (BufferedStream bs = new BufferedStream(fs, 1024 * 4))
                    {
                        // Применим кодировку, по умолчанию установленную в Windows
                        byte[] bytes = Encoding.Default.GetBytes(message);
                        // Дописываем побайтно в обертку
                        for (int i = 0; i < bytes.Length; i++)
                            bs.WriteByte(bytes[i]);
                        // Дополняем символами новой строки "\r\n"
                        bs.Write(Encoding.Default.
                            GetBytes(Environment.NewLine), 0, Environment.NewLine.Length);
                        /********** Вариант не для режима Append **********
                            // Записываем побайтно в обертку
                            while (bs.Position < bytes.Length)
                                bs.WriteByte(bytes[bs.Position]);
                        //************************************************/
                    }
                }
        
                // Читаем
                using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
                {
                    fs.Seek(0, SeekOrigin.Begin);// Необязательно
        
                    using (BufferedStream bs = new BufferedStream(fs))// По умолчанию 4096 bytes
                    {
                        byte[] bytes = new byte[bs.Length];
        
                        int result, i = 0;
                        while ((result = bs.ReadByte()) != -1)
                            bytes[i++] = (byte)result;
                        /*********** Вариант ****************
                            while (bs.Position < bytes.Length)
                                bytes[bs.Position] = (byte)bs.ReadByte();
                        //**********************************/
        
                        // Печатаем результат
                        String msg = Encoding.Default.GetString(bytes);
                        Console.Clear();
                        Console.WriteLine(msg);
                    }
                }
        
                Console.ReadLine();// Тормозим консольное окно
            }
        }
    }

    По умолчанию величина буфера в BufferedStream установлена размером 4 Kb, но в перегрузке конструктора мы вольны установить ее любой. В приведенном коде при каждом запуске информация записывается в файл в режиме добавления. Потоки мы явно не закрываем, поскольку используем инструкцию using().

  • Запустите приложение и испытайте его работу, разберитесь с кодом
  • Для нескольких запусков результат будет таким

    Упражнение 6. Классы-обертки BinaryWriter и BinaryReader для работы с встроенными типами C#

    Встроенными (элементарными, примитивными, базовыми, стандартными, простыми) типами C# называются типы, которые имеют ключевые слова в самом языке как псевдонимы ( alias ). Например, Int32 - int, Single - float, String - string, Double - double, Boolean - bool и т.д. Все они отностятся к типам значений и выведены из абстрактного класса ValueType. Такие типы являются стэковыми, т.е. в стэк функций при выполнении программы помещаются не адресные переменные, а сами данные. Они представляют собой структуры и обозначаются отдельными пиктограммами в панели Object Browser

    Для чтения и записи таких данных в байтовый поток используются обертки BinaryReader и BinaryWriter пространства имен System.IO библиотечной сборки mscorlib.dll (специализированные читатель и писатель). Эти классы расщепляют базовый тип на последовательность байт и передают ее прикрепленному основному потоку для чтения/записи в связанное с потоком устройство.

    Рассмотрим работу этих оберток на примере разнотипных данных, которые мы ранее уже использовали во втором упражнении. По прежнему, в качестве рабочего будем использовать файловый поток FileStream.

  • Добавьте к решению проект консольного приложения App6 и назначьте его стартовым
  • Заполните файл Program.cs следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен
    using System.IO;// Для потоков
        
    namespace App6
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.ForegroundColor = ConsoleColor.White;
                Console.Title = "Упражнение 6. Двоичный ввод/вывод";
                Console.CursorVisible = false;
        
                new MyClass().Foo();
        
                Console.ReadLine();// Задержка консоли
            }
        }
    }
        
    namespace App6
    {
        class MyClass
        {
            public void Foo()
            {
                // Сохраняемые данные
                string sData1 = "Привет всем!";
                string sData2 = "Привет студентам!";
                int iData1 = (int)12345;      
                int iData2 = int.MaxValue;     
                float fData = 67F;             
                double dData = 89D;           
        
                // Сохраняем значения в файле
                String fileName = "Data.dat";// Будет создан в каталоге сборки
        
                // Объявляем рабочий поток
                FileStream fs = null;
                try
                {
                    // Создаем или усекаем файл
                    fs = new FileStream(fileName, FileMode.Create);
                    // Подключаем к писателю
                    BinaryWriter data = new BinaryWriter(fs);
                    // Пишем данные, компилятор по типу сам 
                    // определяет нужную перегрузку метода
                    data.Write(sData1);
                    data.Write(sData2);
                    data.Write(iData1);
                    data.Write(iData2);
                    data.Write(fData);
                    data.Write(dData);
        
                    fs.Flush(); // Сплюнуть кэш (необязательно!)
                }
                finally
                {
                    if (fs != null)
                        fs.Close();
                }
        
                // Читаем значения из файла и печатаем их на консоль
                try
                {
                    // Открываем только для чтения
                    fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
                    if (fs.CanSeek) // Поддерживает ли такую возможность
                        fs.Seek(0L, SeekOrigin.Begin);  // Необязательно!
        
                    // Подключаем к читателю, считываем и печатаем
                    BinaryReader data = new BinaryReader(fs);
                    // Порядок чтения должен строго соответствовать порядку записи
                    Console.WriteLine(data.ReadString());
                    Console.WriteLine(data.ReadString());
                    Console.WriteLine(data.ReadInt32());
                    Console.WriteLine(data.ReadInt32());
                    Console.WriteLine(data.ReadSingle());
                    Console.WriteLine(data.ReadDouble());
        
                    // Измеряем длину потока, пока открыт
                    Console.WriteLine("==============\nВсего байт: {0}", fs.Length);
                }
                finally
                {
                    fs.Close();
                }
        
                // Измеряем длину файла
                Console.WriteLine("Длина файла: {0}", new FileInfo(fileName).Length);
            }
        }
    }
  • Разберитесь с кодом и запустите приложение - результат будет таким
  • Не стоит думать, что данные при записи разбиваются объектом BinaryWriter по такой же схеме, как мы это делали во втором упражнении. Если попробовать записать с помощью этого писателя, а прочитать побайтно как в Упражнении 2, то ничего не получится: у рассмотренных читателя и писателя свои форматы данных. Хотя, расщипление на байты соответствует длине типов (кроме string ), например, тип int состоит из 4 байтов и точно такой же длинной будет записан в поток.

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

    В данном упражнении мы продемонстрировали только простейшие возможности объектов BinaryReader и BinaryWriter. Они могут применяться и для реализации более сложных схем чтения/записи. Для этого имеется все необходимое: перегрузки конструкторов, методы последовательного доступа, методы сдвига указателя. Но мы их рассматривать не будем, поскольку наша основная задача - выполнить обзор потоков как можно шире, а впереди еще много вкусного.

    Упражнение 7. Байтовый поток MemoryStream

    Для определения MemoryStream приведем выдержку из MSDN, поскольку точнее и не скажешь: " Класс MemoryStream создает потоки, которые в качестве резервного хранилища имеют память, а не подключение к сети или диску. Класс MemoryStream инкапсулирует данные, хранимые в виде массива байтов без знака, инициализированного при создании объекта MemoryStream . Массив может быть создан пустым. К инкапсулированным данным в памяти можно получить доступ напрямую. Потоки памяти могут снижать потребность во временных буферах и файлах в приложении ".

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

    Конструктор или член Пояснения
    MemoryStream() Создает пустой поток, размеры которого можно изменять. Открывается для записи и чтения данных
    MemoryStream(byte[ ] buffer) Создает поток неизменяемого размера на базе массива, который сразу этим массивом инициируется. Открывается для записи и чтения данных
    MemoryStream(byte[ ] buffer, bool writable) Создает поток, основанный на внешнем массиве беззнаковых байт неизменяемого размера в качестве резервного хранилища. Параметр writable определяет, будет ли этот поток только для записи или двухсторонний - для записи и чтения (как предыдущий без параметра)
    MemoryStream(byte[ ] buffer, int index, int count) Создает поток, основанный на внешнем беззнаковом байтовом массиве фиксированной длины, из которого потоком контролируется только count байт, начиная с позиции index
    MemoryStream(byte[ ] buffer, int index, int count, bool writable) Создает поток, основанный на внешнем беззнаковом байтовом массиве фиксированной длины, из которого потоку принадлежит только count байт, начиная с позиции index. Параметр writable определяет, будет ли этот диапазон только для записи или двухсторонний - для записи и чтения (как предыдущий без параметра)
    MemoryStream(byte[ ] buffer, int index, int count, bool writable, bool publiclyVisible) Создает поток, основанный на внешнем беззнаковом байтовом буфере фиксированной длины, из которого потоком контролируется только count байт, начиная с позиции index. Параметр writable определяет, будет ли этот поток только для записи или двухсторонний - для записи и чтения. Если последний параметр установлен в true, то конролируемый потоком диапазон байт можно извлечь с помощью его метода GetBuffer()
    MemoryStream(int capacity) Создает поток для записи и чтения данных с внутренним байтовым буфером изменяемой длины и начальной емкостью capacity. Когда в поток добавляются элементы, его емкость Capacity автоматически увеличивается блоками посредством перераспределения
    void Flush() При выполнении этого метода содержимое внутреннего буфера, связанного с потоком, реально перепишется в целевое резервное хранилище (в базовый буфер)
    byte[ ] GetBuffer() Возвращает массив байт без знака из базового хранилища потока MemoryStream, как бы он не создавался
    int Read(byte[ ] buffer, int offset, int count) Читает из потока, начиная с индекса offset, count байт в заданный массив byte[ ]. Возвращает число фактически считанных байтов, которое может быть меньше, чем запрашивалось, если конец потока достигнут раньше. Может вернуть и нуль, если конец потока достигнут прежде, чем любые байты будут считаны
    int ReadByte() Читает текущий байт, на который установлен указатель, и передвигает курсор потока на следующую байтовую позицию. Возвращает содержимое считанного байта в формате Int32 (поэтому результат нужно явно приводить) или -1, если достигнут конец потока
    long Seek(long offset, System.IO.SeekOrigin loc) Устанавливает указатель потока на offset байт относительно начала, конца или текущей позиции. Возвращает новую позицию указателя относительно начала потока
    void SetLength(long value) Устанавливает новую длину потока, у которого внутренний буфер допускает изменение длины и поддерживает запись
    byte[ ] ToArray() Создает байтовый массив, заполняет его полным содержимым потока и возвращает ссылку на этот массив
    void Write(byte[ ] buffer, int offset, int count) Записывает в поток count байт, начиная с текущей позиции указателя потока, блок байтов из указанного buffer массива с данными, начиная с индекса offset этого массива
    void WriteByte(byte value) Пишет байт в текущую позицию потока и передвигает указатель
    void WriteTo(System.IO.Stream stream) Копирует все содержимое текущего потока памяти в указанный целевой поток, возможно и в файловый
    bool CanRead bool CanSeek bool CanWrite Свойства для чтения, подтверждающие поддерживаемые потоком возможности
    int Capacity Свойство получает или устанавливает число байт в потоке, которые он способен вместить, прежде чем потребуется изменить размер его внутреннего буфера
    long Length Число байт, которые поток фактически содержит
    long Position Свойство получает или устанавливает указатель на текущую позицию потока

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

  • Командой File/Add/New Project добавьте к решению новый проект с именем App7, заполнив окно мастера так
  • При открытом на редактирование файле Window1.xaml выполните команду Project/Set as StartUp Project, чтобы назначить новый проект стартовым
  • В панели Solution Explorer раскройте узел App7, двойным щелчком на папке Properties вызовите панель конструктора проектов и для вкладки Application установите выпадающий список Output type в значение Console Application, чтобы включить консольное окно
  • Вначале сделаем заготовку для выбора примеров.

  • Отредактируйте разметку файла Window1.xaml следующим образом
  • <Window x:Class="App7.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Упражнение 7. Поток MemoryStream" 
        Height="300" 
        Width="300"
        MinHeight="300"
        MinWidth="300"
        ResizeMode="CanResizeWithGrip"
        WindowStartupLocation="CenterScreen"
        Closed="Window_Closed"
        >
        <ListBox SelectionChanged="ListBox_SelectionChanged" 
                 ScrollViewer.VerticalScrollBarVisibility="Auto">
            <ListBox.Resources>
                <!--Selected color when the ListBox is selected-->
                <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Aqua" />
            </ListBox.Resources>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10397.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 1</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10398.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 2</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10402.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 3</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10423.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 4</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10418.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 5</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10440.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 6</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10434.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 7</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10436.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 8</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10438.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 9</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10424.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 10</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10408.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 11</Label>
            </StackPanel>
        </ListBox>
    </Window>
  • В панели Solution Explorer вызовите контекстное меню для узла App7 и командой Add/New Folder добавьте к корню проекта папку Smiles для хранения рисунков
  • В панели Solution Explorer вызовите контекстное меню для папки Smiles и командой Add/Existing Item скопируйте в нее все рисунки из папки Source/Smiles, прилагаемой к данной работе (предварительно установите фильтр диалогового окна в значение All Files )
  • Заполните файл процедурного кода Window1.xaml.cs заготовкой, чтобы в итоге он стал таким
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
        
    // Дополнительные пространства имен
    using System.IO;
        
    namespace App7
    {
        public partial class Window1 : Window
        {
            // Объект для сохранения параметров приложения
            Properties.Settings settings = new App7.Properties.Settings();
        
            public Window1()
            {
                // Настройка консоли
                Console.ForegroundColor= ConsoleColor.White;
                Console.CursorVisible = false;
                Console.Title = "Упражнение 7. Поток MemoryStream";
        
                InitializeComponent();
            }
        
            private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                // Очистим консоль перед новым примером
                //Console.Clear();
        
                switch (((ListBox)sender).SelectedIndex + 1)
                {
                    case 1: // Пример 1
                        Console.WriteLine("Пример 1");
                        Example1();
                        break;
                    case 2: // Пример 2
                        Console.WriteLine("Пример 2");
                        Example2();
                        break;
                    case 3: // Пример 3
                        Console.WriteLine("Пример 3");
                        Example3();
                        break;
                    case 4: // Пример 4
                        Console.WriteLine("Пример 4");
                        Example4();
                        break;
                    case 5: // Пример 5
                        Console.WriteLine("Пример 5");
                        Example5();
                        break;
                    case 6: // Пример 6
                        Console.WriteLine("Пример 6");
                        Example6();
                        break;
                    case 7: // Пример 7
                        Console.WriteLine("Пример 7");
                        Example7();
                        break;
                    case 8: // Пример 8
                        Console.WriteLine("Пример 8");
                        Example8();
                        break;
                    case 9: // Пример 9
                        Console.WriteLine("Пример 9");
                        Example9();
                        break;
                    case 10: // Пример 10
                        Console.WriteLine("Пример 10");
                        Example10();
                        break;
                    case 11: // Пример 11
                        Console.WriteLine("Пример 11");
                        Example11();
                        break;
                }
            }
        
            void Example1()
            {
            }
        
            void Example2()
            {
            }
        
            void Example3()
            {
            }
        
            void Example4()
            {
            }
        
            void Example5()
            {
            }
        
            void Example6()
            {
            }
        
            void Example7()
            {
            }
        
            void Example8()
            {
            }
        
            void Example9()
            {
            }
        
            void Example10()
            {
            }
        
            void Example11()
            {
            }
        
            private void Window_Closed(object sender, EventArgs e)
            {
                // Сохраняем параметры приложения
            }
        }
    }
  • Запустите приложение и испытайте работу заготовок вариантов, должно получиться примерно следующее
  • Конечно, в данном упражнении столько примеров у нас не наберется, но эта заготовка нам еще пригодится для выполнения других упражнений. Теперь можно приступать непосредственно к рассмотрению примеров использования потока MemoryStream, заключая код каждого из них в свою функцию ExampleX().

    Пример 1. Из MSDN

    Приведем пример из MSDN (http://msdn.microsoft.com/ru-ru/library/system.io.memorystream%28v=VS.90%29.aspx), который иллюстрирует некоторые аспекты применения класса MemoryStream.

  • В файле Window1.xaml измените заголовок списка для ' Пример 1 ' на ' Пример 1. Код из MSDN '
  • Заполните функцию Example1() следующим кодом
  • void Example1()
            {
                int count;
                byte[] byteArray;// Объявление итогового массива байтов, считанного из потока 
                char[] charArray;// Объявление итогового массива символов, считанного из потока
                UnicodeEncoding unicodeEncoding = new UnicodeEncoding();// Двухбайтный UTF-16
        
                // Генерируем исходные данные
                byte[] bytes1 = // Байтовый массив с допустимыми символами
                    unicodeEncoding.GetBytes("Недопустимые символы в пути файла:\r\n");
                byte[] bytes2 = // Байтовый массив с недопустимыми в путях символами
                    unicodeEncoding.GetBytes(System.IO.Path.GetInvalidPathChars());
        
                // Создает и открываем поток для чтения/записи с внутренним 
                // байтовым буфером изменяемой длины и нулевой начальной емкостью
                using (MemoryStream memoryStream = new MemoryStream())
                {
                    // Пишем в поток допустимые символы блоком 
                    memoryStream.Write(bytes1, 0, bytes1.Length);
                    // Пишем в поток недопустимые символы побайтно
                    count = 0;
                    while (count < bytes2.Length)
                    {
                        memoryStream.WriteByte(bytes2[count++]);
                    }
                    // Сбрасываем буфер
                    memoryStream.Flush();   // Необязательно, данных немного 
        
                    // Печатаем свойства потока на консоль
                    Console.WriteLine("\nСвойства потока MemoryStream:");
                    Console.WriteLine(
                        "Capacity = {0}, Length = {1}, Position = {2}\n",
                        memoryStream.Capacity.ToString(),  // Длина внутреннего буфера
                        memoryStream.Length.ToString(),    // Количество байт содержимого
                        memoryStream.Position.ToString()   // Текущая позиция указателя
                        );
        
                    // Печатаем исходные данные
                    Console.WriteLine("Исходные данные перед записью в MemoryStream:");
                    Console.Write(unicodeEncoding.GetString(bytes1));   // Здесь Write()
                    charArray = new char[unicodeEncoding.GetCharCount(
                        bytes2, 0, bytes2.Length)];
                    unicodeEncoding.GetDecoder().GetChars(
                        bytes2, 0, bytes2.Length, charArray, 0);
                    Console.WriteLine(charArray);
        
                    // Читаем содержимое потока
                    byteArray = new byte[memoryStream.Length]; // Создаем массив под все символы
                    // Устанавливаем указатель в начало потока
                    memoryStream.Seek(0, SeekOrigin.Begin);
                    // Читаем блоком первые 20 байт
                    count = memoryStream.Read(byteArray, 0, 20);
                    // Читаем остальное побайтно, наращивая счетчик 
                    while (count < memoryStream.Length)
                    {
                        byteArray[count++] =
                            Convert.ToByte(memoryStream.ReadByte());
                    }
        
                    // Преобразуем батовый массив в символьный и печатаем
                    Console.WriteLine("\nДанные, прочитанные из MemoryStream:");
                    charArray = new char[unicodeEncoding.GetCharCount(
                        byteArray, 0, count)];
                    unicodeEncoding.GetDecoder().GetChars(
                        byteArray, 0, count, charArray, 0);
                    Console.WriteLine(charArray);
                }
            }

    Приведенный код совсем несложный, подробно прокомментирован и в дальнейших пояснениях вряд ли нуждается.

  • Исполните Пример 1 - должен получиться такой результат
  • Обратите внимание на звуковой сигнал, который издает компьютер при выводе на консоль недопустимых управляющих символов. Когда для потока памяти не определен внешний неизменяемый байтовый массив, а работает внутренний буфер, то при необходимости его длина увеличивается дискретными блоками по 128 байт, удобными для тактовой обработки в регистрах процессора целиком. Емкость потока, которая автоматически увеличивается должным образом посредством перераспределения, может быть уменьшена с помощью явного задания свойства Capacity.

    Пример 2. Создание файловой библиотеки рисунков

    Сюжет предыдущего примера состоял в том, что приложение генерировало какую-то информацию, которую мы записывали в некоторый буфер потока MemoryStream, а затем извлекали ее для проверки, выводя на консоль. Более полезным местом применения MemoryStream будет избирательное извлечение информации из хранилища данных (файл или база данных), обработка ее и, либо представление пользователю, либо отправка на дальнейшее хранение.

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

    Поскольку объем кода задуманного примера предполагается большим, то вынесем его в отдельный файл частичного класса Window1.

  • В панели Solution Explorer вызовите контекстное меню для узла App7 и командой Add/Class создайте новый файл с именем Example2.cs, заготовку которого заполните так
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен
    using System.IO;
        
    namespace App7
    {
        partial class Window1
        {
            void Example2()
            {
                Console.WriteLine("Вынесенный в отдельный файл код функции Example2()");
            }
        }
    }
  • Удалите из файла Window1.xaml.cs каркас функции Example2(), поскольку он теперь в другом месте, и запустите приложение - заготовка Примера 2 работает нормально
  • В этой функции мы создадим код, упаковывающий рисунки в файловую библиотеку, поэтому

  • В файле Window1.xaml измените заголовок списка для ' Пример 2 ' на ' Пример 2. Создание файловой библиотеки '
  • Прежде всего добавим диалог смены каталога для перехода в папку с рисунками. Для этого воспользуемся библиотечным классом FolderBrowserDialog библиотеки System.Windows.Forms.dll. Поскольку мастер при создании заготовки проекта WPF не подключает ссылку на эту библиотеку автоматически, выполним это вручную.

  • В панели Solution Explorer вызовите контекстное меню для папки App7/References и командой Add Reference подключите к проекту библиотеку System.Windows.Forms.dll
  • Формат библиотечного файла выберем таким, как показано на рисунке

    Он определит и код создания библиотеки, который выполнит задачи в следующей последовательности:

  • В выбранном каталоге извлечем полные имена файлов рисунков и измерим их количество
  • Обработаем массив с именами файлов, удалив из них пути и расширения
  • Отсортируем массив имен файлов для быстрого их поиска в будущей библиотеке
  • Откроем файловый поток, запишем в него число рисунков ( block1 ) и отсортированные имена файлов рисунков ( block2 )
  • По отсортированным именам будем считывать рисунки и добавлять их в файловый поток ( block3 ). Одновременно в памяти будем формировать поток с информацией о локализации рисунков в файловом потоке ( block4 )
  • После завершения записи всех рисунков допишем в поток памяти позицию ( block5 ), с которой в библиотеке начнет располагаться block4, и сбросим этот поток памяти в файловый поток библиотеки
  • Размещение блока с рисунками ( block3 ) перед информацией о их локализации в библиотеке ( block4 ) обусловлена тем, что рисунки могут быть большими и их удобнее писать сразу в файл. А информацию о их месте расположения в файле постепенно сформировать в потоке памяти и добавить последней. Изложенная схема позволит в будущем быстро определить место нахождения нужного рисунка и извлечь его из библиотеки. Разумеется, подобных схем можно придумать множество, главное, чтобы они взаимно согласовывались при записи и чтении библиотечных данных.

  • В соответствии с вышесказанным модифицируйте файл Example2.cs следующим кодом (код файла приводится полностью, является окончательным и обжалованию не подлежит!)
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен
    using System.IO;
        
    namespace App7
    {
        using FORMS = System.Windows.Forms; // Псевдоним
        
        partial class Window1
        {
            FORMS.FolderBrowserDialog dialogFolder = new FORMS.FolderBrowserDialog();
            String nameLibrary = "Pictures.my.lib"; // Будет находиться вместе с рисунками
            String extensionPictures = "jpg";       // Расширения файлов рисунков
        
            void Example2()
            {
                // Диалог смены каталога
                String currentDirectory;    // Полный путь
                dialogFolder.Description = "Выберите каталог, где находятся файлы с рисунками";
                dialogFolder.ShowNewFolderButton = false;// Отключить кнопку создания новой папки
                dialogFolder.RootFolder = Environment.SpecialFolder.MyComputer;// Компьютер
                FORMS.DialogResult result = dialogFolder.ShowDialog();  // Запускаем диалог выбора папки
                // Если был отказ или диалог закрыт системной кнопкой
                if (result == FORMS.DialogResult.Cancel)    
                    return;
                currentDirectory = dialogFolder.SelectedPath;// Извлекаем новый каталог
        
                // Принудительная перерисовка окна WPF
                this.Height = this.ActualHeight + 1;
                this.Height = this.ActualHeight - 1;
        
                // Читаем полные имена всех файлов текущего каталога по заданному шаблону
                String[] files = Directory.GetFiles(currentDirectory, "*." + extensionPictures);
                if (files.Length == 0)
                {
                    FORMS.MessageBox.Show(
                        "Нет в выбранном каталоге файлов для упаковки",
                        "Предупреждение",
                        FORMS.MessageBoxButtons.OK,
                        FORMS.MessageBoxIcon.Information);
                    return;
                }
        
                // Получаем сокращенные имена файлов без пути и расширения
                for (int i = 0; i < files.Length; i++)
                {
                    String str = files[i].Substring(files[i].LastIndexOf('\\') + 1);
                    int len = str.LastIndexOf('.');
                    files[i] = str.Substring(0, len);
                }
                // Сортируем массив для красоты и порядка
                Array.Sort<String>(files);
                // Array.Sort<String>(files, new ComparerNames());//Альтернативный компаратор!!!!!!    
                // Создаем поток MemoryStream переменной длины для имен файлов
                MemoryStream memoryStream = new MemoryStream();
                if (!memoryStream.CanWrite) // Проверять необязательно!
                {
                    FORMS.MessageBox.Show("Не могу писать в MemoryStream");
                    return;
                }
                // Создаем писатель-оболочку для записи в MemoryStream элементарных типов
                BinaryWriter binaryWriter = new BinaryWriter(memoryStream);
                memoryStream.Seek(0, SeekOrigin.Begin); // Необязательно!
                // Первым записываем block1 для количества файлов
                binaryWriter.Write((Int32)files.Length);// Приводить тип необязательно, распознает сама 
                //!!!=4 Console.WriteLine(binaryWriter.BaseStream.Position);
                // Закачиваем все имена файлов (block2) из отсортированного массива
                for (int i = 0; i < files.Length; i++)
                    binaryWriter.Write((String)files[i]);// Приводить тип необязательно, распознает сама
                memoryStream.Flush();   // Сливаем буфер
        
                // Создаем файловый поток и сбрасываем в него накопленное
                FileStream fs = null;
                try
                {
                    // Создаем или усекаем файл в том же каталоге
                    fs = new FileStream(
                        Path.Combine(currentDirectory, nameLibrary), FileMode.Create);
                    memoryStream.WriteTo(fs);// Сбрасываем все из памяти в файл
        
                    fs.Flush(); // Слить кэш (необязательно!)
                }
                catch
                {
                    FORMS.MessageBox.Show("Не могу записать в файл " + nameLibrary);
                    fs.Close();
                    return;
                }
                // Закрываем поток-оболочку, которая закроет и базовый поток
                binaryWriter.Close();
        
                // Создаем поток памяти для записи block4 и block5
                MemoryStream dimension = new MemoryStream(
                    files.Length * (sizeof(long) + sizeof(int)) 
                    + sizeof(long));
                dimension.Seek(0, SeekOrigin.Begin);    // Необязательно!
                // Создаем поток-оболочку для записи пар "позиция-длина" в MemoryStream
                BinaryWriter dimensionWriter = new BinaryWriter(dimension);
        
                // Читаем отдельные рисунки и пишем сразу в библиотеку
                MemoryStream pictureStream;
                for (int i = 0; i < files.Length; i++)
                {
                    // Читаем файл с рисунком
                    byte[] bytes = File.ReadAllBytes(Path.Combine(
                        currentDirectory, files[i] + "." + extensionPictures));
        
                    // Формируем в памяти block4 для локализации рисунков
                    dimensionWriter.Write(fs.Position); // Позиция рисунка long в потоке
                    dimensionWriter.Write(bytes.Length);// Длина рисунка int
        
                    // Добавляем очередной рисунок в библиотеку (block3)
                    pictureStream = new MemoryStream(bytes);// Создаем на базе массива
                    pictureStream.WriteTo(fs);   // Добавляем рисунок в библиотечный файл
                    pictureStream.Close();
                }
                fs.Flush(); // Сбрасываем буфер
        
                dimensionWriter.Write(fs.Position); // Дописываем в память block5
                dimensionWriter.Flush();
        
                // Дописываем в библиотеку последними block4 и block5
                dimension.WriteTo(fs);
                dimensionWriter.Close();    // Надстройка закрывает и базовый поток
        
                fs.Close(); // При закрытии автоматически выполнится и Flush()
        
                // Оповещаем пользователя
                FORMS.MessageBox.Show("Библиотека " + nameLibrary +
                    " создана в каталоге:\n" + currentDirectory);
            }
        }
        
        // Альтернативный компаратор!!!!!!! 
        /**************************************************
        class ComparerNames : IComparer<String>
        {
            public int Compare(String x, String y)
            {
                int X, Y, XX, YY;
                int posX = x.IndexOf('_');
                string str1 = posX != -1 ? x.Substring(0, posX) : "";
                bool isNumber1 = int.TryParse(str1, out X);
        
                int posY = y.IndexOf('_');
                string str2 = posY != -1 ? y.Substring(0, posY) : "";
                bool isNumber2 = int.TryParse(str2, out Y);
        
                if (!isNumber1 || !isNumber2)
                    return x.CompareTo(y);
        
                if (X < Y)
                    return -1;
                else if (X == Y)
                {
                    str1 = x.LastIndexOf('d') != -1 ?
                        x.Substring(0, x.Length - 1) : x;
                    str1 = str1.Substring(posX + 1);
                    isNumber1 = int.TryParse(str1, out XX);
        
                    str2 = y.LastIndexOf('d') != -1 ?
                        y.Substring(0, y.Length - 1) : y;
                    str2 = str2.Substring(posY + 1);
                    isNumber2 = int.TryParse(str2, out YY);
        
                    if (!isNumber1 || !isNumber2)
                        return str1.CompareTo(str2);
        
                    if (XX < YY)
                        return -1;
                    else if (XX == YY)
                        if (x.LastIndexOf('d') != -1  y.LastIndexOf('d') != -1)
                            return 0;
                        else if (x.LastIndexOf('d') != -1)
                            return 1;
                        else
                            return -1;
                    else
                        return 1;
                }
                else
                    return 1;
            }
        }
        //************************************************/
    }

    Для сохранения параметров диалогового окна FolderBrowserDialog выполним следующее:

  • В файле Window1.xaml.cs добавьте код, выделенный в листинге
  • public partial class Window1 : Window
        {
            // Объект для сохранения параметров приложения
            Properties.Settings settings = new App7.Properties.Settings();
        
            public Window1()
            {
                // Настройка консоли
                Console.ForegroundColor= ConsoleColor.White;
                Console.CursorVisible = false;
                Console.Title = "Упражнение 7. Поток MemoryStream";
        
                InitializeComponent();
        
                // Читаем параметры, сохраненные ранее
                dialogFolder.SelectedPath = settings.dialogFolder;
            }
        
            .......................................................
        
            private void Window_Closed(object sender, EventArgs e)
            {
                // Сохраняем параметры приложения
                settings.dialogFolder = dialogFolder.SelectedPath;
                settings.Save();
            }
        }
  • В панели Solution Explorer для проекта App7 раскройте узел Properties и двойным щелчком на файле Settings.settings откройте окно установки параметров , которое заполните как показано на рисунке
  • Обратите внимание, что оболочка добавила к проекту файл конфигурации app.config, без которого механизм сохранения параметров работать не будет.

  • Запустите приложение, выберите в прилагаемом к работе каталоге Source подкаталог Images с рисунками *.jpg и сверните их в библиотеку Pictures.my.lib, которая разместится в том же подкаталоге
  • Разберитесь с приведенным кодом файла Example2.cs, который содержит подробные комментарии
  • Пример 3. Использование файловой библиотеки рисунков

    Чтобы воспользоваться созданной библиотекой, а заодно и проверить правильность работы того, что мы так горячо и бодро обсуждали, построим код, позволяющий пользователю извлекать упакованные рисунки и просматривать их. Здесь также без потоков не обойтись. Консольное окно нам здесь не поможет, окно со списком выбора примеров - тоже, придется создать новое окно для управления рисунками. Код вызова нового окна поместим в функцию Example3, а само окно в отдельном файле. Окно просмотра рисунков построим по технологии WPF - там ' красивше ' смотрится растровая графика.

  • В панели Solution Explorer выделите узел проекта App7 и командой меню оболочки Project/Add Window добавьте новое окно WPF с именем Window3.xaml
  • Немного обленился описывать, поэтому выдам готовое, по коду (по ходу) легко разберетесь...

  • Заполните в файле Window1.xaml.cs функцию Example3() следующим кодом (приводится полностью)
  • void Example3()
            {
                // Предотвращение повторного создания дочернего окна
                bool existsWindow = false;
                foreach (Window window in Application.Current.Windows)// Перебираем коллекцию
                {
                    if (window.Name == "_Window3")
                    {
                        // Нашли среди существующих 
                        existsWindow = true;
                        window.Activate();// Сдвинуть на передний план
                        break;// Прервать перебор
                    }
                }
        
                if (!existsWindow)// Если не существует, то создать и показать
                {
                    Window3 wnd = new Window3();
                    // Перед показом проверяем, выбрана ли библиотека
                    if (wnd.NameLibrary != String.Empty)
                        wnd.Show();    // Показываем окно Window3
                    else
                        wnd.Close();   // Закрываем окно Window3
                }
            }
  • В файле Window1.xaml измените заголовок списка для ' Пример 3 ' на ' Пример 3. Использование файловой библиотеки '
  • Заполните файл Window3.xaml следующей интерфейсной разметкой (приводится полностью)
  • <Window x:Class="App7.Window3"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Использование файловой библиотеки рисунков" 
        Height="300"
        Width="470"
        MinHeight="300"
        MinWidth="450" 
        Name="_Window3" 
        Closing="Window_Closing"
        >
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="100" />
            </Grid.ColumnDefinitions>
            <Border Background="Black">
                <Image Name="image" Stretch="Uniform" />
            </Border>
            <ListBox Name="listBox" Grid.Column="1" Padding="5,0"
                     ScrollViewer.VerticalScrollBarVisibility="Visible"
                     SelectionChanged="listBox_SelectionChanged">
            </ListBox>
        </Grid>
    </Window>
  • Заполните файл Window3.xaml.cs следующим процедурным кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
        
    // Дополнительные пространства имен
    using FORMS = System.Windows.Forms; // Псевдоним
    using System.IO;
    using System.Configuration;         // Для SettingsProperty
    using IO = System.IO;               // Псевдоним для адресации Path
        
    namespace App7
    {
        public partial class Window3 : Window
        {
            FileStream fs = null;   // Поле файлового потока для видимости в классе
            BinaryReader reader;    // Поле читателя для видимости в классе
            String nameLibrary;     // Опорное поле для свойства с именем библиотеки
            int count;              // Поле под число рисунков
            string[] names;         // Поле под имена рисунков
            long offsetHead;        // Поле начала блока локализации рисунков
            Properties.Settings settings; // Параметры
        
            // Конструктор
            public Window3()
            {
                InitializeComponent();
        
                // Запускаем диалог выбора библиотеки
                nameLibrary = SelectLibrary();
                // При отказе от выбора ничего не запускаем,
                // а в главном окне Window1 закрываем это окно 
                if (nameLibrary != String.Empty)
                    ExecuteWindow();
            }
        
            // Извлекаем рисунки и управляем показом
            private void ExecuteWindow()
            {
                // Создаем файловый поток библиотеки
                fs = new FileStream(
                    nameLibrary, FileMode.Open, FileAccess.Read, FileShare.Read);
        
                // Создаем читателя элементарных типов
                reader = new BinaryReader(fs);
                count = reader.ReadInt32();             // Возвращает int
                names = new String[count];
                for (int i = 0; i < count; i++)
                    names[i] = reader.ReadString();
                fs.Seek(-sizeof(long), SeekOrigin.End); // Отступаем от конца
                offsetHead = reader.ReadInt64();        // Возвращает long
        
                // Привязываем массив имен рисунков к списку
                listBox.ItemsSource = names;
                // Выделяем первый элемент списка, чтобы вызвать SelectionChanged
                listBox.SelectedIndex = 0;
                listBox.Focus();
            }
        
            // При смене индекса списка показывает выбранный рисунок
            private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                /*
                // Вариант 1. Извлечение рисунка по индексу
                int index = ((ListBox)sender).SelectedIndex;
                ReadOnIndex(index);
                */
        
                // Вариант 2. Извлечение рисунка по имени
                string name = ((ListBox)sender).SelectedItem.ToString();
                ReadOnName(name);
            }
        
            // Вариант 1. Извлечение рисунка по индексу
            private void ReadOnIndex(int index)
            {
                // Локализуем рисунок по индексу
                if (index < 0 || index >= count) return;// Затычка
                fs.Seek(offsetHead + index * (sizeof(long) + sizeof(int)), SeekOrigin.Begin);
                long offset = reader.ReadInt64();   // Позиция рисунка в потоке (long)
                int length = reader.ReadInt32();    // Длина рисунка (int)
        
                // Извлекаем рисунок из потока
                byte[] bytes = new byte[length];
                //fs.Read(bytes, (int)offset, length);
                fs.Seek(offset, SeekOrigin.Begin);
                for (int i = 0; i < length; i++)
                    bytes[i] = (byte)fs.ReadByte();
        
                // Отображаем рисунок пользователю
                BitmapImage bi = new BitmapImage();
                bi.BeginInit();
                bi.StreamSource = new MemoryStream(bytes);
                bi.EndInit();
                image.Source = bi;
            }
        
            // Вариант 2. Извлечение рисунка по имени
            private void ReadOnName(string name)
            {
                // По имени рисунка ищем его индекс в массиве names[]
                int index = -1;
                // Производится обход всего массива Array ПОДРЯД, 
                // начиная с первого элемента и заканчивая последним.
                // Одномерный поиск половинного деления, реализуемый
                // методом Array.BinarySearch() применять нельзя!!!
                String find = Array.Find<String>(names,
                    delegate(String item)// Анонимный делегат
                    {
                        index++;
                        // При первом совпадении заканчивает поиск
                        return String.Equals(item, name);
                    });
                // В случае неудачи возвращает значение по умолчанию для типа шаблона
                if (find == String.Empty)   // Необязательно!
                {
                    return;// Запрошено несуществующее имя
                }
        
                // А дальше в точности как в ReadOnIndex(), но вместо 
                // прямого вызова повторим для самостоятельности кода
                //ReadOnIndex(index);
                // Локализуем рисунок по индексу
                if (index < 0 || index >= count) return;// Затычка
                fs.Seek(offsetHead + index * (sizeof(long) + sizeof(int)), SeekOrigin.Begin);
                long offset = reader.ReadInt64();   // Позиция рисунка в потоке (long)
                int length = reader.ReadInt32();    // Длина рисунка (int)
        
                // Извлекаем рисунок из потока
                byte[] bytes = new byte[length];
                //fs.Read(bytes, (int)offset, length);
                fs.Seek(offset, SeekOrigin.Begin);
                for (int i = 0; i < length; i++)
                    bytes[i] = (byte)fs.ReadByte();
        
                // Отображаем рисунок пользователю
                BitmapImage bi = new BitmapImage();
                bi.BeginInit();
                bi.StreamSource = new MemoryStream(bytes);
                bi.EndInit();
                image.Source = bi;
            }
        
            // Свойство доступа, проверяется в Window1
            public String NameLibrary   
            {
                get { return nameLibrary; }
            }
        
            // Диалог выбора библиотеки. Возвращает
            // пусто, если библиотека не выбрана
            private String SelectLibrary()
            {
                settings = new App7.Properties.Settings();
                String path = settings.dialogOpen;
                String fileName = IO.Path.Combine(path, "Pictures.my.lib");
                  
                // Создаем и настраиваем диалог выбора файла
                FORMS.OpenFileDialog openFileDialog = new FORMS.OpenFileDialog();
                openFileDialog.InitialDirectory = path;
                openFileDialog.FileName = "";
                openFileDialog.Filter = "Library Files(*.my.lib)|*.my.lib";
                FORMS.DialogResult result = openFileDialog.ShowDialog();
                if (result != FORMS.DialogResult.Cancel)
                    fileName = openFileDialog.FileName; // Полное имя
                else
                    fileName = String.Empty;
        
                return fileName;
            }
        
            // Обработчик события Closing, срабатывает перед закрытием приложения
            private void Window_Closing(object sender, 
                System.ComponentModel.CancelEventArgs e)
            {
                // Закрываем библиотечный поток
                if (fs != null)
                    fs.Close(); 
        
                // Сохраняем параметры 
                if (nameLibrary != String.Empty)
                {
                    settings.dialogOpen = 
                        nameLibrary.Substring(0, nameLibrary.LastIndexOf('\\'));
                    settings.Save();
                }
            }
        }
    }
  • В панели Solution Explorer для проекта App7 раскройте узел Properties и двойным щелчком на файле Settings.settings откройте окно установки параметров, которое дополните еще одним параметром, как показано на рисунке
  • Запустите приложение - выберите ранее созданную библиотеку Pictures.my.lib - результат соответствует ожидаемому
  • Разберитесь с кодом примера, который содержит подробные комментарии
  • Пример 4. Создание файловой библиотеки рисунков с сохранением метаданных в OLE DB

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

    В этом примере поставим задачу 'отделить зерна от плевел' - рисунки хранить по прежнему в библиотечном файле, а метаданные разместить в базе данных типа OLE DB (тоже в файле, только 'по турецки'). Это плохая идея и мы ее позже исправим, но пока для тренировки это будет очень даже ничего. Известно, что БД представляет собой набор структурированных данных, поддерживаемый специальным API ( Application Programming Interface ), поэтому записывать и извлекать нужные сведения об основных данных нам будет гораздо легче. А насчет 'быстрее' - вопрос спорный, если не использовать пул ( pool ) соединений.

    Правда, классики могут взвизгнуть - опять 'обрывки каких-то материалов', не относящихся к теме потоков. Мол, "Хотели кока, а съели Кука!" (Высоцкий В.С.). Успокойтесь, "течет речечка по песочечку - золотишко моет!": тема остается, и ее более широкое название - программирование, а не Кама-Сутра или философия! Чем более рельефным будет изложение, тем больше извилин останется в голове, да и руки быстрее выпрямятся. При гладком повествовании со временем можно легко забыть не только начало, но и Мать родную (и ведь забывают...), в крутых поворотах жизни и загогулинах - все вспомнишь!

    Я не призываю слепо следовать за мной, а рекомендую выполнить и изучить: "делай как я, а потом и свое подоспеет". Непонятные места готового кода всегда можно выяснить в документации и на форумах. Главное - осмысливать, искать ( студент - ищущий знаний) и не падать духом - все придет с тренировкой.

    В решении поставленной задачи нужно выполнить 'совсем немного':

  • Создать саму базу данных
  • Упаковать рисунки в файл, а метаданные сохранить в БД
  • Запрашивать данные из соответствующих источников и показывать их на экране
  • Начнем с создания реляционной БД программным способом. Выберем наиболее простую - файловую БД типа Access, позволяющую использовать интерфейсы OLE D B ( Object Linking and Embedding, Databas e ). Нам необходимо определить ее структуру, включающую в себя хотя бы одну таблицу (одна и нужна). База данных будет иметь имя Pictures.my1.md b и содержать одну таблицу с именем MyTable (вначале назвал Local, а оказалось, что это служебное имя в API, - ох и помучился с отладкой!). Структура таблицы в типах OLE D B и объектной модели C # будет такой

    Структура таблицы MyTable
    Имя поля Тип OLE DB Тип объектной модели C# Размер
    Name Type Type DefinedSize
    FileName (уникальное) adWChar string 20 (определим сами, с запасом)
    Offset adInteger int 4 (предопределено)
    Length adInteger int 4 (предопределено)

    Целый тип adInteger имеет длину 4 байта и может адресовать 4Б = 256*256*256*127 = 2'130'706'432, т.е. примерно 2 гигабайта памяти, что для библиотеки разумной длины вполне достаточно. Интересно отметить, что в Office Access он называется длинным целым. Эта терминология соответствует 'старым' временам (когда деревья были большими...), когда long имел длину 4 байта, а int - всего 2 байта.

    У внимательного читателя при знакомстве с приведенной таблицей может возникнуть справедливый вопрос. Как так, переменная Offset в методе сдвига Seek() должна быть знаковой, соответствовать типу long C# и иметь длину 8 байт, и почему это вдруг нам подсовывают тип adInteger? Действительно, в API программного создания структуры таблиц БД, который мы в дальнейшем будем использовать, существует требуемый числовой тип и именуется он adBigInt. Но компилятор с ним почему-то упорно не хочет работать.

    Можно было бы пойти обходным путем: поле Offset объявить строкой длиной 8 символов (или более) и выполнять двухстороннюю конвертацию типов String/Int64 при каждой операции чтения/записи между полем таблицы и объектной моделью. Можно придумать и другие ухищрения. Но для учебного примера пусть все останется так как есть - мы не собираемся создавать файловые библиотеки тяжелея 2 Гб. Да и вообще, то что мы здесь делаем вместе, - это только иллюстрация возможностей и тренировка. Все остальное вы сделаете сами в реальной взрослой работе на производстве, но уже будучи вооруженными (и очень опасными!).

  • В файле Window1.xaml измените заголовок списка для элемента ' Пример 4 ' на ' Пример 4. Создание БД + файловой библиотеки '
  • База данных типа Access не требует установки на клиенте SQL -сервера. Создавать БД и таблицу в ней будем программно с помощью COM -объектов библиотек ADOX.dll и ADODB.dll. Управление данными в БД выполним с помощью классов ADO.NET.

  • В Solution Explorer выделите узел проекта App7 и добавьте к нему командой меню оболочки Project/Add Reference из вкладки COM ссылки на библиотеки ADOX.dll и ADODB.dll последних версий, как показано на снимке
  • Оболочка добавит в каталог ссылок проекта App7 две библиотеки

    Обратите внимание, что библиотека System.Data.dll, в которой находятся классы ADO.NET, уже подключена изначально мастером создания проектов. Почти в любом типе проекта предусмотрено автоматическое подключение System.Data.dll, что свидетельствует о популярности задач с использованием баз данных.

  • В панели Object Browser установите выпадающий список Browse в значение текущего решения и убедитесь, что классы COM -библиотек ADOX.dll и ADODB.dll упакованы в пространствах имен ADODB и ADOX. Эти префиксы мы и будем использовать в API перед именами классов при создании БД
  • В панели Solution Explorer добавьте к узлу App7 командой контекстного меню Add/Class новый файл с именем CreateLibraryAndDB.cs
  • Конечно же, к этому времени класс мною уже написан и отлажен, и я мог бы 'вывалить' его здесь сразу целиком. Но опять же, классики завизжат про непедагогичность, про то, что даже при обильном словоблудии рядом с 'непьющим' студентом должен стоять родитель с ремнем (или с ружьем). Поэтому выльем, но постепенно. Все равно 'пьющие' будут разбираться именно в 'живом' коде, а там комментариев хватает (как и вариантов).

    А, пусть визжат, вываливаю целиком, надоело описывать!

  • Заполните файл CreateLibraryAndDB.cs следующим кодом (приводится полностью)
  • using System;
    using System.Windows.Forms;
    using System.IO;
        
    // Пространства имен для ADO.NET
    using System.Data;
    using System.Data.OleDb;
    using System.Data.Common;
        
    namespace App7
    {
        class CreateLibraryAndDB
        {
            // Локальные поля
            String nameDB = "Pictures.my1.mdb",
                nameLibrary = "Pictures.my1.lib";
            string path, fileName,
                tableName = "MyTable";// Нельзя давать имя "Local"!!!
            String extensionPictures = "jpg";
        
            // Параметризованный конструктор
            public CreateLibraryAndDB(string path)
            {
                // Инициализируем поля
                this.path = path;
                fileName = Path.Combine(path, nameDB);
        
                // Удалим предыдущую базу данных, если она есть
                if (new FileInfo(fileName).Exists)
                    File.Delete(fileName);
            }
        
            // Конструктор по умолчанию. Необязательно!
            public CreateLibraryAndDB() { }
        
            // Строка соединения с БД
            private String ConnectionString(String fileName)
            {
                //string pathToFile = Application.StartupPath.ToString();
                string JetEngineString = "Provider=Microsoft.Jet.OLEDB.4.0;" +
                    "Jet OLEDB:Engine Type=5;Data Source=";
                return JetEngineString + fileName;
            }
        
            // Создание БД
            public bool CreateDatabase()
            {
                Object adoConnection = null;
                ADOX.CatalogClass catalog = new ADOX.CatalogClass();
        
                try
                {
                    catalog.Create(ConnectionString(fileName));
                    adoConnection = catalog.ActiveConnection;
                }
                catch (Exception ex)
                {
                    MessageBox.Show(String.Format("Ошибка в создании БД\n{0}", ex.Message),
                        "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return false;
                }
                finally
                {
                    if (catalog != null  adoConnection != null)
                    {
                        ((ADODB.Connection)adoConnection).Close();// Приводим к интерфейсу
                        /***********  Вариант  *********
                        adoConnection.GetType().InvokeMember(
                            "Close", System.Reflection.BindingFlags.InvokeMethod,
                            null, adoConnection, new object[0]);
                        //*****************************/
        
                        catalog = null;
                        adoConnection = null;
                    }
                }
        
                return true;// Успех, если дошли до этого места
            }
        
            // Создание таблицы
            public bool CreateTable()
            {
                ADODB.Connection conn = new ADODB.ConnectionClass();
                conn.Open(ConnectionString(fileName), null, null, 0);
                ADOX.Catalog catalog = new ADOX.CatalogClass();
                catalog.ActiveConnection = conn;
        
                try
                {
                    ADOX.TableClass table = new ADOX.TableClass();// Объект таблицы
                    table.ParentCatalog = catalog;  // Указали родителя для таблицы
                    table.Name = tableName;         // Имя таблицы
                    catalog.Tables.Append(table);   // Добавили в коллекцию таблиц
        
                    // Задаем параметры настройки таблицы
                    string[] columnNames =
                    {
                        "FileName",
                        "Offset",
                        "Length"
                    };
                    // Для adInteger размер предопределен, можно заменить нулями
                    int[] columnSizes = new int[] { 20, 0, 0 };
                    ADOX.DataTypeEnum[] columnTypes = new ADOX.DataTypeEnum[] 
                        {
                            ADOX.DataTypeEnum.adWChar,      // До 255 символов fixed
                            ADOX.DataTypeEnum.adInteger,    // adBigInt = 8Б знаковое, но не идет
                            ADOX.DataTypeEnum.adInteger     // 4Б = 256*256*256*127 = 2'130'706'432
                        };
        
                    // Настраиваем структуру таблицы 
                    ADOX.ColumnClass column;
                    for (int i = 0; i < columnSizes.Length; i++)
                    {
                        column = new ADOX.ColumnClass();
                        column.ParentCatalog = catalog;  // Указали родителя для столбца
                        column.Name = columnNames[i];
                        column.DefinedSize = columnSizes[i];
                        column.Type = columnTypes[i];
                        column.Attributes = ADOX.ColumnAttributesEnum.adColNullable;// Позволить пусто
                        column.Attributes = ADOX.ColumnAttributesEnum.adColFixed;// Фиксированная длина
                        table.Columns.Append(column, column.Type, column.DefinedSize);
                    }
        
                    // Делаем первый столбец уникальным (совпадения не допускаются)
                    // http://www.sqldev.org/sql-server-data-access/
                    // multi-column-primary-keys-in-access-using-adox-c-60766.shtml
                    ADOX.Table tbl = catalog.Tables[tableName];
                    tbl.Keys.Append(
                        "UniqueKeyLocal", ADOX.KeyTypeEnum.adKeyUnique, "FileName", null, null);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message.ToString());
                    return false;
                }
                finally
                {
                    conn.Close();// Приводим к интерфейсу
                }
        
                return true;// Успех, если дошли до этого места
            }
        
            // Создаем файловую библиотеку и наполняем БД данными
            public bool CreateData()
            {
                OleDbConnection conn = new OleDbConnection(ConnectionString(fileName));
                OleDbDataAdapter adapter = new OleDbDataAdapter(
                    new OleDbCommand("SELECT * FROM " + tableName, conn));
                // Загружаем данные БД в объектную модель таблицы
                DataTable table = new DataTable(tableName);
                // Заполняем объектную таблицу данными из БД (хоть и нет пока данных), 
                // но лучше ограничиться чтением схемы, если собираемся только добавлять
                adapter.Fill(table);
                /***********  Вариант  *********
                adapter.FillSchema(table, SchemaType.Source);
                //*****************************/
        
                // Читаем информацию о всех файлах выбранного каталога
                FileInfo[] filesInfo = new DirectoryInfo(path).
                    GetFiles("*." + extensionPictures);
                if (filesInfo.Length == 0)
                {
                    MessageBox.Show(
                        "Нет в выбранном каталоге файлов для упаковки",
                        "Предупреждение",
                        MessageBoxButtons.OK,
                        MessageBoxIcon.Information);
                    return false;
                }
        
                // Рисунки пишем в библиотеку, а их параметры - в объектную таблицу 
                FileStream fs = new FileStream(
                    Path.Combine(path, nameLibrary), FileMode.Create);
                using (fs)
                {
                    for (int i = 0; i < filesInfo.Length; i++)
                    {
                        // Получаем сокращенное имя файла без пути и расширения
                        String shortName = filesInfo[i].Name; // Уже без пути и есть!!!
                        int len = shortName.LastIndexOf('.');
                        shortName = shortName.Substring(0, len);// Убрали расширение
        
                        // Добавляем в объектную таблицу очередную строку
                        DataRow row = table.NewRow();// Новая строка таблицы
                        row["FileName"] = shortName;
                        row["Offset"] = (int)fs.Position;
                        row["Length"] = (int)filesInfo[i].Length;
                        table.Rows.Add(row);
                        //table.AcceptChanges();// Нельзя, изменения будут считаться принятыми!!!
        
                        // Читаем файл с рисунком и добавляем в библиотеку
                        FileStream fileStream = filesInfo[i].OpenRead();
                        int readByte;
                        while ((readByte = fileStream.ReadByte()) != -1)
                            fs.WriteByte((byte)readByte);
                        /***********  Вариант  *********
                        byte[] bytes = File.ReadAllBytes(
                            Path.Combine(filesInfo[i].DirectoryName, filesInfo[i].Name));
                        MemoryStream pictureStream = new MemoryStream(bytes);
                        pictureStream.WriteTo(fs);
                        pictureStream.Close();
                        //*****************************/
                    }
        
                    fs.Flush(); // Необязательно! Сбросится при закрытии потока 
                }
        
                // Записываем объектную таблицу в таблицу OLE DB
                try
                {
                    // Создаем построитель команд по настройке свойства SelectCommand
                    // адаптера. Адаптер сам сгенерирует нужные команды при обновлении
                    OleDbCommandBuilder commandBuilder =
                        new OleDbCommandBuilder(adapter);
                    /***********  Вариант  *********
                    OleDbCommandBuilder commandBuilder = 
                        new OleDbCommandBuilder();
                    adapter.InsertCommand = commandBuilder.GetInsertCommand();
                    adapter.DeleteCommand = commandBuilder.GetDeleteCommand();
                    adapter.UpdateCommand = commandBuilder.GetUpdateCommand();
                    //*****************************/
        
                    // Адаптер сам откроет соединение, сам его закроет и сам пометит
                    // объектную таблицу методом table.AcceptChanges() - изменения приняты
                    adapter.Update(table);
        
                    // Пометить как удаленный объект
                    //table.Clear();
                    //table.Dispose();
                }
                catch (DataException ex)
                {
                    MessageBox.Show(ex.Message);
                    return false;
                }
                finally
                {
                    conn.Close();
                }
        
                return true;// Успех, если дошли до этого места
            }
        
            /*  http://yandex.ru/yandsearch?p=1text=ADOX.CatalogClassclid=14586
            Byte[] getResource(String name)
            {
                BinaryReader br = new BinaryReader(this.GetType().Assembly.GetManifestResourceStream(name));
        
                Byte[] bytes = br.ReadBytes((int)br.BaseStream.Length);
                br.Close();
                return bytes;
            }
            */
        }
    }

    Построитель команд OleDbCommandBuilder может создавать команды обновления только для одной таблицы БД. При более изощренных ручных настройках адаптера данные для одной объектной таблицы можно собирать из разных таблиц БД и, соответственно, обновлять во многих. Но в этом случае OleDbCommandBuilder применять нельзя. Кроме того, он правильно работает только тогда, когда в загруженной таблице БД имеются ключевой или уникальный столбцы. Поэтому в функции CreateTable() при создании таблицы БД первый столбец мы назначили уникальным. Это правомерно, поскольку все равно в одном каталоге не может быть несколько одноименных файлов, а мы собираем рисунки из одного каталога.

    Столбцы, помеченные атрибутом adColNullable, могут быть не заполнены значениями и оставаться пустыми, что мы на всякий случай и предусмотрели. Экземпляры классов ADOX и ADODB могут адресоваться ссылками типа класса или наследуемого интерфейса, это мы тоже использовали в коде.

    Для заполнения созданной таблицы БД мы открываем соединение с ней и считываем все поля в объектную таблицу методом Fill() адаптера. Но поскольку таблица БД пустая, то фактически этот метод считывает только схему, настраивающую объектную таблицу. Поэтому вполне можно было бы обойтись методом FillSchema(). И вообще, когда мы хотим только добавлять данные в таблицу БД, а не редактировать их, то так лучше и поступать.

    Оба метода сами открывают соединение с БД и сами закрывают. Но если принудительно открыть соединение методом Open() объекта соединения, то упомянутые методы его не закроют и нужно явно вызывать метод Close() объекта соединения. Открытие соединения - медленная операция, но она вполне оправдана. С коллективным доступом к одной и той же БД оптимальным является отсоединенный режим, при котором доступ к БД открывается кратковременно на время загрузки или обновления данных. После чего немедленно приходится закрывать соединение, иначе база останется блокированной текущим пользователем.

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

  • В файле Window1.xaml.cs найдите функцию Example4() и заполните ее следующим вызывающим кодом (приводится полностью)
  • void Example4()
            {
                // Диалог выбора каталога
                dialogFolder = 
                    new System.Windows.Forms.FolderBrowserDialog();
                dialogFolder.Description = "Выберите каталог для создания БД";
                dialogFolder.ShowNewFolderButton = false;// Отключить кнопку создания новой папки
                dialogFolder.RootFolder = Environment.SpecialFolder.MyComputer;// Компьютер
                dialogFolder.SelectedPath = settings.dialogFolder;
                System.Windows.Forms.DialogResult result = dialogFolder.ShowDialog();
                // Если был отказ или диалог закрыт системной кнопкой
                if (result == System.Windows.Forms.DialogResult.Cancel)
                    return;
                
                // Извлекаем новый каталог
                String path = dialogFolder.SelectedPath;// Полный путь
                // Создаем структуру БД
                CreateLibraryAndDB create = new CreateLibraryAndDB(path);
                bool success;
                if (success = create.CreateDatabase())
                    success = create.CreateTable();
                if (!success)
                {
                    MessageBox.Show("Структура БД не создана");
                    return;
                }
        
                success = create.CreateData();
                if (success)
                    MessageBox.Show("Файловая библиотека и база данных созданы");
                else
                    MessageBox.Show("Файловая библиотека и база данных не созданы");
        
                // Принудительная перерисовка окна WPF
                this.Height = this.ActualHeight + 1;
                this.Height = this.ActualHeight - 1;
            }

    Здесь мы используем параметры того же диалога выбора каталога, каторый применяли ранее при создании файловой библиотеки. Возвращаемые функциями флаги позволяют вовремя прервать последовательное выполнение, если предыдущий этап закончился неудачей. Принудительная перерисовка окна WPF немного компенсирует недостатки этой новой технологии, поскольку после закрытия заслоняющего окна сообщения само WPF -окно почему-то не перерисовывается и остается размазанным.

  • Запустите пример и зайдите в папку Images, расположенную в прилагаемом к работе каталоге Source
  • Мы видим, что библиотечный файл Pictures.my1.lib и файловая БД Pictures.my1.mdb создаются и наполняются данными. Если открыть нашу БД в Office Access, то она будет такой

    Пока вроде бы все складывается так, как задумали. Мы получили какие-то результаты. Но правильные ли они? Чтобы уверенно это заявить, придется создавать еще один код, отображающий рисунки пользователю, как мы это делали после создания файловой библиотеки. А это опять программирование - т.е. опять именно то, что нам и нужно.

    Пример 5. Совместное применение файловой библиотеки рисунков и базы данных

    Для отображения упакованных в предыдущем примере рисунков применим отдельное окно WPF, подобное Window3.xaml, но отличающееся процедурной частью.

  • Закройте все текущие файлы командой Window/Close All Documents
  • В панели проводнике решений Solution Explorer вызовите контекстное меню для файла Window3.xaml и выполните команду Copy
  • Там же, вызовите контекстное меню для узла App7 самого проекта и выполните команду Paste
  • Там же, командой Rename переименуйте копию Copy of Window3.xaml в Window5.xaml
  • Откройте файл Window5.xaml, замените в нем заголовок окна и индекс 3 на 5, чтобы разметка стала такой
  • <Window x:Class="App7.Window5"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Использование БД + файловой библиотеки" 
        Height="300"
        Width="470"
        MinHeight="300"
        MinWidth="450" 
        Name="_Window5" 
        Closing="Window_Closing"
        >
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="100" />
            </Grid.ColumnDefinitions>
            <Border Background="Black">
                <Image Name="image" Stretch="Uniform" />
            </Border>
            <ListBox Name="listBox" Grid.Column="1" Padding="5,0"
                     ScrollViewer.VerticalScrollBarVisibility="Visible"
                     SelectionChanged="listBox_SelectionChanged">
            </ListBox>
        </Grid>
    </Window>
  • Удалите из файла Window5.xaml.cs ненужный пока процедурный код и добавьте в начало подключение пространств имен ADO.NET, чтобы осталась следующая заготовка, которую мы дальше и будем наполнять
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
        
    // Дополнительные пространства имен
    using FORMS = System.Windows.Forms; // Псевдоним
    using System.IO;
    using System.Configuration;         // Для SettingsProperty
    using IO = System.IO;               // Псевдоним для адресации Path
        
    // Дополнительные пространства имен для ADO.NET
    using System.Data;
    using System.Data.OleDb;
    using System.Data.Common;
        
    namespace App7
    {
        public partial class Window5 : Window
        {
            FileStream fs = null;   // Поле файлового потока для видимости в классе
            String nameLibrary;     // Опорное поле для свойства с именем библиотеки
            int count;              // Поле под число рисунков
            string[] names;         // Поле под имена рисунков
            Properties.Settings settings; // Параметры
        
            // Конструктор
            public Window5()
            {
                InitializeComponent();
        
                // Запускаем диалог выбора библиотеки
                nameLibrary = SelectLibrary();
                // При отказе от выбора ничего не запускаем,
                // а в главном окне Window1 закрываем это окно 
                if (nameLibrary != String.Empty)
                    ExecuteWindow();
            }
        
            // Извлекаем рисунки и управляем показом
            private void ExecuteWindow()
            {
            }
        
            // При смене индекса списка показывает выбранный рисунок
            private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
            }
        
            // Свойство доступа, проверяется в Window1
            public String NameLibrary   
            {
                get { return nameLibrary; }
            }
        
            // Диалог выбора библиотеки. Возвращает
            // пусто, если библиотека не выбрана
            private String SelectLibrary()
            {
                settings = new App7.Properties.Settings();
                String path = settings.dialogOpen;
                String fileName = IO.Path.Combine(path, "Pictures.my1.lib");
                  
                // Создаем и настраиваем диалог выбора файла
                FORMS.OpenFileDialog openFileDialog = new FORMS.OpenFileDialog();
                openFileDialog.InitialDirectory = path;
                openFileDialog.FileName = "";
                openFileDialog.Filter = "Library Files(*.my1.lib)|*.my1.lib";
                FORMS.DialogResult result = openFileDialog.ShowDialog();
                if (result != FORMS.DialogResult.Cancel)
                    fileName = openFileDialog.FileName; // Полное имя
                else
                    fileName = String.Empty;
        
                return fileName;
            }
        
            // Обработчик события Closing, срабатывает перед закрытием приложения
            private void Window_Closing(object sender, 
                System.ComponentModel.CancelEventArgs e)
            {
                // Закрываем библиотечный поток
                if (fs != null)
                    fs.Close(); 
        
                // Сохраняем параметры 
                if (nameLibrary != String.Empty)
                {
                    settings.dialogOpen = 
                        nameLibrary.Substring(0, nameLibrary.LastIndexOf('\\'));
                    settings.Save();
                }
            }
        }
    }
  • В файле Window1.xaml измените заголовок списка для ' Пример 5 ' на ' Пример 5. Использование БД + файловой библиотеки '
  • Заполните в файле Window1.xaml.cs функцию Example5() следующим кодом (окончательный, приводится полностью)
  • void Example5()
            {
                // Предотвращение повторного создания дочернего окна
                bool existsWindow = false;
                foreach (Window window in Application.Current.Windows)// Перебираем коллекцию
                {
                    if (window.Name == "_Window5")
                    {
                        // Нашли среди существующих 
                        existsWindow = true;
                        window.Activate();// Сдвинуть на передний план
                        break;// Прервать перебор
                    }
                }
        
                if (!existsWindow)// Если не существует, то создать и показать
                {
                    Window5 wnd = new Window5();
                    // Перед показом проверяем, выбрана ли библиотека
                    if (wnd.NameLibrary != String.Empty)
                        wnd.Show();    // Показываем окно Window5
                    else
                        wnd.Close();   // Закрываем окно Window5
                }
            }
  • Запустите приложение - окно Window5 запускается, но пока не функционирует
  • Добавьте в файл Window5.xaml.cs к членам класса Window5 функцию, возвращающую строку соединения с БД
  • public partial class Window5 : Window
        {
            ........................................................
            // Строка соединения с БД 
            private String ConnectionString(String fileName)
            {
                //string pathToFile = Application.StartupPath.ToString();
                string JetEngineString = "Provider=Microsoft.Jet.OLEDB.4.0;" +
                    "Jet OLEDB:Engine Type=5;Data Source=";
                return JetEngineString + fileName;
            }
            ........................................................
        }
  • Заполните функцию ExecuteWindow() следующим кодом
  • // Извлекаем рисунки и управляем показом 
            private void ExecuteWindow()
            {
                // Формируем имя БД из имени файловой библиотеки
                string nameDB = nameLibrary.Substring(0, nameLibrary.LastIndexOf('.')) + ".mdb";
        
                // Читаем некоторые данные из БД
                OleDbConnection connection = new OleDbConnection(ConnectionString(nameDB));
                OleDbCommand command = new OleDbCommand("SELECT COUNT(*) FROM MyTable");
                command.Connection = connection;
                connection.Open();
                // Извлекаем число рисунков
                count = (int)command.ExecuteScalar(); 
                // Меняем команду и извлекаем имена рисунков
                command.CommandText = "SELECT FileName FROM MyTable";
                OleDbDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection);
                names = new string[count];
                int i = 0;
                foreach (DbDataRecord rec in reader)// Равносильно while(dataReader.Read())
                    names[i++] = ((string)rec[0]).Trim();// Сразу убираем пробелы
                // Соединение здесь закроет сам объект DataReader после прочтения всех данных
                // в соответствии с соглашением при его создании CommandBehavior.CloseConnection
        
                // Привязываем массив имен рисунков к списку
                listBox.ItemsSource = names;
                // Выделяем первый элемент списка, чтобы вызвать SelectionChanged
                listBox.SelectedIndex = 0;
                listBox.Focus();
            }
  • Разберитесь с кодом метода ExecuteWindow() и запустите пример - результат для ранее созданной БД Pictures.my1.mdb пока будет таким
  • Список с именами, полученными из БД Pictures.my1.mdb, функционирует нормально, но сами рисунки еще нужно извлечь из библиотеки Pictures.my1.lib и присоединить к элементу отображения Image.

  • Заполните заготовку обработчика listBox_SelectionChanged() следующим кодом
  • // При смене индекса списка показывает выбранный рисунок
            private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                // Формируем имя БД из имени файловой библиотеки
                string nameDB = nameLibrary.Substring(0, nameLibrary.LastIndexOf('.')) + ".mdb";
        
                // Создаем и настраиваем объект соединения с БД
                OleDbConnection connection = new OleDbConnection();
                connection.ConnectionString = ConnectionString(nameDB);
        
                // Создаем и настраиваем объект команды, параметризованной по имени рисунка
                OleDbCommand command = new OleDbCommand();
                command.Connection = connection;
                command.CommandType = CommandType.Text; // Необязательно! Установлено по умолчанию
                command.CommandText = "SELECT Offset, Length FROM MyTable WHERE FileName=?";
                command.Parameters.Add(new OleDbParameter());
                command.Parameters[0].Value = ((ListBox)sender).SelectedValue.ToString();// Имя рисунка
        
                // Извлекаем данные из БД 
                connection.Open();
                OleDbDataReader reader = command.ExecuteReader();
                reader.Read();  // Читаем только одну строку (она и будет всего одна)
                long offset = (int)reader["Offset"];
                int length = (int)reader["Length"];
                connection.Close(); // Раньше этого места закрывать нельзя!
        
                // Создаем и открываем один раз файловый поток библиотеки с рисункам
                if (fs == null)
                    fs = new FileStream(
                        nameLibrary, FileMode.Open, FileAccess.Read, FileShare.Read);
        
                // Извлекаем рисунок из потока
                byte[] bytes = new byte[length];
                fs.Seek(offset, SeekOrigin.Begin);
                for (int i = 0; i < length; i++)
                    bytes[i] = (byte)fs.ReadByte();
        
                // Отображаем рисунок пользователю
                BitmapImage bi = new BitmapImage();
                bi.BeginInit();
                bi.StreamSource = new MemoryStream(bytes);
                bi.EndInit();
                image.Source = bi;
            }
  • Разберитесь с кодом обработчика и запустите пример - результат для ранее созданной БД Pictures.my1.mdb и библиотеки Pictures.my1.lib соответствует ожидаемому
  • Пример 6. Создание базы данных OLE DB и заполнение ее рисунками

    Если уж мы ранее решили использовать базу данных для управления рисунками, то более конструктивным будет подход, когда сами рисунки тоже хранятся в базе данных. Чтобы его реализовать, по прежнему нужно разработать две утилиты: одна создает БД и заполняет ее рисунками из выбранного каталога; вторая извлекает эти рисунки из БД и показывает пользователю. В данном примере мы создадим первую утилиту (правда, без потоков), а в следующем - вторую (с потоком). Итак, приступим...

    Начнем с создания реляционной БД программным способом. База данных будет иметь имя Pictures.my2.md b и содержать одну таблицу с именем MyTable, структура которой в типах OLE D B и объектной модели C # будет такой

    Структура таблицы MyTable
    Имя поля Тип OLE DB Тип объектной модели C# Размер
    Name Type Type DefinedSize
    FileName (уникальное) adWChar string 20 (определим сами, с запасом)
    Picture adLongVarBinary (BLOB) byte[] 0 (неопределено)
  • В файле Window1.xaml измените заголовок списка для элемента ' Пример 6 ' на ' Пример 6. Создание базы данных с рисунками '
  • В панели Solution Explorer добавьте к узлу App7 командой контекстного меню Add/Class новый файл с именем CreateDB.cs и заполните его следующим кодом (приводится полностью)
  • using System;
    using System.Windows.Forms;
    using System.IO;
        
    // Пространства имен для ADO.NET
    using System.Data;
    using System.Data.OleDb;
    using System.Data.Common;
        
    namespace App7
    {
        class CreateDB
        {
            // Локальные поля
            String nameDB = "Pictures.my2.mdb";
            string path, fileName,
                tableName = "MyTable";// Нельзя давать имя "Local"!!!
            String extensionPictures = "jpg";
        
            // Параметризованный конструктор
            public CreateDB(string path)
            {
                // Инициализируем поля
                this.path = path;
                fileName = Path.Combine(path, nameDB);
        
                // Удалим предыдущую базу данных, если она есть
                if (new FileInfo(fileName).Exists)
                    File.Delete(fileName);
            }
        
            // Строка соединения с БД
            private String ConnectionString(String fileName)
            {
                //string pathToFile = Application.StartupPath.ToString();
                string JetEngineString = "Provider=Microsoft.Jet.OLEDB.4.0;" +
                    "Jet OLEDB:Engine Type=5;Data Source=";
                return JetEngineString + fileName;
            }
        
            // Создание БД
            public bool CreateDatabase()
            {
                Object adoConnection = null;
                ADOX.CatalogClass catalog = new ADOX.CatalogClass();
        
                try
                {
                    catalog.Create(ConnectionString(fileName));
                    adoConnection = catalog.ActiveConnection;
                }
                catch (Exception ex)
                {
                    MessageBox.Show(String.Format("Ошибка в создании БД\n{0}", ex.Message),
                        "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return false;
                }
                finally
                {
                    if (catalog != null  adoConnection != null)
                    {
                        ((ADODB.Connection)adoConnection).Close();// Приводим к интерфейсу
                        /***********  Вариант  *********
                        adoConnection.GetType().InvokeMember(
                            "Close", System.Reflection.BindingFlags.InvokeMethod,
                            null, adoConnection, new object[0]);
                        //*****************************/
        
                        catalog = null;
                        adoConnection = null;
                    }
                }
        
                return true;// Успех, если дошли до этого места
            }
        
            // Создание таблицы
            public bool CreateTable()
            {
                ADODB.Connection conn = new ADODB.ConnectionClass();
                conn.Open(ConnectionString(fileName), null, null, 0);
                ADOX.Catalog catalog = new ADOX.CatalogClass();
                catalog.ActiveConnection = conn;
        
                try
                {
                    ADOX.TableClass table = new ADOX.TableClass();// Объект таблицы
                    table.ParentCatalog = catalog;  // Указали родителя для таблицы
                    table.Name = tableName;         // Имя таблицы
                    catalog.Tables.Append(table);   // Добавили в коллекцию таблиц
        
                    // Задаем параметры настройки таблицы
                    string[] columnNames =
                    {
                        "FileName",
                        "Picture"
                    };
                    // Неопределенный размер заменяем нулем
                    int[] columnSizes = new int[] { 20, 0 };
                    ADOX.DataTypeEnum[] columnTypes = new ADOX.DataTypeEnum[] 
                        {
                            ADOX.DataTypeEnum.adWChar,          // До 255 символов fixed
                            ADOX.DataTypeEnum.adLongVarBinary   // BLOB
                        };
        
                    // Настраиваем структуру таблицы 
                    ADOX.ColumnClass column;
                    for (int i = 0; i < columnSizes.Length; i++)
                    {
                        column = new ADOX.ColumnClass();
                        column.ParentCatalog = catalog;  // Указали родителя для столбца
                        column.Name = columnNames[i];
                        column.DefinedSize = columnSizes[i];
                        column.Type = columnTypes[i];
                        column.Attributes = ADOX.ColumnAttributesEnum.adColNullable;// Позволить пусто
                        //column.Attributes = ADOX.ColumnAttributesEnum.adColFixed;// Фиксированная длина
                        table.Columns.Append(column, column.Type, column.DefinedSize);
                    }
        
                    // Делаем первый столбец уникальным (совпадения не допускаются)
                    // http://www.sqldev.org/sql-server-data-access/
                    // multi-column-primary-keys-in-access-using-adox-c-60766.shtml
                    ADOX.Table tbl = catalog.Tables[tableName];
                    tbl.Keys.Append(
                        "UniqueKeyLocal", ADOX.KeyTypeEnum.adKeyUnique, "FileName", null, null);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message.ToString());
                    return false;
                }
                finally
                {
                    conn.Close();// Приводим к интерфейсу
                }
        
                return true;// Успех, если дошли до этого места
            }
        
            // Наполняем файловую БД данными
            public bool CreateData()
            {
                // Читаем информацию о всех файлах выбранного каталога
                FileInfo[] filesInfo = new DirectoryInfo(path).
                    GetFiles("*." + extensionPictures);
                if (filesInfo.Length == 0)
                {
                    MessageBox.Show(
                        "В выбранном каталоге нет файлов для упаковки в БД",
                        "Предупреждение",
                        MessageBoxButtons.OK,
                        MessageBoxIcon.Information);
                    return false;
                }
        
                // Сортируем объекты массива FileInfo по именам файлов
                Array.Sort<FileInfo>(filesInfo, 
                    delegate(FileInfo x, FileInfo y)// Анонимный делегат вместо функции обратного вызова
                    {
                        return String.Compare(x.Name, y.Name);
                    });
        
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection(ConnectionString(fileName));
                OleDbCommand cmd = new OleDbCommand();
                cmd.CommandType = CommandType.Text;  // Необязательно!
                cmd.CommandText = "SELECT * FROM " + tableName;
                cmd.Connection = conn;
                OleDbDataAdapter adapter = new OleDbDataAdapter();
                adapter.SelectCommand = cmd;
                new OleDbCommandBuilder(adapter);   // Донастраиваем адаптер на запись
        
                // Пишем построчно в объектную таблицу и затем сразу в БД,
                // чтобы экономить оперативную память, если рисунков много
                DataTable table = null;
                conn.Open();// Метод FillSchema() теперь уже не закроет!!!
                for (int i = 0; i < filesInfo.Length; i++)
                {
                    // Создаем и настраиваем объектную модель таблицы
                    if (table == null)
                    {
                        // Только один раз
                        table = new DataTable(tableName);
                        adapter.FillSchema(table, SchemaType.Source);
                    }
                    else   
                        table.Clear(); // Теперь только чистим, структура таблицы остается
        
                    // Получаем сокращенное имя файла без пути и расширения
                    String shortName = filesInfo[i].Name; // Уже без пути и есть!!!
                    int len = shortName.LastIndexOf('.');
                    shortName = shortName.Substring(0, len);// Убрали расширение
        
                    // Добавляем в строку имя рисунка (и порядковый номер, чтобы було!)
                    DataRow row = table.NewRow();
                    row["FileName"] = (i + 1).ToString() + ") " + shortName;
        
                    // Читаем файл с рисунком
                    byte[] bytes = File.ReadAllBytes(
                        Path.Combine(filesInfo[i].DirectoryName, filesInfo[i].Name));
                    /***********  Вариант  *********
                    FileStream fileStream = filesInfo[i].OpenRead();
                    byte[] bytes = new byte[fileStream.Length];
                    int readByte, count = 0;
                    while ((readByte = fileStream.ReadByte()) != -1)
                        bytes[count++] = (byte)readByte;
                    fileStream.Close();
                    //*****************************/
        
                    // Добавляем в строку сам рисунок, адресуясь по имени столбца
                    row["Picture"] = bytes;
                    table.Rows.Add(row);    // Скидываем в объектную таблицу
                    adapter.Update(table);  // Записываем эту одну строку в БД
                }
                // Удаляем объектную таблицу (Необязательно!)
                table.Dispose();
                // Закрываем соединение
                conn.Close();
        
                return true;// Успех, если дошли до этого места
            }
        }
    }

    Учитывая, что открытие соединения с БД является очень медленной операцией, в методе CreateData() перед вхождением в цикл заполнения БД рисунками мы открываем соединение только один раз. Когда соединение открыто, то методы адаптера FillSchema() и Fill() его не закрывают, поэтому после цикла нужно его закрыть самим. Экземпляр объекта построителя команд OleDbCommandBuilder нам нужен только для настройки адаптера на обновление данных в БД (по умолчанию последний настроен только на чтение), для этого нет необходимости создавать на него ссылку.

    Желательно, чтобы рисунки попадали в БД уже отсортированными по именам. Поэтому после получения из выбранного каталога всех объектов FileInfo, мы их предварительно сортируем и только потом считываем с диска файлы в установленном порядке. Для сортировки используем перегрузку типизированного статического метода с делегатом, чтобы не выносить отдельно другую статическую функцию - обратного вызова (компаратор). В ходе сортировки метод Sort() автоматически будет обращаться к этой функции (анонимному делегату) при оценке весов Name очередных сравниваемых пар элементов массива.

  • В файле Window1.xaml.cs найдите функцию Example6() и заполните ее следующим вызывающим кодом (приводится полностью)
  • void Example6()
            {
                // Диалог выбора каталога
                dialogFolder =
                    new System.Windows.Forms.FolderBrowserDialog();
                dialogFolder.Description = "Выберите каталог для создания БД";
                dialogFolder.ShowNewFolderButton = false;// Отключить кнопку создания новой папки
                dialogFolder.RootFolder = Environment.SpecialFolder.MyComputer;// Компьютер
                dialogFolder.SelectedPath = settings.dialogFolder;
                System.Windows.Forms.DialogResult result = dialogFolder.ShowDialog();
                // Если был отказ или диалог закрыт системной кнопкой
                if (result == System.Windows.Forms.DialogResult.Cancel)
                    return;
        
                // Извлекаем новый каталог
                String path = dialogFolder.SelectedPath;// Полный путь
                // Создаем структуру БД
                CreateDB create = new CreateDB(path);
                bool success;
                if (success = create.CreateDatabase())
                    success = create.CreateTable();
                if (!success)
                {
                    MessageBox.Show("Структура БД не создана");
                    return;
                }
        
                success = create.CreateData();
                if (success)
                    MessageBox.Show("База данных создана");
                else
                    MessageBox.Show("База данных не создана");
        
                // Принудительная перерисовка окна WPF
                this.Height = this.ActualHeight + 1;
                this.Height = this.ActualHeight - 1;
            }
  • Запустите пример и зайдите в папку Images, расположенную в прилагаемом к работе каталоге Source
  • Мы видим, что файловая БД Pictures.my2.mdb создается и наполняется данными. Если открыть эту БД в Office Access, то она (с учетом автоматической сортировки Office Access ) будет такой

    Если проверить разработанную утилиту на создании БД с большим количеством рисунков (1000), то в сравнении с формированием файловой библиотеки она работает ничуть не медленнее. Основная выгода заключается в том, что при записи каждого рисунка мы пользуемся уже открытым соединением. Точно также для библиотечного файла поток создается только один раз. Мы дальше убедимся, что при чтении файловой библиотеки и БД скорость тоже мало различима (и это хорошо), поскольку там и там извлекается только один рисунок и скорость открытия примерно равна.

    Можно было бы, конечно, сначала прочитать все рисунки с диска, а потом сразу записать их в БД, и это было бы еще быстрее. Но рисунков может оказаться много и большого размера. В этом случае оперативной памяти компьютера просто нехватит и он вынужден будет создавать на жестком диске временные файлы промежуточного хранения данных (такой механизм называется спуллинг - Simultaneous Peripherial Operation Off Line ). Это тоже существенно замедлит работу.

    Базы данных - это большое достижение 'всего прогрессивного человечества' (и нас с вами!): они универсальны, надежны, элегантны, незаменимы, а программировать их просто приятно и 'просто просто' (это элементарно, Ватсон).

    Пример 7. Использование базы данных с рисунками

    Теперь создадим утилиту для просмотра рисунков из полученной ранее БД.

  • В файле Window1.xaml измените заголовок списка для элемента ' Пример 7 ' на ' Пример 7. Использование БД с рисунками '
  • В панели Solution Explorer добавьте к узлу App7 командой контекстного меню Add/Window новый файл с именем Window7.xaml
  • Заполните дескрипторную часть окна Window7 (файл Window7.xaml ) следующим кодом (приводится полностью)
  • <Window x:Class="App7.Window7"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Пример 7. Использование БД с рисунками" 
        Height="300"
        Width="470"
        MinHeight="300"
        MinWidth="450" 
        Name="_Window7" 
        Closing="Window_Closing"
        >
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="110" />
            </Grid.ColumnDefinitions>
            <Border Background="Black">
                <Image Name="image" Stretch="Fill" />
            </Border>
            <ListBox Name="listBox" Grid.Column="1" Padding="5,0"
                     ScrollViewer.VerticalScrollBarVisibility="Visible"
                     SelectionChanged="listBox_SelectionChanged">
            </ListBox>
        </Grid>
    </Window>
  • Заполните процедурную часть окна Window7 (файл Window7.xaml.cs ) следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
        
    // Дополнительные пространства имен
    using FORMS = System.Windows.Forms; // Псевдоним
    using System.IO;
    using System.Configuration;         // Для SettingsProperty
    using IO = System.IO;               // Псевдоним для адресации Path
        
    // Дополнительные пространства имен для ADO.NET
    using System.Data;
    using System.Data.OleDb;
    using System.Data.Common;
        
    namespace App7
    {
        public partial class Window7 : Window
        {
            String nameDB;     // Опорное поле для свойства с именем БД
            int count;              // Поле под число рисунков
            string[] names;         // Поле под имена рисунков
            Properties.Settings settings; // Параметры
        
            // Конструктор
            public Window7()
            {
                InitializeComponent();
        
                // Запускаем диалог выбора БД
                nameDB = SelectDB();
                // При отказе от выбора ничего не запускаем,
                // а в главном окне Window1 закрываем это окно 
                if (nameDB != String.Empty)
                    ExecuteWindow();
            }
        
            // Извлекаем рисунки и управляем показом 
            private void ExecuteWindow()
            {
                // Читаем некоторые данные из БД
                OleDbConnection conn = new OleDbConnection(ConnectionString(nameDB));
                OleDbCommand cmd = new OleDbCommand("SELECT COUNT(*) FROM MyTable");
                cmd.Connection = conn;
                conn.Open();
                // Извлекаем число рисунков
                count = (int)cmd.ExecuteScalar();
                // Меняем команду и извлекаем имена рисунков
                cmd.CommandText = "SELECT FileName FROM MyTable";
                OleDbDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
                names = new string[count];
                int i = 0;
                while (reader.Read())// Равносильно foreach (DbDataRecord rec in reader)
                    names[i++] = ((string)reader["FileName"]).Trim();// Сразу убираем пробелы
                // Соединение здесь закроет сам объект DataReader после прочтения всех данных
                // в соответствии с соглашением при его создании CommandBehavior.CloseConnection
        
                // Привязываем массив имен рисунков к списку
                listBox.ItemsSource = names;
                // Выделяем первый элемент списка, чтобы вызвать SelectionChanged
                listBox.SelectedIndex = 0;
                listBox.Focus();
            }
        
            // Строка соединения с БД 
            private String ConnectionString(String fileName)
            {
                //string pathToFile = Application.StartupPath.ToString();
                string JetEngineString = "Provider=Microsoft.Jet.OLEDB.4.0;" +
                    "Jet OLEDB:Engine Type=5;Data Source=";
                return JetEngineString + fileName;
            }
        
            // При смене индекса списка показывает выбранный рисунок
            private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection();
                conn.ConnectionString = ConnectionString(nameDB);
        
                // Создаем и настраиваем объект команды, параметризованной по имени рисунка
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = conn;
                cmd.CommandType = CommandType.Text; // Необязательно! Установлено по умолчанию
                cmd.CommandText = "SELECT Picture FROM MyTable WHERE FileName=?";
                cmd.CommandTimeout = 1;// Пытаться X секунд, потом сгенерировать ошибку (=30 сек)
                cmd.Parameters.Add(new OleDbParameter());
                cmd.Parameters[0].Value = ((ListBox)sender).SelectedValue.ToString();// Имя рисунка
        
                OleDbDataAdapter adapter = new OleDbDataAdapter(cmd);
        
                // Извлекаем рисунок из БД 
                DataTable table = new DataTable();
                adapter.Fill(table);
                byte[] bytes = (byte[])table.Rows[0]["Picture"]; // Подключаемся к рисунку
        
                // Отображаем рисунок пользователю
                BitmapImage bi = new BitmapImage();
                bi.BeginInit();
                bi.StreamSource = new MemoryStream(bytes);// Обертываем извлеченный рисунок в поток
                bi.EndInit();
                image.Source = bi;
            }
        
            // Свойство доступа, проверяется в Window1
            public String NameDB
            {
                get { return nameDB; }
            }
        
            // Диалог выбора БД. Возвращает
            // пусто, если БД не выбрана
            private String SelectDB()
            {
                settings = new App7.Properties.Settings();
                String path = settings.dialogOpen;
                String fileName = IO.Path.Combine(path, "Pictures.my2.mdb");
        
                // Создаем и настраиваем диалог выбора файла
                FORMS.OpenFileDialog openFileDialog = new FORMS.OpenFileDialog();
                openFileDialog.InitialDirectory = path;
                openFileDialog.FileName = "";
                openFileDialog.Filter = "DB Files(*.my2.mdb)|*.my2.mdb";
                FORMS.DialogResult result = openFileDialog.ShowDialog();
                if (result != FORMS.DialogResult.Cancel)
                    fileName = openFileDialog.FileName; // Полное имя
                else
                    fileName = String.Empty;
        
                return fileName;
            }
        
            // Обработчик события Closing, срабатывает перед закрытием приложения
            private void Window_Closing(object sender,
                System.ComponentModel.CancelEventArgs e)
            {
                // Сохраняем путь в параметрах 
                if (nameDB != String.Empty)
                {
                    settings.dialogOpen =
                        nameDB.Substring(0, nameDB.LastIndexOf('\\'));
                    settings.Save();
                }
            }
        }
    }

    Свойство CommandTimeout объекта OleDbCommand получает или задает время ожидания (в секундах) перед завершением попытки выполнения команды и созданием ошибки. По умолчанию установлено 30 секунд, что соответствует большой сети и сложным запросам. Для нашей задачи и 1 секунды очень много, но меньше нельзя, поскольку это свойство типа int.

  • Заполните в файле Window1.xaml.cs функцию Example7() следующим кодом (окончательный, приводится полностью)
  • void Example7()
            {
                // Предотвращение повторного создания дочернего окна
                bool existsWindow = false;
                foreach (Window window in Application.Current.Windows)// Перебираем коллекцию
                {
                    if (window.Name == "_Window7")
                    {
                        // Нашли среди существующих 
                        existsWindow = true;
                        window.Activate();// Сдвинуть на передний план
                        break;// Прервать перебор
                    }
                }
        
                if (!existsWindow)// Если не существует, то создать и показать
                {
                    Window7 wnd = new Window7();
                    // Перед показом проверяем, выбрана ли БД
                    if (wnd.NameDB != String.Empty)
                        wnd.Show();    // Показываем окно Window7
                    else
                        wnd.Close();   // Закрываем окно Window7
                }
            }
  • Разберитесь с кодом и запустите пример - результат для ранее созданной БД Pictures.my2.mdb соответствует ожидаемому и будет таким
  • Все, иллюстрацию потока MemoryStream мы закончили и попутно еще кое в чем потренировались. Остальные неиспользованные пункты списка главного окна в файле Window1.xaml можно удалить, как и соответствующий им процедурный код в файле Window1.xaml.cs. Или пусть так и болтаются.

    Задание к упражнению

    Напоследок можно сделать еще одно замечание к приведенному коду Упражнения 7. В окнах просмотра Window3, Window5 и Window7 мы создавали обработчик события Closing и старательно сохраняли переменную dialogOpen в параметрах приложения. Переменную dialogFolder мы тоже сохраняем, но только при закрытии главного окна. Отсюда дизгармония (физгармоника): пока не закрыли главное окно, при выполнении четных примеров диалог выбора каталогов всегда стартует с того места, которое было сохранено при закрытии главного окна в последнем запуске.

  • Исправьте этот недостаток, чтобы пока длится сеанс, приемственность диалога выбора папки в четных примерах соблюдалась
  • Сохранять путь и переменную dialogFolder можно сразу после выдачи сообщения MessageBox.Show() об удачном создании файловой библиотеки или БД.

    Упражнение 8. Сетевые приложения на базе потока NetworkStream

    Еще одним потомком абстрактного класса Stream является сетевой поток NetworkStream пространства имен System.Net.Sockets библиотеки System.dll. Можно использовать класс NetworkStream для выполнения как синхронной, так и асинхронной передачи данных. В синхронном режиме перед возвращением управления вызывающей программе вызовы функций, которые выполняют сетевые операции, ожидают окончания выполнения этих операции. В асинхронном режиме вызовы немедленно возвращаются инициировавшему их коду.

    Рассмотрим применение NetworkStream на примере совместной работы нескольких запущенных копий клиентского приложения и одной копии серверного. Работу построим на основе созданной ранее БД Pictures.my2.mdb с рисунками. Сервер будет иметь доступ к этой базе и посылать все необходимые данные клиентам по их запросу. Интерфейсное окно клиента реализуем по технологии Windows Forms. А вот сервер создадим как резидентную программу ( Службу Windows ) и запустим в фоновом режиме. Протокол обмена для контроля будем выводить в текстовый файл. Так мы максимально разнообразим демонстрацию применения потоков.

    Шаги выполнения упражнения оформим в виде примеров. Вначале познакомимся с некоторыми понятиями, взятыми из Википедии - свободной энциклопедии.

    Сокет ( Socket ) - программный интерфейс ( API - набор программ) для обеспечения информационного обмена между процессами (загрущенными в ОЗУ приложениями, которые уже выполняются процессором). Потоковые сокеты работают с установкой соединения. Они обеспечивают надежную идентификацию обеих сторон, гарантируя целостность и успешность доставки данных, и опираются на протокол TCP. Условно можно представить себе сокеты в виде двух разъемов, в которые включен канал связи, предназначенный для передачи данных через сеть (поток данных). При передачи данных сокет можно считать резервным хранилищем - аналогом файла. Данные между сокетами перекачивает поток.

    TCP ( Transmission Control Protocol - протокол управления передачей) - один из основных сетевых протоколов Интернета, предназначенный для управления передачей данных в сетях и подсетях TCP/IP. Это транспортный механизм, предоставляющий поток данных, с предварительной установкой соединения, за счёт этого дающий уверенность в достоверности получаемых данных. TCP осуществляет повторный запрос данных в случае потери данных и устраняет дублирование при получении двух копий одного пакета. TCP гарантирует, что приложение получит данные точно в такой же последовательности, в какой они были отправлены, и без потерь. TCP устанавливает соединение, передает данные и завершает соединение.

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

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

    Порт - в протоколах TCP и UDP (семейства TCP/IP ) порт представляет собой идентифицируемый номером системный ресурс, выделяемый приложению, выполняемому на некотором сетевом хосте, для связи с приложениями, выполняемыми на других сетевых хостах (в том числе c другими приложениями на этом же хосте).

    Для каждого из протоколов TCP и UDP стандарт определяет возможность одновременного выделения на хосте до 65536 уникальных портов, идентифицирующихся номерами от 0 до 65535. При передаче по сети номер порта в заголовке пакета используется (вместе с IP -адресом хоста) для адресации конкретного приложения (и конкретного, принадлежащего ему, сетевого соединения).

    В обычной клиент-серверной модели одно приложение ожидает входящих данных или запроса на соединение (сервер "слушает порт"), другое посылает данные или запрос на соединение в известный порт, открытый приложением-сервером (клиент).

    По умолчанию приложению выдается порт с произвольным (например, ближайшим свободным и большим 1024) номером. При необходимости приложение может запросить конкретный (предопределенный) номер порта. Так, веб-серверы обычно открывают для ожидания соединения предопределенный порт 80 протокола TCP.

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

    Очень хорошо вопросы сетевого программирования описаны в книге (скорее, единственной такой):

    ".NET - Сетевое программирование для профессионалов"

    Авторы: Эндрю Кровчик, Винод Кумар, Номан Лагари, Аджит Мунгале, Кристиан Нагел, Тим Паркер,

    Шриниваса Шивакумар

    Издательство: Лори

    Год: 2005

    Пример 1. Создание простой тестовой Службы Windows

    Сервером в C# может служить приложение любого из существующих исполнимых типов: Console Application, Windows Forms Application, WPF Application или Windows Service. Для разнообразия, вначале построим сервер на основе Windows Service. Такие программы не имеют пользовательского интерфейса (он им и не нужен - висят в оперативной памяти и делают свое дело) и раньше назывались резидентными, а теперь Службами.

    Позже мы создадим сервер и как Console Application, в котором консольное окно может быть использовано для запуска, настроек и контроля состояния сервера. А пока немного отвлечемся и на простом примере научимся создавать Службу Windows как прототип нашего будущего сервера, затем опять продолжим рассмотрение темы потоков.

  • Командой File/New/Project создайте в каталоге решений Stream новое решение с именем NetworkStream и новый проект с именем TestService типа Windows Service
  • Оболочка создаст новый проект и покажет окно графического конструктора сервисов.

  • Закройте окно конструктора сервисов и через панель Solution Explorer в узле Service1.cs удалите подчиненный файл Service1.Designer.cs за ненадобностью, поскольку компоненты в этом примере мы использовать не будем
  • В панели Solution Explorer вызовите контекстное меню для узла Service1.cs и выполните команду View Code
  • Заполните файл Service1.cs следующим кодом ('http://msdn.microsoft.com/ru-ru/library/9k985bc9(VS.90).aspx')
  • using System;
    using System.ServiceProcess;
    using System.Timers;
    using System.IO;
        
    namespace TestService
    {
        public partial class Service1 : ServiceBase
        {
            Timer timer;// Системный таймер
            String logFile = "C:\\Report.txt";
        
            // Конструктор
            public Service1()
            {
                // Создаем и настраиваем системный таймер
                timer = new Timer();
                timer.Elapsed += timer_Elapsed;// Регистрируем обработчик
                timer.Interval = 5000; // 5 секунд
            }
        
            // Обработчик события таймера
            void timer_Elapsed(object sender, ElapsedEventArgs e)
            {
                StreamWriter writer = File.AppendText(logFile);
                writer.WriteLine(DateTime.Now.ToString());
                writer.Close(); // Закрываем поток
            }
        
            // Метод срабатывает при запуске Службы
            protected override void OnStart(string[] args)
            {
                timer.Start();
            }
        
            // Метод срабатывает при остановке Службы
            protected override void OnStop()
            {
                timer.Stop();
            }
        }
    }

    Запускать на выполнение через оболочку эту Службу нельзя. Она запускается операционной системой через Диспетчер Служб (или через Диспетчер задач Windows по жесту Ctrl-Alt-Del). Мы ее можем пока только откомпилировать, но и это преждевременно. Еще нужно зарегистрировать Службу на текущем компьютере, чтобы она была видна диспетчеру Служб, а для этого нужно к проекту добавить класс настроек регистрации Службы.

  • В панели Solution Explorer вызовите на узле TestService проекта контекстное меню и командой Add/New Item в появишемся окне мастера выберите элемент Installer Class
  • Закройте окно графического конструктора и через панель Solution Explorer в узле Installer1.cs удалите подчиненный файл Installer1.Designer.cs за ненадобностью
  • В панели Solution Explorer вызовите контекстное меню для узла Installer1.cs и выполните команду View Code
  • Заполните файл Installer1.cs следующим кодом настроек регистрации Службы ('http://msdn.microsoft.com/ru-ru/library/ddhy0byf(v=VS.90).aspx')
  • using System;
    using System.ComponentModel;
    using System.Configuration.Install;
    using System.ServiceProcess;
        
    namespace TestService
    {
        [RunInstaller(true)]// Во время установки сборки следует вызвать установщик
        public partial class Installer1 : Installer
        {
            private ServiceInstaller serviceInstaller;
            private ServiceProcessInstaller serviceProcessInstaller;
        
            public Installer1()
            {
                // Создаем объекты настройки для Службы
                serviceInstaller = new ServiceInstaller();
                serviceProcessInstaller = new ServiceProcessInstaller();
        
                // Имя Службы для машины и пользователя
                serviceInstaller.ServiceName = "TestServiceServiceName";
                serviceInstaller.DisplayName = "TestServiceDisplayName";
        
                // Контекст безопасности - как будет запускаться Служба
                this.serviceProcessInstaller.Account = ServiceAccount.LocalSystem;
                this.serviceProcessInstaller.Password = null;
                this.serviceProcessInstaller.Username = null;
        
                // Добавляем настройки в коллекцию текущего объекта
                this.Installers.AddRange(
                    new Installer[]         
                    {
                        serviceInstaller,
                        serviceProcessInstaller
                    });
            }
        }
    }
  • Откомпилируйте проект командой Build/Build Solution
  • Мы создали Службу, которая должна быть зарегистрирована в операционной системе. Как это делается, описано в MSDN ('http://msdn.microsoft.com/ru-ru/library/sd8zc8ha(VS.90).aspx') и мы это сейчас выполним. Для регистрации Служб приложений .NET используется утилита InstallUtil.exe, которая находится в каталоге " C:\Windows\Microsoft.NET\Framework\v2.0.50727 ". Для удаления регистрации Службы используется та же самая утилита, но с опцией /u.

  • В меню Пуск/Выполнить введете в текстовое поле имя запускаемой программы cmd и откройте окно команд Windows
  • Следующей командой перейдите в каталог с утилитой InstallUtil.exe >cd C:\Windows\Microsoft.NET\Framework\v2.0.50727
  • В панели Solution Explorer щелкните на пиктограмме Show All Files, раскройте узел bin/Debug и скопируйте полное имя сборки E:\Tmp\Stream\NetworkStream\TestService\bin\Debug\TestService.exe
  • В окне команд Windows введите команду >InstallUtil.exe и добавьте мышью скопированное имя сборки, должно получиться >InstallUtil.exe E:\Tmp\Stream\NetworkStream\TestService\bin\Debug\TestService.exe
  • После выполнения этой команды откройте окно Пуск/Настройка/Панель управления/Администрирование/Службы и запустите нашу Службу (или жестом Ctrl-Alt-Del запустите Диспетчер задач и откройте вкладку Службы)
  • Убедитесь, что в файле Report.txt стала накапливаться информация, остановите Службу командой Остановить и удалите этот пробный файл
  • Замечания

  • Вызвать окно со списком Служб можно и через Диспетчер задач Windows комбинацией клавиш Ctrl-Alt-Del (когда-то заветная комбинация для программистов DOS!)
  • Созданная нами Служба запускается вручную, но через команду Свойства контекстного меню ее можно сделать запускаемой автоматически при загрузке компьютера
  • После регистрации Службы в системе нельзя менять место расположения созданной нами сборки, иначе при запуске система эту Службу просто не найдет. При необходимости Службу нужно вначале разрегистрировать, перенести и вновь зарегистрировать для нового места
  • В каталоге сборки утилита инсталляции InstallUtil.exe создала два протокольных текстовых файла (без расширения): TestService.InstallLog и TestService.InstallState, которые не влияют на дальнейшую работу Службы и их можно спокойно удалить
  • В окне команд Windows разрегистрируйте Службу командой >InstallUtil.exe /u E:\Tmp\Stream\NetworkStream\TestService\bin\Debug\TestService.exe
  • Перейдите в окно Службы, обновите его командой Действие/Обновить и убедитесь, что Служба была удалена
  • Итак, на этом простом примере мы научились создавать Службы Windows (не путайте с Web -Службами) и теперь можно бы приступить к созданию серверной программы, которая по запросу клиентов будет извлекать рисунки из БД и посылать им (их) для просмотра. Но не будем торопиться и рассмотрим еще один тестовый пример, теперь уже иллюстрирующий взаимодействие клиента и сервера.

    Пример 2. Создание простого клиент-серверного приложения

    В этом примере мы разработаем простой сервер и простую клиентскую программу, ведущих между собой 'неспешный' диалог. Клиента построим по технике Windows Forms, а сервер - Windows Service. Сервер будет иметь набор готовых маркированных ответов, ждать маркированных запросов клиентов и отвечать им соответствующими сообщениями. Это настроит нас на создание еще более сложной системы - просмотру дистанционных рисунков из БД, которой мы займемся позже.

    Создание клиента

    Начнем с клиентской программы, которая может запускаться во многих экземплярах ('http://msdn.microsoft.com/ru-ru/library/system.net.sockets.tcplistener.accepttcpclient.aspx')

  • Командой File/Add/New Project добавьте к решению NetworkStream новый проект с именем SimpleClient и назначьте его стартовым
  • Разместите на форме элементы ListBox, Button, TextBox и настройте их в соответствии с таблицей свойств
  • Элемент Свойство Значение
    Form Text Client
    Size 300; 300
    ListBox (Name) listBox
    Dock Top
    Font Arial; 12pt
    Items
  • Привет!
  • Лелик
  • Как жизнь
  • Оттопыремся сегодня?
  • Ну тогда пока!
  • SelectionMode One
    Size 292; 119
    Button (Name) btnSubmit
    AutoSize True
    Font Arial; 10pt
    Location 96; 127
    Size 101; 29
    Text Отправить
    TextBox (Name) textBox
    Dock Bottom
    Location 0; 162
    Multiline True
    ScrollBars Vertical
    Size 292; 105

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

  • Откройте файл Form1.cs и сделайте его таким (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
        
    namespace SimpleClient
    {
        public partial class Form1 : Form
        {
            int port = 12000;
            String hostName = "127.0.0.1";// local
            TcpClient client = null;// Ссылка на клиента
        
            public Form1()
            {
                InitializeComponent();
        
                // Выделить первый элемент списка
                listBox.SelectedIndex = 0;
                listBox.Focus();
        
                // Контекстное меню для очистки TextBox
                ContextMenuStrip contextMenu = new ContextMenuStrip();
                textBox.ContextMenuStrip = contextMenu;
                ToolStripMenuItem item = new ToolStripMenuItem("Очистить");
                contextMenu.Items.Add(item);
                item.MouseDown += new MouseEventHandler(item_MouseDown);
            }
        
            // Отослать запрос и получить ответ
            private void btnSubmit_Click(object sender, EventArgs e)
            {
                if (listBox.SelectedIndices.Count == 0)
                {
                    MessageBox.Show("Выделите сообщение");
                    return;
                }
        
                try
                {
                    // Создаем клиента, соединенного с сервером
                    client = new TcpClient(hostName, port);
                    // Сами задаем размеры буферов обмена (Необязательно!)
                    client.SendBufferSize = client.ReceiveBufferSize = 1024;
                }
                catch
                {
                    MessageBox.Show("Сервер не готов!");
                    return;
                }
        
                // Записываем запрос в протокол
                AddString("Клиент: " + listBox.SelectedItem.ToString());
        
                // Создаем потоки NetworkStream, соединенные с сервером
                NetworkStream streamIn = client.GetStream();
                NetworkStream streamOut = client.GetStream();
                StreamReader readerStream = new StreamReader(streamIn);
                StreamWriter writerStream = new StreamWriter(streamOut);
        
                // Отсылаем запрос серверу
                writerStream.WriteLine(listBox.SelectedItem.ToString());
                writerStream.Flush();
        
                // Читаем ответ
                String receiverData = readerStream.ReadLine();
        
                // Записываем ответ в протокол 
                AddString("Сервер: " + receiverData);
        
                // Закрываем соединение и потоки, порядок неважен
                client.Close();
                writerStream.Close();
                readerStream.Close();
            }
        
            // Добавление строки, когда TextBox включен в режиме Multiline
            private void AddString(String line)
            {
                StringBuilder sb = new StringBuilder(textBox.Text);
                StringWriter sw = new StringWriter(sb);
                sw.WriteLine(line);
                textBox.Text = sw.ToString();
            }
        
            // Очистка списка через контекстное меню
            void item_MouseDown(object sender, MouseEventArgs e)
            {
                textBox.Clear();
                listBox.Focus();
            }
        }
    }

    Получив соединение с сервером, мы создаем два потока NetworkStream и упаковываем их в оболочки, удобные для управления чтением/записью. Обмен с сервером отображаем в протоколе TextBox. Для очистки протокола динамически создали контекстное меню.

    Класс TcpClient, который мы использовали в коде, является высокоуровневой (и упрощенной) оболочкой сокета (класса Socket ). Если потребуется более низкоуровневое управление сокетом (более детальное), то ссылка на него хранится в свойстве TcpClient.Client. Но поскольку это свойство защищенное ( protected ), то доступ к нему возможен только из производного от TcpClient класса.

  • Разберитесь с кодом клиента
  • Если сейчас запустить приложение SimpleClient, то оно будет работать, но при попытке что-то отослать на сервер, будет выдаваться сообщение, что сервер не готов. Сервера-то еще пока вообще нет, создадим его.

    Создание сервера

    Серверную программу сделаем резидентной по шаблону Windows Service, как мы это делали в предыдущем примере, хотя можно сделать и с интерфейсом, главное, чтобы он был запущен в единственном экземпляре на локальном компьютере. Если программа-сервер включена в глобальную сеть, то с используемым IP и портом она должна быть единственной в этой сети. Поэтому на использование сетевого IP для глобальной сети нужно получать разрешение.

  • Командой File/Add/New Project добавьте к решению NetworkStream новый проект с именем SimpleServer по шаблону Windows Service
  • В панели Solution Explorer вызовите на узле SimpleServer проекта контекстное меню и командой Add/New Item в появишемся окне мастера выберите элемент Installer Class
  • Закройте окно графического конструктора и через панель Solution Explorer в узле Installer1.cs удалите подчиненный файл Installer1.Designer.cs за ненадобностью
  • В панели Solution Explorer вызовите контекстное меню для узла Installer1.cs и выполните команду View Code
  • Заполните файл Installer1.cs проекта SimpleServer следующим кодом настроек регистрации Службы (приводится полностью)
  • using System;
    using System.ComponentModel;
    using System.Configuration.Install;
    using System.ServiceProcess;
        
    namespace SimpleServer
    {
        [RunInstaller(true)]// Во время установки сборки следует вызвать установщик
        public partial class Installer1 : Installer
        {
            private ServiceInstaller serviceInstaller;
            private ServiceProcessInstaller serviceProcessInstaller;
        
            public Installer1()
            {
                // Создаем настройки для Службы
                serviceInstaller = new ServiceInstaller();
                serviceProcessInstaller = new ServiceProcessInstaller();
        
                // Имя Службы для машины и пользователя
                serviceInstaller.ServiceName = "SimpleServerServiceName";
                serviceInstaller.DisplayName = "SimpleServer";
                serviceInstaller.StartType = ServiceStartMode.Manual;// Запуск вручную
        
                // Как будет запускаться Служба
                this.serviceProcessInstaller.Account = ServiceAccount.LocalService;
                this.serviceProcessInstaller.Password = null;
                this.serviceProcessInstaller.Username = null;
        
                // Добавляем настройки в коллекцию текущего объекта
                this.Installers.AddRange(
                    new Installer[]         
                    {
                        serviceInstaller,
                        serviceProcessInstaller
                    });
            }
        }
    }
  • Через панель Solution Explorer в узле Service1.cs удалите подчиненный файл Service1.Designer.cs за ненадобностью, поскольку компоненты в этом примере мы использовать не будем
  • В панели Solution Explorer вызовите контекстное меню для узла Service1.cs и выполните команду View Code
  • Заполните файл Service1.cs проекта SimpleServer следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.ServiceProcess;
    using System.Collections;
        
    namespace SimpleServer
    {
        class Service1 : ServiceBase
        {
            TcpListener server = null;// Ссылка на сервер
            int port = 12000;
            String hostName = "127.0.0.1";// local
            IPAddress localAddr;
            String[] answers = {
                                   "1. Ты кто?",
                                   "2. Привет, Лелик!",
                                   "3. Лучше всех!",
                                   "4. Конечно, на полную катушку",
                                   "5. До вечера!"
                               };
        
            // Конструктор
            public Service1()
            {
                localAddr = IPAddress.Parse(hostName);// Конвертируем в другой формат
        
                Thread thread = new Thread(ExecuteLoop);
                thread.IsBackground = true;
                thread.Start();
            }
        
            private void ExecuteLoop()
            {
                try
                {
                    server = new TcpListener(localAddr, port);// Создаем сервер-слушатель
                    server.Start();// Запускаем сервер
        
                    String data;
        
                    // Бесконечный цикл прослушивания клиентов
                    while (true)
                    {
                        if (!server.Pending())// Очередь запросов пуста
                            continue;
                        TcpClient client = server.AcceptTcpClient();// Текущий клиент
                        // Сами задаем размеры буферов обмена (Необязательно!)
                        // По умолчанию оба буфера установлены размером по 8192 байта
                        client.SendBufferSize = client.ReceiveBufferSize = 1024;
        
                        // Подключаем NetworkStream и погружаем для удобства в оболочки
                        NetworkStream streamIn = client.GetStream();
                        NetworkStream streamOut = client.GetStream();
                        StreamReader readerStream = new StreamReader(streamIn);
                        StreamWriter writerStream = new StreamWriter(streamOut);
        
                        // Читаем запрос
                        data = readerStream.ReadLine();
                        // Отправляем ответ
                        int index;
                        if (int.TryParse(data.Substring(0, data.IndexOf('.')), out index))
                            data = answers[index - 1];
                        else
                            data = data.ToUpper();
                        writerStream.WriteLine(data);
                        writerStream.Flush();
        
                        // Закрываем соединение и потоки, порядок неважен
                        client.Close();
                        readerStream.Close();
                        writerStream.Close();
                    }
                }
                catch (SocketException)
                {
                }
                finally
                {
                    // Останавливаем сервер
                    server.Stop();
                }
            }
        }
    }

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

    На другой стороне соединения сервер TcpListener в бесконечном цикле прослушивает очередь соединений с клиентами. Если какой-то клиент с ним соединился ( server.Pending()!=false ), то сервер извлекает этого клиента методом AcceptTcpClient() - создает сокет для приема/передачи с готовым обратным адресом, создает двунаправленный поток (или два однонаправленных), затем читает запрос и передает ответ.

  • Откомпилируйте проект командой Build/Build Solution
  • В меню Пуск/Выполнить введете в текстовое поле имя запускаемой программы cmd и откройте окно команд Windows
  • Следующей командой перейдите в каталог с утилитой InstallUtil.exe >cd C:\Windows\Microsoft.NET\Framework\v2.0.50727
  • В панели Solution Explorer выделите проект SimpleServer, щелкните на пиктограмме Show All Files, раскройте узел bin/Debug и командой Copy контекстного меню скопируйте полное имя сборки E:\Tmp\Stream\NetworkStream\SimpleServer\bin\Debug\SimpleServer.exe
  • В окне команд Windows введите команду >InstallUtil.exe и добавьте мышью скопированное имя сборки, должно получиться >InstallUtil.exe E:\Tmp\Stream\NetworkStream\SimpleServer\bin\Debug\SimpleServer.exe
  • После выполнения этой команды откройте окно Пуск/Настройка/Панель управления/Администрирование/Службы (или Диспетчер задач жестом Ctrl-Alt-Del) и запустите программу SimpleServer
  • Запустите проект SimpleClient - все работает и результат такой
  • Через Проводник Windows зайдите в каталог SimpleClient\bin\Debug размещения сборки SimpleClient.exe и запустите несколько копий приложения (несколько клиентов) - все они работают нормально
  • Разберитесь в файле Service1.cs с кодом построенной нами серверной программы, сравните ее с клиентской частью
  • Остановите службу SimpleServer и разрегистрируйте ее, как мы это делали в предыдущем примере
  • Обратите внимание, что если код работы нашей серверной программы не упаковать в отдельную нить Thread (поток выполнения), то в окне служб эта программа операционной системой запускаться не будет (попробуйте!). Причина в том, что в коде метода ExecuteLoop() в сервере используется бесконечный цикл прослушивания очереди запросов клиентов. Если этот цикл оставить в основном потоке выполнения ( Thread ) приложения, то оно просто зациклится и не сможет само нормально завершиться. Поэтому код с циклом мы помещаем в отдельный поток (трэд) и делаем его фоновым, чтобы он закрывался вместе с основным потоком приложения (трэдом сервера).

    Важное замечание

    Поток NetworkStream является двухсторонним фиксированной длины. Методом GetStream() он только устанавливает адресное соединение между сокетами клиента и сервера. Но реальная его длина определяется сообщением отправляющей стороны. Можно для приема/передачи использовать один поток, но тогда длина сообщения, отправляемого сервером, не должна превышать длину сообщения, принятого им от клиента (чуть глаза не отсидел!). Поэтому мы и используем на каждой стороне два потока для раздельной однонаправленной передачи между двумя узлами сетевого соединения.

    Пример 3. Клиент-серверное приложение просмотра рисунков из БД

    На предыдущем простом примере мы познакомились (чуть-чуть) с пронципами создания сетевых приложений. А теперь построим более сложный пример, когда клиент запрашивает рисунки, а сервер извлекает их из хранилища и посылает клиенту. В Упражнении 7 нами было разработано три разных хранилища рисунков и три программы просмотра. В данном примере воспользуемся БД Pictures.my2.mdb с готовыми рисунками и на ее основе создадим сетевое приложение (для тех, кто не делал Упражнение 7, БД прилагается в каталоге Source/Data ).

    Построение клиента

    Для клиента построим оконное приложение типа WPF с пользовательским интерфейсом, частично заимствованным из Примера 6 Упражнения 7.

  • Командой File/Add/New Project добавьте к решению NetworkStream новый проект с именем PicturesClientDB типа WPF и назначьте его стартовым
  • Заполните дескрипторную часть окна Window1 (файл Window1.xaml ) следующим кодом (приводится полностью)
  • <Window x:Class="PicturesClientDB.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Клиентское приложение просмотра рисунков из БД " 
        Height="300"
        Width="470"
        MinHeight="300"
        MinWidth="450" 
        xmlns:local="clr-namespace:PicturesClientDB"
        >
        <Window.Resources>
            <local:Pictures x:Key="promptImage" />
        </Window.Resources>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="110" />
            </Grid.ColumnDefinitions>
            <Border Name="border"
                    Background="{Binding Source={StaticResource promptImage},
                    Path=Picture, Mode=OneWay}">
                <Viewbox Name="Prompt">
                    <Border 
                        BorderBrush="Black" 
                        BorderThickness="5" 
                        Background="Red" Margin="15,0">
                        <TextBlock
                            TextAlignment="Center"
                            FontWeight="Bold"
                            Padding="5,5"
                            >
                        <TextBlock.Foreground>
                            <SolidColorBrush Color="Yellow" />
                        </TextBlock.Foreground>
                        Сервер не готов, ждите!
                        <LineBreak />
                        Мы пытаемся связаться,
                        <LineBreak />
                        извините за неудобства...
                        </TextBlock>
                    </Border>
                </Viewbox>
            </Border>
            <ListBox Name="listBox" Grid.Column="1" Padding="5,0"
                     ScrollViewer.VerticalScrollBarVisibility="Visible"
                     SelectionChanged="listBox_SelectionChanged">
            </ListBox>
        </Grid>
    </Window>

    Для вывода заставки с текстом о неготовности сервера мы применили элемент Viewbox, в который поместили еще один элемент Border с текстовым содержимым. Такой 'огород' позволит увеличивать заставку пропорционально размеру окна. Однако введение элемента Viewbox начинает заметно притормаживать перерисовку интерфейса при перемещениях окна, потому что он пытается постоянно пересчитывать масштабы своих дочерних элементов. Имена мы присвоили только тем интерфейсным элементам, которыми собираемся управлять в процедурном коде.

  • Заполните файл Window1.xaml.cs следующим процедурным кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
        
    // Дополнительные пространства имен для Stream
    using System.IO;
    using IO = System.IO;               // Псевдоним для адресации Path
    using System.Windows.Threading;     // Для DispatcherTimer
        
    // Дополнительные пространства имен для Socket
    //using System.Net;
    using System.Net.Sockets;
    using System.Collections;   // List<byte>
        
    namespace PicturesClientDB
    {
        public partial class Window1 : Window
        {
            int port = 12000;
            String hostName = "127.0.0.1";  // local
            TcpClient client = null;        // Ссылка на клиента
            String sendMessage = "!!!GetNames!!!";    // Запрос на список (позаковыристей)
            Char[] separator = { '#' };     // Для преобразования ответа в массив имен
            DispatcherTimer timer;          // Таймер  
        
            // Конструктор
            public Window1()
            {
                InitializeComponent();
        
                // Создаем и запускаем таймер
                timer = new DispatcherTimer();
                timer.Tick += new EventHandler(timer_Tick);
                timer.Interval = TimeSpan.FromSeconds(1);
                timer.Start();
            }
        
            // Инициирует обращение к серверу
            void timer_Tick(object sender, EventArgs e)
            {
                Execute(listBox);
            }
        
            private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                Execute((ListBox)sender);
            }
        
            void Execute(ListBox lst)
            {
                // Заполняем список именами рисунков
                try
                {
                    // Если сервер доступен, создаем клиента
                    client = new TcpClient(hostName, port);
                }
                catch
                {
                    // Сервер не готов, запускаем таймер и выходим
                    if (Prompt.Visibility != Visibility.Visible)
                    {
                        Prompt.Visibility = Visibility.Visible;
                        timer.Start();
                    }
                    return;
                }
        
                switch (sendMessage)
                {
                    case "!!!GetNames!!!":
                        // Получаем и привязываем имена рисунков к списку
                        lst.ItemsSource = GetNames();
                        // Выделяем первый элемент списка, чтобы вызвать SelectionChanged
                        lst.SelectedIndex = 0;
                        lst.Focus();
                        sendMessage = "";
                        break;
                    default:
                        // Скрываем сообщение и останавливаем таймер
                        if (Prompt.Visibility == Visibility.Visible)
                        {
                            Prompt.Visibility = Visibility.Hidden;
                            timer.Stop();
                        }
        
                        // Получаем рисунок и отображаем пользователю кистью
                        String name = lst.SelectedValue.ToString();
                        BitmapImage bi = new BitmapImage();
                        bi.BeginInit();
                        // Получаем от сервера рисунок и обертываем его в поток памяти
                        bi.StreamSource = new MemoryStream(GetPicture(name));
                        bi.EndInit();
                        Pictures.picture.ImageSource = bi;// Передаем рисунок фону Border
                        break;
                }
            }
        
            private String[] GetNames()
            {
                String[] names;
        
                // Создаем потоки сетевых соединений
                StreamReader readerStream = new StreamReader(client.GetStream());
                StreamWriter writerStream = new StreamWriter(client.GetStream());
                // Отсылаем запрос серверу
                writerStream.WriteLine(sendMessage);
                writerStream.Flush();
        
                // Читаем ответ
                String receiverData = readerStream.ReadLine();
                names = receiverData.Split(separator);// Преобразуем в строковый массив
        
                // Закрываем соединение и потоки, порядок неважен
                client.Close();
                writerStream.Close();
                readerStream.Close();
        
                return names;
            }
        
            Byte[] GetPicture(String name)
            {
                // Создаем потоки сетевых соединений
                NetworkStream readerStream = client.GetStream();
                StreamWriter writerStream = new StreamWriter(client.GetStream());
        
                // Отсылаем запрос серверу
                writerStream.WriteLine(name);
                writerStream.Flush();
        
                // Читаем ответ 
                // ReceiveBufferSize - размер буфера для входящих данных
                // SendBufferSize - размер буфера для исходящих данных
                List<byte> list = new List<byte>(client.ReceiveBufferSize);// С приращением capacity
                Byte[] bytes = new Byte[client.ReceiveBufferSize]; // Размер буфера сокета
                int count = 0;  // Порции входящих данных
                while ((count = readerStream.Read(bytes, 0, bytes.Length)) != 0)
                    for (int i = 0; i < count; i++)
                        list.Add(bytes[i]);
        
                // Преобразуем в массив результата
                bytes = new Byte[list.Count];
                list.CopyTo(bytes);
        
                // Закрываем соединение и потоки, порядок неважен
                client.Close();
                writerStream.Close();
                readerStream.Close();
        
                return bytes;
            }
        }
        
        // Для привязки к ресурсу
        class Pictures
        {
            // Поле
            public static ImageBrush picture = new ImageBrush();
        
            static Pictures()
            {
                // Дежурный рисунок заставки
                picture.ImageSource = new BitmapImage(
                    new Uri(@"flower2.jpg", UriKind.Relative));
                picture.Stretch = Stretch.Fill;
                picture.Opacity = 1.0D;
            }
        
            // Привязываемое в интерфейсе свойство
            public static ImageBrush Picture { get { return picture; } }
        }
    }

    Обратите внимание, что при отображении рисунков мы отказались от традиционного элемента Image, как это делали в предыдущем упражнении. А для разнообразия поступили совершенно нетрадиционно (по турецки). Теперь мы рисунки будем отображать кистью ImageBrush в фоне прямоугольника Border через привязанный к нему объект Pictures. Конечно, в жизни так извращаться вряд ли придется, но и такой вариант где-нибудь может пригодиться.

  • В панели Solution Explorer контекстной командой Add/Existing Item добавьте в корень проекта из прилагаемой к лабораторной работе папки Source/Images рисунок для заставки flower2.jpg, проверьте его свойства
  • Build Action=None
  • Copy to Output Directory=Copy if newer
  • Запустите приложение - пока получилась пустое окно с заставкой и траурным сообщением, что сервер не готов
  • Заставка появится сразу же, как будет обнаружен факт отсутствия или остановки сервера. А после обнаружения сервера заставка исчезнет. Этот механизм немедленно сработает благодаря используемому нами системному таймеру. Однако, сервера пока еще совсем нет и следует его изготовить.

    Построение сервера БД как службу

  • Командой File/Add/New Project добавьте к решению NetworkStream новый проект с именем PicturesServerDB типа Windows Service
  • Закройте окно конструктора сервисов и через панель Solution Explorer в узле Service1.cs удалите подчиненный файл Service1.Designer.cs за ненадобностью, поскольку компоненты в этом примере мы использовать не будем
  • В панели Solution Explorer вызовите контекстное меню для узла Service1.cs и выполните команду View Code
  • Заполните файл Service1.cs следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Diagnostics;
    using System.ServiceProcess;
    using System.Text;
        
    // Дополнительные пространства имен для ADO.NET
    using System.Data.OleDb;
    using System.Data.Common;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.Collections;
        
    namespace PicturesServerDB
    {
        public partial class Service1 : ServiceBase
        {
            int port = 12000;
            String hostName = "127.0.0.1";  // local
            IPAddress localAddr;
            TcpListener server = null;      // Ссылка на сервер
            String separator = "#";         // Разделитель имен в строке ответа
            String connectionString;        // Строка соединения с БД
        
            public Service1()
            {
                // Извлекаем в поле строку соединения с БД из файла App.config
                connectionString = System.Configuration.ConfigurationManager.
                    ConnectionStrings["PicturesDB"].ConnectionString;
        
                // Конвертируем IP в другой формат
                localAddr = IPAddress.Parse(hostName);
        
                // Запускаем в новом потоке (ните)
                Thread thread = new Thread(ExecuteLoop);
                thread.IsBackground = true;
                thread.Start();
            }
        
            private void ExecuteLoop()
            {
                try
                {
                    server = new TcpListener(localAddr, port);// Создаем сервер-слушатель
                    server.Start();// Запускаем сервер
        
                    // Бесконечный цикл прослушивания клиентов
                    while (true)
                    {
                        // Проверяем очередь соединений
                        if (!server.Pending())// Очередь запросов пуста
                            continue;
                        TcpClient client = server.AcceptTcpClient();// Текущий клиент
        
                        // Создаем потоки сетевых соединений
                        StreamReader readerStream = new StreamReader(client.GetStream());
                        NetworkStream streamOut = client.GetStream();
                        StreamWriter writerStream = new StreamWriter(streamOut);
        
                        // Читаем команду клиента 
                        String receiverData = readerStream.ReadLine();
        
                        // Распознаем и исполняем
                        switch (receiverData)
                        {
                            case "!!!GetNames!!!":// Посылаем имена, разделенные сепаратором
                                String names = GetNames();
                                writerStream.WriteLine(names);  // Используем через оболочку
                                writerStream.Flush();
                                break;
                            default:// Посылаем рисунок
                                Byte[] bytes = GetPicture(receiverData);
                                streamOut.Write(bytes, 0, bytes.Length);// Используем напрямую
                                streamOut.Flush();
                                break;
                        }
        
                        // Закрываем соединение и потоки, порядок неважен
                        client.Close();
                        readerStream.Close();
                        writerStream.Close();
                    }
                }
                finally
                {
                    // Останавливаем сервер
                    server.Stop();
                }
            }
        
            // Извлечение из БД имен рисунков и упаковка их в одну строку для пересылки клиенту
            string GetNames()
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection(connectionString);
                OleDbCommand cmd = new OleDbCommand("SELECT FileName FROM MyTable");
                cmd.Connection = conn;
                conn.Open();
        
                // Извлекаем имена рисунков
                OleDbDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
        
                // Формируем строку исходящих данных
                StringBuilder sb = new StringBuilder();
                foreach (DbDataRecord record in reader)// Равносильно чтению reader.Read()
                    sb.Append(((string)record["FileName"]).Trim() + separator);
                // Соединение здесь закроет сам объект DataReader после прочтения всех данных
                // в соответствии с соглашением при его создании CommandBehavior.CloseConnection
        
                // Удаляем лишний последний символ сепаратора
                sb.Replace(separator, String.Empty, sb.ToString().
                    LastIndexOf(separator), separator.Length);
        
                return sb.ToString();
            }
        
            // Извлечение из БД самого рисунка для отправки клиенту
            byte[] GetPicture(String name)
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection();
                conn.ConnectionString = connectionString;
        
                // Создаем и настраиваем объект команды, параметризованной по имени рисунка
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = conn;
                cmd.CommandType = CommandType.Text; // Необязательно! Установлено по умолчанию
                cmd.CommandText = "SELECT Picture FROM MyTable WHERE FileName=?";
                cmd.Parameters.Add(new OleDbParameter());
                cmd.Parameters[0].Value = name;// Имя рисунка
                OleDbDataAdapter adapter = new OleDbDataAdapter(cmd);
        
                // Извлекаем рисунок из БД 
                DataTable table = new DataTable();
                adapter.Fill(table);
                byte[] bytes = (byte[])table.Rows[0]["Picture"]; // Подключаемся к рисунку
        
                return bytes;
            }
        }
    }
  • В панели Solution Explorer контекстной командой Add/New Item для корня проекта добавьте в него конфигурационный файл App.config
  • Заполните файл App.config следующим кодом (приводится полностью)
  • <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <connectionStrings>
        <add name="PicturesDB"
            connectionString="Provider=Microsoft.Jet.OLEDB.4.0;
            Jet OLEDB:Engine Type=5;
            Data Source=|DataDirectory|\Data\Pictures.my2.mdb"
            providerName="System.Data.OleDb" />
      </connectionStrings>
    </configuration>
  • В панели Solution Explorer контекстной командой Add/References для узла References добавьте к проекту ссылку на библиотеку System.Configuration.dll
  • В панели Solution Explorer контекстной командой Add/New Folder для корня проекта добавьте новую папку с именем Data
  • В панели Solution Explorer контекстной командой Add/New Item для папки Data добавьте в нее ранее созданную базу данных Pictures.my2.mdb (или возьмите готовую из прилагаемой к лабораторной работе папки Source/Data ). На предложение мастера создать для БД типизированный DataSet ответьте Cancel
  • Выделите базу данных Pictures.my2.mdb и в панели Properties установите (или проверьте) ее свойства, которые должны быть такими
  • Build Action=None
  • Copy to Output Directory=Copy if newer
  • Теперь создадим для сервиса инсталляционный файл.

  • В панели Solution Explorer вызовите на узле PicturesServerDB проекта контекстное меню и командой Add/New Item в появишемся окне мастера выберите элемент Installer Class
  • Закройте окно графического конструктора и через панель Solution Explorer в узле Installer1.cs удалите подчиненный файл Installer1.Designer.cs за ненадобностью
  • В панели Solution Explorer вызовите контекстное меню для узла Installer1.cs и выполните команду View Code
  • Заполните файл Installer1.cs следующим кодом настроек регистрации сервиса (приводится полностью)
  • using System;
    using System.ComponentModel;
    using System.Configuration.Install;
    using System.ServiceProcess;
        
    namespace SimpleServer
    {
        [RunInstaller(true)]// Во время установки сборки следует вызвать установщик
        public partial class Installer1 : Installer
        {
            private ServiceInstaller serviceInstaller;
            private ServiceProcessInstaller serviceProcessInstaller;
        
            public Installer1()
            {
                // Создаем настройки для Службы
                serviceInstaller = new ServiceInstaller();
                serviceProcessInstaller = new ServiceProcessInstaller();
        
                // Имя Службы для машины и пользователя
                serviceInstaller.ServiceName = "PicturesServerDB";
                serviceInstaller.DisplayName = "PicturesServerDB";
                serviceInstaller.StartType = ServiceStartMode.Manual;// Запуск вручную
        
                // Как будет запускаться Служба
                this.serviceProcessInstaller.Account = ServiceAccount.LocalService;
                this.serviceProcessInstaller.Password = null;
                this.serviceProcessInstaller.Username = null;
        
                // Добавляем настройки в коллекцию текущего объекта
                this.Installers.AddRange(
                    new Installer[]         
                    {
                        serviceInstaller,
                        serviceProcessInstaller
                    });
            }
        }
    }
  • Откомпилируйте проект командой Build/Build Solution
  • Теперь нужно зарегистрировать созданный сервис (для нас - сервер) в операционной системе.

  • В меню Пуск/Выполнить введете в текстовое поле имя запускаемой программы cmd и откройте окно команд Windows
  • Следующей командой перейдите в каталог с утилитой InstallUtil.exe >cd C:\Windows\Microsoft.NET\Framework\v2.0.50727
  • В панели Solution Explorer выделите узел PicturesServerDB, щелкните на пиктограмме Show All Files, раскройте узел bin/Debug и скопируйте полное имя созданной сборки E:\Tmp\Stream\NetworkStream\PicturesServerDB\bin\Debug\PicturesServerDB.exe
  • В окне команд Windows введите команду >InstallUtil.exe и добавьте мышью скопированное имя сборки, должно получиться >InstallUtil.exe E:\Tmp\Stream\NetworkStream\PicturesServerDB\bin\Debug\PicturesServerDB.exe
  • После выполнения этой команды откройте окно Пуск/Настройка/Панель управления/Администрирование/Службы (или жестом Ctrl-Alt-Del запустите Диспетчер задач Windows и откройте вкладку Службы) и запустите созданный сервис (для нас - сервер)
  • Запустите одного или нескольких клиентов PicturesClientDB.exe и испытайте работу полученного клиент-серверного приложения
  • Понаблюдайте за поведением клиентов при отключении и включении серверной программы, разберитесь с кодом (по возможности)
  • Вот одна из картинок, когда работа клиента (или всех клиентов) была временно прекращена остановкой сервера

    Чтобы созданный сервис не загружал компьютер, его можно деинсталлировать той же утилитой InstallUtil.exe, только с опцией /u.

    В последнем примере мы немного перегорячились и сразу стали делать сервер как службу Windows, чтобы не было видно пользовательского интерфейса (даже консольного окна). Это совершенно необязательно, сервер можно сделать и как обычное приложение с интерфейсом пользователя и запускать его по мере необходимости. Главное, что нужно помнить и что послужило причиной создания нами сервера как службы, - на одном IP -адресе не может сидеть сразу несколько запущенных серверов. Иначе бы получилась неопределенность, с каким именно сервером должны общаться клиенты. А службы как раз и запускаются в одном экземпляре.

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

    Пример 4. Создание сервера БД как Console Application

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

  • Добавьте к решению командой File/Add/New Project консольное приложение с именем ConsoleServerDB
  • В панели Solution Explorer в корень нового проекта скопируйте из предыдущего проекта PicturesServerDB файл App.config и папку Data. На предложение мастера создать типизированный DataSet скажите Cancel
  • В панели Solution Explorer через контекстное меню для узла References добавьте к проекту ссылку на System.Configuration.dll
  • Откройте файл Program.cs проекта консольного приложения и наполните его следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен для ADO.NET
    using System.Data;
    using System.Data.OleDb;
    using System.Data.Common;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.Collections;
    using System.Configuration;
        
    namespace ConsoleServerDB
    {
        class Program
        {
            int port = 12000;
            String hostName = "127.0.0.1";  // local
            IPAddress localAddr;
            TcpListener server = null;      // Ссылка на сервер
            String separator = "#";     // Разделитель имен в строке ответа
            String connectionString;        // Строка соединения с БД
        
            // Добавили конструктор
            public Program()
            {
                // Извлекаем в поле строку соединения с БД из файла App.config
                connectionString = System.Configuration.ConfigurationManager.
                    ConnectionStrings["PicturesDB"].ConnectionString;
        
                // Конвертируем IP в другой формат
                localAddr = IPAddress.Parse(hostName);
        
                // Запускаем в новом потоке (ните)
                Thread thread = new Thread(ExecuteLoop);
                thread.IsBackground = true;
                thread.Start();
            }
        
            static void Main(string[] args)
            {
                Console.WindowWidth = 20;
                Console.WindowHeight = 5;
                new Program();
        
                Console.ReadLine();// Держим основной поток (нить) приложения
            }
        
            private void ExecuteLoop()
            {
                try
                {
                    server = new TcpListener(localAddr, port);// Создаем сервер-слушатель
                    server.Start();// Запускаем сервер
        
                    // Бесконечный цикл прослушивания клиентов
                    while (true)
                    {
                        // Проверяем очередь соединений
                        if (!server.Pending())// Очередь запросов пуста
                            continue;
                        TcpClient client = server.AcceptTcpClient();// Текущий клиент
        
                        // Создаем потоки сетевых соединений
                        StreamReader readerStream = new StreamReader(client.GetStream());
                        NetworkStream streamOut = client.GetStream();
                        StreamWriter writerStream = new StreamWriter(streamOut);
        
                        // Читаем команду клиента 
                        String receiverData = readerStream.ReadLine();
        
                        // Распознаем и исполняем
                        switch (receiverData)
                        {
                            case "!!!GetNames!!!":// Посылаем имена, разделенные сепаратором
                                String names = GetNames();
                                writerStream.WriteLine(names);  // Используем через оболочку
                                writerStream.Flush();
                                break;
                            default:// Посылаем рисунок
                                Byte[] bytes = GetPicture(receiverData);
                                streamOut.Write(bytes, 0, bytes.Length);// Используем напрямую
                                streamOut.Flush();
                                break;
                        }
        
                        // Закрываем соединение и потоки, порядок неважен
                        client.Close();
                        readerStream.Close();
                        writerStream.Close();
                    }
                }
                finally
                {
                    // Останавливаем сервер
                    server.Stop();
                }
            }
        
            // Извлечение из БД имен рисунков и упаковка их в одну строку для пересылки клиенту
            string GetNames()
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection(connectionString);
                OleDbCommand cmd = new OleDbCommand("SELECT FileName FROM MyTable");
                cmd.Connection = conn;
                conn.Open();
        
                // Извлекаем имена рисунков
                OleDbDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
        
                // Формируем строку исходящих данных
                StringBuilder sb = new StringBuilder();
                foreach (DbDataRecord record in reader)// Равносильно чтению reader.Read()
                    sb.Append(((string)record["FileName"]).Trim() + separator);
                // Соединение здесь закроет сам объект DataReader после прочтения всех данных
                // в соответствии с соглашением при его создании CommandBehavior.CloseConnection
        
                // Удаляем лишний последний символ сепаратора
                sb.Replace(separator, String.Empty, sb.ToString().
                    LastIndexOf(separator), separator.Length);
        
                return sb.ToString();
            }
        
            // Извлечение из БД самого рисунка для отправки клиенту
            byte[] GetPicture(String name)
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection();
                conn.ConnectionString = connectionString;
        
                // Создаем и настраиваем объект команды, параметризованной по имени рисунка
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = conn;
                cmd.CommandType = CommandType.Text; // Необязательно! Установлено по умолчанию
                cmd.CommandText = "SELECT Picture FROM MyTable WHERE FileName=?";
                cmd.Parameters.Add(new OleDbParameter());
                cmd.Parameters[0].Value = name;// Имя рисунка
                OleDbDataAdapter adapter = new OleDbDataAdapter(cmd);
        
                // Извлекаем рисунок из БД 
                DataTable table = new DataTable();
                adapter.Fill(table);
                byte[] bytes = (byte[])table.Rows[0]["Picture"]; // Подключаемся к рисунку
        
                return bytes;
            }
        }
    }

    Приведенный код в точности соответствует серверу, созданному нами ранее как сервис, только дополнительно добавилась точка входа Main() и класс стал называться по другому.

  • Откомпилируйте проект командой Build/Build Solution
  • Через Проводник Windows зайдите в каталоги bin/Debug проектов ConsoleServerDB и PicturesClientDB
  • Для удобства запуска создайте для исполнимых сборок ConsoleServerDB.exe и PicturesClientDB.exe ярлыки на рабочем столе компьютера, попробуйте запустить несколько копий каждого из приложений
  • Мы видим, что копии клиентских приложений запускаются нормально, а для консольного сервера дублирующие копии вылетают с выбросом исключения операционной системы. А если запустить ранее созданный сервисный сервер (в Службах Windows ), то консольный сервер будет вылетать даже при первом запуске.

  • Поэкспериментируйте с построенными приложениями, осмыслите происходящее и разберитесь с кодом
  • Пример 5. Создание сервера БД как Windows Forms

    Чтобы показать, что сервером может выступать приложение любого типа, повторим предыдущую задачу на приложении с оконным интерфейсом. Учитывая, что в сети прослушивать клиентов по одному и тому же адресу может только один запущенный экземпляр сервера, заодно покажем, как блокировать повторный его запуск. Нового здесь будет мало, только создадим оконную заготовку приложения Windows Forms и просто скопируем код сервера в это приложение, изменив самую малость. А потом еще добавим в файл Program.cs код блокирования повторного запуска, - вот и все.

    Создание оконного сервера

  • Добавьте к проекту командой File/Add/New Project оболочки новый проект с именем WindowServerDB (стартовым его не делайте)
  • В панели Solution Explorer вызовите для узла WindowServerDB контекстное меню и выполните команду Build
  • В Проводнике Windows зайдите в каталог исполнимой сборки проекта bin\Debug и отправьте ярлык исполнимой сборки WindowServerDB.exe на рабочий стол контекстной командой Отправить
  • Заполните файл Form1.cs следующим кодом (приводится полностью и почти в точности повторяет код файла Program.cs проекта ConsoleServerDB )
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен для ADO.NET
    using System.Data.OleDb;
    using System.Data.Common;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.Collections;
    using System.Configuration;
        
    namespace WindowServerDB
    {
        public partial class Form1 : Form
        {
            int port = 12000;
            String hostName = "127.0.0.1";  // local
            IPAddress localAddr;
            TcpListener server = null;      // Ссылка на сервер
            String separator = "#";     // Разделитель имен в строке ответа
            String connectionString;        // Строка соединения с БД
        
            public Form1()
            {
                InitializeComponent();
                // Извлекаем в поле строку соединения с БД из файла App.config
                connectionString = System.Configuration.ConfigurationManager.
                    ConnectionStrings["PicturesDB"].ConnectionString;
        
                // Конвертируем IP в другой формат
                localAddr = IPAddress.Parse(hostName);
        
                // Запускаем в новом потоке (ните)
                Thread thread = new Thread(ExecuteLoop);
                thread.IsBackground = true;
                thread.Start();
            }
        
            private void ExecuteLoop()
            {
                try
                {
                    server = new TcpListener(localAddr, port);// Создаем сервер-слушатель
                    server.Start();// Запускаем сервер
        
                    // Бесконечный цикл прослушивания клиентов
                    while (true)
                    {
                        // Проверяем очередь соединений
                        if (!server.Pending())// Очередь запросов пуста
                            continue;
                        TcpClient client = server.AcceptTcpClient();// Текущий клиент
        
                        // Создаем потоки сетевых соединений
                        StreamReader readerStream = new StreamReader(client.GetStream());
                        NetworkStream streamOut = client.GetStream();
                        StreamWriter writerStream = new StreamWriter(streamOut);
        
                        // Читаем команду клиента 
                        String receiverData = readerStream.ReadLine();
        
                        // Распознаем и исполняем
                        switch (receiverData)
                        {
                            case "!!!GetNames!!!":// Посылаем имена, разделенные сепаратором
                                String names = GetNames();
                                writerStream.WriteLine(names);  // Используем через оболочку
                                writerStream.Flush();
                                break;
                            default:// Посылаем рисунок
                                Byte[] bytes = GetPicture(receiverData);
                                streamOut.Write(bytes, 0, bytes.Length);// Используем напрямую
                                streamOut.Flush();
                                break;
                        }
        
                        // Закрываем соединение и потоки, порядок неважен
                        client.Close();
                        readerStream.Close();
                        writerStream.Close();
                    }
                }
                finally
                {
                    // Останавливаем сервер
                    server.Stop();
                }
            }
        
            // Извлечение из БД имен рисунков и упаковка их в одну строку для пересылки клиенту
            string GetNames()
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection(connectionString);
                OleDbCommand cmd = new OleDbCommand("SELECT FileName FROM MyTable");
                cmd.Connection = conn;
                conn.Open();
        
                // Извлекаем имена рисунков
                OleDbDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
        
                // Формируем строку исходящих данных
                StringBuilder sb = new StringBuilder();
                foreach (DbDataRecord record in reader)// Равносильно чтению reader.Read()
                    sb.Append(((string)record["FileName"]).Trim() + separator);
                // Соединение здесь закроет сам объект DataReader после прочтения всех данных
                // в соответствии с соглашением при его создании CommandBehavior.CloseConnection
        
                // Удаляем лишний последний символ сепаратора
                sb.Replace(separator, String.Empty, sb.ToString().
                    LastIndexOf(separator), separator.Length);
        
                return sb.ToString();
            }
        
            // Извлечение из БД самого рисунка для отправки клиенту
            byte[] GetPicture(String name)
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection();
                conn.ConnectionString = connectionString;
        
                // Создаем и настраиваем объект команды, параметризованной по имени рисунка
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = conn;
                cmd.CommandType = CommandType.Text; // Необязательно! Установлено по умолчанию
                cmd.CommandText = "SELECT Picture FROM MyTable WHERE FileName=?";
                cmd.Parameters.Add(new OleDbParameter());
                cmd.Parameters[0].Value = name;// Имя рисунка
                OleDbDataAdapter adapter = new OleDbDataAdapter(cmd);
        
                // Извлекаем рисунок из БД 
                DataTable table = new DataTable();
                adapter.Fill(table);
                byte[] bytes = (byte[])table.Rows[0]["Picture"]; // Подключаемся к рисунку
        
                return bytes;
            }
        }
    }
  • В панели Solution Explorer раскройте узел проекта ConsoleServerDB и, удерживая клавишу Ctrl, выделите папку Data и файл App.config, затем перетащите их курсором в корень проекта WindowServerDB. На предложение мастера создать набор данных скажите Cancel
  • Убедитесь, чтобы скопированная база данных Data/Pictures.my2.mdb имела в панели Properties значение:
  • Copy to Output Directory=Copy if newer
  • В панели Solution Explorer выделите любой элемент проекта WindowServerDB (например, корень), откройте через меню оболочки панель View/Object Browser, задайте в ней режим Browse: All Components, а в поле поиска - пространство имен System.Configuration. Для найденного пространства имен выделите родительскую сборку System.configuration.dll и кнопкой с пиктограммой Add to References in Selected Project in Solution Explorer добавьте ссылку в папку References текущего проекта (таким способом можно добавлять сразу несколько ссылок) для видимости компилятором класса ConfigurationManager
  • Откомпилируйте проект командой Rebuild и запустите этот оконный сервер через ярлык рабочего стола компьютера - сервер хоть и с пустым окном, но работает
  • Проверьте работу сервера WindowServerDB.exe совместно с клиентской сборкой PicturesClientDB.exe - все должно работать исправно!
  • Блокирование дублирующего запуска оконного сервера

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

    Не всегда у управляемой среды исполнения CLR хватает своих средств для решения любых задач и в некоторых случаях приходится напрямую обращаться к библиотекам и функциям операционной системы. Для таких целей предусмотрен атрибут DllImport, с помощью которого можно вызвать любую функцию API Windows. Это относится и к нашей задаче - когда требуется блокировать повторный запуск приложения, то можно воспользоваться таблицей глобальных атомов самой операционной системы. А к ней из управляемого кода C# доступ невозможен.

  • Измените код файла Program.cs проекта WindowServerDB на следующий (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.Windows.Forms;
        
    // Дополнительное пространство имен импорта типов API
    using System.Runtime.InteropServices;
        
    namespace WindowServerDB
    {
        static class Program
        {
            [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            static extern ushort GlobalAddAtom(string lpString);
            [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            static extern ushort GlobalFindAtom(string lpString);
            [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            static extern ushort GlobalDeleteAtom(ushort atom);
        
            [STAThread]
            static void Main()
            {
                string strAtom = Application.ProductName
                    + Application.ProductVersion;
                ushort atom = GlobalFindAtom(strAtom);
        
                if (atom != 0)
                {
                    MessageBox.Show("Прежде чем создать новый\n"
                        + "экземпляр этого приложения,\n"
                        + "сначала следует закрыть текущий!");
                }
                else
                {
                    // Атом не найден в таблице атомов, создаем его
                    atom = GlobalAddAtom(strAtom);
        
                    // Настройка приложения 
                    Application.EnableVisualStyles();
                    Application.SetCompatibleTextRenderingDefault(false);
                    try
                    {
                        // Рабочий цикл Windows 
                        Application.Run(new Form1());
                    }
                    finally
                    {
                        // Удаляем атом в любом случае
                        GlobalDeleteAtom(atom);
                    }
                }
            }
        }
    }

    Вопросы взаимодействия CLR с операционной системой, потоки выполнения Thread, атрибуты и многое другое прекрасно описаны в:

    Дубовцев А.В. Microsoft .NET в подлиннике. - СПб.: БХВ-Петербург, 2004. - 704 с.

  • Запустите оконный сервер WindowServerDB и убедитесь, что дублирующий запуск приложения теперь заблокирован
  • Сокрытие окна сервера

    Сейчас нам стыдно смотреть на окно сервера - оно просто пустое. В дальнейшем мы можем его задействовать. Например, для регистрации администратором клиентов, редактирования настроек сервера и другими задачами, связанными с нашим приложением. А пока для тренировки его отображение на экране можно просто отключить: оно есть и работает, просто его нигде не видно (почти нигде). Для этого воспользуемся событием Shown объекта окна.

  • Дополните класс формы в файле Form1.cs следующим кодом
  • namespace WindowServerDB
    {
        public partial class Form1 : Form
        {
            ................................................
        
            public Form1()
            {
                ................................................
        
                // Подписываемся на событие для сокрытия окна
                this.Shown += Form1_Shown;
            }
        
            void Form1_Shown(object sender, EventArgs e)
            {
                this.Hide();
            }
        
            ................................................
        }
    }
  • Откомпилируйте сервер WindowServerDB и запустите через ярлыки его и клиента PicturesClientDB - все работает, но сам сервер невидно и как его остановить!
  • Вызовите жестом Ctrl-Alt-Del Диспетчер задач Windows, откройте вкладку Процессы - сервер работает и его можно даже закрыть
  • Замечания

    Если сейчас закрыть невидимый сервер через Диспетчер задач Windows, то он повторно не запустится - сработает наша блокировка. Причина в том, что в данном случае приложение завершится нестандартно, а именно: не через среду CLR, а напрямую через Диспетчер задач Windows. Соответственно, и атом (наша метка блокировки) в таблице глобальных атомов останется. Он будет удален только при перезагрузке Windows. Здесь нужно выбирать что-то одно: либо оставить блокировку и не скрывать сервер, либо убрать блокировку и сделать сервер невидимым для пользователя.

    Теперь мы знаем, что необязательно делать сервер как Службу Windows ( Windows Service ), чтобы скрыть его от пользователя. Но можно сделать сервер как приложение Windows Forms, пусть даже с пустой формой, скрыть его и включить в список программ, автоматически запускаемых при загрузке Windows. Это уже дело вкуса - наша задача состояла в том, чтобы проверить такую возможность.

    Кстати, консольное приложение не имеет события, подобного событию Shown объекта окна. Оно показывается самой CLR при запуске и скрыть его программно невозможно, хотя мы его и применяли при создании сервера.

    Есть еще один вариант: установить состояние окна при запуске свернутым, тогда его можно завершить через среду CLR естественным образом.

  • Внесите в конструктор формы следующие очевидные изменения
  • namespace WindowServerDB
    {
        public partial class Form1 : Form
        {
            ................................................
        
            public Form1()
            {
                ................................................
        
                // Подписываемся на событие для сокрытия окна
                //this.Shown += Form1_Shown;
                // Минимизируем окно при запуске
                this.WindowState = FormWindowState.Minimized;
            }
        
            void Form1_Shown(object sender, EventArgs e)
            {
                this.Hide();
            }
        
            ................................................
        }
    }
  • Запустите сервер - теперь и блокировка работает, и сервер можно закрыть, и миокард целый!
  • Пример 6. Сетевой обмен сериализованными объектами данных

    Мы уже рассматривали сериализацию объектов данных. Она позволяет легко упаковывать и восстанавливать информацию при передаче ее в потоках. Сейчас мы построим клиент-серверное приложение, на примере которого убедимся, насколько удобным является сетевой обмен данным с использованием сериализации. Нам не надо будет ничего подсчитывать и учитывать, вводя какие-то специальные метки для данных, их типе и длине, все будет гораздо проще. Мы просто создадим объект необходимой нам структуры, тип которого будет известен и серверу, и клиентам, и который будет служить контейнером данных. Этот объект стороны будут наполнять своими данными и отправлять по сети в сериализованной форме (в блестящей и хрустящей упаковке!).

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

    Но клиенты бывают разные. Злонамеренный пользователь (одушевленный клиент) может потребовать от сервера такие действия, которые ему заведомо навредят (например, скомандует удалить или прислать какой-то важный файл). Поэтому естественно, что права у клиентов на команды (на действия) должны быть разными. Защищенный сервер должен регистрировать клиентов, присваивать им статус и при каждом запросе проверять их полномочия на поступившую команду или затребованную информацию. Отсюда и берутся учетные записи, пароли и их шифрование, администраторы и т.д. Стараются защититься как программными методами, так и аппаратно.

    До этого момента нашей главной задачей было определение типа пересылаемых данных, чтобы правильно использовать инструменты их чтения и записи. Теперь такая задача станет проще, поскольку тип данных будет назначаться при конструировании объекта транспортировки. А сама транспортировка будет выполняться оболочкой сериализации унифицированным образом.

    Чтобы убедительно продемонстрировать сказанное, будем сериализовать и пересылать разнородные типы данных: текст, рисунки, звук и видео. Все это, в конечном итоге, будет представлять собой потоки байтов с хорошо распознаваемыми типами при десериализации. Работа предстоит большая, но чрезвычайно полезная, поэтому прошу ее не игнорировать, а выполнить внимательно. Постараемся код сделать интересным и разнообразным (да простят меня классики!). Итак, приступим...

    Создание объекта транспортировки

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

  • Командой оболочки File/Add/New Project добавьте к решению NetworkStream новый проект с именем MyLibrary типа Class Library
  • Заполните созданный оболочкой файл Class1.cs следующим кодом (приводится полностью)
  • using System;
    using System.IO;
        
    namespace MyLibrary.MyDataSerialization
    {
        /// <summary>
        /// Перечисление команд, передаваемых 
        /// серверу для исполнения на нем
        /// </summary>
        [Serializable()]    // Атрибут
        public enum Command
        {
            /// <summary>Добавить сообщение в журнал</summary>
            AddTextInLog,
            /// <summary>Прислать журнал</summary>
            Log,
            /// <summary>Удалить журнал</summary>
            DeleteLog,
            /// <summary>Прислать сообщение</summary>
            Text,
            /// <summary>Прислать рисунок</summary>
            Image,
            /// <summary>Прислать звук</summary>
            Sound,
            /// <summary>Прислать видео</summary>
            Video
        }
        
        /// <summary>
        /// Сериализуемый объект для транспортировки
        /// </summary>
        [Serializable()]    // Атрибут
        public class DataObject : ICloneable
        {
            // Поля с сохраняемыми данными
            Command command;        // Идентификатор команды
            string displayCommand;  // Представление команды для пользователя
            string text;            // Для передачи текстовых данных
            string logName;         // Имя файла с журналом
            string imageName;       // Имя файла с рисунком
            string soundName;       // Имя файла со звуком
            string videoName;       // Имя файла с видео
            Stream image;           // Сам рисунок
            Stream sound;           // Сам звук
            Stream video;           // Само видео
        
            // Свойства доступа к полям
            public Command Command
            {
                get { return command; }
                set { command = value; }
            }
            // Строковый тип
            public String DisplayCommand
            {
                get { return displayCommand; }
                set { displayCommand = value; }
            }
            public String Text
            {
                get { return text; }
                set { text = value; }
            }
            public String LogName
            {
                get { return logName; }
                set { logName = value; }
            }
            public String ImageName
            {
                get { return imageName; }
                set { imageName = value; }
            }
            public String SoundName
            {
                get { return soundName; }
                set { soundName = value; }
            }
            public String VideoName
            {
                get { return videoName; }
                set { videoName = value; }
            }
            // Потоковый тип
            public Stream Image
            {
                get { return image; }
                set { image = value; }
            }
            public Stream Sound
            {
                get { return sound; }
                set { sound = value; }
            }
            public Stream Video
            {
                get { return video; }
                set { video = value; }
            }
        
            /// <summary>
            /// Оригинальный метод C#
            /// Реализация интерфейса ICloneable для создания копии объекта
            /// </summary>
            public Object Clone()
            {
                // Создаем клон
                DataObject clone = new DataObject();
        
                // Копируем значимые свойства
                clone.command = this.command;
                clone.displayCommand = this.displayCommand;
                clone.text = this.text;
                clone.logName = this.logName;
                clone.imageName = this.imageName;
                clone.soundName = this.soundName;
                clone.videoName = this.videoName;
        
                // Значения ссылок не копируем
                clone.image = null;
                clone.sound = null;
                clone.video = null;
        
                return clone;
            }
        
            /// <summary>
            /// Альтернативный метод создания копии объекта 
            /// на основе шаблона, потока памяти и сериализации.
            /// А он мне нравится, нравится, нравится...!!!!!!!
            /// </summary>
            public T BinaryClone<T>()
            {
                using (var stream = new System.IO.MemoryStream())
                {
                    var binaryFormatter = new System.Runtime.Serialization.
                        Formatters.Binary.BinaryFormatter();
                    binaryFormatter.Serialize(stream, this);
                    stream.Position = 0;
        
                    return (T)binaryFormatter.Deserialize(stream);
                }
            }
        }
    }

    Метод Clone() этого класса является реализацией наследуемого библиотечного интерфейса ICloneable. Этот метод нам понадобится в коде сервера для создания объекта отправки как копии присланного объекта. Интерфейс ICloneable языка C# является альтернативой конструктора копий языка C++. Типизированный метод BinaryClone() является еще более мощной этому заменой (предложен в ' http://weblogs.asp.net/esanchez/archive/2008/05/18/cloning-objects-in-net.aspx'). Он создает копии не только значимых данных, но и копии ссылок вместе с копиями адресуемых этими ссылками данных. И все это - с использованием рассматриваемых нами интруменов. Именно его (но попробуйте и Clone ) мы включим в код сервера.

    Ключевое слово var в описании метода BinaryClone() предназначено для ленивых и означает объявление типа ссылки, определяемого типом адресуемого объекта. На более превычном нам синтаксисе метод должен выглядеть так

    public T BinaryClone<T>()
            {
                using (MemoryStream stream = new System.IO.MemoryStream())
                {
                    BinaryFormatter binaryFormatter = new System.Runtime.Serialization.
                        Formatters.Binary.BinaryFormatter();
                    binaryFormatter.Serialize(stream, this);
                    stream.Position = 0;
        
                    return (T)binaryFormatter.Deserialize(stream);
                }
            }
  • Откомпилируйте проект MyLibrary командой Build - теперь это полноправный библиотечный объект и его можно подключать к любому проекту в узле References
  • Создание клиента транспортировки

    Клиента построим с пользовательским интерфейсом по технике Windows Forms. Сервер должен 'сидеть' на компьютере с данными и по запросам клиентов отправлять им эти данные. Клиенты должны иметь интерфейс для формирования запроса и отображения присланных данных. Для разнообразия, видеоданные будем отображать с помощью плеера из WPF, хотя можно было бы воспользоваться COM -объектом " Windows Media Player ", как мы это делали в лабораторной работе №45. Поскольку в пределах одного окна интерфейсные элементы двух техник ( Windows Forms и WPF ) несовместимы без специального приема, используем для совместимости переходник ElementHost из пространства имен System.Windows.Forms.Integration 'http://msdn.microsoft.com/ru-ru/library/system.windows.forms.integration.elementhost.aspx'

  • Добавьте к решению NetworkStream новый проект с именем SerializeClient и назначьте его стартовым
  • В панели Solution Explorer выделите узел проекта SerializeClient и командой Project/Add Reference добавьте к проекту ссылки на библиотеки:
  • из вкладки .NET для видеоплеера WPF:
  • PresentationCore.dll
  • PresentationFramework.dll
  • WindowsBase.dll
  • WindowsFormsIntegration.dll
  • из вкладки Projects для нашего объекта транспортировки:
  • MyLibrary.dll
  • Настройте элементы формы в соответствии с таблицей свойств
  • Элемент Свойство Значение
    Form Text Simple SerializeClient
    Size 300; 300
    StartPosition CenterScreen
    MaximizeBox False
    Label Text Command:
    Location 3; 16
    Size 75; 17
    AutoSize True
    ComboBox (Name) cbCommand
    Location 79; 13
    Size 201; 24
    Label Text Message:
    Location 9; 53
    Size 69; 17
    AutoSize True
    TextBox (Name) txtMessage
    Location 79; 50
    Size 201; 22
    Label Text Display:
    Location 20; 144
    Size 58; 17
    AutoSize True
    ElementHost Переходник WPF->WindowsForms. Ищите в панели Toolbox на вкладке 'WPF Interoperability', если там нет, добавьте в нее контекстной командой Choose Items или создайте объект динамически в процедурном коде
    (Name) videoHost
    Location 79; 85
    Size 201; 127
    PictureBox (Name) pictureBox
    Location 79; 85
    Size 201; 127
    TextBox (Name) txtReceived
    Location 79; 85
    Size 201; 127
    Multiline True
    Button (Name) btnSend
    AutoSize True
    Location 142; 228
    Size 75; 27
    Text Send

    В итоге, пользовательский интерфейс формы должен выглядеть так

    Обратите внимание, что мы совместили позиции элементов ElementHost, PictureBox и TextBox, причем видимым на верхнем слое будет элемент TextBox, добавленный на форму последним. Естественно, что из них одновременно мы будем показывать только один элемент, делая в коде остальные невидимыми.

  • Заполните файл Form1.cs клиента SerializeClient следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.Runtime.Serialization.Formatters.Binary;
    using System.Media; // Для проигрывателя wav-файлов SoundPlayer
        
    // Своя библиотека
    using Data = MyLibrary.MyDataSerialization.DataObject;// Псевдоним класса
    using MyLibrary.MyDataSerialization;
        
    namespace SerializeClient
    {
        public partial class Form1 : Form
        {
            int port = 12000;
            String hostName = "127.0.0.1";  // local
            TcpClient client;               // Ссылка на клиента
            NetworkStream readerStream, writerStream;// Объявили ссылки на транспортный поток
            DataTable table;// Для привязки к списку ComboBox
        
            System.Windows.Controls.MediaElement player;// Для просмотра видео
            bool playerReady = true;// Блокировка повторного запуска на время проигрывания
        
            public Form1()
            {
                InitializeComponent();
        
                // Формируем объектную таблицу и привязываем к списку
                cbCommand.DataSource = DataCommand();
                cbCommand.DisplayMember = "CommandName";
                cbCommand.ValueMember = "CommandValue";
                //cbCommand.SelectedIndex = 0;// Первый
                // Устанавливаем курсор списка в конец
                cbCommand.SelectedIndex = cbCommand.Items.Count - 1;// Последний
            }
        
            DataTable DataCommand()
            {
                table = new DataTable();// Таблица, привязываемая к ComboBox
        
                // Задаем структуру таблицы
                DataColumn[] cols = new DataColumn[2];
                cols[0] = new DataColumn("CommandName", typeof(String));
                cols[1] = new DataColumn("CommandValue", typeof(Command));
                table.Columns.AddRange(cols);
        
                // Формируем строки таблицы
                DataRow row;
                String[] names ={   // Видимое в ComboBox
                                    "Добавить в журнал",
                                    "Прислать журнал",
                                    "Удалить журнал",
                                    "Прислать сообщение",
                                    "Прислать рисунок",
                                    "Прислать звук",
                                    "Прислать видео"
                                };
                //String[] commands = Enum.GetNames(typeof(Command));   // Сортированы по значению
                int[] values = (int[])Enum.GetValues(typeof(Command));  // Сортированы по значению
                for (int i = 0; i < values.Length; i++)
                {
                    row = table.NewRow();
                    row[0] = names[i];
                    row[1] = (Command)values[i];
                    table.Rows.Add(row);
                }
        
                return table;// Возвращаем заполненную таблицу для привязки к ComboBox
            }
        
            // По щелчку кнопки выполняет обмен данными с сервером
            private void btnSend_Click(object sender, EventArgs e)
            {
                try
                {
                    // Создать клиента
                    client = new TcpClient(hostName, port);
                }
                catch
                {
                    MessageBox.Show("Сервер не готов!");
                    return;
                }
        
                //
                //********************* Отправка *********************
                //
                // Собираем данные с элементов управления для отправки через объект
                Data dataSend = new Data();
                dataSend.Command = (Command)cbCommand.SelectedValue;
                dataSend.DisplayCommand =
                    table.Rows[cbCommand.SelectedIndex]["CommandName"].ToString(); ;
                dataSend.Text = txtMessage.Text.Trim();// Берем из текстового поля Message
                // Обнуляем ссылки на потоки
                dataSend.Image = null;
                dataSend.Sound = null;
                dataSend.Video = null;
                // Имена зададим жестко в коде, хотя можно 
                // сделать диалог опроса пользователя
                dataSend.LogName = "Log.txt";               // Имя журнала, локализация на сервере C:\server
                dataSend.ImageName = @"Data\flower2.jpg";   // Разместить в каталоге сборки сервера
                dataSend.SoundName = @"Data\Trumpet1.wav";  // Разместить в каталоге сборки сервера
                dataSend.VideoName = @"Data\tits.mpe";      // Разместить в каталоге сборки сервера
        
                // Подключаем транспортный поток для отправки
                writerStream = client.GetStream();
                // Сериализуем объект в транспортный поток и отправляем 
                BinaryFormatter formatter = new BinaryFormatter();
                formatter.Serialize(writerStream, dataSend);// Отправленный объект
        
                //
                //********************* Получение *********************
                //
                // Подключаем транспортный поток для получения данных
                readerStream = client.GetStream();
                // Прочитать и десериализовать в объект Data
                formatter = new BinaryFormatter();
                Data dataReceived = (Data)formatter.Deserialize(readerStream);// Полученный объект
        
                readerStream.Close();// Освобождаем поток
                writerStream.Close();// Освобождаем поток
                client.Close();// Освобождаем сокет клиента
        
                // Анализ и отображение присланного
                // Сервер может поменять команду, поэтому проверяем
                // не по cbCommand.SelectedIndex, а по присланному факту
                String dir = @"c:\Client";
                String path = Path.Combine(dir, dataReceived.LogName);
                if (!Directory.Exists(dir))
                    Directory.CreateDirectory(dir);// Создаем каталог, если не существует
        
                // Разбор и исполнение вариантов
                switch (dataReceived.Command)
                {
                    // Прислали текст, покажем на форме
                    case Command.Text:
                        if (!String.IsNullOrEmpty(dataReceived.Text))
                        {
                            txtReceived.Visible = true;
                            pictureBox.Visible = false;
                            videoHost.Visible = false;
                            txtReceived.Text = dataReceived.Text;
                        }
                        break;
                    // Прислали журнал, покажем через Блокнот
                    case Command.Log:
                        if (!String.IsNullOrEmpty(dataReceived.Text))
                        {
                            // Сохраним содержимое журнала в файл
                            FileStream fs = File.Create(path);
                            StreamWriter sw = new StreamWriter(fs);
                            sw.Write(dataReceived.Text);
                            sw.Close();
        
                            // Покажем журнал через блокнот
                            System.Diagnostics.Process exe = new System.Diagnostics.Process();
                            exe.StartInfo.FileName = "Notepad.exe"; //Имя программы для запуска
                            exe.StartInfo.Arguments = path;         //Аргументы
                            exe.Start();        // Запускаем внешний процесс
                            //exe.WaitForExit();// Останавливаем наш процесс до закрытия блокнота
                            exe.Close();        // Освобождаем связанные с процессом ресурсы
                        }
                        break;
                    // Прислали рисунок, покажем на форме
                    case Command.Image:
                        if (dataReceived.Image != null)
                        {
                            txtReceived.Visible = false;
                            pictureBox.Visible = true;
                            videoHost.Visible = false;
                            pictureBox.SizeMode = PictureBoxSizeMode.StretchImage;
                            txtMessage.Text = dataReceived.Text;// Сообщение
                            pictureBox.Image = new Bitmap(dataReceived.Image);
                        }
                        break;
                    // Прислали звук, проиграем (можно и в карты!)
                    case Command.Sound:
                        if (dataReceived.Sound != null)
                        {
                            txtReceived.Visible = true;
                            pictureBox.Visible = false;
                            videoHost.Visible = false;
                            txtReceived.Text = dataReceived.Text;
                            txtMessage.Text = dataReceived.Text;// Сообщение
                            SoundPlayer soundPlayer = new SoundPlayer(dataReceived.Sound);
                            //soundPlayer.LoadAsync(); //Играть фоново (Необязательно!)
                            soundPlayer.Play();
                        }
                        break;
                    // Прислали видео, посмотрим
                    case Command.Video:
                        if (dataReceived.Video != null  playerReady)
                        {
                            txtReceived.Visible = false;
                            pictureBox.Visible = false;
                            videoHost.Visible = true;
                            txtMessage.Text = dataReceived.Text;// Сообщение
        
                            // Создаем WPF-проигрыватель
                            player = new System.Windows.Controls.MediaElement();
                            playerReady = false;// Затычка от повторного запуска, пока играет
                            // Передаем переходнику как контейнеру
                            videoHost.Child = player;
                            // player.SetStream(dataReceived.Video); // Не работает, хотя есть в MSDN
        
                            // Поэтому по-турецки: загружаем видео через файл на клиенте
                            // Получили поток памяти и пишем его в файл на диске клиента
                            String[] tmp = dataReceived.VideoName.Split('\\');
                            string shortName = tmp[tmp.Length - 1];// Короткое имя видеофайла  
                            string fullName = Path.Combine(dir, shortName);// Имя с путем
                            FileStream fs = File.Create(fullName);// Создаем или усекаем
                            MemoryStream ms = dataReceived.Video as MemoryStream;// Приводим к исходному
                            ms.WriteTo(fs);// Сливаем в файл
                            ms.Flush();// Необязательно! Сбросится при закрытии потока
                            fs.Close();
                            ms.Close();
        
                            // Настраиваем WPF-проигрыватель
                            player.Source = new Uri(fullName, UriKind.Relative);// Держит файл!!!
                            player.Stretch = System.Windows.Media.Stretch.Fill;// На весь контейнер
                            // Сработает, когда проигрывание закончится
                            player.MediaEnded += new System.Windows.RoutedEventHandler(player_MediaEnded);
                            // Запускать вручную - только после команды Play()
                            player.LoadedBehavior = System.Windows.Controls.MediaState.Manual;
                            // Останавливать вручную - только после команды Stop()
                            player.UnloadedBehavior = System.Windows.Controls.MediaState.Stop;
                            player.IsMuted = true; // Проигрывать с отлюченным звуком
                            player.Play();// Запустить проигрывание
        
                            /*******************************************************************
                            // Это альтернативный код. Он только для WPF и не конвертируется в Windows Forms
                            System.Windows.Media.MediaPlayer player =
                                new System.Windows.Media.MediaPlayer();
                            player.Open(new Uri("WellWait - 02.AVI", UriKind.Relative));
                            System.Windows.Media.VideoDrawing aVideoDrawing =
                                new System.Windows.Media.VideoDrawing();
                            aVideoDrawing.Player = player;
                            System.Windows.Rect rect = new System.Windows.Rect(0, 0, 100, 100);
                            aVideoDrawing.Rect = rect;// Здесь несоответствие типов!!!
                            player.Play();    
                            //******************************************************************/
                        }
        
                        break;
                }
            }
        
            // Плеер блокирует файл на время жизни, поэтому ждем 
            // окончания проигрывания и полность уничтожаем объект
            void player_MediaEnded(object sender, System.Windows.RoutedEventArgs e)
            {
                player.Close();
                player = null;// Необязательно, итак работает!
                GC.WaitForFullGCComplete();// Остановить основной процесс до конца уборки
                GC.Collect();// Запускаем сборщик мусора
                // Здесь уже плеер полность удален из памяти и отпустил файл
                playerReady = true; // Открываем доступ к коду создания нового проигрывателя
            }
        }
    }
  • Запустите проект на выполнение - ошибок компиляции быть не должно и клиент выдаст сообщение, что сервер не готов
  • Создание сервера транспортировки

  • Добавьте к решению NetworkStream новый проект с именем SerializeServer типа консольного приложения (стартовым не назначайте, стартовым будет приложение SerializeClient )
  • В панели Solution Explorer для узла References проекта SerializeServer вызовите контекстное меню и командой Add Reference через вкладку Projects добавьте к проекту ссылку на библиотеку MyLibrary.dll для нашего объекта транспортировки
  • В панели Solution Explorer вызовите контекстное меню для корня проекта SerializeServer и командой Add/New Folder создайте папку Data
  • В панели Solution Explorer вызовите контекстное меню для папки Data и командой Add/Existing Item добавьте к проекту из прилагаемого каталога Source файлы:
  • flower2.jpg
  • Trumpet1.wav
  • tits.mpe
  • Выделите все три скопированных в проект файла и через панель Properties установите для них свойства:
  • Build Action=None
  • Copy to Output Directory=Copy if newer
  • Откройте файл Program.cs проекта SerializeServer и заполните его следующим процедурным кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен для ADO.NET
    using System.Data;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.Collections;
    using System.Configuration;
    using System.Runtime.Serialization.Formatters.Binary;
        
    // Своя библиотека
    using Data = MyLibrary.MyDataSerialization.DataObject;// Псевдоним класса
    using MyLibrary.MyDataSerialization;// Пространство имен в библиотеке
        
    namespace SerializeServer
    {
        class Program
        {
            int port = 12000;
            String hostName = "127.0.0.1";  // local
            IPAddress localAddr;
            TcpListener server = null;      // Ссылка на сервер
            NetworkStream readerStream, writerStream;// Объявили ссылку на транспортный поток
        
            // Добавили конструктор
            public Program()
            {
                // Конвертируем IP в другой формат
                localAddr = IPAddress.Parse(hostName);
        
                // Запускаем в новом потоке (ните)
                Thread thread = new Thread(ExecuteLoop);
                thread.IsBackground = true;
                thread.Start();
            }
        
            static void Main(string[] args)
            {
                Console.WindowWidth = 20;
                Console.WindowHeight = 5;
                new Program();
        
                Console.ReadLine();// Держим основной поток (Thread) приложения
            }
        
            private void ExecuteLoop()
            {
                try
                {
                    server = new TcpListener(localAddr, port);// Создаем сервер-слушатель
                    server.Start();// Запускаем сервер
        
                    // Бесконечный цикл прослушивания очереди клиентов
                    while (true)
                    {
                        // Проверяем очередь соединений
                        if (!server.Pending())// Очередь запросов пуста
                            continue;
                        TcpClient client = server.AcceptTcpClient();// Текущий клиент
        
                        //
                        //********************* Получение *********************
                        //
                        // Подключаемся к сокету для чтения
                        readerStream = client.GetStream();
        
                        // Получить объект данных и десериализовать
                        BinaryFormatter formatter = new BinaryFormatter();
                        Data dataReceived = (Data)formatter.Deserialize(readerStream);
        
                        //
                        //************ Выполнение действий и отправка **********
                        //
                        // Создаем объект-клон для отправки и постепенно заполняем данными
                        //Data dataSend = (Data)dataReceived.Clone();    // Оригинальный метод
                        Data dataSend = dataReceived.BinaryClone<Data>();// Альтернативный метод
        
                        // Распознаем присланное, исполняем и формируем данные для отправки
                        String dir = @"c:\Server";
                        String path = Path.Combine(dir, dataReceived.LogName);
                        if (!Directory.Exists(dir))
                            Directory.CreateDirectory(dir);
                        String message = "";
                        FileStream fs;
                        Byte[] bytes;
                        switch (dataReceived.Command)
                        {
                            case Command.AddTextInLog:// Клиент хочет сообщение добавить в журнал
                                fs = new FileStream(path, FileMode.Append);
                                StreamWriter sw = new StreamWriter(fs);
                                sw.WriteLine("Сервер: " + DateTime.Now.ToString());
                                message = "Получил команду: \"" + dataReceived.DisplayCommand;
                                sw.WriteLine(message + "\"");// Три конкатенации сразу нельзя
                                sw.WriteLine("Идентификатор команды: " +
                                    Enum.GetName(typeof(Command), dataReceived.Command));
                                sw.WriteLine("Получил сообщение: " + dataReceived.Text);
                                message = "Добавлено сообщение в журнал!";
                                sw.WriteLine(message);
                                sw.WriteLine();
                                sw.Close();
                                dataSend.Text = message;// Отчет для отправки
                                dataSend.Command = Command.Text;// Изменили команду
                                break;
                            case Command.DeleteLog:// Клиент хочет удалить журнал с диска сервера
                                if (File.Exists(path))
                                    File.Delete(path);
                                dataSend.Text = "Журнал удален";// Отчет для отправки
                                dataSend.Command = Command.Text;// Изменили команду
                                break;
                            case Command.Text:// Клиент хочет получить сообщение 
                                message = String.Format(
                                    "Получил сообщение \"{0}\"\r\nОчень рад!", dataReceived.Text);
                                dataSend.Text = message;// Отчет для отправки
                                break;
                            case Command.Image:// Клиент хочет получить рисунок
                                // Файл должен быть в каталоге сборки, иначе надо дополнить путем
                                if (!File.Exists(dataReceived.ImageName))
                                {
                                    dataSend.Text = "Рисунок не существует";// Отчет для отправки
                                    dataSend.Command = Command.Text;// Изменили команду
                                    break;
                                }
                                // Читаем файл с рисунком
                                bytes = File.ReadAllBytes(dataReceived.ImageName);
                                // Подключаем поток к свойству объекта
                                dataSend.Image = new MemoryStream(bytes);
                                dataSend.Text = "Посылаю рисунок";// Отчет для отправки
                                break;
                            case Command.Log:// Клиент требует содержимое журнала
                                if (!File.Exists(path))
                                {
                                    dataSend.Text = "Журнал еще не создан";// Отчет для отправки
                                    dataSend.Command = Command.Text;// Изменили команду
                                    break;
                                }
                                fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
                                StreamReader sr = new StreamReader(fs);
                                dataSend.Text = sr.ReadToEnd();
                                sr.Close();
                                break;
                            case Command.Sound:// Клиент хочет музычку
                                // Файл должен быть в каталоге сборки, иначе надо дополнить путем
                                if (!File.Exists(dataReceived.SoundName))
                                {
                                    dataSend.Text = "Файл с музычкой не существует";// Отчет для отправки
                                    dataSend.Command = Command.Text;// Изменили команду
                                    break;
                                }
                                // Читаем файл с музычкой
                                bytes = File.ReadAllBytes(dataReceived.SoundName);
                                // Подключаем поток к свойству объекта
                                dataSend.Sound = new MemoryStream(bytes);
                                dataSend.Text = "Посылаю музычку";// Отчет для отправки
                                break;
                            case Command.Video:// Клиент хочет видео
                                // Файл должен быть в каталоге сборки, иначе надо дополнить путем
                                if (!File.Exists(dataReceived.VideoName))
                                {
                                    dataSend.Text = "Файл с video не существует";// Отчет для отправки
                                    dataSend.Command = Command.Text;// Изменили команду
                                    break;
                                }
                                // Читаем файл с video
                                bytes = File.ReadAllBytes(dataReceived.VideoName);
                                // Подключаем поток к свойству объекта
                                dataSend.Video = new MemoryStream(bytes);
                                dataSend.Text = "Там у них другие мерки...";// Отчет для отправки
                                break;
                        }
        
                        // Отправляем данные
                        writerStream = client.GetStream();
                        formatter = new BinaryFormatter();
                        formatter.Serialize(writerStream, dataSend);// Отправленный объект
        
                        readerStream.Close();// Освобождаем поток
                        writerStream.Close();// Освобождаем поток
                        client.Close();// Освобождаем сокет текущего клиента
                    }
                }
                finally
                {
                    // Останавливаем сервер
                    server.Stop();
                }
            }
        }
    }
  • Откомпилируйте проект SerializeServer и через Windows Explorer выведите на рабочий стол ярлык для запуска консольной сборки сервера
  • Запустите оба созданных приложения и испытайте их работу
  • Если запустить сразу несколько клиентов (не через оболочку, а через сборку), то наша защелка playerReady помогает только тогда, когда видеопроигрыватель в данный момент активен только на одном клиенте. При одновременном запуске в нескольких сборках WPF -проигрыватель MediaElement дает системное исключение. Да к тому же MediaElement еще требует наличия на компьютере установленного проигрывателя Windows Media версии 10 и выше.

    На сегодняшний день только Windows Vista поставляется вместе с проигрывателем Windows Media 11 (у меня установлен и пример работает). Если на вашем компьютере такого проигрывателя нет (хотя и установлен WPF ), замените его на COM -объект, как в работе №45. Суть примера не в проигрывании видео, а в транспортировке данных, упакованных в сериализованный объект.

  • Попытайтесь осмыслить код примера - в нем много поучительного и полезного (как и в других предыдущих примерах!)
  • Клиент SerializeClient при запущенном сервере SerializeServer может выглядеть так

    Упражнение 9. Защита данных с помощью потока CryptoStream

    Часто встречаются ситуации, когда при хранении и передаче данных их защита является основополагающим требованием к приложению. Для защиты данных обычно применяют шифрование с секретным ключем. Одним из способов решения задачи шифрования является применение класса CryptoStream из пространства имен System.Security.Cryptography библиотеки mscorlib.dll. Этот класс служит оболочкой над другими потоками и выполняет криптографические операции над передаваемыми данными.

    В основе шифрования .NET лежат три абстрактных класса, которые порождают экземплярные классы для реализации следующих криптографических алгоритмов:

  • SymmetricAlgorithm - симметричные алгоритмы, которые для шифрования и дешифрования информации используют один и тот же ключ. Производными от класса SymmetricAlgorithm являются тоже абстрактные классы:
  • DES
  • RC2
  • Rijndael
  • TripleDES
  • AsymmetricAlgorithm - асимметричные алгоритмы, которые для шифрования и дешифрования применяют разные ключи. Асимметричные криптографические алгоритмы также называют инфраструктурой открытого ключа ( PKI - Public Key Infrastructure ) Производными от класса AsymmetricAlgorithm являются тоже абстрактные классы:
  • DSA
  • ECDiffieHellman
  • ECDsa
  • RSA
  • HashAlgorithm - алгоритмы хеширования или дайджеста сообщения, преобразующие первоначальный текст любой длины в шифрованный текст фиксированной длины. Они выполняют односторонне шифрование, а это значит, что из полученного хешированием шифротекста нельзя восстановить первоначальный документ. Фиксированная длина хеша зависит от применяемого алгоритма и находится в пределах от 16 до 32 байтов. Производными от класса HashAlgorithm являются тоже абстрактные классы:
  • KeyedHashAlgorithm
  • MD5
  • RIPEMD160
  • SHA1
  • SHA256
  • SHA384
  • SHA512
  • Поскольку перечисленные классы являются абстрактными, то для создания экземплярных объектов они используют классы провайдеров ( Provider - поставщик) или менеджеров ( Manager - управленец).

    Криптография - это целая ветвь математики, создающая модели и алгоритмы для шифрования и дешифрования сообщений. Не вдаваясь в подробности криптографии мы просто проиллюстрируем работу потока CryptoStream на нескольких примерах.

    Введение в симметричные алгоритмы .NET

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

    Симметричные алгоритмы обычно намного быстрее асимметричных и они хорошо выполняют операции шифрования-дешифрования объемных файлов. Пространство имен System.Security.Cryptography поддерживает симметричные алгоритмы DES, RC2, Rijndael и TripleDES. Только у алгоритма Rijndael есть своя управляемая реализация, остальные поставляются из Microsoft Crypto API готовыми с помощью провайдеров.

    Все классы симметричных алгоритмов наследуют от своего общего предка SymmetricAlgorithm, который предоставляет им ряд общих методов и свойств, некоторые из которых приведены в таблице

    Некоторые члены класса SymmetricAlgorithm
    Член Описание
    Key Свойство возвращает или устанавливает секретный ключ, представляющий собой байтовый массив ( bytes )
    KeySize Свойство возвращает или устанавливает размер секретного ключа в битах ( bits )
    LegalKeySize Свойство возвращает целые величины диапазона корректных размеров секретного ключа в битах: MinSize, MaxSize и допустимый для выбранного алгоритма шаг дискретизации SkipSize
    IV Свойство возвращает или устанавливает байтовый вектор инициализации симметричного алгоритма ( Initialization Vector ). Поскольку алгоритмы прокручивают цепочку кодирования несколько раз, беря в качестве исходного результат предыдущего сдвига, то для самой первой итерации требуется начальная информация, которую и содержит IV
    BlockSize Свойство возвращает или устанавливает размер блока данных в битах ( bits ), обрабатываемых за один проход. Длинные данные делятся на блоки установленного размера и если для последнего блока данных не хватает, он будет дополнен значениями по умолчанию
    LegalBlockSize Свойство возвращает целые величины диапазона корректных размеров блока в битах: MinSize, MaxSize и допустимый для выбранного алгоритма шаг дискретизации SkipSize
    Mode Устанавливает режим работы симметричного алгоритма и принимает значения от перечисления CipherMode:
  • CBC ( Cipher Block Chaining ) - сцепление блоков шифра. Каждый следующий блок исходного текста шифруется с учетом результатов шифрования предыдущего блока. Здесь для самого первого блока используется IV
  • ECB ( Electronic Codebook ) - электронная книга шифров. Режим шифрует каждый блок индивидуально. Это означает, что любые одинаковые блоки исходного текста, зашифрованные с тем же самым ключом, будут преобразованы в идентичные блоки шифрованного текста. Такой режим не очень устойчив к взлому, но быстрый и не требует IV
  • CFB ( Cipher Feedback ) - шифр с обратной связью. Режим делит каждый блок на маленькие разделы и обрабатывает их индивидуально со сдвигом, вместо того, чтобы обработать весь блок сразу
  • CTS ( Cipher Text Stealing ) - режим обрабатывает исходный текст любой длины и производит шифрованный текст той же длины
  • OFB ( Output Feedback ) - выходная обратная связь. Режим похож на CFB, но отличается способом заполнения сдвигового регистра
  • ICryptoTransform CreateEncryptor() Метод создает объект симметричного шифрования и возвращает ссылку на него. Принимает вектор ключа и вектор инициализации
    ICryptoTransform CreateDecryptor() Метод создает объект симметричного дешифрования и возвращает ссылку на него. Принимает вектор ключа и вектор инициализации
    void GenerateKey() Метод изменяет значение свойства Key случайным образом
    void GenerateIV() Метод изменяет значение свойства IV случайным образом
    bool ValidKeySize(int bitLength) Проверяет, подходит ли выбранный размер ключа для работы алгоритма

    Рассмотрим работу семметричных алгоритмов на примерах.

    Пример 1. Поток CryptoStream и шифрование в память

  • Создайте новое решение CryptoStream с проектом Demo1 так
  • Настройте форму в соответствии со следующей таблице свойств
  • Элемент Свойство Значение
    Сама форма
    Form Text Demo1 CryptoStream
    Size 563; 438
    StartPosition CenterScreen
    MaximizeBox False
    Метки и тектовые блоки для шифруемой информации
    Label Text Source Text:
    Location 95; 9
    Label Text Encrypted Text:
    Location 86; 128
    Label Text Decrypted Text:
    Location 85; 247
    TextBox (Name) txtSourceData
    Location 2; 31
    Size 284; 88
    Multiline True
    Lines

    Привет всем!

    Пусть классики не волнуются:

    это не тема по криптографии, это тема про поток CryptoStream

    ScrollBars Vertical
    TextBox (Name) txtEncryptedData
    Location 2; 150
    Size 284; 88
    Multiline True
    ScrollBars Vertical
    TextBox (Name) txtDecryptedData
    Location 2; 269
    Size 284; 88
    Multiline True
    ScrollBars Vertical
    Блок радиокнопок для выбора алгоритма шифрования
    GroupBox Text SymmetricAlgorithm
    Location 291; 9
    Size 259; 136
    RadioButton Checked True
    Location 8; 16
    Size 199; 21
    Tag DES
    Text DESCryptoServiceProvider
    RadioButton Location 8; 40
    Size 194; 21
    Tag RC2
    Text RC2CryptoServiceProvider
    RadioButton Location 8; 64
    Size 136; 21
    Tag Rijndael
    Text RijndaelManaged
    RadioButton Location 8; 88
    Size 231; 21
    Tag TripleDES
    Text TripleDESCryptoServiceProvider
    RadioButton Location 8; 112
    Size 175; 21
    Tag SymmetricAlgorithm
    Text DefaultServiceProvider
    Блок с характеристиками выбранного алгоритма шифрования
    GroupBox Text Properties SymmetricAlgorithm
    Location 291; 151
    Size 259; 206
    Label Text Key:
    Location 5; 26
    Label Text IV:
    Location 17; 63
    Label Text KeySize (bits):
    Location 5; 99
    Label Text BlockSize:
    Location 155; 100
    Label Text LegalKeySizes:
    Location 15; 137
    Label Text LegalBlockSizes:
    Location 5; 174
    TextBox (Name) txtKey
    Location 42; 23
    Size 209; 22
    TextBox (Name) txtIV
    Location 42; 60
    Size 209; 22
    TextBox (Name) txtKeySize
    Location 102; 97
    Size 25; 22
    TextBox (Name) txtBlockSize
    Location 226; 98
    Size 25; 22
    TextBox (Name) txtLegalKeySizes
    Location 124; 134
    Size 127; 22
    TextBox (Name) txtLegalBlockSizes
    Location 124; 171
    Size 128; 22
    Кнопка для выполнения шифрования
    Button (Name) btnExecute
    Text Execute
    Location 240; 368
  • Выделите все элементы формы командой Edit/Select All и замкните созданный интерфейс командой Format/Lock Controls
  • Выделите все текстовые блоки, кроме txtSourceData, и задайте для них
  • BorderStyle=FixedSingle
  • Выделите все элементы, кроме TextBox и GroupBox, и задайте для них
  • AutoSize=True
  • Созданный интерфейс должен получиться таким

  • Выделите все текстовые блоки, кроме txtSourceData, и создайте для них общие обработчики событий KeyDown и KeyPress для блокировки редактирования (имена обработчиков введите вручную)
  • Командой Project/Add Class добавьте новый класс с именем ExecuteCryptography.cs и заполните его следующим кодом (приводится полностью)
  • using System;
    using System.IO;
    using System.Text;
    using System.Security.Cryptography;
        
    namespace Demo1
    {
        class ExecuteCryptography
        {
            // Создаем кодировщик символов
            private UTF7Encoding utf7 = new UTF7Encoding();
        
            // Объявляем ссылки на шифровальщик,
            // на массивы Key и IV (Initialization Vector) 
            private SymmetricAlgorithm codec;
            private byte[] key;
            private byte[] initVector;// IV
        
            // Конструктор
            public ExecuteCryptography(SymmetricAlgorithm codec)
            {
                this.key = codec.Key;
                this.initVector = codec.IV;
                this.codec = codec;
            }
        
            public string Encrypt(string text)
            {
                byte[] input = utf7.GetBytes(text);
                byte[] output = Transform(input,
                                codec.CreateEncryptor(key, initVector));
                return Convert.ToBase64String(output);
            }
            public string Decrypt(string text)
            {
                byte[] input = Convert.FromBase64String(text);
                byte[] output = Transform(input,
                                codec.CreateDecryptor(key, initVector));
                return utf7.GetString(output);
            }
        
            private byte[] Transform(byte[] input, ICryptoTransform cryptoTransform)
            {
                // Создаем потоки
                MemoryStream memoryStream = // Поток памяти
                    new MemoryStream();
                CryptoStream cryptoStream = // Шифропоток-оболочка
                    new CryptoStream(memoryStream, 
                        cryptoTransform, CryptoStreamMode.Write);
        
                // Кодируем в поток памяти через шифропоток
                cryptoStream.Write(input, 0, input.Length);
                cryptoStream.FlushFinalBlock();
        
                // Преобразуем заполненный поток памяти в массив байтов
                byte[] result = memoryStream.ToArray();
        
                // Останавливаем потоки
                memoryStream.Close();
                cryptoStream.Close();
        
                // Возвращаем кодированное/раскодированное
                return result;
            }
        
            // Перегруженные методы
            public byte[] Encrypt(byte[] input)
            {
                return Transform(input,
                       codec.CreateEncryptor(key, initVector));
            }
            public byte[] Decrypt(byte[] input)
            {
                return Transform(input,
                       codec.CreateDecryptor(key, initVector));
            }
        }
    }

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

    MemoryStream memoryStream = // Поток памяти
                    new MemoryStream();
                CryptoStream cryptoStream = // Шифропоток-оболочка
                    new CryptoStream(memoryStream, 
                        cryptoTransform, CryptoStreamMode.Write);
                StreamWriter streamWriter = new StreamWriter(cryptoStream);
                streamWriter.Write(txtSourceData.Text);
                streamWriter.Flush();            
                cryptoStream.FlushFinalBlock();
  • Заполните файл Form1.cs следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.Security.Cryptography;
    using System.IO;
        
    namespace Demo1
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
        
                // Подписываемся на событие Click кнопки
                btnExecute.Click += btnExecute_Click;
            }
               
            // Обработчик кнопки 
            private void btnExecute_Click(object sender, EventArgs e)
            {
                // Создаем объект симметричного шифрования
                SymmetricAlgorithm codec = null;// Тип общего предка 
                switch (SelectedRadio().Trim())
                {
                    case "DES":
                        codec = new DESCryptoServiceProvider();
                        break;
                    case "RC2":
                        codec = new RC2CryptoServiceProvider();
                        break;
                    case "Rijndael":
                        codec = new RijndaelManaged();
                        break;
                    case "TripleDES":
                        codec = new TripleDESCryptoServiceProvider();
                        break;
                    case "SymmetricAlgorithm":
                        codec = SymmetricAlgorithm.Create();
                        break;
                    default:
                        MessageBox.Show("Выделите провайдер шифрования");
                        return;
                }
        
                // Создаем экземпляр нашего класса
                // и передаем ему выбранный шифровальшик
                ExecuteCryptography executeCryptography =
                    new ExecuteCryptography(codec);
        
                // Шифруем текст
                string sourceData = txtSourceData.Text;
                string encryptedData = executeCryptography.Encrypt(sourceData);
                txtEncryptedData.Text = encryptedData;
        
                // Расшифровываем текст
                string decryptedData = executeCryptography.Decrypt(encryptedData);
                txtDecryptedData.Text = decryptedData;
        
                //////////////////////////////////////////////////
                // Показываем характеристики алгоритма шифрования
                //////////////////////////////////////////////////
                {
                    txtKey.Text = Encoding.UTF7.GetString(codec.Key);// Key
                    txtIV.Text = Encoding.UTF7.GetString(codec.IV);// Initialization Vector
        
                    txtKeySize.Text = codec.KeySize.ToString();// KeySize
                    StringBuilder sb = new StringBuilder();
                    foreach (KeySizes sizes in codec.LegalKeySizes)
                    {
                        sb.Append(sizes.MinSize.ToString() + "-" +
                            sizes.MaxSize.ToString() + 
                            " (" + sizes.SkipSize.ToString() + ") ");
                    }
                    txtLegalKeySizes.Text = sb.ToString();// LegalKeySizes
        
                    txtBlockSize.Text = codec.BlockSize.ToString();// BlockSize
                    sb = new StringBuilder();
                    foreach (KeySizes sizes in codec.LegalBlockSizes)
                    {
                        sb.Append(sizes.MinSize.ToString() + "-" +
                            sizes.MaxSize.ToString() + 
                            " (" + sizes.SkipSize.ToString() + ") ");
                    }
                    txtLegalBlockSizes.Text = sb.ToString();// LegalBlockSizes
                }
            }
        
            // Динамически ищем включенный RadioButton
            string SelectedRadio()
            {
                string tag = "";
                foreach (Control ctrl in groupBox1.Controls)
                {
                    RadioButton radio = ctrl as RadioButton;// Приводим ссылку
                    if (radio.Checked)
                    {
                        tag = (String)radio.Tag;// Приводим ссылку
                        break;
                    }
                }
        
                return tag;
            }
        
            // Блокировка редактирования всех целевых TextBox
            private void txtTarget_KeyDown(object sender, KeyEventArgs e)
    
            {
                e.Handled = true;
            }
            private void txtTarget_KeyPress(object sender, KeyPressEventArgs e)
            {
                e.Handled = true;
            }
        }
    }
  • Запустите приложение и испытайте его работу - результат может быть таким
  • Приложение при каждом новом создании объекта шифрования создает для него новый ключ и вектор инициализации. Если шифрованную информацию сохранять в файле, то одновременно нужно где-то запомнить имя алгоритма и его характеристики (можно только имя и секретный ключ). Это необходимо для последующей настройки объекта симметричного алгоритма шифрования на корректное восстановление информации.

  • Разберитесь с кодом примера
  • Пример 2. Поток CryptoStream и шифрование в файл

    В данном примере мы разработаем диалоговое приложение Windows Forms, иллюстрирующее работу алгоритмов шифрования/дешифрования в файл. Вернее, приложение уже разработано и ниже приводятся его составляющие, с которыми нужно разобраться самостоятельно. Но прежде нужно собрать приложение полностью и только потом испытывать, иначе посыпятся ошибки! Итак, собираем...

  • Добавьте к решению CryptoStream новый проект с именем Demo2 и назначьте его стартовым
  • В панели Solution Explorer откройте интерфейсный файл Form1.Designer.cs и заполните его следующим кодом (приводится полностью)
  • namespace Demo2
    {
        partial class Form1
        {
            private System.ComponentModel.IContainer components = null;
        
            protected override void Dispose(bool disposing)
            {
                if (disposing  (components != null))
                {
                    components.Dispose();
                }
                base.Dispose(disposing);
            }
        
            #region Windows Form Designer generated code
        
            private void InitializeComponent()
            {
                this.cmbCodec = new System.Windows.Forms.ComboBox();
                this.label1 = new System.Windows.Forms.Label();
                this.label2 = new System.Windows.Forms.Label();
                this.txtSourceFile = new System.Windows.Forms.TextBox();
                this.btnSelectFile = new System.Windows.Forms.Button();
                this.txtDestinationDir = new System.Windows.Forms.TextBox();
                this.btnDestinationDir = new System.Windows.Forms.Button();
                this.label4 = new System.Windows.Forms.Label();
                this.txtKey = new System.Windows.Forms.TextBox();
                this.btnClose = new System.Windows.Forms.Button();
                this.numKeySize = new System.Windows.Forms.NumericUpDown();
                this.numBlockSize = new System.Windows.Forms.NumericUpDown();
                this.label5 = new System.Windows.Forms.Label();
                this.label6 = new System.Windows.Forms.Label();
                this.label3 = new System.Windows.Forms.Label();
                this.btnEncrypt = new System.Windows.Forms.Button();
                this.btnDecrypt = new System.Windows.Forms.Button();
                this.btnShowFile = new System.Windows.Forms.Button();
                this.btnNewFile = new System.Windows.Forms.Button();
                ((System.ComponentModel.ISupportInitialize)(this.numKeySize)).BeginInit();
                ((System.ComponentModel.ISupportInitialize)(this.numBlockSize)).BeginInit();
                this.SuspendLayout();
                // 
                // cmbCodec
                // 
                this.cmbCodec.FormattingEnabled = true;
                this.cmbCodec.Location = new System.Drawing.Point(94, 21);
                this.cmbCodec.Name = "cmbCodec";
                this.cmbCodec.Size = new System.Drawing.Size(280, 24);
                this.cmbCodec.TabIndex = 0;
                this.cmbCodec.SelectedIndexChanged += new System.EventHandler(this.cmbCodec_SelectedIndexChanged);
                // 
                // label1
                // 
                this.label1.AutoSize = true;
                this.label1.Location = new System.Drawing.Point(17, 24);
                this.label1.Name = "label1";
                this.label1.Size = new System.Drawing.Size(71, 17);
                this.label1.TabIndex = 1;
                this.label1.Text = "Algorithm:";
                // 
                // label2
                // 
                this.label2.AutoSize = true;
                this.label2.Location = new System.Drawing.Point(5, 156);
                this.label2.Name = "label2";
                this.label2.Size = new System.Drawing.Size(83, 17);
                this.label2.TabIndex = 2;
                this.label2.Text = "Source File:";
                // 
                // txtSourceFile
                // 
                this.txtSourceFile.Location = new System.Drawing.Point(94, 155);
                this.txtSourceFile.Name = "txtSourceFile";
                this.txtSourceFile.Size = new System.Drawing.Size(280, 22);
                this.txtSourceFile.TabIndex = 3;
                this.txtSourceFile.KeyDown += new System.Windows.Forms.KeyEventHandler(this.EditKeyDown);
                this.txtSourceFile.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.EditKeyPress);
                // 
                // btnSelectFile
                // 
                this.btnSelectFile.Font = new System.Drawing.Font("Agency FB", 7.2F, System.Drawing.FontStyle.Bold, 
                	System.Drawing.GraphicsUnit.Point, ((byte)(0)));
                this.btnSelectFile.Location = new System.Drawing.Point(380, 155);
                this.btnSelectFile.Name = "btnSelectFile";
                this.btnSelectFile.Size = new System.Drawing.Size(25, 23);
                this.btnSelectFile.TabIndex = 4;
                this.btnSelectFile.Text = "...";
                this.btnSelectFile.UseVisualStyleBackColor = true;
                this.btnSelectFile.Click += new System.EventHandler(this.btnSelectFile_Click);
                // 
                // txtDestinationDir
                // 
                this.txtDestinationDir.Location = new System.Drawing.Point(94, 198);
                this.txtDestinationDir.Name = "txtDestinationDir";
                this.txtDestinationDir.Size = new System.Drawing.Size(280, 22);
                this.txtDestinationDir.TabIndex = 6;
                // 
                // btnDestinationDir
                // 
                this.btnDestinationDir.Font = new System.Drawing.Font("Agency FB", 7.2F, System.Drawing.FontStyle.Bold, 
                	System.Drawing.GraphicsUnit.Point, ((byte)(0)));
                this.btnDestinationDir.Location = new System.Drawing.Point(380, 198);
                this.btnDestinationDir.Name = "btnDestinationDir";
                this.btnDestinationDir.Size = new System.Drawing.Size(25, 23);
                this.btnDestinationDir.TabIndex = 7;
                this.btnDestinationDir.Text = "...";
                this.btnDestinationDir.UseVisualStyleBackColor = true;
                this.btnDestinationDir.Click += new System.EventHandler(this.btnDestinationDir_Click);
                // 
                // label4
                // 
                this.label4.AutoSize = true;
                this.label4.Location = new System.Drawing.Point(52, 67);
                this.label4.Name = "label4";
                this.label4.Size = new System.Drawing.Size(36, 17);
                this.label4.TabIndex = 8;
                this.label4.Text = "Key:";
                // 
                // txtKey
                // 
                this.txtKey.Location = new System.Drawing.Point(94, 66);
                this.txtKey.Name = "txtKey";
                this.txtKey.Size = new System.Drawing.Size(280, 22);
                this.txtKey.TabIndex = 9;
                // 
                // btnClose
                // 
                this.btnClose.AutoSize = true;
                this.btnClose.Location = new System.Drawing.Point(353, 256);
                this.btnClose.Name = "btnClose";
                this.btnClose.Size = new System.Drawing.Size(75, 27);
                this.btnClose.TabIndex = 12;
                this.btnClose.Text = "Close";
                this.btnClose.UseVisualStyleBackColor = true;
                this.btnClose.Click += new System.EventHandler(this.btnClose_Click);
                // 
                // numKeySize
                // 
                this.numKeySize.Location = new System.Drawing.Point(111, 110);
                this.numKeySize.Name = "numKeySize";
                this.numKeySize.Size = new System.Drawing.Size(55, 22);
                this.numKeySize.TabIndex = 13;
                this.numKeySize.ValueChanged += new System.EventHandler(this.Numeric_ValueChanged);
                this.numKeySize.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.EditKeyPress);
                this.numKeySize.KeyDown += new System.Windows.Forms.KeyEventHandler(this.EditKeyDown);
                // 
                // numBlockSize
                // 
                this.numBlockSize.Location = new System.Drawing.Point(299, 110);
                this.numBlockSize.Name = "numBlockSize";
                this.numBlockSize.Size = new System.Drawing.Size(55, 22);
                this.numBlockSize.TabIndex = 14;
                this.numBlockSize.ValueChanged += new System.EventHandler(this.Numeric_ValueChanged);
                this.numBlockSize.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.EditKeyPress);
                this.numBlockSize.KeyDown += new System.Windows.Forms.KeyEventHandler(this.EditKeyDown);
                // 
                // label5
                // 
                this.label5.AutoSize = true;
                this.label5.Location = new System.Drawing.Point(38, 111);
                this.label5.Name = "label5";
                this.label5.Size = new System.Drawing.Size(67, 17);
                this.label5.TabIndex = 15;
                this.label5.Text = "Key Size:";
                // 
                // label6
                // 
                this.label6.AutoSize = true;
                this.label6.Location = new System.Drawing.Point(216, 111);
                this.label6.Name = "label6";
                this.label6.Size = new System.Drawing.Size(77, 17);
                this.label6.TabIndex = 16;
                this.label6.Text = "Block Size:";
                // 
                // label3
                // 
                this.label3.AutoSize = true;
                this.label3.Location = new System.Drawing.Point(8, 186);
                this.label3.Name = "label3";
                this.label3.Size = new System.Drawing.Size(79, 34);
                this.label3.TabIndex = 5;
                this.label3.Text = "Destination\r\ndirectory:";
                this.label3.TextAlign = System.Drawing.ContentAlignment.TopRight;
                // 
                // btnEncrypt
                // 
                this.btnEncrypt.AutoSize = true;
                this.btnEncrypt.Location = new System.Drawing.Point(98, 256);
                this.btnEncrypt.Name = "btnEncrypt";
                this.btnEncrypt.Size = new System.Drawing.Size(75, 27);
                this.btnEncrypt.TabIndex = 19;
                this.btnEncrypt.Text = "Encrypt";
                this.btnEncrypt.UseVisualStyleBackColor = true;
                this.btnEncrypt.Click += new System.EventHandler(this.btnEncrypt_Click);
                // 
                // btnDecrypt
                // 
                this.btnDecrypt.AutoSize = true;
                this.btnDecrypt.Location = new System.Drawing.Point(182, 256);
                this.btnDecrypt.Name = "btnDecrypt";
                this.btnDecrypt.Size = new System.Drawing.Size(75, 27);
                this.btnDecrypt.TabIndex = 20;
                this.btnDecrypt.Text = "Decrypt";
                this.btnDecrypt.UseVisualStyleBackColor = true;
                this.btnDecrypt.Click += new System.EventHandler(this.btnDecrypt_Click);
                // 
                // btnShowFile
                // 
                this.btnShowFile.AutoSize = true;
                this.btnShowFile.Location = new System.Drawing.Point(266, 256);
                this.btnShowFile.Name = "btnShowFile";
                this.btnShowFile.Size = new System.Drawing.Size(78, 27);
                this.btnShowFile.TabIndex = 21;
                this.btnShowFile.Text = "Show File";
                this.btnShowFile.UseVisualStyleBackColor = true;
                this.btnShowFile.Click += new System.EventHandler(this.btnShowFile_Click);
                // 
                // btnNewFile
                // 
                this.btnNewFile.AutoSize = true;
                this.btnNewFile.Location = new System.Drawing.Point(14, 256);
                this.btnNewFile.Name = "btnNewFile";
                this.btnNewFile.Size = new System.Drawing.Size(75, 27);
                this.btnNewFile.TabIndex = 22;
                this.btnNewFile.Text = "New File";
                this.btnNewFile.UseVisualStyleBackColor = true;
                this.btnNewFile.Click += new System.EventHandler(this.btnNewFile_Click);
                // 
                // Form1
                // 
                this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
                this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
                this.ClientSize = new System.Drawing.Size(442, 296);
                this.Controls.Add(this.btnNewFile);
                this.Controls.Add(this.btnShowFile);
                this.Controls.Add(this.btnDecrypt);
                this.Controls.Add(this.btnEncrypt);
                this.Controls.Add(this.label6);
                this.Controls.Add(this.label5);
                this.Controls.Add(this.numBlockSize);
                this.Controls.Add(this.numKeySize);
                this.Controls.Add(this.btnClose);
                this.Controls.Add(this.txtKey);
                this.Controls.Add(this.label4);
                this.Controls.Add(this.btnDestinationDir);
                this.Controls.Add(this.txtDestinationDir);
                this.Controls.Add(this.label3);
                this.Controls.Add(this.btnSelectFile);
                this.Controls.Add(this.txtSourceFile);
                this.Controls.Add(this.label2);
                this.Controls.Add(this.label1);
                this.Controls.Add(this.cmbCodec);
                this.MaximizeBox = false;
                this.Name = "Form1";
                this.Text = "SymmetricAlgorithm with Files";
                this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
                ((System.ComponentModel.ISupportInitialize)(this.numKeySize)).EndInit();
                ((System.ComponentModel.ISupportInitialize)(this.numBlockSize)).EndInit();
                this.ResumeLayout(false);
                this.PerformLayout();
            }
        
            #endregion
        
            private System.Windows.Forms.ComboBox cmbCodec;
            private System.Windows.Forms.Label label1;
            private System.Windows.Forms.Label label2;
            private System.Windows.Forms.TextBox txtSourceFile;
            private System.Windows.Forms.Button btnSelectFile;
            private System.Windows.Forms.TextBox txtDestinationDir;
            private System.Windows.Forms.Button btnDestinationDir;
            private System.Windows.Forms.Label label4;
            private System.Windows.Forms.TextBox txtKey;
            private System.Windows.Forms.Button btnClose;
            private System.Windows.Forms.NumericUpDown numKeySize;
            private System.Windows.Forms.NumericUpDown numBlockSize;
            private System.Windows.Forms.Label label5;
            private System.Windows.Forms.Label label6;
            private System.Windows.Forms.Label label3;
            private System.Windows.Forms.Button btnEncrypt;
            private System.Windows.Forms.Button btnDecrypt;
            private System.Windows.Forms.Button btnShowFile;
            private System.Windows.Forms.Button btnNewFile;
        }
    }

    Этот код сразу определит интерфейс формы и мне не нужно будет писать (а вам - читать) таблицу свойств элементов.

  • Добавьте к текущему проекту командой Project/Add Class новый файл с именем MySettings.cs и заполните его следующим кодом (приводится полностью)
  • using System;
    using System.Configuration;
        
    namespace Demo2
    {
        // Класс для сохранения параметров приложения
        class MySettings : ApplicationSettingsBase
        {
            [UserScopedSettingAttribute()]       // Область действия - 'Пользователь' 
            [DefaultSettingValueAttribute("C:\\")]   // Значение параметра по умолчанию
            public String DestinationDir
            {
                get
                {
                    return ((String)this["DestinationDir"]);
                }
                set
                {
                    this["DestinationDir"] = (String)value;
                }
            }
        
            [UserScopedSettingAttribute()]       // Область действия - 'Пользователь' 
            [DefaultSettingValueAttribute("")]   // Значение параметра по умолчанию
            public String SourceFile
            {
                get
                {
                    return ((String)this["SourceFile"]);
                }
                set
                {
                    this["SourceFile"] = (String)value;
                }
            }
        }
    }
  • Откройте файл Form1.cs в режиме View Code и заполните его следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.Security.Cryptography;
    using System.IO;
        
    namespace Demo2
    {
        public partial class Form1 : Form
        {
            // Создаем объект симметричного шифрования
            SymmetricAlgorithm codec = null;// Тип общего предка 
            // Создаем объект сохранения параметров
            MySettings settings = new MySettings();// Это наш класс в файле MySettings.cs
        
            public Form1()
            {
                InitializeComponent();
        
                // Привязка именованных параметров к 
                // сохраняемым свойствам элементов управления
                txtDestinationDir.DataBindings.Add(new Binding("Text", settings, "DestinationDir"));
                txtSourceFile.DataBindings.Add(new Binding("Text", settings, "SourceFile"));
        
                // Настраиваем ComboBox динамически
                cmbCodec.Items.AddRange(
                    new string[]
                    {
                        "DESCryptoServiceProvider",
                        "RC2CryptoServiceProvider",
                        "RijndaelManaged",
                        "TripleDESCryptoServiceProvider",
                        "DefaultServiceProvider "
                    });
        
                cmbCodec.SelectedIndex = 0;// Выделяем первый элемент
            }
        
            // Меняем алгоритм шифрования
            private void cmbCodec_SelectedIndexChanged(object sender, EventArgs e)
            {
                // Блокируем обработчик Numeric_ValueChanged
                initFlag = false;
        
                ComboBox cmb = sender as ComboBox;
                switch (cmb.SelectedIndex)
                {
                    case 0:
                        codec = new DESCryptoServiceProvider();
                        break;
                    case 1:
                        codec = new RC2CryptoServiceProvider();
                        break;
                    case 2:
                        codec = new RijndaelManaged();
                        break;
                    case 3:
                        codec = new TripleDESCryptoServiceProvider();
                        break;
                    case 4:
                        codec = SymmetricAlgorithm.Create();
                        break;
                }
        
                // Заполняем NumericUpDown
                foreach (KeySizes sizes in codec.LegalKeySizes)
                {
                    numKeySize.Minimum = sizes.MinSize;
                    numKeySize.Maximum = sizes.MaxSize;
                    numKeySize.Increment = sizes.SkipSize;
                    break;
                }
                numKeySize.Value = codec.KeySize;
                CorrectTextKey();
                foreach (KeySizes sizes in codec.LegalBlockSizes)
                {
                    numBlockSize.Minimum = sizes.MinSize;
                    numBlockSize.Maximum = sizes.MaxSize;
                    numBlockSize.Increment = sizes.SkipSize;
                    break;
                }
                numBlockSize.Value = codec.BlockSize;
        
                // Разблокируем обработчик Numeric_ValueChanged
                initFlag = true;
            }
        
            // Устанавливаем допустимую длину ввода
            void CorrectTextKey()
            {
                // Корректируем длину поля ввода ключа
                int lenKey = codec.KeySize / 8;
                int lenTextBox= ASCIIEncoding.ASCII.GetByteCount(txtKey.Text);
                if (lenTextBox > lenKey)// Убираем лишнее
                    txtKey.Text = txtKey.Text.Substring(0, lenKey);
                txtKey.MaxLength = lenKey;
            }
        
            bool initFlag = false;
            private void Numeric_ValueChanged(object sender, EventArgs e)
            {
                if (initFlag)
                {
                    codec.KeySize = (int)numKeySize.Value;
                    codec.BlockSize = (int)numBlockSize.Value;
                    CorrectTextKey();
                }
            }
        
            // Создать новый текстовый файл через блокнот
            private void btnNewFile_Click(object sender, EventArgs e)
            {
                // Создаем новый текстовый файл через блокнот
                System.Diagnostics.Process exe = new System.Diagnostics.Process();
                exe.StartInfo.FileName = "Notepad.exe"; //Имя программы для запуска
                //exe.StartInfo.Arguments = path;         //Аргументы
                exe.Start();        // Запускаем внешний процесс
                //exe.WaitForExit();// Останавливаем наш процесс до закрытия блокнота
                exe.Close();        // Освобождаем связанные с процессом ресурсы
            }
        
            // Посмотреть через блокнот результаты шифрования
            private void btnShowFile_Click(object sender, EventArgs e)
            {
                // Покажем выбранный файл через блокнот
                System.Diagnostics.Process exe = new System.Diagnostics.Process();
                exe.StartInfo.FileName = "Notepad.exe"; //Имя программы для запуска
                exe.StartInfo.Arguments = SelectFile(); //Выбрать файл через диалог
                exe.Start();        // Запускаем внешний процесс
                //exe.WaitForExit();// Останавливаем наш процесс до закрытия блокнота
                exe.Close();        // Освобождаем связанные с процессом ресурсы
            }
        
            // Выполнить шифрование
            private void btnEncrypt_Click(object sender, EventArgs e)
            {
                if (txtKey.Text == "")
                {
                    MessageBox.Show(this, "Введите ключ для шифрования", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
        
                string destinationFile = "";
                if (Path.GetExtension(txtSourceFile.Text) != ".txt")
                {
                    MessageBox.Show(this, "Выберите файл '*.txt' для шифрования", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
                else
                {
                    destinationFile = txtSourceFile.Text.Replace(".txt", String.Empty) + ".enc";
                    destinationFile = Path.GetFileName(destinationFile);// Укороченное имя
                    destinationFile = Path.Combine(txtDestinationDir.Text, destinationFile);
                }
        
                if (encryptData(txtSourceFile.Text, destinationFile, txtKey.Text))
                    MessageBox.Show(this, "Выполнено!", "Шифрование файла",
                        MessageBoxButtons.OK, MessageBoxIcon.Information);
                else
                    MessageBox.Show(this, "Процесс шифрования закончился неудачей!", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
            }
        
            // Выполнить дешифрование
            private void btnDecrypt_Click(object sender, EventArgs e)
            {
                if (txtKey.Text == "")
                {
                    MessageBox.Show(this, "Введите ключ для дешифрования", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
        
                string destinationFile = "";
                if (Path.GetExtension(txtSourceFile.Text) != ".enc")
                {
                    MessageBox.Show(this, "Выберите файл '*.enc' для дешифрования", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
                else
                {
                    destinationFile = txtSourceFile.Text + ".txt";
                    destinationFile = Path.GetFileName(destinationFile);// Укороченное имя
                    destinationFile = Path.Combine(txtDestinationDir.Text, destinationFile);
                }
        
                if (decryptData(txtSourceFile.Text, destinationFile, txtKey.Text))
                    MessageBox.Show(this, "Выполнено!", "Дешифрование файла",
                        MessageBoxButtons.OK, MessageBoxIcon.Information);
                else
                    MessageBox.Show(this, "Процесс дешифрования закончился неудачей!", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
            }
        
            // Шифруем заданный файл
            private bool encryptData(
                string sourceFile, string destinationFile, string cryptoKey)
            {
                FileStream inFileStream = null, outFileStream = null;
                CryptoStream cryptoStream = null;
                
                // Заготавливаем пустой ключ полного размера
                byte[] key = new byte[(int)numKeySize.Value / 8];
        
                try
                {
                    // Создаем и настраиваем кодировщик
                    ASCIIEncoding.ASCII.GetBytes(cryptoKey.Trim()).CopyTo(key, 0);// Заполняем ключ
                    codec.Key = key;
                    //codec.GenerateIV();
                    ICryptoTransform encryptor = codec.CreateEncryptor();
        
                    // Открываем исходный и результирующий файлы, используя файловый поток
                    inFileStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
                    outFileStream = new FileStream(destinationFile, FileMode.Create, FileAccess.Write);
        
                    // Создаем оболочку CryptoStream для записи шифрованного файла
                    cryptoStream = new CryptoStream(outFileStream, encryptor, CryptoStreamMode.Write);
        
                    // Объявляем массив файлов с длиной входного файла 
                    byte[] bytes = new byte[inFileStream.Length];
        
                    // Считываем входной файловый поток в массив байтов 
                    // и записываем байты в CryptoStream
                    inFileStream.Read(bytes, 0, bytes.Length);
                    cryptoStream.Write(bytes, 0, bytes.Length);
        
                    inFileStream.Flush();
                    cryptoStream.FlushFinalBlock();
        
                    return true;
                }
                catch (Exception e)
                {
                    MessageBox.Show(this, e.ToString(), "Encryption Error",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
        
                    return false;
                }
                finally
                {
                    // Освобождаем ресурсы в любом случае
                    if (cryptoStream != null) cryptoStream.Close();
                    if (inFileStream != null) inFileStream.Close();
                    if (outFileStream != null) outFileStream.Close();
                }
            }
        
            // Расшифровываем указанный файл с тем же ключем и алгоритмом
            private bool decryptData(
                string sourceFile, string destinationFile, string cryptoKey)
            {
                CryptoStream cryptoStream = null;
                StreamWriter decryptedOut = null;
                FileStream decryptedFile = null;
                StreamReader streamReader = null;
        
                // Заготавливаем пустой ключ полного размера
                byte[] key = new byte[(int)numKeySize.Value / 8];
        
                try
                {
                    // Создаем и настраиваем кодировщик
                    ASCIIEncoding.ASCII.GetBytes(cryptoKey.Trim()).CopyTo(key, 0);// Заполняем ключ
                    codec.Key = key;
                    //codec.GenerateIV();
                    ICryptoTransform decryptor = codec.CreateDecryptor();
        
                    // Открываем исходный и результирующий файлы, используя файловый поток
                    decryptedFile = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
        
                    // Создаем оболочку CryptoStream для записи шифрованного файла
                    cryptoStream = new CryptoStream(decryptedFile, decryptor, CryptoStreamMode.Read);
        
                    decryptedOut = new StreamWriter(destinationFile);
                    streamReader = new StreamReader(cryptoStream, Encoding.GetEncoding("windows-1251"));
                    decryptedOut.Write(streamReader.ReadToEnd());
        
                    return true;
                }
                catch (Exception e)
                {
                    MessageBox.Show(this, e.ToString(), "Decryption Error",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
        
                    return false;
                }
                finally
                {
                    // Освобождаем ресурсы в любом случае
                    if (streamReader != null) streamReader.Close();
                    if (cryptoStream != null) cryptoStream.Close();
                    if (decryptedOut != null) decryptedOut.Close();
                    if (decryptedFile != null) decryptedFile.Close();
                }
            }
        
            // Диалог выбора файла для обработки
            OpenFileDialog openFileDialog = new OpenFileDialog();
            String SelectFile()
            {
                // Настраиваем диалог выбора файла
                if (txtSourceFile.Text == String.Empty)
                    openFileDialog.InitialDirectory = txtDestinationDir.Text;
                else
                    openFileDialog.InitialDirectory = Path.GetDirectoryName(txtSourceFile.Text);
        
                openFileDialog.FileName = "";
                openFileDialog.Filter = "Source Files(*.txt;*.enc)|*.txt;*.enc";
                DialogResult result = openFileDialog.ShowDialog();
                if (result != DialogResult.Cancel)
                    return openFileDialog.FileName; // Полное имя
                else
                    return txtSourceFile.Text;
            }
        
            // Запуск диалога выбора файла
            private void btnSelectFile_Click(object sender, EventArgs e)
            {
                txtSourceFile.Text = SelectFile();
                txtSourceFile.Focus();// Без этого не сохранится в установках приложения
            }
        
            // Запуск диалога выбора папки назначения
            FolderBrowserDialog dialogFolder = new FolderBrowserDialog();
            private void btnDestinationDir_Click(object sender, EventArgs e)
            {
                if (txtDestinationDir.Text == String.Empty ||
                    Directory.Exists(txtDestinationDir.Text) == false)
                {
                    dialogFolder.RootFolder =
                        System.Environment.SpecialFolder.MyComputer;
                    dialogFolder.SelectedPath = "";
                    txtDestinationDir.Text = String.Empty;
                }
                else
                {
                    dialogFolder.SelectedPath = txtDestinationDir.Text;
                }
        
                dialogFolder.Description =
                    "Выберите место размещения целевого файла";
        
                dialogFolder.ShowDialog();
                txtDestinationDir.Text = dialogFolder.SelectedPath;
                txtDestinationDir.Focus();
            }
        
            // Блокировка редактирования TextBox и NumericUpDown вручную
            private void EditKeyDown(object sender, KeyEventArgs e)
            {
                e.Handled = true;
            }
            private void EditKeyPress(object sender, KeyPressEventArgs e)
            {
                e.Handled = true;
            }
        
            // При закрытии формы сохраняем параметры
            private void Form1_FormClosing(object sender, FormClosingEventArgs e)
            {
                settings.Save();
            }
        
            // Закрываем форму
            private void btnClose_Click(object sender, EventArgs e)
            {
                this.Close();
            }
        }
    }

    Каждый используемый алгоритм шифрования имеет свой фиксированный размер ключа, нарушение которого генерирует исключение. Поэтому мы вначале создаем байтовый массив фиксированного размера, а потом копируем в него содержимое пользовательского ключа, не превышающего по размеру допустимого. Незаполненные байты массива остаются нулевыми, зато этот прием позволяет принимать от пользователя ключи любой длины, не превышающей допустимую. В дополнение к этому на поле ввода ключа txtKey наложено ограничение по максимальной длине текста.

  • Запустите приложение Demo2 - оно вполне работоспособно и справляется с шифрованием при правильном обращении с ним
  • Разберитесь с кодом ( кому надо!), да простят меня классики
  • Внешний вид окна будет таким

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

    Пример 3. Поток CryptoStream и электронные подписи документов

    Чтобы уж совсем завершить тему потока CryptoStream, рассмотрим еще одну сферу его применения - электронные подписи документов. Иногда получатель документа должен быть уверен, что этот документ не был искажен. В этом случае отправитель и получатель договариваются об одном и том же секретном ключе и применяют специальные алгоритмы хеширования, использующие ключ.

    Напомним, что любые хеш-алгоритмы всегда генерируют значение постоянной длины (каждый своей) независимо от длины хешируемого текста. Но алгоритмы хеширования с ключами HMAC ( Hash Message Authentication ) отличаются от обычных алгоритмов хеширования тем, что они генерируют хеш на основании ключа. Отправитель документа вычисляет его хеш на основании секретного ключа и отправляет вместе с ключем получателю. Получатель извлекает хеш из присланного документа и повторно хеширует сам оставшийся документ с применением своего дубликата ключа. Если вычисленный хеш совпадает с полученным, то подлинность документа считается установленной.

    Библиотека .NET Framework поддерживает задачи электронных подписей несколькими классами-алгоритмами пространства имен System.Security.Cryptography. Они принимают ключи разного размера и производят хеш-значение разной длины. Если длина применяемых ключей отличается от разрешенной, то любой из алгоритмов генерирует исключение.

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

  • Добавьте к решению CryptoStream новый проект с именем Demo3 и назначьте его стартовым
  • Откройте интерфейсный файл Form1.Designer.cs и заполните его следующим кодом (приводится полностью)
  • namespace Demo3
    {
        partial class Form1
        {
            private System.ComponentModel.IContainer components = null;
        
            protected override void Dispose(bool disposing)
            {
                if (disposing  (components != null))
                {
                    components.Dispose();
                }
                base.Dispose(disposing);
            }
        
            #region Windows Form Designer generated code
        
            private void InitializeComponent()
            {
                this.cmbHMAC = new System.Windows.Forms.ComboBox();
                this.label1 = new System.Windows.Forms.Label();
                this.label2 = new System.Windows.Forms.Label();
                this.txtSourceFile = new System.Windows.Forms.TextBox();
                this.btnSelectFile = new System.Windows.Forms.Button();
                this.txtDestinationDir = new System.Windows.Forms.TextBox();
                this.btnDestinationDir = new System.Windows.Forms.Button();
                this.label4 = new System.Windows.Forms.Label();
                this.txtKey = new System.Windows.Forms.TextBox();
                this.btnClose = new System.Windows.Forms.Button();
                this.lblInfo = new System.Windows.Forms.Label();
                this.label3 = new System.Windows.Forms.Label();
                this.btnSigned = new System.Windows.Forms.Button();
                this.btnChecked = new System.Windows.Forms.Button();
                this.btnShowFile = new System.Windows.Forms.Button();
                this.btnNewFile = new System.Windows.Forms.Button();
                this.SuspendLayout();
                // 
                // cmbHMAC
                // 
                this.cmbHMAC.FormattingEnabled = true;
                this.cmbHMAC.Location = new System.Drawing.Point(94, 21);
                this.cmbHMAC.Name = "cmbHMAC";
                this.cmbHMAC.Size = new System.Drawing.Size(280, 24);
                this.cmbHMAC.TabIndex = 0;
                this.cmbHMAC.SelectedIndexChanged += new System.EventHandler(this.cmbHMAC_SelectedIndexChanged);
                // 
                // label1
                // 
                this.label1.AutoSize = true;
                this.label1.Location = new System.Drawing.Point(36, 24);
                this.label1.Name = "label1";
                this.label1.Size = new System.Drawing.Size(51, 17);
                this.label1.TabIndex = 1;
                this.label1.Text = "HMAC:";
                // 
                // label2
                // 
                this.label2.AutoSize = true;
                this.label2.Location = new System.Drawing.Point(5, 156);
                this.label2.Name = "label2";
                this.label2.Size = new System.Drawing.Size(83, 17);
                this.label2.TabIndex = 2;
                this.label2.Text = "Source File:";
                // 
                // txtSourceFile
                // 
                this.txtSourceFile.Location = new System.Drawing.Point(94, 155);
                this.txtSourceFile.Name = "txtSourceFile";
                this.txtSourceFile.Size = new System.Drawing.Size(280, 22);
                this.txtSourceFile.TabIndex = 3;
                this.txtSourceFile.KeyDown += new System.Windows.Forms.KeyEventHandler(this.EditKeyDown);
                this.txtSourceFile.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.EditKeyPress);
                // 
                // btnSelectFile
                // 
                this.btnSelectFile.Font = new System.Drawing.Font("Agency FB", 7.2F, System.Drawing.FontStyle.Bold, 
                	System.Drawing.GraphicsUnit.Point, ((byte)(0)));
                this.btnSelectFile.Location = new System.Drawing.Point(380, 155);
                this.btnSelectFile.Name = "btnSelectFile";
                this.btnSelectFile.Size = new System.Drawing.Size(25, 23);
                this.btnSelectFile.TabIndex = 4;
                this.btnSelectFile.Text = "...";
                this.btnSelectFile.UseVisualStyleBackColor = true;
                this.btnSelectFile.Click += new System.EventHandler(this.btnSelectFile_Click);
                // 
                // txtDestinationDir
                // 
                this.txtDestinationDir.Location = new System.Drawing.Point(94, 198);
                this.txtDestinationDir.Name = "txtDestinationDir";
                this.txtDestinationDir.Size = new System.Drawing.Size(280, 22);
                this.txtDestinationDir.TabIndex = 6;
                // 
                // btnDestinationDir
                // 
                this.btnDestinationDir.Font = new System.Drawing.Font("Agency FB", 7.2F, System.Drawing.FontStyle.Bold, 
                	System.Drawing.GraphicsUnit.Point, ((byte)(0)));
                this.btnDestinationDir.Location = new System.Drawing.Point(380, 198);
                this.btnDestinationDir.Name = "btnDestinationDir";
                this.btnDestinationDir.Size = new System.Drawing.Size(25, 23);
                this.btnDestinationDir.TabIndex = 7;
                this.btnDestinationDir.Text = "...";
                this.btnDestinationDir.UseVisualStyleBackColor = true;
                this.btnDestinationDir.Click += new System.EventHandler(this.btnDestinationDir_Click);
                // 
                // label4
                // 
                this.label4.AutoSize = true;
                this.label4.Location = new System.Drawing.Point(52, 67);
                this.label4.Name = "label4";
                this.label4.Size = new System.Drawing.Size(36, 17);
                this.label4.TabIndex = 8;
                this.label4.Text = "Key:";
                // 
                // txtKey
                // 
                this.txtKey.Location = new System.Drawing.Point(94, 66);
                this.txtKey.Name = "txtKey";
                this.txtKey.Size = new System.Drawing.Size(280, 22);
                this.txtKey.TabIndex = 9;
                // 
                // btnClose
                // 
                this.btnClose.AutoSize = true;
                this.btnClose.Location = new System.Drawing.Point(353, 256);
                this.btnClose.Name = "btnClose";
                this.btnClose.Size = new System.Drawing.Size(75, 27);
                this.btnClose.TabIndex = 12;
                this.btnClose.Text = "Close";
                this.btnClose.UseVisualStyleBackColor = true;
                this.btnClose.Click += new System.EventHandler(this.btnClose_Click);
                // 
                // lblInfo
                // 
                this.lblInfo.AutoSize = true;
                this.lblInfo.Location = new System.Drawing.Point(104, 110);
                this.lblInfo.Name = "lblInfo";
                this.lblInfo.Size = new System.Drawing.Size(45, 17);
                this.lblInfo.TabIndex = 15;
                this.lblInfo.Text = "lblInfo";
                // 
                // label3
                // 
                this.label3.AutoSize = true;
                this.label3.Location = new System.Drawing.Point(8, 186);
                this.label3.Name = "label3";
                this.label3.Size = new System.Drawing.Size(79, 34);
                this.label3.TabIndex = 5;
                this.label3.Text = "Destination\r\ndirectory:";
                this.label3.TextAlign = System.Drawing.ContentAlignment.TopRight;
                // 
                // btnSigned
                // 
                this.btnSigned.AutoSize = true;
                this.btnSigned.Location = new System.Drawing.Point(98, 256);
                this.btnSigned.Name = "btnSigned";
                this.btnSigned.Size = new System.Drawing.Size(75, 27);
                this.btnSigned.TabIndex = 19;
                this.btnSigned.Text = "Signed";
                this.btnSigned.UseVisualStyleBackColor = true;
                this.btnSigned.Click += new System.EventHandler(this.btnSigned_Click);
                // 
                // btnChecked
                // 
                this.btnChecked.AutoSize = true;
                this.btnChecked.Location = new System.Drawing.Point(182, 256);
                this.btnChecked.Name = "btnChecked";
                this.btnChecked.Size = new System.Drawing.Size(75, 27);
                this.btnChecked.TabIndex = 20;
                this.btnChecked.Text = "Checked";
                this.btnChecked.UseVisualStyleBackColor = true;
                this.btnChecked.Click += new System.EventHandler(this.btnChecked_Click);
                // 
                // btnShowFile
                // 
                this.btnShowFile.AutoSize = true;
                this.btnShowFile.Location = new System.Drawing.Point(266, 256);
                this.btnShowFile.Name = "btnShowFile";
                this.btnShowFile.Size = new System.Drawing.Size(78, 27);
                this.btnShowFile.TabIndex = 21;
                this.btnShowFile.Text = "Show File";
                this.btnShowFile.UseVisualStyleBackColor = true;
                this.btnShowFile.Click += new System.EventHandler(this.btnShowFile_Click);
                // 
                // btnNewFile
                // 
                this.btnNewFile.AutoSize = true;
                this.btnNewFile.Location = new System.Drawing.Point(14, 256);
                this.btnNewFile.Name = "btnNewFile";
                this.btnNewFile.Size = new System.Drawing.Size(75, 27);
                this.btnNewFile.TabIndex = 22;
                this.btnNewFile.Text = "New File";
                this.btnNewFile.UseVisualStyleBackColor = true;
                this.btnNewFile.Click += new System.EventHandler(this.btnNewFile_Click);
                // 
                // Form1
                // 
                this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
                this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
                this.ClientSize = new System.Drawing.Size(442, 296);
                this.Controls.Add(this.btnNewFile);
                this.Controls.Add(this.btnShowFile);
                this.Controls.Add(this.btnChecked);
                this.Controls.Add(this.btnSigned);
                this.Controls.Add(this.lblInfo);
                this.Controls.Add(this.btnClose);
                this.Controls.Add(this.txtKey);
                this.Controls.Add(this.label4);
                this.Controls.Add(this.btnDestinationDir);
                this.Controls.Add(this.txtDestinationDir);
                this.Controls.Add(this.label3);
                this.Controls.Add(this.btnSelectFile);
                this.Controls.Add(this.txtSourceFile);
                this.Controls.Add(this.label2);
                this.Controls.Add(this.label1);
                this.Controls.Add(this.cmbHMAC);
                this.MaximizeBox = false;
                this.Name = "Form1";
                this.Text = "HMAC Algorithm with Files";
                this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
                this.ResumeLayout(false);
                this.PerformLayout();
            }
        
            #endregion
        
            private System.Windows.Forms.ComboBox cmbHMAC;
            private System.Windows.Forms.Label label1;
            private System.Windows.Forms.Label label2;
            private System.Windows.Forms.TextBox txtSourceFile;
            private System.Windows.Forms.Button btnSelectFile;
            private System.Windows.Forms.TextBox txtDestinationDir;
            private System.Windows.Forms.Button btnDestinationDir;
            private System.Windows.Forms.Label label4;
            private System.Windows.Forms.TextBox txtKey;
            private System.Windows.Forms.Button btnClose;
            private System.Windows.Forms.Label lblInfo;
            private System.Windows.Forms.Label label3;
            private System.Windows.Forms.Button btnSigned;
            private System.Windows.Forms.Button btnChecked;
            private System.Windows.Forms.Button btnShowFile;
            private System.Windows.Forms.Button btnNewFile;
        }
    }
  • Откройте файл Form1.cs в режиме View Code и заполните его следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.Security.Cryptography;
    using System.IO;
        
    namespace Demo3
    {
        public partial class Form1 : Form
        {
            // Объявляем ссылку на объект для хеширования документов
            KeyedHashAlgorithm hmac = null;// Тип общего предка 
            // Создаем объект сохранения параметров
            MySettings settings = new MySettings();// Это наш класс в файле MySettings.cs
        
            public Form1()
            {
                InitializeComponent();
        
                // Привязка именованных параметров к 
                // сохраняемым свойствам элементов управления
                txtDestinationDir.DataBindings.Add(new Binding("Text", settings, "DestinationDir"));
                txtSourceFile.DataBindings.Add(new Binding("Text", settings, "SourceFile"));
        
                // Настраиваем ComboBox динамически
                cmbHMAC.Items.AddRange(
                    new string[]
                    {
                        "HMACSHA1",
                        "MACTripleDES",
                        "HMACMD5",
                        "HMACRIPEMD160",
                        "HMACSHA256",
                        "HMACSHA384",
                        "HMACSHA512"
                    });
        
                cmbHMAC.SelectedIndex = 0;// Выделяем первый элемент
            }
        
            // Меняем алгоритм хеширования
            private void cmbHMAC_SelectedIndexChanged(object sender, EventArgs e)
            {
                ComboBox cmb = sender as ComboBox;
                switch (cmb.SelectedIndex)
                {
                    case 0:
                        hmac = new HMACSHA1();
                        break;
                    case 1:
                        hmac = new MACTripleDES();
                        break;
                    case 2:
                        hmac = new HMACMD5();
                        break;
                    case 3:
                        hmac = new HMACRIPEMD160();
                        break;
                    case 4:
                        hmac = new HMACSHA256();
                        break;
                    case 5:
                        hmac = new HMACSHA384();
                        break;
                    case 6:
                        hmac = new HMACSHA512();
                        break;
                }
        
                CorrectTextKey();
            }
        
            // Устанавливаем допустимую длину ввода
            void CorrectTextKey()
            {
                // Корректируем длину поля ввода ключа
                int lenKey = hmac.Key.Length;
                int lenTextBox = ASCIIEncoding.ASCII.GetByteCount(txtKey.Text);
                if (lenTextBox > lenKey)// Убираем лишнее
                    txtKey.Text = txtKey.Text.Substring(0, lenKey);
                txtKey.MaxLength = lenKey;// Ограничение в поле ввода
                // Отображаем в текстовой метке
                lblInfo.Text = String.Format("Key Size: {0} bytes   Hash Size: {1} bytes",
                    hmac.Key.Length, hmac.HashSize / 8);
            }
        
            // Создать новый текстовый файл через блокнот
            private void btnNewFile_Click(object sender, EventArgs e)
            {
                // Создаем новый текстовый файл через блокнот
                System.Diagnostics.Process exe = new System.Diagnostics.Process();
                exe.StartInfo.FileName = "Notepad.exe"; //Имя программы для запуска
                //exe.StartInfo.Arguments = path;         //Аргументы
                exe.Start();        // Запускаем внешний процесс
                //exe.WaitForExit();// Останавливаем наш процесс до закрытия блокнота
                exe.Close();        // Освобождаем связанные с процессом ресурсы
            }
        
            // Посмотреть через блокнот результаты 
            private void btnShowFile_Click(object sender, EventArgs e)
            {
                // Покажем выбранный файл через блокнот
                System.Diagnostics.Process exe = new System.Diagnostics.Process();
                exe.StartInfo.FileName = "Notepad.exe"; //Имя программы для запуска
                exe.StartInfo.Arguments = SelectFile(); //Выбрать файл через диалог
                exe.Start();        // Запускаем внешний процесс
                //exe.WaitForExit();// Останавливаем наш процесс до закрытия блокнота
                exe.Close();        // Освобождаем связанные с процессом ресурсы
            }
        
            // Подписать документ
            private void btnSigned_Click(object sender, EventArgs e)
            {
                if (txtKey.Text == "")
                {
                    MessageBox.Show(this, "Введите ключ для хеширования", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
        
                string destinationFile = "";
                if (Path.GetExtension(txtSourceFile.Text) != ".txt")
                {
                    MessageBox.Show(this, "Выберите файл '*.txt' для хеширования", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
                else
                {
                    destinationFile = txtSourceFile.Text.Replace(".txt", String.Empty) + ".sign";
                    destinationFile = Path.GetFileName(destinationFile);// Укороченное имя
                    destinationFile = Path.Combine(txtDestinationDir.Text, destinationFile);
                }
        
                if (signedData(txtSourceFile.Text, destinationFile, txtKey.Text))
                    MessageBox.Show(this, "Выполнено!", "Подписание файла",
                        MessageBoxButtons.OK, MessageBoxIcon.Information);
                else
                    MessageBox.Show(this, "Процесс подписания закончился неудачей!", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
            }
        
            // Выполнить аутентификацию документа
            private void btnChecked_Click(object sender, EventArgs e)
            {
                if (txtKey.Text == "")
                {
                    MessageBox.Show(this, "Введите ключ для аутентификации", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
        
                string destinationFile = "";
                if (Path.GetExtension(txtSourceFile.Text) != ".sign")
                {
                    MessageBox.Show(this, "Выберите файл '*.sign' для аутентификации", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
                else
                {
                    destinationFile = txtSourceFile.Text + ".txt";
                    destinationFile = Path.GetFileName(destinationFile);// Укороченное имя
                    destinationFile = Path.Combine(txtDestinationDir.Text, destinationFile);
                }
        
                if (checkedData(txtSourceFile.Text, destinationFile, txtKey.Text))
                    MessageBox.Show(this, "Выполнено!", "Аутентификация файла",
                        MessageBoxButtons.OK, MessageBoxIcon.Information);
                else
                    MessageBox.Show(this, "Процесс аутентификации закончился неудачей!", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
            }
        
            // Подписываем заданный файл
            private bool signedData(
                string sourceFile, string destinationFile, string cryptoKey)
            {
                FileStream inFileStream = null, outFileStream = null;
        
                // Создаем заново, иначе барахлит
                switch (cmbHMAC.SelectedIndex)
                {
                    case 0:
                        hmac = new HMACSHA1();
                        break;
                    case 1:
                        hmac = new MACTripleDES();
                        break;
                    case 2:
                        hmac = new HMACMD5();
                        break;
                    case 3:
                        hmac = new HMACRIPEMD160();
                        break;
                    case 4:
                        hmac = new HMACSHA256();
                        break;
                    case 5:
                        hmac = new HMACSHA384();
                        break;
                    case 6:
                        hmac = new HMACSHA512();
                        break;
                }
        
                try
                {
                    // Настраиваем алгоритм хеширования
                    byte[] key = new byte[hmac.Key.Length];// Заготавливаем требуемый размер ключа
                    ASCIIEncoding.ASCII.GetBytes(cryptoKey.Trim()).CopyTo(key, 0);// Заполняем ключ
                    hmac.Key = key;
        
                    // Создаем оболочку CryptoStream для хеширования 'в никуда'
                    CryptoStream cryptoStream = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write);
        
                    // Открываем исходный файл, используя файловый поток
                    inFileStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
                    // Объявляем массив байтов с длиной входного файла 
                    byte[] bytes = new byte[inFileStream.Length];
        
                    // Считываем входной файловый поток в массив байтов и записываем 
                    // байты в CryptoStream, чтобы получить хеш исходного документа
                    inFileStream.Read(bytes, 0, bytes.Length);
                    cryptoStream.Write(bytes, 0, bytes.Length);
                    cryptoStream.Close();
        
                    // Извлекаем хеш и записываем все в выходной документ
                    outFileStream = new FileStream(destinationFile, FileMode.Create, FileAccess.Write);
                    BinaryWriter bw = new BinaryWriter(outFileStream);
                    bw.Write(hmac.Hash.Length);// Пишем длину хеша
                    outFileStream.Write(hmac.Hash, 0, hmac.Hash.Length);// Пишем сам хеш
                    inFileStream.Position = 0;// Указатель в начало
                    inFileStream.Read(bytes, 0, bytes.Length); // Читаем исходный файл
                    outFileStream.Write(bytes, 0, bytes.Length); // Пишем сам документ
        
                    return true;
                }
                catch (Exception e)
                {
                    MessageBox.Show(this, e.ToString(), "Hashed Error",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
        
                    return false;
                }
                finally
                {
                    // Освобождаем файловые ресурсы в любом случае
                    if (inFileStream != null) inFileStream.Close();
                    if (outFileStream != null) outFileStream.Close();
                }
            }
        
            // Хешируем указанный файл с тем же ключем и алгоритмом
            private bool checkedData(
                string sourceFile, string destinationFile, string cryptoKey)
            {
                FileStream inFileStream = null, outFileStream = null;
        
                // Создаем заново, иначе барахлит
                switch (cmbHMAC.SelectedIndex)
                {
                    case 0:
                        hmac = new HMACSHA1();
                        break;
                    case 1:
                        hmac = new MACTripleDES();
                        break;
                    case 2:
                        hmac = new HMACMD5();
                        break;
                    case 3:
                        hmac = new HMACRIPEMD160();
                        break;
                    case 4:
                        hmac = new HMACSHA256();
                        break;
                    case 5:
                        hmac = new HMACSHA384();
                        break;
                    case 6:
                        hmac = new HMACSHA512();
                        break;
                }
        
                try
                {
                    // Настраиваем алгоритм хеширования
                    byte[] key = new byte[hmac.Key.Length];// Заготавливаем требуемый размер ключа
                    ASCIIEncoding.ASCII.GetBytes(cryptoKey.Trim()).CopyTo(key, 0);// Заполняем ключ
                    hmac.Key = key;
        
                    // Создаем оболочку CryptoStream для хеширования 'в никуда'
                    CryptoStream cryptoStream = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write);
        
                    // Открываем исходный файл и извлекаем хеш
                    inFileStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
                    BinaryReader br = new BinaryReader(inFileStream);// Вспомогательная оболочка
                    int hashLength = br.ReadInt32();// Читаем длину хеша
                    byte[] receivedHash = new byte[hashLength];// Место под хеш
                    inFileStream.Read(receivedHash, 0, receivedHash.Length);// Извлекаем хеш
                    long posBeginDoc = inFileStream.Position;// Начало документа
        
                    // Объявляем массив байтов тела документа
                    byte[] bytes = new byte[inFileStream.Length - posBeginDoc];
        
                    // Дочитываем документ в массив байтов и записываем байты 
                    // в CryptoStream, чтобы получить хеш исходного документа
                    inFileStream.Read(bytes, 0, bytes.Length);
                    cryptoStream.Write(bytes, 0, bytes.Length);
                    cryptoStream.Close();
        
                    // Извлекаем хеш и сравниваем с присланным
                    // Преобразование ASCIIEncoding.ASCII.GetString() сбоит!
                    String strCurrentHash = Encoding.Unicode.GetString(hmac.Hash);// byte[] -> string
                    String strReceivedHash = Encoding.Unicode.GetString(receivedHash);// Присланный хеш
                    if (strCurrentHash.CompareTo(strReceivedHash) != 0)
                        MessageBox.Show("Документ фальсифицирован!");
                    else
                    {
                        inFileStream.Position = posBeginDoc;
                        inFileStream.Read(bytes, 0, bytes.Length);
                        outFileStream = new FileStream(destinationFile, FileMode.Create, FileAccess.Write);
                        outFileStream.Write(bytes, 0, bytes.Length);
                        outFileStream.Flush();
                        MessageBox.Show("Документ подлинный и очищен от хеша!");
                    }
        
                    return true;
                }
                catch (Exception e)
                {
                    MessageBox.Show(this, e.ToString(), "Checked Error",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
        
                    return false;
                }
                finally
                {
                    // Освобождаем файловые ресурсы в любом случае
                    if (inFileStream != null) inFileStream.Close();
                    if (outFileStream != null) outFileStream.Close();
                }
            }
        
            // Диалог выбора файла для обработки
            OpenFileDialog openFileDialog = new OpenFileDialog();
            String SelectFile()
            {
                // Настраиваем диалог выбора файла
                if (txtSourceFile.Text == String.Empty)
                    openFileDialog.InitialDirectory = txtDestinationDir.Text;
                else
                    openFileDialog.InitialDirectory = Path.GetDirectoryName(txtSourceFile.Text);
        
                openFileDialog.FileName = "";
                openFileDialog.Filter = "Source Files(*.txt;*.sign)|*.txt;*.sign";
                DialogResult result = openFileDialog.ShowDialog();
                if (result != DialogResult.Cancel)
                    return openFileDialog.FileName; // Полное имя
                else
                    return txtSourceFile.Text;
            }
        
            // Запуск диалога выбора файла
            private void btnSelectFile_Click(object sender, EventArgs e)
            {
                txtSourceFile.Text = SelectFile();
                txtSourceFile.Focus();// Без этого не сохранится в установках приложения
            }
        
            // Запуск диалога выбора папки назначения
            FolderBrowserDialog dialogFolder = new FolderBrowserDialog();
            private void btnDestinationDir_Click(object sender, EventArgs e)
            {
                if (txtDestinationDir.Text == String.Empty ||
                    Directory.Exists(txtDestinationDir.Text) == false)
                {
                    dialogFolder.RootFolder =
                        System.Environment.SpecialFolder.MyComputer;
                    dialogFolder.SelectedPath = "";
                    txtDestinationDir.Text = String.Empty;
                }
                else
                {
                    dialogFolder.SelectedPath = txtDestinationDir.Text;
                }
        
                dialogFolder.Description =
                    "Выберите место размещения целевого файла";
        
                dialogFolder.ShowDialog();
                txtDestinationDir.Text = dialogFolder.SelectedPath;
                txtDestinationDir.Focus();
            }
        
            // Блокировка редактирования TextBox вручную
            private void EditKeyDown(object sender, KeyEventArgs e)
            {
                e.Handled = true;
            }
            private void EditKeyPress(object sender, KeyPressEventArgs e)
            {
                e.Handled = true;
            }
        
            // При закрытии формы сохраняем параметры
            private void Form1_FormClosing(object sender, FormClosingEventArgs e)
            {
                settings.Save();
            }
        
            // Закрываем форму
            private void btnClose_Click(object sender, EventArgs e)
            {
                this.Close();
            }
        }
    }

    Здесь мы применили двоичные потоки BinaryReader и BinaryWriter вместо текстовых StreamReader и StreamWriter, поскольку последние оставляют в документе невидимые метки и меняют хеш (пачкают). Кодировщики Unicode тоже точнее преобразуют хеш, который может содержать отличные от ASCII символы.

  • Добавьте к текущему проекту командой Project/Add Class новый файл с именем MySettings.cs и заполните его следующим кодом (приводится полностью)
  • using System;
    using System.Configuration;
        
    namespace Demo3
    {
        // Класс для сохранения параметров приложения
        class MySettings : ApplicationSettingsBase
        {
            [UserScopedSettingAttribute()]       // Область действия - 'Пользователь' 
            [DefaultSettingValueAttribute("C:\\")]   // Значение параметра по умолчанию
            public String DestinationDir
            {
                get
                {
                    return ((String)this["DestinationDir"]);
                }
                set
                {
                    this["DestinationDir"] = (String)value;
                }
            }
        
            [UserScopedSettingAttribute()]       // Область действия - 'Пользователь' 
            [DefaultSettingValueAttribute("")]   // Значение параметра по умолчанию
            public String SourceFile
            {
                get
                {
                    return ((String)this["SourceFile"]);
                }
                set
                {
                    this["SourceFile"] = (String)value;
                }
            }
        }
    }
  • Запустите приложение Demo3 - оно вполне работоспособно, подписывает документы и проводит их аутентификацию
  • Разберитесь с кодом ( кому надо!), да пусть еще больше простят меня классики
  • Внешний вид окна будет таким

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

    Дальше пока сами... (опять по шпалам!)

    Страницы:

    Полезные ссылки

    Все необходимые для выполнения данной работы программы можно найти в http://old.intuit.ru/department/se/prcsharp08/19/Setup.zipприлагаемом каталоге.

  • Работа с потоками слегка описана в intuit:
  • http://www.intuit.ru/department/se/csharpdevtools/11/
  • Работа с альтернативными потоками:
  • http://habrahabr.ru/blogs/windows/46990/
  • http://wladm.narod.ru/C_Sharp/filesstreams.html#top
  • Динамическое создание классов в модели CodeDOM:
  • http://msdn.microsoft.com/ru-ru/library/ms404245.aspx
  • Динамическое создание классов в модели System.Reflection.Emit:
  • http://www.codeproject.com/KB/cs/introreflection.aspx
  • Внедрение WPF в Windows Forms
  • http://msdn.microsoft.com/ru-ru/library/ms745781.aspx
  • Как в WPF управлять проигрыванием видео с помощью MediaElement
  • http://msdn.microsoft.com/ru-ru/library/system.windows.controls.mediaelement.aspx
  • Пример по криптографии
  • http://www.codeproject.com/KB/security/crypto_net.aspx
  • Класс Type
  • http://msdn.microsoft.com/ru-ru/library/system.type.aspx
  • Введение

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

    Под Stream понимают логическую абстракцию, с помощью которой можно подключаться к различным типам источников данных одинаковым образом. Stream - это модель, через которую последовательно считываются или записываются элементы данных. Для исключения разночтения некоторые авторы предлагают такие термины: Thread - нить (или поток выполнения ), Stream - поток (или поток данных ), чем мы с удовольствием и воспользуемся.

    Абстракция Stream создает канал связи между приложением и устройством хранения информации (резервным хранилищем), а также предоставляет необходимые для манипулирования данными инструменты. Для потока характерно направление: входной поток - данные передаются от устройства в приложение, выходной поток - от приложения к устройству. Есть однонаправленные потоки - например, файловый поток может открываться только для чтения или только для записи, с клавиатуры символы можно только считывать, и т.д. Есть и двунаправленные потоки - например, сессия по протоколу telnet или обмен данными между компьютером и модемом, которые представляют собой чередование однонаправленных потоков. Файл тоже может быть открыт для чтения и записи одновременно.

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

    Потоки, которые непосредственно связываются с устройствами, можно назвать рабочими (основными). На самом низком уровне система ввода-вывода .NET оперирует байтами, поэтому все рабочие потоки являются байтовыми. Однако есть несколько классов-надстроек (оболочек) к рабочим потокам, которые устанавливают свой формат данных при чтении/записи или облегчают пересылку данных определенного типа.

    Создав основной поток и погрузив его в надстройку, можно значительно облегчить решение задачи пересылки некоторых типов данных. Так, для упрощения пересылки текстов имеются специализированные классы, автоматически преобразующие байтовые потоки в символьные и наоборот. Чтение информации с клавиатуры и вывод на консоль обеспечивают стандартные потоки, которые являются одновременно и символьными.

    Базовым классом большей части потоков является класс Stream, который в свою очередь наследует от класса System.MarshalByRefObject

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

    Члены класса Stream
    Член Описание
    BeginRead() Начинает операцию асинхронного чтения, возможно, в отдельно созданной ните Thread. Основная нить действий продолжает выполняться
    BeginWrite() Начинает операцию асинхронной записи, возможно, в отдельно созданной ните Thread. Основная нить действий продолжает выполняться
    Close() Закрывает поток, освобождая связанные с ним системные ресурсы
    EndRead() Переводит систему в режим ожидания, пока не будет завершено чтение данных или не произойдет ошибка
    EndWrite() Переводит систему в режим ожидания, пока не будет завершена запись данных или не произойдет ошибка
    Flush() Сбрасывает (выталкивает, сливает) кэш буфера записи
    Read() Читает в массив с нужного места потока заданное количество байт. Возвращает число фактически прочитанных байт
    ReadByte() Читает очередной байт потока
    Seek() Смещяет курсор потока на заданное количество байт относительно заданного начала отсчета (начала, конца или текущей позиции) для последующей работы
    Write() Записывает в поток фрагмент данных из массива, характеризуемых начальным индексом и количеством элементов
    WriteByte() Записывает в выходной поток один байт
    CanRead CanSeek CanTimeout CanWrite Проверяются в экземплярах потомков для определения поддержки соответствующей операции
    Length Длина потока в байтах. Не во всех потомках реализована и может вызвать исключение!
    Position Определяет или устанавливает текущую позицию (курсор) потока
    ReadTimeout Определяет или устанавливает время в миллисекундах, отпущенное для попыток чтения ожидаемых данных, по безрезультатному истечению которого нить Thread будет синхронизирована с основной нитью приложения
    WriteTimeout Определяет или устанавливает время в миллисекундах, отпущенное для попыток записи, по безрезультатному истечению которого асинхронная нить Thread будет синхронизирована с основной нитью приложения

    Упражнение 1. Работа с FileStream как с чисто байтовым потоком

    Знакомство с потоками начнем с файлового потока FileStream как инструмента для чтения/записи информации в файлы. Нужно понимать, что объектная модель FileStream является высокоуровневой оболочкой API -функций операционной системы по работе с файлами. Но в то же время сам FileStream имеет несколько специализированных высокоуровневых оболочек для облегчения чтения/записи в файл данных различных форматов.

    Вначале рассмотрим использование только одного FileStream в чистом виде. Начнем с упражнения, иллюстрирующего пример из MSDN, который попутно немного 'подкрутим'.

  • Создайте новый проект App1 консольного типа в решении Stream
  • Стартовым его назначать нет смысла, поскольку в решении он пока будет единственным.

  • Заполните файл Program.cs следующим кодом
  • using System;
    using System.Text;
        
    using System.IO;
        
    namespace App1
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.ForegroundColor = ConsoleColor.White;
                string newLine = System.Environment.NewLine;// Пара "\r\n"
        
                string path = @"e:\temp1\MyTest.txt";// Задаем полное имя файла
                string dir = Path.GetDirectoryName(path);   // Выделяем каталог
        
                // Если каталог не существует, создаем его
                if (!Directory.Exists(dir))
                {
                    Directory.CreateDirectory(dir);
                }
        
                // Создаем новый или усекаем существующий файл
                // и сразу открываем его для записи
                using (FileStream fs = File.Create(path))
                {
                    AddText(fs, "Буря мглою ");
                    AddText(fs, "небо кроет,");
                    AddText(fs, "\r\nВихри снежные крутя.");
                    AddText(fs, "\r\nТо как зверь она завоет,\r\nТо заплачет как дитя.\r\n");
        
                    AddText(fs, newLine + "Сгенерированные символы:" + newLine);
                    for (int i = 32; i < 123; i++)  // Начинаем с пробела ASCII=32
                    {
                        // Добавляем символ в поток
                        AddText(fs, Convert.ToChar(i).ToString());
        
                        // Через каждые десять символов вставляем новую строку,
                        // когда i делится без остатка
                        //if (i % 10 == 0)  // Вариант
                        if (Math.IEEERemainder(Convert.ToDouble(i), 10) == 0)
                        {
                            // Вариант с библиотечным свойством
                            //AddText(fs, newLine);
                            // Вариант в управляющих символах
                            //AddText(fs, "\r\n");  
                            // Вариант в шестнадцатиричных кодах ASCII
                            //AddText(fs, Convert.ToChar(0x0D).ToString() +
                            //    Convert.ToChar(0x0A).ToString());
                            // Вариант в целых кодах ASCII
                            AddText(fs, 
                                Convert.ToChar(13).ToString() + // Попробуйте закомментировать эту строку!!!
                                Convert.ToChar(10).ToString());
                        }
                    }
                } // Здесь, по окончании инструкции using поток автоматически закрывается
        
                // Открываем файловый поток для чтения и обрабатываем его
                //using (FileStream fs = File.OpenRead(path))   // Вариант
                using(Stream fs = new FileInfo(path).OpenRead())    // Вариант для разнообразия
                {
                    byte[] bytes = new byte[fs.Length]; // Заготавливаем байтовый массив
                    int count = fs.Read(bytes, 0, bytes.Length);// Заполняем массив из файла
        
                    UTF8Encoding tmp = new UTF8Encoding(true);// Объект кодировки символов
                    String str = tmp.GetString(bytes);// Преобразуем массив в строку
                    Console.WriteLine(str);// Печатаем на консоль
        
                    Console.WriteLine("\nВсего прочитано байт: {0}", count);
                    Console.WriteLine("Из них отображаемых символов: {0}", str.Length);
                } // Здесь, по окончании инструкции using поток автоматически закрывается
        
                // Печатаем ASCII 'перевод строки' - 'возврат каретки'
                int a = '\r', b = '\n';
                String ascii = String.Format("Управляющие символы: ");
                ascii += String.Format("\\r={0}, \\n={1}", a, b);
                Console.WriteLine(ascii);
        
                // Проверяем строку NewLine
                byte[] bts = new ASCIIEncoding().GetBytes(newLine);
                a = bts[0]; b = bts[1];
                ascii = String.Format("Управляющие символы NewLine: ");
                ascii += String.Format("\\r={0}, \\n={1}", a, b);
                Console.WriteLine(ascii);
        
                Console.ReadLine();
            }
        
            private static void AddText(FileStream fs, string value)
            {
                // Преобразуем строку в массив байтов через объект кодировки символов
                byte[] bytes = new UTF8Encoding(true).GetBytes(value);
                // Записываем в открытый поток
                fs.Write(bytes, 0, bytes.Length);
            }
        }
    }
  • Запустите приложение - результат должен быть таким
  • Разберитесь с кодом
  • Интересно отметить следующее... Если не записывать в файл управляющий символ ' \r ' - возврат каретки, а только ' \n ' - новая строка, то при чтении файла на консоль функция Console.WriteLine() этого не заметит, а текстовый редактор Notepad заметит (попробуйте). Это значит, что при побайтовой записи нужно полностью обеспечивать документ всей необходимой информацией, не надеясь на умные устройства вывода. Альтернативной заменой строки " \r\n " может служить статическое свойство System.Environment.NewLine, которое означает то же самое и в приведенном коде частично используется.

    Всякий раз, когда мы открываем файл через объект FileStream, следует помнить, что по завершении работы этот файл следует закрыть, чтобы освободить дескриптор файла и вернуть его операционной системе. В противном случае файл будет считаться занятым и будет освобожден только после завершения приложения. Закрыть файл можно методом Close() объекта FileStream, либо открывать в инструкции using(), по завершению работы которой файл автоматически будет закрыт. Последний прием и был использован нами в приведенном выше коде.

    Упражнение 2. Подробности использования байтового потока FileStream

    В данном упражнении рассмотрим файловый поток более подробно. Класс FileStream обеспечивает практически все мыслимые потребности программного управления чтением/записью информации в файлы. После создания экземпляра потока (объекта) связанный с ним файл будет представлен этим объектом и все дальнейшие манипуляции с файлом будут выполняться через этот объект.

    Для создания экземпляра класс FileStream имеется множество перегрузок его конструктора, с которыми можно ознакомиться в MSDN. Наиболее типовая из них следующая:

    FileStream(string path, System.IO.FileMode mode, 
       System.IO.FileAccess access, System.IO.FileShare share)
  • path - относительный или абсолютный путь для файла, который будет представлен потоком
  • mode - значение перечисления FileMode, устанавливающее режим открытия или создания файла
  • Append - открывает файл, если он существует, и переходит в его конец, либо создает новый файл. Этот режим может быть использован только в сочетании с доступом FileAccess.Write. Попытка чтения или перемещения указателя на другую позицию генерирует исключение
  • Create - создает новый файл и открывает его для записи. Если файл уже существует, то он будет перезаписан на пустой
  • CreateNew - создает новый файл и открывает его для записи. Если файл уже существует, генерируется исключение IOException
  • Open - открывает существующий файл в режиме, определяемом перечислением FileAccess. Если указанный файл не существует, генерируется исключение FileNotFoundException
  • OpenOrCreate - открывает существующий или создает новый файл с режимом доступа, определяемым перечислением FileAccess
  • Truncate - открывает существующий файл для записи и усекает его длину до нуля байт. При попытке чтения генерируется исключение
  • access - значение перечисления FileAccess, устанавливающее режим доступа к файлу. Оно влияет на значения флагов CanRead - возможно чтение, CanWrite - возможна запись, CanSeek - возможен параллельный доступ к дисковому файлу
  • Read - только для чтения
  • ReadWrite - для чтения и записи
  • Write - только для записи
  • share (разделять, делить, совместно использовать) - значение перечисления FileShare, устанавливающее режим доступа к тому же самому файлу других процессов или объектов FileStream
  • Delete - разрешает последующее удаление файла
  • Inheritable - делает дескриптор файла наследуемым дочерними процессами. Этот режим непосредственно не поддерживается Win32
  • None - отключает режим разделения и переводит файл в монопольный доступ только текущего процесса
  • Read - открывает совместный доступ только для чтения
  • ReadWrite - открывает совместный доступ для чтения и записи
  • Write - открывает совместный доступ только для записи
  • Не стоит забывать, что класс FileStream имеет в своем распоряжении и весь набор членов, наследуемых от базового класса Stream.

  • В панели Solution Explorer вызовите контекстное меню для узла решения (не проекта!), командой Add/New Project добавьте новый проект с именем App2 типа консольного приложения и назначьте его стартовым
  • Заполните файл Program.cs следующим кодом, иллюстрирующим работу с байтовым потоком FileStream
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен
    using System.IO;// Для потоков
        
    namespace App2
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.ForegroundColor = ConsoleColor.White;
                Console.Title = "Упражнение 2. Файловые потоки";
        
                new MyClass().Foo();
        
                Console.ReadLine();// Задержка консоли
            }
        }
    }
        
    namespace App2
    {
        class MyClass
        {
            public void Foo()
            {
                byte[] bytes;   // Только объявили
        
                // Сохраняемые данные
                string sData = "Привет всем!";  // В Unicode займет 12*2 байт
                int iData1 = (int)12345;        // 4 байта
                int iData2 = int.MaxValue;      // 4 байта
                float fData = 67F;              // 4 байта
                double dData = 89D;             // 8 байт
        
                // Сохраняем значения в файле
                String fileName = "Data.dat";// Без пути - будет создан в каталоге сборки
                FileStream fileStream = null;
                try
                {
                    // Создаем или усекаем
                    fileStream = new FileStream(fileName, FileMode.Create);
        
                    // Сохраняем строку в универсальной кодировке, 
                    // занимающей 2 байта на каждый символ
                    bytes = new UnicodeEncoding().GetBytes(sData);  // Кодировка UTF-16
                    for (int i = 0; i < bytes.Length; i++)
                        fileStream.WriteByte(bytes[i]); // Побайтно
        
                    // Сохраняем первое целое число
                    bytes = new byte[sizeof(int)];
                    for (int i = 0; i < sizeof(int); i++)
                        bytes[i] = (byte)(iData1 >> 8 * i);  // Делим на байты, начиная с младшего
                    fileStream.Write(bytes, 0, sizeof(int));// Нулевой сдвиг относительно текущего
        
                    // Сохраняем второе целое число
                    bytes = new byte[sizeof(int)];
                    bytes = BitConverter.GetBytes(iData2);
                    fileStream.Write(bytes, 0, sizeof(int));// Нулевой сдвиг относительно текущего
        
                    // Сохраняем внутренне представление вещественного float (Single)
                    bytes = BitConverter.GetBytes(fData);// Массив той же длины int
                    fileStream.Write(bytes, 0, sizeof(float));// Нулевой сдвиг относительно текущего
        
                    // Сохраняем внутренне представление удвоенного double
                    bytes = new byte[sizeof(double)];
                    bytes = BitConverter.GetBytes(dData);
                    fileStream.Write(bytes, 0, sizeof(double));// Нулевой сдвиг относительно текущего
        
                    fileStream.Flush(); // Слить кэш (необязательно!)
                }
                finally
                {
                    if (fileStream != null)
                        fileStream.Close();
                }
        
                // Читаем значения из файла и печатаем их на консоль
                try
                {
                    // Открываем только для чтения
                    fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
                    if (fileStream.CanSeek) // Поддерживает ли такую возможность
                        fileStream.Seek(0L, SeekOrigin.Begin);  // Необязательно!
        
                    // Читаем строку, зная, сколько нужно прочитать
                    bytes = new byte[sData.Length * 2];// Для UTF-16 это 2 байта на каждый символ
                    fileStream.Read(bytes, 0, sData.Length * 2);
                    Console.WriteLine((new UnicodeEncoding()).GetString(bytes));
        
                    // Далее идут байты для представления первого целого числа
                    bytes = new byte[sizeof(int)];
                    fileStream.Read(bytes, 0, sizeof(int));
                    int a = 0;
                    for (int i = 0; i < sizeof(int); i++)
                        a |= bytes[i] << 8 * i; // Заполняем все байты, начиная с младшего
                    Console.WriteLine(a);
        
                    // Далее идут байты для представления второго целого числа
                    fileStream.Read(bytes, 0, sizeof(int));
                    a = BitConverter.ToInt32(bytes, 0);
                    Console.WriteLine(a);
        
                    // Считываем и печатаем вещественное float 
                    // Побитовые операции к типам с плавающей точкой применять нельзя
                    bytes = new byte[sizeof(float)];
                    fileStream.Read(bytes, 0, sizeof(float));
                    float b = BitConverter.ToSingle(bytes, 0);
                    Console.WriteLine(b);
        
                    // Считываем и печатаем double 
                    bytes = new byte[sizeof(Double)];
                    fileStream.Read(bytes, 0, sizeof(double));
                    double c = BitConverter.ToDouble(bytes, 0);
                    Console.WriteLine(c);
        
                    // Измеряем длину потока, пока открыт
                    Console.WriteLine("==============\nВсего байт: {0}", fileStream.Length);
                }
                finally
                {
                    fileStream.Close();
                }
        
                // Измеряем длину файла
                Console.WriteLine("Длина файла: {0}", new FileInfo(fileName).Length);
            }
        }
    }

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

  • Запустите приложение App2 - результат будет таким
  • Упражнение 3. Сериализация данных в потоке FileStream

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

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

    Для поддержки сериализации объекту необходимо следующее:

  • Наличие атрибута Serializable перед объявлением класса (типа)
  • Поля класса должны быть сериализуемы, что для значимых (в отличие от ссылочных!) типов всегда выполнимо
  • Если сериализуется производный класс, то вся предшествующая цепочка наследования тоже должна состоять из сериализуемых классов
  • Для выполнения сериализации библиотека .NET Framework имеет три класса:

  • BinaryFormatter - сериализует объект в компактном двоичном представлении
  • SoapFormatter - сериализует объект в текстовое представление на основе XML
  • XmlSerializer - сериализует объект в текстовое представление на основе 'чистого' XML
  • Рассмотрим эти способы на предыдущем примере, чуток изменив сохраняемые данные.

  • Командой File/Add/New Project добавьте к решение новый проект согласно следующему снимку мастера
  • В панели Solution Explorer вызовите контекстное меню для узла App3 и выполните команду Set as StartUp Project, чтобы назначить проект стартовым
  • В панели Solution Explorer выделите узел App3 и щелкните на пиктограмме Properties, чтобы открыть панель конструктора проектов
  • На вкладке Application установите выпадающий список Output type в значение Console Application, включив параллельно окну формы еще и консоль для вывода информации
  • В режиме Design конструктора форм поместите на форму из панели Toolbox интерфейсные элементы управления и настройте их в соответствии с таблицей свойств
  • Элемент Свойство Значение
    Form Text Сериализация объектов
    LinkLabel (Name) saveBinary
    Text Serialize BinaryFormatter and save to file
    LinkLabel (Name) loadBinary
    Text Load file and deserialize BinaryFormatter
    LinkLabel (Name) saveSoap
    Text Serialize SoapFormatter and save to file
    LinkLabel (Name) loadSoap
    Text Load file and deserialize SoapFormatter
    LinkLabel (Name) saveXml
    Text Serialize XmlSerializer and save to file
    LinkLabel (Name) loadXml
    Text Load file and deserialize XmlSerializer

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

    Мы подготовили интерфейс пользователя, который больше менять не будем. Поэтому разумно будет его замкнуть, чтобы защитить от случайных изменений.

  • В режиме Design конструктора форм выполните команду Edit/Select All, а затем - команду Format/Lock Controls, элементы интерфейса станут недоступными для редактирования в графическом режиме проектирования
  • Последовательно двойным щелчком на каждом экземпляре объекта LinkLabel создайте заготовки обработчиков и их регистрацию для событий LinkClicked
  • Для тренировки мы уберем регистрацию обработчиков, созданную автоматически в файле Form1.Designer.cs, и перенесем ее в конструктор класса формы. Для этого

  • Внесите в заготовки обработчиков хотя бы комментарии, чтобы при выполнении следующего действия оболочка их не удалила
  • В режиме Design выделите на форме последовательно объекты LinkLabel, в панели Properties щелкните на пиктограмме Events и очистите поля события LinkClicked
  • Библиотечные классы BinaryFormatter, SoapFormatter, XmlSerializer находятся в отдельных сборках: mscorlib.dll, System.Runtime.Serialization.Formatters.Soap.dll и System.Xml.dll соответственно. Они сгруппированы в пространствах имен System.Runtime.Serialization.Formatters.Binary, System.Runtime.Serialization.Formatters.Soap и System.Xml.Serialization. Библиотека mscorlib.dll является основной и всегда подключена к проектам оболочки. Библиотека System.Xml.dll тоже подключается при создании проекта, а вот System.Runtime.Serialization.Formatters.Soap.dll нужно подключить самостоятельно.

  • В панели Solution Explorer вызовите контекстное меню для узла References проекта App3, командой Add Reference вызовите одноименное окно и подключите библиотеку System.Runtime.Serialization.Formatters.Soap.dll
  • В панели Solution Explorer вызовите контекстное меню для узла App3 и командой Add/Class добавьте к проекту новый файл с именем DataObject.cs, который заполните так
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    namespace App3
    {
        [Serializable()]    // Атрибут
        public class DataObject
        {
            // Сохраняемые данные
            string stringData = "Привет всем!";
            int integerData = (int)12345;       
            float floatData = 67F; 
            double doubleData = 89D;
            DateTime date;  // Инициализируем в конструкторе
        
            // Конструкторы
            public DataObject(string message)
            {
                this.stringData = message;
                this.date = DateTime.Now;
            }
            public DataObject()
            {
                this.date = DateTime.Now;
            }
        
            // Свойства доступа к полям
            public String StringData
            {
                get { return stringData; }
                set { stringData = value; }
            }
            public Int32 IntegerData
            {
                get { return integerData; }
                set { integerData = value; }
            }
            public Single FloatData
            {
                get { return floatData; }
                set { floatData = value; }
            }
            public Double DoubleData
            {
                get { return doubleData; }
                set { doubleData = value; }
            }
            public DateTime Date
            {
                get { return date; }
                set { date = value; }
            }
        }
    }
  • Модифицируйте файл Form1.cs, чтобы он стал таким
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Runtime.Serialization.Formatters.Binary;   // Для класса BinaryFormatter
    using System.Runtime.Serialization.Formatters.Soap;     // Для класса SoapFormatter
    using System.Xml.Serialization;                         // Для класса XmlSerializer
        
    namespace App3
    {
        public partial class Form1 : Form
        {
            String fileNameBinary = "DataBinary.dat";   // Без пути - будет создан в каталоге сборки
            String fileNameSoap = "DataSoap.dat";       // Без пути - будет создан в каталоге сборки
            String fileNameXml = "DataXml.dat";         // Без пути - будет создан в каталоге сборки
        
            public Form1()
            {
                InitializeComponent();
        
                // Настраиваем консоль
                Console.ForegroundColor = ConsoleColor.White;
                Console.Title = "Упражнение 3. Сериализация объектов";
        
                // Регистрируем обработчики вручную
                saveBinary.LinkClicked += saveBinary_LinkClicked;
                loadBinary.LinkClicked += loadBinary_LinkClicked;
                saveSoap.LinkClicked += saveSoap_LinkClicked;
                loadSoap.LinkClicked += loadSoap_LinkClicked;
                saveXml.LinkClicked += saveXml_LinkClicked;
                loadXml.LinkClicked += loadXml_LinkClicked;
            }
        
            private void saveBinary_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                FileMode mode = FileMode.Create;
                if (new FileInfo(fileNameBinary).Exists)
                    mode = FileMode.Append; // Будем добавлять!
        
                using (FileStream fs = new FileStream(fileNameBinary, mode))
                {
                    // Создаем объект с данными
                    DataObject data = new DataObject();
                    // Создаем форматировщик
                    BinaryFormatter formatter = new BinaryFormatter();
                    // Сериализуем объект в файл
                    formatter.Serialize(fs, data);
                }
            }
        
            private void loadBinary_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                if (!File.Exists(fileNameBinary))
                {
                    MessageBox.Show("Нет данных для отображения",
                        "Предупреждение", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                    return;
                }
        
                using (FileStream fs = new FileStream(fileNameBinary, FileMode.Open))
                {
                    // Создать форматировщик
                    BinaryFormatter formatter = new BinaryFormatter();
                    // Прочитать и десериализовать все экземпляры объекта в файле
                    Console.Clear();
                    while (fs.Position < fs.Length)
                    {
                        // Прочитать, десериализовать и переместить указатель на следующий объект
                        DataObject data = (DataObject)formatter.Deserialize(fs);
        
                        // Распечатать
                        Console.WriteLine(data.StringData);
                        Console.WriteLine(data.IntegerData);
                        Console.WriteLine(data.FloatData);
                        Console.WriteLine(data.DoubleData);
                        Console.WriteLine(data.Date.ToString());
                    }
                }
            }
        
            private void saveSoap_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                FileMode mode = FileMode.Create;
                if (new FileInfo(fileNameSoap).Exists)
                    mode = FileMode.Append; // Будем добавлять!
        
                using (FileStream fs = new FileStream(fileNameSoap, mode))
                {
                    // Создаем объект с данными
                    DataObject data = new DataObject();
                    // Создаем форматировщик
                    SoapFormatter formatter = new SoapFormatter();
                    // Сериализуем объект в файл
                    formatter.Serialize(fs, data);
                }
            }
        
            private void loadSoap_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                if (!File.Exists(fileNameSoap))
                {
                    MessageBox.Show("Нет данных для отображения",
                        "Предупреждение", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                    return;
                }
        
                using (FileStream fs = new FileStream(fileNameSoap, FileMode.Open))
                {
                    // Создать форматировщик
                    SoapFormatter formatter = new SoapFormatter();
                    // Прочитать все сериализованные объекты
                    Console.Clear();
                    while (fs.Position < fs.Length)
                    {
                        // Десериализовать
                        DataObject data = (DataObject)formatter.Deserialize(fs);
        
                        // Распечатать
                        Console.WriteLine(data.StringData);
                        Console.WriteLine(data.IntegerData);
                        Console.WriteLine(data.FloatData);
                        Console.WriteLine(data.DoubleData);
                        Console.WriteLine(data.Date.ToString());
                    }
                }
            }
        
            private void saveXml_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                // Создаем или усекаем файл 
                using (FileStream fs = new FileStream(fileNameXml, FileMode.Create))
                {
                    // Создаем объект с данными
                    DataObject data = new DataObject();
                    // Создаем форматировщик
                    XmlSerializer formatter = new XmlSerializer(typeof(DataObject));
                    // Сериализуем объект в файл
                    formatter.Serialize(fs, data);
                }
            }
        
            private void loadXml_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                if (!File.Exists(fileNameXml))
                {
                    MessageBox.Show("Нет данных для отображения",
                        "Предупреждение", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                    return;
                }
        
                using (FileStream fs = new FileStream(fileNameXml, FileMode.Open))
                {
                    // Создать форматировщик
                    XmlSerializer formatter = new XmlSerializer(typeof(DataObject));
                    Console.Clear();
                    // Прочитать и десериализовать
                    DataObject data = (DataObject)formatter.Deserialize(fs);
        
                    // Распечатать
                    Console.WriteLine(data.StringData);
                    Console.WriteLine(data.IntegerData);
                    Console.WriteLine(data.FloatData);
                    Console.WriteLine(data.DoubleData);
                    Console.WriteLine(data.Date.ToString());
                }
            }
        }
    }
  • Запустите приложение и испытайте все три способа - объект с данными сохраняется в файле и полностью восстанавливается
  • Обратите внимание на важную особенность приведенного вывода: показано время 92729 - симметричное число, хороший знак свыше ('верной дорогой идете товарищи')!

  • В панели Solution Explorer выделите узел App3, щелкните на пиктограмме Show All Files, выделите файлы DataBinary.dat, DataSoap.dat и DataXml.dat (если они созданы)
  • Командой Open контекстного меню панели Solution Explorer откройте эти файлы и ознакомтесь с форматом хранения сериализованной информации
  • Файловый поток FileStream все равно сам связывается с файлом, только он используется другим классом, выполняющим передачу данных в специфическом формате (сериализованных).

  • Разберитесь с кодом
  • Упражнение 4. Сжатие данных в потоке FileStream

    В данном упражнении познакомимся с техникой чтения/записи сжатых данных в файловом потоке FileStream, хотя то же самое будет справедливо и для некоторых других рабочих потоков. Так же как и ранее, с файлом будет связываться поток FileStream, но формат данных в потоке будут обеспечивать специализированные классы сборки System.dll:

  • System.IO.Compression.DeflateStream
  • System.IO.Compression. GZipStream
  • Оба класса-надстройки реализуют эффективные алгоритмы сжатия без потерь информации.

  • Командой File/Add/New Project добавьте к решение новый проект согласно следующему снимку мастера
  • В панели Solution Explorer вызовите контекстное меню для узла App4 и выполните команду Set as StartUp Project, чтобы назначить проект стартовым
  • В панели Solution Explorer выделите узел App4 и щелкните на пиктограмме Properties, чтобы открыть панель конструктора проектов
  • На вкладке Application установите выпадающий список Output type в значение Console Application, включив параллельно окну формы еще и консоль для вывода информации
  • В режиме Design конструктора форм поместите на форму из панели Toolbox четыре экземпляра кнопки LinkLabel, две текстовые метки Label и настройте их в соответствии с таблицей свойств
  • Элемент Свойство Значение
    Form Text Сжимающие потоки
    LinkLabel (Name) saveDeflate
    Text Compression DeflateStream and save to file
    LinkLabel (Name) loadDeflate
    Text Load file and decompression DeflateStream
    LinkLabel (Name) saveGZip
    Text Compression GZipStream and save to file
    LinkLabel (Name) loadGZip
    Text Load file and decompression GZipStream

    Интерфейс формы должен выглядеть так

    Мы подготовили интерфейс пользователя, который больше менять не будем и его следует замкнуть.

  • В режиме Design конструктора форм выполните команду Edit/Select All, а затем - команду Format/Lock Controls, элементы интерфейса станут недоступными для редактирования в графическом режиме проектирования
  • Последовательно двойным щелчком на каждом экземпляре объекта LinkLabel создайте заготовки обработчиков и их регистрацию для событий LinkClicked
  • Теперь осталось заполнить обработчики кодом, реализующим сжатие и декомпрессию данных при файловых операциях записи/чтения.

  • Щелкните правой кнопкой мыши в любом месте формы Form1 (или вне формы) и выполните команду View Code, чтобы вызвать редактор процедурного кода
  • Заполните файл Form1.cs следующим образом (код файла приведен полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.IO.Compression;
    using System.IO;
        
    namespace App4
    {
        public partial class Form1 : Form
        {
            String fileNameDeflate = "DataDeflate.dat";
            String fileNameGZip = "DataGZip.dat";
            String messageDeflate = "Привет всем от DeflateStream!";
            String messageGZip = "Привет всем от GZipStream!";
            const Int32 lenBuf = 4;
        
            public Form1()
            {
                InitializeComponent();
        
                // Настраиваем консоль
                Console.ForegroundColor = ConsoleColor.White;
                Console.Title = "Упражнение 4. Сжатие данных в потоке FileStream";
                Console.CursorVisible = false;
            }
        
            private void saveDeflate_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                using (FileStream fs = new FileStream(fileNameDeflate, FileMode.Create))
                {
                    // Погружаем файловый поток в оболочку сжимающего потока
                    DeflateStream compress = new DeflateStream(fs, CompressionMode.Compress);
                    // Преобразуем строковые данные в массив байт кодировки UTF-16
                    byte[] bytes = new UnicodeEncoding().GetBytes(messageDeflate);
                    // Записываем данные в файл через сжимающий поток
                    compress.Write(bytes, 0, bytes.Length);
        
                    // Сливаем кэш и закрываем
                    compress.Flush();
                    compress.Close();
                }
            }
        
            private void loadDeflate_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                if (!new FileInfo(fileNameDeflate).Exists)
                {
                    MessageBox.Show("Нет данных для отображения",
                        "Предупреждение", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                    return;
                }
        
                using (FileStream fs = new FileStream(fileNameDeflate, FileMode.Open, FileAccess.Read))
                {
                    // Создаем буфер нужной длины, обычно 1024b (1Кb)
                    byte[] buffer = new byte[lenBuf / 2 * 2];// Округляем до четного
                    // Погружаем файловый поток в оболочку декомпрессионного потока
                    DeflateStream decompress = new DeflateStream(fs, CompressionMode.Decompress);
        
                    int count = 0;
                    String msg = String.Empty;
                    // Читаем все данные через декомпрессионный поток
                    while ((count = decompress.Read(buffer, 0, buffer.Length)) != 0)
                        msg += new UnicodeEncoding().GetString(buffer, 0, count);
        
                    // Сливаем кэш и закрываем
                    decompress.Flush();
                    decompress.Close();
        
                    // Печатаем прочитанное 
                    Console.Clear();
                    Console.WriteLine(msg);
                }
            }
        
            private void saveGZip_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                using (FileStream fs = new FileStream(fileNameGZip, FileMode.Create))
                {
                    // Погружаем файловый поток в оболочку сжимающего потока
                    GZipStream compress = new GZipStream(fs, CompressionMode.Compress);
                    // Преобразуем строковые данные в массив байт кодировки UTF-16
                    byte[] bytes = new UnicodeEncoding().GetBytes(messageGZip);
                    // Записываем данные в файл через сжимающий поток
                    compress.Write(bytes, 0, bytes.Length);
        
                    // Сливаем кэш и закрываем
                    compress.Flush();
                    compress.Close();
                }
            }
        
            private void loadGZip_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                if (!new FileInfo(fileNameGZip).Exists)
                {
                    MessageBox.Show("Нет данных для отображения",
                        "Предупреждение", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                    return;
                }
        
                using (FileStream fs = new FileStream(fileNameGZip, FileMode.Open, FileAccess.Read))
                {
                    // Создаем буфер нужной длины, обычно 1024b (1Кb)
                    byte[] buffer = new byte[lenBuf / 2 * 2];// Округляем до четного
                    // Погружаем файловый поток в оболочку декомпрессионного потока
                    GZipStream decompress = new GZipStream(fs, CompressionMode.Decompress);
        
                    int count = 0;
                    String msg = String.Empty;
                    // Читаем все данные через декомпрессионный поток
                    while ((count = decompress.Read(buffer, 0, buffer.Length)) != 0)
                        msg += new UnicodeEncoding().GetString(buffer, 0, count);
        
                    // Сливаем кэш и закрываем
                    decompress.Flush();
                    decompress.Close();
        
                    // Печатаем прочитанное 
                    Console.Clear();
                    Console.WriteLine(msg);
                }
            }
        }
    }
  • Испытайте работу приложения и разберитесь с кодом
  • Один из результатов будет таким

    Упражнение 5. Буферизованная обертка BufferedStream

    Когда создается рабочий поток, связанный с каким-либо устройством, он работает синхронно с кодом приложения и для медленных устройств приложение будет ждать завершения соответствующих операций ввода/вывода данных. Буферизация таких операций в кэше памяти, где данные могут группироваться в большие порции, может существенно ускорить работу приложения. Для этого и существует обертка BufferedStream, в которую можно погрузить любой основной поток. Тогда все манипуляции с данными будут выполняться с помощью этого вспомогательного инструмента.

    Рассмотрим работу с буферизованным потоком на примере его совместного использования с файловым потоком FileStream.

  • Командой File/Add/New Project создайте новый проект App5 типа консольного приложения и назначьте его стартовым
  • Заполните файл Program.cs следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен
    using System.IO;
        
    namespace App5
    {
        class Program
        {
            static void Main(string[] args)
            {
                // Настраиваем консоль
                Console.ForegroundColor = ConsoleColor.White;
                Console.Title = "Упражнение 5. Буферизованная обертка BufferedStream";
        
                String fileName = "DataBuffered.dat";
                String message = "Привет всем от BufferedStream!";
        
                FileMode mode = FileMode.Create;
                if (File.Exists(fileName))
                    mode = FileMode.Append;
        
                // Пишем
                using (FileStream fs = new FileStream(fileName, mode))
                {
                    using (BufferedStream bs = new BufferedStream(fs, 1024 * 4))
                    {
                        // Применим кодировку, по умолчанию установленную в Windows
                        byte[] bytes = Encoding.Default.GetBytes(message);
                        // Дописываем побайтно в обертку
                        for (int i = 0; i < bytes.Length; i++)
                            bs.WriteByte(bytes[i]);
                        // Дополняем символами новой строки "\r\n"
                        bs.Write(Encoding.Default.
                            GetBytes(Environment.NewLine), 0, Environment.NewLine.Length);
                        /********** Вариант не для режима Append **********
                            // Записываем побайтно в обертку
                            while (bs.Position < bytes.Length)
                                bs.WriteByte(bytes[bs.Position]);
                        //************************************************/
                    }
                }
        
                // Читаем
                using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read))
                {
                    fs.Seek(0, SeekOrigin.Begin);// Необязательно
        
                    using (BufferedStream bs = new BufferedStream(fs))// По умолчанию 4096 bytes
                    {
                        byte[] bytes = new byte[bs.Length];
        
                        int result, i = 0;
                        while ((result = bs.ReadByte()) != -1)
                            bytes[i++] = (byte)result;
                        /*********** Вариант ****************
                            while (bs.Position < bytes.Length)
                                bytes[bs.Position] = (byte)bs.ReadByte();
                        //**********************************/
        
                        // Печатаем результат
                        String msg = Encoding.Default.GetString(bytes);
                        Console.Clear();
                        Console.WriteLine(msg);
                    }
                }
        
                Console.ReadLine();// Тормозим консольное окно
            }
        }
    }

    По умолчанию величина буфера в BufferedStream установлена размером 4 Kb, но в перегрузке конструктора мы вольны установить ее любой. В приведенном коде при каждом запуске информация записывается в файл в режиме добавления. Потоки мы явно не закрываем, поскольку используем инструкцию using().

  • Запустите приложение и испытайте его работу, разберитесь с кодом
  • Для нескольких запусков результат будет таким

    Упражнение 6. Классы-обертки BinaryWriter и BinaryReader для работы с встроенными типами C#

    Встроенными (элементарными, примитивными, базовыми, стандартными, простыми) типами C# называются типы, которые имеют ключевые слова в самом языке как псевдонимы ( alias ). Например, Int32 - int, Single - float, String - string, Double - double, Boolean - bool и т.д. Все они отностятся к типам значений и выведены из абстрактного класса ValueType. Такие типы являются стэковыми, т.е. в стэк функций при выполнении программы помещаются не адресные переменные, а сами данные. Они представляют собой структуры и обозначаются отдельными пиктограммами в панели Object Browser

    Для чтения и записи таких данных в байтовый поток используются обертки BinaryReader и BinaryWriter пространства имен System.IO библиотечной сборки mscorlib.dll (специализированные читатель и писатель). Эти классы расщепляют базовый тип на последовательность байт и передают ее прикрепленному основному потоку для чтения/записи в связанное с потоком устройство.

    Рассмотрим работу этих оберток на примере разнотипных данных, которые мы ранее уже использовали во втором упражнении. По прежнему, в качестве рабочего будем использовать файловый поток FileStream.

  • Добавьте к решению проект консольного приложения App6 и назначьте его стартовым
  • Заполните файл Program.cs следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен
    using System.IO;// Для потоков
        
    namespace App6
    {
        class Program
        {
            static void Main(string[] args)
            {
                Console.ForegroundColor = ConsoleColor.White;
                Console.Title = "Упражнение 6. Двоичный ввод/вывод";
                Console.CursorVisible = false;
        
                new MyClass().Foo();
        
                Console.ReadLine();// Задержка консоли
            }
        }
    }
        
    namespace App6
    {
        class MyClass
        {
            public void Foo()
            {
                // Сохраняемые данные
                string sData1 = "Привет всем!";
                string sData2 = "Привет студентам!";
                int iData1 = (int)12345;      
                int iData2 = int.MaxValue;     
                float fData = 67F;             
                double dData = 89D;           
        
                // Сохраняем значения в файле
                String fileName = "Data.dat";// Будет создан в каталоге сборки
        
                // Объявляем рабочий поток
                FileStream fs = null;
                try
                {
                    // Создаем или усекаем файл
                    fs = new FileStream(fileName, FileMode.Create);
                    // Подключаем к писателю
                    BinaryWriter data = new BinaryWriter(fs);
                    // Пишем данные, компилятор по типу сам 
                    // определяет нужную перегрузку метода
                    data.Write(sData1);
                    data.Write(sData2);
                    data.Write(iData1);
                    data.Write(iData2);
                    data.Write(fData);
                    data.Write(dData);
        
                    fs.Flush(); // Сплюнуть кэш (необязательно!)
                }
                finally
                {
                    if (fs != null)
                        fs.Close();
                }
        
                // Читаем значения из файла и печатаем их на консоль
                try
                {
                    // Открываем только для чтения
                    fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
                    if (fs.CanSeek) // Поддерживает ли такую возможность
                        fs.Seek(0L, SeekOrigin.Begin);  // Необязательно!
        
                    // Подключаем к читателю, считываем и печатаем
                    BinaryReader data = new BinaryReader(fs);
                    // Порядок чтения должен строго соответствовать порядку записи
                    Console.WriteLine(data.ReadString());
                    Console.WriteLine(data.ReadString());
                    Console.WriteLine(data.ReadInt32());
                    Console.WriteLine(data.ReadInt32());
                    Console.WriteLine(data.ReadSingle());
                    Console.WriteLine(data.ReadDouble());
        
                    // Измеряем длину потока, пока открыт
                    Console.WriteLine("==============\nВсего байт: {0}", fs.Length);
                }
                finally
                {
                    fs.Close();
                }
        
                // Измеряем длину файла
                Console.WriteLine("Длина файла: {0}", new FileInfo(fileName).Length);
            }
        }
    }
  • Разберитесь с кодом и запустите приложение - результат будет таким
  • Не стоит думать, что данные при записи разбиваются объектом BinaryWriter по такой же схеме, как мы это делали во втором упражнении. Если попробовать записать с помощью этого писателя, а прочитать побайтно как в Упражнении 2, то ничего не получится: у рассмотренных читателя и писателя свои форматы данных. Хотя, расщипление на байты соответствует длине типов (кроме string ), например, тип int состоит из 4 байтов и точно такой же длинной будет записан в поток.

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

    В данном упражнении мы продемонстрировали только простейшие возможности объектов BinaryReader и BinaryWriter. Они могут применяться и для реализации более сложных схем чтения/записи. Для этого имеется все необходимое: перегрузки конструкторов, методы последовательного доступа, методы сдвига указателя. Но мы их рассматривать не будем, поскольку наша основная задача - выполнить обзор потоков как можно шире, а впереди еще много вкусного.

    Упражнение 7. Байтовый поток MemoryStream

    Для определения MemoryStream приведем выдержку из MSDN, поскольку точнее и не скажешь: " Класс MemoryStream создает потоки, которые в качестве резервного хранилища имеют память, а не подключение к сети или диску. Класс MemoryStream инкапсулирует данные, хранимые в виде массива байтов без знака, инициализированного при создании объекта MemoryStream . Массив может быть создан пустым. К инкапсулированным данным в памяти можно получить доступ напрямую. Потоки памяти могут снижать потребность во временных буферах и файлах в приложении ".

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

    Конструктор или член Пояснения
    MemoryStream() Создает пустой поток, размеры которого можно изменять. Открывается для записи и чтения данных
    MemoryStream(byte[ ] buffer) Создает поток неизменяемого размера на базе массива, который сразу этим массивом инициируется. Открывается для записи и чтения данных
    MemoryStream(byte[ ] buffer, bool writable) Создает поток, основанный на внешнем массиве беззнаковых байт неизменяемого размера в качестве резервного хранилища. Параметр writable определяет, будет ли этот поток только для записи или двухсторонний - для записи и чтения (как предыдущий без параметра)
    MemoryStream(byte[ ] buffer, int index, int count) Создает поток, основанный на внешнем беззнаковом байтовом массиве фиксированной длины, из которого потоком контролируется только count байт, начиная с позиции index
    MemoryStream(byte[ ] buffer, int index, int count, bool writable) Создает поток, основанный на внешнем беззнаковом байтовом массиве фиксированной длины, из которого потоку принадлежит только count байт, начиная с позиции index. Параметр writable определяет, будет ли этот диапазон только для записи или двухсторонний - для записи и чтения (как предыдущий без параметра)
    MemoryStream(byte[ ] buffer, int index, int count, bool writable, bool publiclyVisible) Создает поток, основанный на внешнем беззнаковом байтовом буфере фиксированной длины, из которого потоком контролируется только count байт, начиная с позиции index. Параметр writable определяет, будет ли этот поток только для записи или двухсторонний - для записи и чтения. Если последний параметр установлен в true, то конролируемый потоком диапазон байт можно извлечь с помощью его метода GetBuffer()
    MemoryStream(int capacity) Создает поток для записи и чтения данных с внутренним байтовым буфером изменяемой длины и начальной емкостью capacity. Когда в поток добавляются элементы, его емкость Capacity автоматически увеличивается блоками посредством перераспределения
    void Flush() При выполнении этого метода содержимое внутреннего буфера, связанного с потоком, реально перепишется в целевое резервное хранилище (в базовый буфер)
    byte[ ] GetBuffer() Возвращает массив байт без знака из базового хранилища потока MemoryStream, как бы он не создавался
    int Read(byte[ ] buffer, int offset, int count) Читает из потока, начиная с индекса offset, count байт в заданный массив byte[ ]. Возвращает число фактически считанных байтов, которое может быть меньше, чем запрашивалось, если конец потока достигнут раньше. Может вернуть и нуль, если конец потока достигнут прежде, чем любые байты будут считаны
    int ReadByte() Читает текущий байт, на который установлен указатель, и передвигает курсор потока на следующую байтовую позицию. Возвращает содержимое считанного байта в формате Int32 (поэтому результат нужно явно приводить) или -1, если достигнут конец потока
    long Seek(long offset, System.IO.SeekOrigin loc) Устанавливает указатель потока на offset байт относительно начала, конца или текущей позиции. Возвращает новую позицию указателя относительно начала потока
    void SetLength(long value) Устанавливает новую длину потока, у которого внутренний буфер допускает изменение длины и поддерживает запись
    byte[ ] ToArray() Создает байтовый массив, заполняет его полным содержимым потока и возвращает ссылку на этот массив
    void Write(byte[ ] buffer, int offset, int count) Записывает в поток count байт, начиная с текущей позиции указателя потока, блок байтов из указанного buffer массива с данными, начиная с индекса offset этого массива
    void WriteByte(byte value) Пишет байт в текущую позицию потока и передвигает указатель
    void WriteTo(System.IO.Stream stream) Копирует все содержимое текущего потока памяти в указанный целевой поток, возможно и в файловый
    bool CanRead bool CanSeek bool CanWrite Свойства для чтения, подтверждающие поддерживаемые потоком возможности
    int Capacity Свойство получает или устанавливает число байт в потоке, которые он способен вместить, прежде чем потребуется изменить размер его внутреннего буфера
    long Length Число байт, которые поток фактически содержит
    long Position Свойство получает или устанавливает указатель на текущую позицию потока

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

  • Командой File/Add/New Project добавьте к решению новый проект с именем App7, заполнив окно мастера так
  • При открытом на редактирование файле Window1.xaml выполните команду Project/Set as StartUp Project, чтобы назначить новый проект стартовым
  • В панели Solution Explorer раскройте узел App7, двойным щелчком на папке Properties вызовите панель конструктора проектов и для вкладки Application установите выпадающий список Output type в значение Console Application, чтобы включить консольное окно
  • Вначале сделаем заготовку для выбора примеров.

  • Отредактируйте разметку файла Window1.xaml следующим образом
  • <Window x:Class="App7.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Упражнение 7. Поток MemoryStream" 
        Height="300" 
        Width="300"
        MinHeight="300"
        MinWidth="300"
        ResizeMode="CanResizeWithGrip"
        WindowStartupLocation="CenterScreen"
        Closed="Window_Closed"
        >
        <ListBox SelectionChanged="ListBox_SelectionChanged" 
                 ScrollViewer.VerticalScrollBarVisibility="Auto">
            <ListBox.Resources>
                <!--Selected color when the ListBox is selected-->
                <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Aqua" />
            </ListBox.Resources>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10397.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 1</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10398.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 2</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10402.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 3</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10423.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 4</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10418.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 5</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10440.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 6</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10434.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 7</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10436.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 8</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10438.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 9</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10424.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 10</Label>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <Image Source="Smiles/10408.jpg" Width="24" Height="24" Margin="2" />
                <Label VerticalContentAlignment="Center">Пример 11</Label>
            </StackPanel>
        </ListBox>
    </Window>
  • В панели Solution Explorer вызовите контекстное меню для узла App7 и командой Add/New Folder добавьте к корню проекта папку Smiles для хранения рисунков
  • В панели Solution Explorer вызовите контекстное меню для папки Smiles и командой Add/Existing Item скопируйте в нее все рисунки из папки Source/Smiles, прилагаемой к данной работе (предварительно установите фильтр диалогового окна в значение All Files )
  • Заполните файл процедурного кода Window1.xaml.cs заготовкой, чтобы в итоге он стал таким
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Navigation;
    using System.Windows.Shapes;
        
    // Дополнительные пространства имен
    using System.IO;
        
    namespace App7
    {
        public partial class Window1 : Window
        {
            // Объект для сохранения параметров приложения
            Properties.Settings settings = new App7.Properties.Settings();
        
            public Window1()
            {
                // Настройка консоли
                Console.ForegroundColor= ConsoleColor.White;
                Console.CursorVisible = false;
                Console.Title = "Упражнение 7. Поток MemoryStream";
        
                InitializeComponent();
            }
        
            private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                // Очистим консоль перед новым примером
                //Console.Clear();
        
                switch (((ListBox)sender).SelectedIndex + 1)
                {
                    case 1: // Пример 1
                        Console.WriteLine("Пример 1");
                        Example1();
                        break;
                    case 2: // Пример 2
                        Console.WriteLine("Пример 2");
                        Example2();
                        break;
                    case 3: // Пример 3
                        Console.WriteLine("Пример 3");
                        Example3();
                        break;
                    case 4: // Пример 4
                        Console.WriteLine("Пример 4");
                        Example4();
                        break;
                    case 5: // Пример 5
                        Console.WriteLine("Пример 5");
                        Example5();
                        break;
                    case 6: // Пример 6
                        Console.WriteLine("Пример 6");
                        Example6();
                        break;
                    case 7: // Пример 7
                        Console.WriteLine("Пример 7");
                        Example7();
                        break;
                    case 8: // Пример 8
                        Console.WriteLine("Пример 8");
                        Example8();
                        break;
                    case 9: // Пример 9
                        Console.WriteLine("Пример 9");
                        Example9();
                        break;
                    case 10: // Пример 10
                        Console.WriteLine("Пример 10");
                        Example10();
                        break;
                    case 11: // Пример 11
                        Console.WriteLine("Пример 11");
                        Example11();
                        break;
                }
            }
        
            void Example1()
            {
            }
        
            void Example2()
            {
            }
        
            void Example3()
            {
            }
        
            void Example4()
            {
            }
        
            void Example5()
            {
            }
        
            void Example6()
            {
            }
        
            void Example7()
            {
            }
        
            void Example8()
            {
            }
        
            void Example9()
            {
            }
        
            void Example10()
            {
            }
        
            void Example11()
            {
            }
        
            private void Window_Closed(object sender, EventArgs e)
            {
                // Сохраняем параметры приложения
            }
        }
    }
  • Запустите приложение и испытайте работу заготовок вариантов, должно получиться примерно следующее
  • Конечно, в данном упражнении столько примеров у нас не наберется, но эта заготовка нам еще пригодится для выполнения других упражнений. Теперь можно приступать непосредственно к рассмотрению примеров использования потока MemoryStream, заключая код каждого из них в свою функцию ExampleX().

    Пример 1. Из MSDN

    Приведем пример из MSDN (http://msdn.microsoft.com/ru-ru/library/system.io.memorystream%28v=VS.90%29.aspx), который иллюстрирует некоторые аспекты применения класса MemoryStream.

  • В файле Window1.xaml измените заголовок списка для ' Пример 1 ' на ' Пример 1. Код из MSDN '
  • Заполните функцию Example1() следующим кодом
  • void Example1()
            {
                int count;
                byte[] byteArray;// Объявление итогового массива байтов, считанного из потока 
                char[] charArray;// Объявление итогового массива символов, считанного из потока
                UnicodeEncoding unicodeEncoding = new UnicodeEncoding();// Двухбайтный UTF-16
        
                // Генерируем исходные данные
                byte[] bytes1 = // Байтовый массив с допустимыми символами
                    unicodeEncoding.GetBytes("Недопустимые символы в пути файла:\r\n");
                byte[] bytes2 = // Байтовый массив с недопустимыми в путях символами
                    unicodeEncoding.GetBytes(System.IO.Path.GetInvalidPathChars());
        
                // Создает и открываем поток для чтения/записи с внутренним 
                // байтовым буфером изменяемой длины и нулевой начальной емкостью
                using (MemoryStream memoryStream = new MemoryStream())
                {
                    // Пишем в поток допустимые символы блоком 
                    memoryStream.Write(bytes1, 0, bytes1.Length);
                    // Пишем в поток недопустимые символы побайтно
                    count = 0;
                    while (count < bytes2.Length)
                    {
                        memoryStream.WriteByte(bytes2[count++]);
                    }
                    // Сбрасываем буфер
                    memoryStream.Flush();   // Необязательно, данных немного 
        
                    // Печатаем свойства потока на консоль
                    Console.WriteLine("\nСвойства потока MemoryStream:");
                    Console.WriteLine(
                        "Capacity = {0}, Length = {1}, Position = {2}\n",
                        memoryStream.Capacity.ToString(),  // Длина внутреннего буфера
                        memoryStream.Length.ToString(),    // Количество байт содержимого
                        memoryStream.Position.ToString()   // Текущая позиция указателя
                        );
        
                    // Печатаем исходные данные
                    Console.WriteLine("Исходные данные перед записью в MemoryStream:");
                    Console.Write(unicodeEncoding.GetString(bytes1));   // Здесь Write()
                    charArray = new char[unicodeEncoding.GetCharCount(
                        bytes2, 0, bytes2.Length)];
                    unicodeEncoding.GetDecoder().GetChars(
                        bytes2, 0, bytes2.Length, charArray, 0);
                    Console.WriteLine(charArray);
        
                    // Читаем содержимое потока
                    byteArray = new byte[memoryStream.Length]; // Создаем массив под все символы
                    // Устанавливаем указатель в начало потока
                    memoryStream.Seek(0, SeekOrigin.Begin);
                    // Читаем блоком первые 20 байт
                    count = memoryStream.Read(byteArray, 0, 20);
                    // Читаем остальное побайтно, наращивая счетчик 
                    while (count < memoryStream.Length)
                    {
                        byteArray[count++] =
                            Convert.ToByte(memoryStream.ReadByte());
                    }
        
                    // Преобразуем батовый массив в символьный и печатаем
                    Console.WriteLine("\nДанные, прочитанные из MemoryStream:");
                    charArray = new char[unicodeEncoding.GetCharCount(
                        byteArray, 0, count)];
                    unicodeEncoding.GetDecoder().GetChars(
                        byteArray, 0, count, charArray, 0);
                    Console.WriteLine(charArray);
                }
            }

    Приведенный код совсем несложный, подробно прокомментирован и в дальнейших пояснениях вряд ли нуждается.

  • Исполните Пример 1 - должен получиться такой результат
  • Обратите внимание на звуковой сигнал, который издает компьютер при выводе на консоль недопустимых управляющих символов. Когда для потока памяти не определен внешний неизменяемый байтовый массив, а работает внутренний буфер, то при необходимости его длина увеличивается дискретными блоками по 128 байт, удобными для тактовой обработки в регистрах процессора целиком. Емкость потока, которая автоматически увеличивается должным образом посредством перераспределения, может быть уменьшена с помощью явного задания свойства Capacity.

    Пример 2. Создание файловой библиотеки рисунков

    Сюжет предыдущего примера состоял в том, что приложение генерировало какую-то информацию, которую мы записывали в некоторый буфер потока MemoryStream, а затем извлекали ее для проверки, выводя на консоль. Более полезным местом применения MemoryStream будет избирательное извлечение информации из хранилища данных (файл или база данных), обработка ее и, либо представление пользователю, либо отправка на дальнейшее хранение.

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

    Поскольку объем кода задуманного примера предполагается большим, то вынесем его в отдельный файл частичного класса Window1.

  • В панели Solution Explorer вызовите контекстное меню для узла App7 и командой Add/Class создайте новый файл с именем Example2.cs, заготовку которого заполните так
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен
    using System.IO;
        
    namespace App7
    {
        partial class Window1
        {
            void Example2()
            {
                Console.WriteLine("Вынесенный в отдельный файл код функции Example2()");
            }
        }
    }
  • Удалите из файла Window1.xaml.cs каркас функции Example2(), поскольку он теперь в другом месте, и запустите приложение - заготовка Примера 2 работает нормально
  • В этой функции мы создадим код, упаковывающий рисунки в файловую библиотеку, поэтому

  • В файле Window1.xaml измените заголовок списка для ' Пример 2 ' на ' Пример 2. Создание файловой библиотеки '
  • Прежде всего добавим диалог смены каталога для перехода в папку с рисунками. Для этого воспользуемся библиотечным классом FolderBrowserDialog библиотеки System.Windows.Forms.dll. Поскольку мастер при создании заготовки проекта WPF не подключает ссылку на эту библиотеку автоматически, выполним это вручную.

  • В панели Solution Explorer вызовите контекстное меню для папки App7/References и командой Add Reference подключите к проекту библиотеку System.Windows.Forms.dll
  • Формат библиотечного файла выберем таким, как показано на рисунке

    Он определит и код создания библиотеки, который выполнит задачи в следующей последовательности:

  • В выбранном каталоге извлечем полные имена файлов рисунков и измерим их количество
  • Обработаем массив с именами файлов, удалив из них пути и расширения
  • Отсортируем массив имен файлов для быстрого их поиска в будущей библиотеке
  • Откроем файловый поток, запишем в него число рисунков ( block1 ) и отсортированные имена файлов рисунков ( block2 )
  • По отсортированным именам будем считывать рисунки и добавлять их в файловый поток ( block3 ). Одновременно в памяти будем формировать поток с информацией о локализации рисунков в файловом потоке ( block4 )
  • После завершения записи всех рисунков допишем в поток памяти позицию ( block5 ), с которой в библиотеке начнет располагаться block4, и сбросим этот поток памяти в файловый поток библиотеки
  • Размещение блока с рисунками ( block3 ) перед информацией о их локализации в библиотеке ( block4 ) обусловлена тем, что рисунки могут быть большими и их удобнее писать сразу в файл. А информацию о их месте расположения в файле постепенно сформировать в потоке памяти и добавить последней. Изложенная схема позволит в будущем быстро определить место нахождения нужного рисунка и извлечь его из библиотеки. Разумеется, подобных схем можно придумать множество, главное, чтобы они взаимно согласовывались при записи и чтении библиотечных данных.

  • В соответствии с вышесказанным модифицируйте файл Example2.cs следующим кодом (код файла приводится полностью, является окончательным и обжалованию не подлежит!)
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен
    using System.IO;
        
    namespace App7
    {
        using FORMS = System.Windows.Forms; // Псевдоним
        
        partial class Window1
        {
            FORMS.FolderBrowserDialog dialogFolder = new FORMS.FolderBrowserDialog();
            String nameLibrary = "Pictures.my.lib"; // Будет находиться вместе с рисунками
            String extensionPictures = "jpg";       // Расширения файлов рисунков
        
            void Example2()
            {
                // Диалог смены каталога
                String currentDirectory;    // Полный путь
                dialogFolder.Description = "Выберите каталог, где находятся файлы с рисунками";
                dialogFolder.ShowNewFolderButton = false;// Отключить кнопку создания новой папки
                dialogFolder.RootFolder = Environment.SpecialFolder.MyComputer;// Компьютер
                FORMS.DialogResult result = dialogFolder.ShowDialog();  // Запускаем диалог выбора папки
                // Если был отказ или диалог закрыт системной кнопкой
                if (result == FORMS.DialogResult.Cancel)    
                    return;
                currentDirectory = dialogFolder.SelectedPath;// Извлекаем новый каталог
        
                // Принудительная перерисовка окна WPF
                this.Height = this.ActualHeight + 1;
                this.Height = this.ActualHeight - 1;
        
                // Читаем полные имена всех файлов текущего каталога по заданному шаблону
                String[] files = Directory.GetFiles(currentDirectory, "*." + extensionPictures);
                if (files.Length == 0)
                {
                    FORMS.MessageBox.Show(
                        "Нет в выбранном каталоге файлов для упаковки",
                        "Предупреждение",
                        FORMS.MessageBoxButtons.OK,
                        FORMS.MessageBoxIcon.Information);
                    return;
                }
        
                // Получаем сокращенные имена файлов без пути и расширения
                for (int i = 0; i < files.Length; i++)
                {
                    String str = files[i].Substring(files[i].LastIndexOf('\\') + 1);
                    int len = str.LastIndexOf('.');
                    files[i] = str.Substring(0, len);
                }
                // Сортируем массив для красоты и порядка
                Array.Sort<String>(files);
                // Array.Sort<String>(files, new ComparerNames());//Альтернативный компаратор!!!!!!    
                // Создаем поток MemoryStream переменной длины для имен файлов
                MemoryStream memoryStream = new MemoryStream();
                if (!memoryStream.CanWrite) // Проверять необязательно!
                {
                    FORMS.MessageBox.Show("Не могу писать в MemoryStream");
                    return;
                }
                // Создаем писатель-оболочку для записи в MemoryStream элементарных типов
                BinaryWriter binaryWriter = new BinaryWriter(memoryStream);
                memoryStream.Seek(0, SeekOrigin.Begin); // Необязательно!
                // Первым записываем block1 для количества файлов
                binaryWriter.Write((Int32)files.Length);// Приводить тип необязательно, распознает сама 
                //!!!=4 Console.WriteLine(binaryWriter.BaseStream.Position);
                // Закачиваем все имена файлов (block2) из отсортированного массива
                for (int i = 0; i < files.Length; i++)
                    binaryWriter.Write((String)files[i]);// Приводить тип необязательно, распознает сама
                memoryStream.Flush();   // Сливаем буфер
        
                // Создаем файловый поток и сбрасываем в него накопленное
                FileStream fs = null;
                try
                {
                    // Создаем или усекаем файл в том же каталоге
                    fs = new FileStream(
                        Path.Combine(currentDirectory, nameLibrary), FileMode.Create);
                    memoryStream.WriteTo(fs);// Сбрасываем все из памяти в файл
        
                    fs.Flush(); // Слить кэш (необязательно!)
                }
                catch
                {
                    FORMS.MessageBox.Show("Не могу записать в файл " + nameLibrary);
                    fs.Close();
                    return;
                }
                // Закрываем поток-оболочку, которая закроет и базовый поток
                binaryWriter.Close();
        
                // Создаем поток памяти для записи block4 и block5
                MemoryStream dimension = new MemoryStream(
                    files.Length * (sizeof(long) + sizeof(int)) 
                    + sizeof(long));
                dimension.Seek(0, SeekOrigin.Begin);    // Необязательно!
                // Создаем поток-оболочку для записи пар "позиция-длина" в MemoryStream
                BinaryWriter dimensionWriter = new BinaryWriter(dimension);
        
                // Читаем отдельные рисунки и пишем сразу в библиотеку
                MemoryStream pictureStream;
                for (int i = 0; i < files.Length; i++)
                {
                    // Читаем файл с рисунком
                    byte[] bytes = File.ReadAllBytes(Path.Combine(
                        currentDirectory, files[i] + "." + extensionPictures));
        
                    // Формируем в памяти block4 для локализации рисунков
                    dimensionWriter.Write(fs.Position); // Позиция рисунка long в потоке
                    dimensionWriter.Write(bytes.Length);// Длина рисунка int
        
                    // Добавляем очередной рисунок в библиотеку (block3)
                    pictureStream = new MemoryStream(bytes);// Создаем на базе массива
                    pictureStream.WriteTo(fs);   // Добавляем рисунок в библиотечный файл
                    pictureStream.Close();
                }
                fs.Flush(); // Сбрасываем буфер
        
                dimensionWriter.Write(fs.Position); // Дописываем в память block5
                dimensionWriter.Flush();
        
                // Дописываем в библиотеку последними block4 и block5
                dimension.WriteTo(fs);
                dimensionWriter.Close();    // Надстройка закрывает и базовый поток
        
                fs.Close(); // При закрытии автоматически выполнится и Flush()
        
                // Оповещаем пользователя
                FORMS.MessageBox.Show("Библиотека " + nameLibrary +
                    " создана в каталоге:\n" + currentDirectory);
            }
        }
        
        // Альтернативный компаратор!!!!!!! 
        /**************************************************
        class ComparerNames : IComparer<String>
        {
            public int Compare(String x, String y)
            {
                int X, Y, XX, YY;
                int posX = x.IndexOf('_');
                string str1 = posX != -1 ? x.Substring(0, posX) : "";
                bool isNumber1 = int.TryParse(str1, out X);
        
                int posY = y.IndexOf('_');
                string str2 = posY != -1 ? y.Substring(0, posY) : "";
                bool isNumber2 = int.TryParse(str2, out Y);
        
                if (!isNumber1 || !isNumber2)
                    return x.CompareTo(y);
        
                if (X < Y)
                    return -1;
                else if (X == Y)
                {
                    str1 = x.LastIndexOf('d') != -1 ?
                        x.Substring(0, x.Length - 1) : x;
                    str1 = str1.Substring(posX + 1);
                    isNumber1 = int.TryParse(str1, out XX);
        
                    str2 = y.LastIndexOf('d') != -1 ?
                        y.Substring(0, y.Length - 1) : y;
                    str2 = str2.Substring(posY + 1);
                    isNumber2 = int.TryParse(str2, out YY);
        
                    if (!isNumber1 || !isNumber2)
                        return str1.CompareTo(str2);
        
                    if (XX < YY)
                        return -1;
                    else if (XX == YY)
                        if (x.LastIndexOf('d') != -1  y.LastIndexOf('d') != -1)
                            return 0;
                        else if (x.LastIndexOf('d') != -1)
                            return 1;
                        else
                            return -1;
                    else
                        return 1;
                }
                else
                    return 1;
            }
        }
        //************************************************/
    }

    Для сохранения параметров диалогового окна FolderBrowserDialog выполним следующее:

  • В файле Window1.xaml.cs добавьте код, выделенный в листинге
  • public partial class Window1 : Window
        {
            // Объект для сохранения параметров приложения
            Properties.Settings settings = new App7.Properties.Settings();
        
            public Window1()
            {
                // Настройка консоли
                Console.ForegroundColor= ConsoleColor.White;
                Console.CursorVisible = false;
                Console.Title = "Упражнение 7. Поток MemoryStream";
        
                InitializeComponent();
        
                // Читаем параметры, сохраненные ранее
                dialogFolder.SelectedPath = settings.dialogFolder;
            }
        
            .......................................................
        
            private void Window_Closed(object sender, EventArgs e)
            {
                // Сохраняем параметры приложения
                settings.dialogFolder = dialogFolder.SelectedPath;
                settings.Save();
            }
        }
  • В панели Solution Explorer для проекта App7 раскройте узел Properties и двойным щелчком на файле Settings.settings откройте окно установки параметров , которое заполните как показано на рисунке
  • Обратите внимание, что оболочка добавила к проекту файл конфигурации app.config, без которого механизм сохранения параметров работать не будет.

  • Запустите приложение, выберите в прилагаемом к работе каталоге Source подкаталог Images с рисунками *.jpg и сверните их в библиотеку Pictures.my.lib, которая разместится в том же подкаталоге
  • Разберитесь с приведенным кодом файла Example2.cs, который содержит подробные комментарии
  • Пример 3. Использование файловой библиотеки рисунков

    Чтобы воспользоваться созданной библиотекой, а заодно и проверить правильность работы того, что мы так горячо и бодро обсуждали, построим код, позволяющий пользователю извлекать упакованные рисунки и просматривать их. Здесь также без потоков не обойтись. Консольное окно нам здесь не поможет, окно со списком выбора примеров - тоже, придется создать новое окно для управления рисунками. Код вызова нового окна поместим в функцию Example3, а само окно в отдельном файле. Окно просмотра рисунков построим по технологии WPF - там ' красивше ' смотрится растровая графика.

  • В панели Solution Explorer выделите узел проекта App7 и командой меню оболочки Project/Add Window добавьте новое окно WPF с именем Window3.xaml
  • Немного обленился описывать, поэтому выдам готовое, по коду (по ходу) легко разберетесь...

  • Заполните в файле Window1.xaml.cs функцию Example3() следующим кодом (приводится полностью)
  • void Example3()
            {
                // Предотвращение повторного создания дочернего окна
                bool existsWindow = false;
                foreach (Window window in Application.Current.Windows)// Перебираем коллекцию
                {
                    if (window.Name == "_Window3")
                    {
                        // Нашли среди существующих 
                        existsWindow = true;
                        window.Activate();// Сдвинуть на передний план
                        break;// Прервать перебор
                    }
                }
        
                if (!existsWindow)// Если не существует, то создать и показать
                {
                    Window3 wnd = new Window3();
                    // Перед показом проверяем, выбрана ли библиотека
                    if (wnd.NameLibrary != String.Empty)
                        wnd.Show();    // Показываем окно Window3
                    else
                        wnd.Close();   // Закрываем окно Window3
                }
            }
  • В файле Window1.xaml измените заголовок списка для ' Пример 3 ' на ' Пример 3. Использование файловой библиотеки '
  • Заполните файл Window3.xaml следующей интерфейсной разметкой (приводится полностью)
  • <Window x:Class="App7.Window3"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Использование файловой библиотеки рисунков" 
        Height="300"
        Width="470"
        MinHeight="300"
        MinWidth="450" 
        Name="_Window3" 
        Closing="Window_Closing"
        >
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="100" />
            </Grid.ColumnDefinitions>
            <Border Background="Black">
                <Image Name="image" Stretch="Uniform" />
            </Border>
            <ListBox Name="listBox" Grid.Column="1" Padding="5,0"
                     ScrollViewer.VerticalScrollBarVisibility="Visible"
                     SelectionChanged="listBox_SelectionChanged">
            </ListBox>
        </Grid>
    </Window>
  • Заполните файл Window3.xaml.cs следующим процедурным кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
        
    // Дополнительные пространства имен
    using FORMS = System.Windows.Forms; // Псевдоним
    using System.IO;
    using System.Configuration;         // Для SettingsProperty
    using IO = System.IO;               // Псевдоним для адресации Path
        
    namespace App7
    {
        public partial class Window3 : Window
        {
            FileStream fs = null;   // Поле файлового потока для видимости в классе
            BinaryReader reader;    // Поле читателя для видимости в классе
            String nameLibrary;     // Опорное поле для свойства с именем библиотеки
            int count;              // Поле под число рисунков
            string[] names;         // Поле под имена рисунков
            long offsetHead;        // Поле начала блока локализации рисунков
            Properties.Settings settings; // Параметры
        
            // Конструктор
            public Window3()
            {
                InitializeComponent();
        
                // Запускаем диалог выбора библиотеки
                nameLibrary = SelectLibrary();
                // При отказе от выбора ничего не запускаем,
                // а в главном окне Window1 закрываем это окно 
                if (nameLibrary != String.Empty)
                    ExecuteWindow();
            }
        
            // Извлекаем рисунки и управляем показом
            private void ExecuteWindow()
            {
                // Создаем файловый поток библиотеки
                fs = new FileStream(
                    nameLibrary, FileMode.Open, FileAccess.Read, FileShare.Read);
        
                // Создаем читателя элементарных типов
                reader = new BinaryReader(fs);
                count = reader.ReadInt32();             // Возвращает int
                names = new String[count];
                for (int i = 0; i < count; i++)
                    names[i] = reader.ReadString();
                fs.Seek(-sizeof(long), SeekOrigin.End); // Отступаем от конца
                offsetHead = reader.ReadInt64();        // Возвращает long
        
                // Привязываем массив имен рисунков к списку
                listBox.ItemsSource = names;
                // Выделяем первый элемент списка, чтобы вызвать SelectionChanged
                listBox.SelectedIndex = 0;
                listBox.Focus();
            }
        
            // При смене индекса списка показывает выбранный рисунок
            private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                /*
                // Вариант 1. Извлечение рисунка по индексу
                int index = ((ListBox)sender).SelectedIndex;
                ReadOnIndex(index);
                */
        
                // Вариант 2. Извлечение рисунка по имени
                string name = ((ListBox)sender).SelectedItem.ToString();
                ReadOnName(name);
            }
        
            // Вариант 1. Извлечение рисунка по индексу
            private void ReadOnIndex(int index)
            {
                // Локализуем рисунок по индексу
                if (index < 0 || index >= count) return;// Затычка
                fs.Seek(offsetHead + index * (sizeof(long) + sizeof(int)), SeekOrigin.Begin);
                long offset = reader.ReadInt64();   // Позиция рисунка в потоке (long)
                int length = reader.ReadInt32();    // Длина рисунка (int)
        
                // Извлекаем рисунок из потока
                byte[] bytes = new byte[length];
                //fs.Read(bytes, (int)offset, length);
                fs.Seek(offset, SeekOrigin.Begin);
                for (int i = 0; i < length; i++)
                    bytes[i] = (byte)fs.ReadByte();
        
                // Отображаем рисунок пользователю
                BitmapImage bi = new BitmapImage();
                bi.BeginInit();
                bi.StreamSource = new MemoryStream(bytes);
                bi.EndInit();
                image.Source = bi;
            }
        
            // Вариант 2. Извлечение рисунка по имени
            private void ReadOnName(string name)
            {
                // По имени рисунка ищем его индекс в массиве names[]
                int index = -1;
                // Производится обход всего массива Array ПОДРЯД, 
                // начиная с первого элемента и заканчивая последним.
                // Одномерный поиск половинного деления, реализуемый
                // методом Array.BinarySearch() применять нельзя!!!
                String find = Array.Find<String>(names,
                    delegate(String item)// Анонимный делегат
                    {
                        index++;
                        // При первом совпадении заканчивает поиск
                        return String.Equals(item, name);
                    });
                // В случае неудачи возвращает значение по умолчанию для типа шаблона
                if (find == String.Empty)   // Необязательно!
                {
                    return;// Запрошено несуществующее имя
                }
        
                // А дальше в точности как в ReadOnIndex(), но вместо 
                // прямого вызова повторим для самостоятельности кода
                //ReadOnIndex(index);
                // Локализуем рисунок по индексу
                if (index < 0 || index >= count) return;// Затычка
                fs.Seek(offsetHead + index * (sizeof(long) + sizeof(int)), SeekOrigin.Begin);
                long offset = reader.ReadInt64();   // Позиция рисунка в потоке (long)
                int length = reader.ReadInt32();    // Длина рисунка (int)
        
                // Извлекаем рисунок из потока
                byte[] bytes = new byte[length];
                //fs.Read(bytes, (int)offset, length);
                fs.Seek(offset, SeekOrigin.Begin);
                for (int i = 0; i < length; i++)
                    bytes[i] = (byte)fs.ReadByte();
        
                // Отображаем рисунок пользователю
                BitmapImage bi = new BitmapImage();
                bi.BeginInit();
                bi.StreamSource = new MemoryStream(bytes);
                bi.EndInit();
                image.Source = bi;
            }
        
            // Свойство доступа, проверяется в Window1
            public String NameLibrary   
            {
                get { return nameLibrary; }
            }
        
            // Диалог выбора библиотеки. Возвращает
            // пусто, если библиотека не выбрана
            private String SelectLibrary()
            {
                settings = new App7.Properties.Settings();
                String path = settings.dialogOpen;
                String fileName = IO.Path.Combine(path, "Pictures.my.lib");
                  
                // Создаем и настраиваем диалог выбора файла
                FORMS.OpenFileDialog openFileDialog = new FORMS.OpenFileDialog();
                openFileDialog.InitialDirectory = path;
                openFileDialog.FileName = "";
                openFileDialog.Filter = "Library Files(*.my.lib)|*.my.lib";
                FORMS.DialogResult result = openFileDialog.ShowDialog();
                if (result != FORMS.DialogResult.Cancel)
                    fileName = openFileDialog.FileName; // Полное имя
                else
                    fileName = String.Empty;
        
                return fileName;
            }
        
            // Обработчик события Closing, срабатывает перед закрытием приложения
            private void Window_Closing(object sender, 
                System.ComponentModel.CancelEventArgs e)
            {
                // Закрываем библиотечный поток
                if (fs != null)
                    fs.Close(); 
        
                // Сохраняем параметры 
                if (nameLibrary != String.Empty)
                {
                    settings.dialogOpen = 
                        nameLibrary.Substring(0, nameLibrary.LastIndexOf('\\'));
                    settings.Save();
                }
            }
        }
    }
  • В панели Solution Explorer для проекта App7 раскройте узел Properties и двойным щелчком на файле Settings.settings откройте окно установки параметров, которое дополните еще одним параметром, как показано на рисунке
  • Запустите приложение - выберите ранее созданную библиотеку Pictures.my.lib - результат соответствует ожидаемому
  • Разберитесь с кодом примера, который содержит подробные комментарии
  • Пример 4. Создание файловой библиотеки рисунков с сохранением метаданных в OLE DB

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

    В этом примере поставим задачу 'отделить зерна от плевел' - рисунки хранить по прежнему в библиотечном файле, а метаданные разместить в базе данных типа OLE DB (тоже в файле, только 'по турецки'). Это плохая идея и мы ее позже исправим, но пока для тренировки это будет очень даже ничего. Известно, что БД представляет собой набор структурированных данных, поддерживаемый специальным API ( Application Programming Interface ), поэтому записывать и извлекать нужные сведения об основных данных нам будет гораздо легче. А насчет 'быстрее' - вопрос спорный, если не использовать пул ( pool ) соединений.

    Правда, классики могут взвизгнуть - опять 'обрывки каких-то материалов', не относящихся к теме потоков. Мол, "Хотели кока, а съели Кука!" (Высоцкий В.С.). Успокойтесь, "течет речечка по песочечку - золотишко моет!": тема остается, и ее более широкое название - программирование, а не Кама-Сутра или философия! Чем более рельефным будет изложение, тем больше извилин останется в голове, да и руки быстрее выпрямятся. При гладком повествовании со временем можно легко забыть не только начало, но и Мать родную (и ведь забывают...), в крутых поворотах жизни и загогулинах - все вспомнишь!

    Я не призываю слепо следовать за мной, а рекомендую выполнить и изучить: "делай как я, а потом и свое подоспеет". Непонятные места готового кода всегда можно выяснить в документации и на форумах. Главное - осмысливать, искать ( студент - ищущий знаний) и не падать духом - все придет с тренировкой.

    В решении поставленной задачи нужно выполнить 'совсем немного':

  • Создать саму базу данных
  • Упаковать рисунки в файл, а метаданные сохранить в БД
  • Запрашивать данные из соответствующих источников и показывать их на экране
  • Начнем с создания реляционной БД программным способом. Выберем наиболее простую - файловую БД типа Access, позволяющую использовать интерфейсы OLE D B ( Object Linking and Embedding, Databas e ). Нам необходимо определить ее структуру, включающую в себя хотя бы одну таблицу (одна и нужна). База данных будет иметь имя Pictures.my1.md b и содержать одну таблицу с именем MyTable (вначале назвал Local, а оказалось, что это служебное имя в API, - ох и помучился с отладкой!). Структура таблицы в типах OLE D B и объектной модели C # будет такой

    Структура таблицы MyTable
    Имя поля Тип OLE DB Тип объектной модели C# Размер
    Name Type Type DefinedSize
    FileName (уникальное) adWChar string 20 (определим сами, с запасом)
    Offset adInteger int 4 (предопределено)
    Length adInteger int 4 (предопределено)

    Целый тип adInteger имеет длину 4 байта и может адресовать 4Б = 256*256*256*127 = 2'130'706'432, т.е. примерно 2 гигабайта памяти, что для библиотеки разумной длины вполне достаточно. Интересно отметить, что в Office Access он называется длинным целым. Эта терминология соответствует 'старым' временам (когда деревья были большими...), когда long имел длину 4 байта, а int - всего 2 байта.

    У внимательного читателя при знакомстве с приведенной таблицей может возникнуть справедливый вопрос. Как так, переменная Offset в методе сдвига Seek() должна быть знаковой, соответствовать типу long C# и иметь длину 8 байт, и почему это вдруг нам подсовывают тип adInteger? Действительно, в API программного создания структуры таблиц БД, который мы в дальнейшем будем использовать, существует требуемый числовой тип и именуется он adBigInt. Но компилятор с ним почему-то упорно не хочет работать.

    Можно было бы пойти обходным путем: поле Offset объявить строкой длиной 8 символов (или более) и выполнять двухстороннюю конвертацию типов String/Int64 при каждой операции чтения/записи между полем таблицы и объектной моделью. Можно придумать и другие ухищрения. Но для учебного примера пусть все останется так как есть - мы не собираемся создавать файловые библиотеки тяжелея 2 Гб. Да и вообще, то что мы здесь делаем вместе, - это только иллюстрация возможностей и тренировка. Все остальное вы сделаете сами в реальной взрослой работе на производстве, но уже будучи вооруженными (и очень опасными!).

  • В файле Window1.xaml измените заголовок списка для элемента ' Пример 4 ' на ' Пример 4. Создание БД + файловой библиотеки '
  • База данных типа Access не требует установки на клиенте SQL -сервера. Создавать БД и таблицу в ней будем программно с помощью COM -объектов библиотек ADOX.dll и ADODB.dll. Управление данными в БД выполним с помощью классов ADO.NET.

  • В Solution Explorer выделите узел проекта App7 и добавьте к нему командой меню оболочки Project/Add Reference из вкладки COM ссылки на библиотеки ADOX.dll и ADODB.dll последних версий, как показано на снимке
  • Оболочка добавит в каталог ссылок проекта App7 две библиотеки

    Обратите внимание, что библиотека System.Data.dll, в которой находятся классы ADO.NET, уже подключена изначально мастером создания проектов. Почти в любом типе проекта предусмотрено автоматическое подключение System.Data.dll, что свидетельствует о популярности задач с использованием баз данных.

  • В панели Object Browser установите выпадающий список Browse в значение текущего решения и убедитесь, что классы COM -библиотек ADOX.dll и ADODB.dll упакованы в пространствах имен ADODB и ADOX. Эти префиксы мы и будем использовать в API перед именами классов при создании БД
  • В панели Solution Explorer добавьте к узлу App7 командой контекстного меню Add/Class новый файл с именем CreateLibraryAndDB.cs
  • Конечно же, к этому времени класс мною уже написан и отлажен, и я мог бы 'вывалить' его здесь сразу целиком. Но опять же, классики завизжат про непедагогичность, про то, что даже при обильном словоблудии рядом с 'непьющим' студентом должен стоять родитель с ремнем (или с ружьем). Поэтому выльем, но постепенно. Все равно 'пьющие' будут разбираться именно в 'живом' коде, а там комментариев хватает (как и вариантов).

    А, пусть визжат, вываливаю целиком, надоело описывать!

  • Заполните файл CreateLibraryAndDB.cs следующим кодом (приводится полностью)
  • using System;
    using System.Windows.Forms;
    using System.IO;
        
    // Пространства имен для ADO.NET
    using System.Data;
    using System.Data.OleDb;
    using System.Data.Common;
        
    namespace App7
    {
        class CreateLibraryAndDB
        {
            // Локальные поля
            String nameDB = "Pictures.my1.mdb",
                nameLibrary = "Pictures.my1.lib";
            string path, fileName,
                tableName = "MyTable";// Нельзя давать имя "Local"!!!
            String extensionPictures = "jpg";
        
            // Параметризованный конструктор
            public CreateLibraryAndDB(string path)
            {
                // Инициализируем поля
                this.path = path;
                fileName = Path.Combine(path, nameDB);
        
                // Удалим предыдущую базу данных, если она есть
                if (new FileInfo(fileName).Exists)
                    File.Delete(fileName);
            }
        
            // Конструктор по умолчанию. Необязательно!
            public CreateLibraryAndDB() { }
        
            // Строка соединения с БД
            private String ConnectionString(String fileName)
            {
                //string pathToFile = Application.StartupPath.ToString();
                string JetEngineString = "Provider=Microsoft.Jet.OLEDB.4.0;" +
                    "Jet OLEDB:Engine Type=5;Data Source=";
                return JetEngineString + fileName;
            }
        
            // Создание БД
            public bool CreateDatabase()
            {
                Object adoConnection = null;
                ADOX.CatalogClass catalog = new ADOX.CatalogClass();
        
                try
                {
                    catalog.Create(ConnectionString(fileName));
                    adoConnection = catalog.ActiveConnection;
                }
                catch (Exception ex)
                {
                    MessageBox.Show(String.Format("Ошибка в создании БД\n{0}", ex.Message),
                        "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return false;
                }
                finally
                {
                    if (catalog != null  adoConnection != null)
                    {
                        ((ADODB.Connection)adoConnection).Close();// Приводим к интерфейсу
                        /***********  Вариант  *********
                        adoConnection.GetType().InvokeMember(
                            "Close", System.Reflection.BindingFlags.InvokeMethod,
                            null, adoConnection, new object[0]);
                        //*****************************/
        
                        catalog = null;
                        adoConnection = null;
                    }
                }
        
                return true;// Успех, если дошли до этого места
            }
        
            // Создание таблицы
            public bool CreateTable()
            {
                ADODB.Connection conn = new ADODB.ConnectionClass();
                conn.Open(ConnectionString(fileName), null, null, 0);
                ADOX.Catalog catalog = new ADOX.CatalogClass();
                catalog.ActiveConnection = conn;
        
                try
                {
                    ADOX.TableClass table = new ADOX.TableClass();// Объект таблицы
                    table.ParentCatalog = catalog;  // Указали родителя для таблицы
                    table.Name = tableName;         // Имя таблицы
                    catalog.Tables.Append(table);   // Добавили в коллекцию таблиц
        
                    // Задаем параметры настройки таблицы
                    string[] columnNames =
                    {
                        "FileName",
                        "Offset",
                        "Length"
                    };
                    // Для adInteger размер предопределен, можно заменить нулями
                    int[] columnSizes = new int[] { 20, 0, 0 };
                    ADOX.DataTypeEnum[] columnTypes = new ADOX.DataTypeEnum[] 
                        {
                            ADOX.DataTypeEnum.adWChar,      // До 255 символов fixed
                            ADOX.DataTypeEnum.adInteger,    // adBigInt = 8Б знаковое, но не идет
                            ADOX.DataTypeEnum.adInteger     // 4Б = 256*256*256*127 = 2'130'706'432
                        };
        
                    // Настраиваем структуру таблицы 
                    ADOX.ColumnClass column;
                    for (int i = 0; i < columnSizes.Length; i++)
                    {
                        column = new ADOX.ColumnClass();
                        column.ParentCatalog = catalog;  // Указали родителя для столбца
                        column.Name = columnNames[i];
                        column.DefinedSize = columnSizes[i];
                        column.Type = columnTypes[i];
                        column.Attributes = ADOX.ColumnAttributesEnum.adColNullable;// Позволить пусто
                        column.Attributes = ADOX.ColumnAttributesEnum.adColFixed;// Фиксированная длина
                        table.Columns.Append(column, column.Type, column.DefinedSize);
                    }
        
                    // Делаем первый столбец уникальным (совпадения не допускаются)
                    // http://www.sqldev.org/sql-server-data-access/
                    // multi-column-primary-keys-in-access-using-adox-c-60766.shtml
                    ADOX.Table tbl = catalog.Tables[tableName];
                    tbl.Keys.Append(
                        "UniqueKeyLocal", ADOX.KeyTypeEnum.adKeyUnique, "FileName", null, null);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message.ToString());
                    return false;
                }
                finally
                {
                    conn.Close();// Приводим к интерфейсу
                }
        
                return true;// Успех, если дошли до этого места
            }
        
            // Создаем файловую библиотеку и наполняем БД данными
            public bool CreateData()
            {
                OleDbConnection conn = new OleDbConnection(ConnectionString(fileName));
                OleDbDataAdapter adapter = new OleDbDataAdapter(
                    new OleDbCommand("SELECT * FROM " + tableName, conn));
                // Загружаем данные БД в объектную модель таблицы
                DataTable table = new DataTable(tableName);
                // Заполняем объектную таблицу данными из БД (хоть и нет пока данных), 
                // но лучше ограничиться чтением схемы, если собираемся только добавлять
                adapter.Fill(table);
                /***********  Вариант  *********
                adapter.FillSchema(table, SchemaType.Source);
                //*****************************/
        
                // Читаем информацию о всех файлах выбранного каталога
                FileInfo[] filesInfo = new DirectoryInfo(path).
                    GetFiles("*." + extensionPictures);
                if (filesInfo.Length == 0)
                {
                    MessageBox.Show(
                        "Нет в выбранном каталоге файлов для упаковки",
                        "Предупреждение",
                        MessageBoxButtons.OK,
                        MessageBoxIcon.Information);
                    return false;
                }
        
                // Рисунки пишем в библиотеку, а их параметры - в объектную таблицу 
                FileStream fs = new FileStream(
                    Path.Combine(path, nameLibrary), FileMode.Create);
                using (fs)
                {
                    for (int i = 0; i < filesInfo.Length; i++)
                    {
                        // Получаем сокращенное имя файла без пути и расширения
                        String shortName = filesInfo[i].Name; // Уже без пути и есть!!!
                        int len = shortName.LastIndexOf('.');
                        shortName = shortName.Substring(0, len);// Убрали расширение
        
                        // Добавляем в объектную таблицу очередную строку
                        DataRow row = table.NewRow();// Новая строка таблицы
                        row["FileName"] = shortName;
                        row["Offset"] = (int)fs.Position;
                        row["Length"] = (int)filesInfo[i].Length;
                        table.Rows.Add(row);
                        //table.AcceptChanges();// Нельзя, изменения будут считаться принятыми!!!
        
                        // Читаем файл с рисунком и добавляем в библиотеку
                        FileStream fileStream = filesInfo[i].OpenRead();
                        int readByte;
                        while ((readByte = fileStream.ReadByte()) != -1)
                            fs.WriteByte((byte)readByte);
                        /***********  Вариант  *********
                        byte[] bytes = File.ReadAllBytes(
                            Path.Combine(filesInfo[i].DirectoryName, filesInfo[i].Name));
                        MemoryStream pictureStream = new MemoryStream(bytes);
                        pictureStream.WriteTo(fs);
                        pictureStream.Close();
                        //*****************************/
                    }
        
                    fs.Flush(); // Необязательно! Сбросится при закрытии потока 
                }
        
                // Записываем объектную таблицу в таблицу OLE DB
                try
                {
                    // Создаем построитель команд по настройке свойства SelectCommand
                    // адаптера. Адаптер сам сгенерирует нужные команды при обновлении
                    OleDbCommandBuilder commandBuilder =
                        new OleDbCommandBuilder(adapter);
                    /***********  Вариант  *********
                    OleDbCommandBuilder commandBuilder = 
                        new OleDbCommandBuilder();
                    adapter.InsertCommand = commandBuilder.GetInsertCommand();
                    adapter.DeleteCommand = commandBuilder.GetDeleteCommand();
                    adapter.UpdateCommand = commandBuilder.GetUpdateCommand();
                    //*****************************/
        
                    // Адаптер сам откроет соединение, сам его закроет и сам пометит
                    // объектную таблицу методом table.AcceptChanges() - изменения приняты
                    adapter.Update(table);
        
                    // Пометить как удаленный объект
                    //table.Clear();
                    //table.Dispose();
                }
                catch (DataException ex)
                {
                    MessageBox.Show(ex.Message);
                    return false;
                }
                finally
                {
                    conn.Close();
                }
        
                return true;// Успех, если дошли до этого места
            }
        
            /*  http://yandex.ru/yandsearch?p=1text=ADOX.CatalogClassclid=14586
            Byte[] getResource(String name)
            {
                BinaryReader br = new BinaryReader(this.GetType().Assembly.GetManifestResourceStream(name));
        
                Byte[] bytes = br.ReadBytes((int)br.BaseStream.Length);
                br.Close();
                return bytes;
            }
            */
        }
    }

    Построитель команд OleDbCommandBuilder может создавать команды обновления только для одной таблицы БД. При более изощренных ручных настройках адаптера данные для одной объектной таблицы можно собирать из разных таблиц БД и, соответственно, обновлять во многих. Но в этом случае OleDbCommandBuilder применять нельзя. Кроме того, он правильно работает только тогда, когда в загруженной таблице БД имеются ключевой или уникальный столбцы. Поэтому в функции CreateTable() при создании таблицы БД первый столбец мы назначили уникальным. Это правомерно, поскольку все равно в одном каталоге не может быть несколько одноименных файлов, а мы собираем рисунки из одного каталога.

    Столбцы, помеченные атрибутом adColNullable, могут быть не заполнены значениями и оставаться пустыми, что мы на всякий случай и предусмотрели. Экземпляры классов ADOX и ADODB могут адресоваться ссылками типа класса или наследуемого интерфейса, это мы тоже использовали в коде.

    Для заполнения созданной таблицы БД мы открываем соединение с ней и считываем все поля в объектную таблицу методом Fill() адаптера. Но поскольку таблица БД пустая, то фактически этот метод считывает только схему, настраивающую объектную таблицу. Поэтому вполне можно было бы обойтись методом FillSchema(). И вообще, когда мы хотим только добавлять данные в таблицу БД, а не редактировать их, то так лучше и поступать.

    Оба метода сами открывают соединение с БД и сами закрывают. Но если принудительно открыть соединение методом Open() объекта соединения, то упомянутые методы его не закроют и нужно явно вызывать метод Close() объекта соединения. Открытие соединения - медленная операция, но она вполне оправдана. С коллективным доступом к одной и той же БД оптимальным является отсоединенный режим, при котором доступ к БД открывается кратковременно на время загрузки или обновления данных. После чего немедленно приходится закрывать соединение, иначе база останется блокированной текущим пользователем.

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

  • В файле Window1.xaml.cs найдите функцию Example4() и заполните ее следующим вызывающим кодом (приводится полностью)
  • void Example4()
            {
                // Диалог выбора каталога
                dialogFolder = 
                    new System.Windows.Forms.FolderBrowserDialog();
                dialogFolder.Description = "Выберите каталог для создания БД";
                dialogFolder.ShowNewFolderButton = false;// Отключить кнопку создания новой папки
                dialogFolder.RootFolder = Environment.SpecialFolder.MyComputer;// Компьютер
                dialogFolder.SelectedPath = settings.dialogFolder;
                System.Windows.Forms.DialogResult result = dialogFolder.ShowDialog();
                // Если был отказ или диалог закрыт системной кнопкой
                if (result == System.Windows.Forms.DialogResult.Cancel)
                    return;
                
                // Извлекаем новый каталог
                String path = dialogFolder.SelectedPath;// Полный путь
                // Создаем структуру БД
                CreateLibraryAndDB create = new CreateLibraryAndDB(path);
                bool success;
                if (success = create.CreateDatabase())
                    success = create.CreateTable();
                if (!success)
                {
                    MessageBox.Show("Структура БД не создана");
                    return;
                }
        
                success = create.CreateData();
                if (success)
                    MessageBox.Show("Файловая библиотека и база данных созданы");
                else
                    MessageBox.Show("Файловая библиотека и база данных не созданы");
        
                // Принудительная перерисовка окна WPF
                this.Height = this.ActualHeight + 1;
                this.Height = this.ActualHeight - 1;
            }

    Здесь мы используем параметры того же диалога выбора каталога, каторый применяли ранее при создании файловой библиотеки. Возвращаемые функциями флаги позволяют вовремя прервать последовательное выполнение, если предыдущий этап закончился неудачей. Принудительная перерисовка окна WPF немного компенсирует недостатки этой новой технологии, поскольку после закрытия заслоняющего окна сообщения само WPF -окно почему-то не перерисовывается и остается размазанным.

  • Запустите пример и зайдите в папку Images, расположенную в прилагаемом к работе каталоге Source
  • Мы видим, что библиотечный файл Pictures.my1.lib и файловая БД Pictures.my1.mdb создаются и наполняются данными. Если открыть нашу БД в Office Access, то она будет такой

    Пока вроде бы все складывается так, как задумали. Мы получили какие-то результаты. Но правильные ли они? Чтобы уверенно это заявить, придется создавать еще один код, отображающий рисунки пользователю, как мы это делали после создания файловой библиотеки. А это опять программирование - т.е. опять именно то, что нам и нужно.

    Пример 5. Совместное применение файловой библиотеки рисунков и базы данных

    Для отображения упакованных в предыдущем примере рисунков применим отдельное окно WPF, подобное Window3.xaml, но отличающееся процедурной частью.

  • Закройте все текущие файлы командой Window/Close All Documents
  • В панели проводнике решений Solution Explorer вызовите контекстное меню для файла Window3.xaml и выполните команду Copy
  • Там же, вызовите контекстное меню для узла App7 самого проекта и выполните команду Paste
  • Там же, командой Rename переименуйте копию Copy of Window3.xaml в Window5.xaml
  • Откройте файл Window5.xaml, замените в нем заголовок окна и индекс 3 на 5, чтобы разметка стала такой
  • <Window x:Class="App7.Window5"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Использование БД + файловой библиотеки" 
        Height="300"
        Width="470"
        MinHeight="300"
        MinWidth="450" 
        Name="_Window5" 
        Closing="Window_Closing"
        >
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="100" />
            </Grid.ColumnDefinitions>
            <Border Background="Black">
                <Image Name="image" Stretch="Uniform" />
            </Border>
            <ListBox Name="listBox" Grid.Column="1" Padding="5,0"
                     ScrollViewer.VerticalScrollBarVisibility="Visible"
                     SelectionChanged="listBox_SelectionChanged">
            </ListBox>
        </Grid>
    </Window>
  • Удалите из файла Window5.xaml.cs ненужный пока процедурный код и добавьте в начало подключение пространств имен ADO.NET, чтобы осталась следующая заготовка, которую мы дальше и будем наполнять
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
        
    // Дополнительные пространства имен
    using FORMS = System.Windows.Forms; // Псевдоним
    using System.IO;
    using System.Configuration;         // Для SettingsProperty
    using IO = System.IO;               // Псевдоним для адресации Path
        
    // Дополнительные пространства имен для ADO.NET
    using System.Data;
    using System.Data.OleDb;
    using System.Data.Common;
        
    namespace App7
    {
        public partial class Window5 : Window
        {
            FileStream fs = null;   // Поле файлового потока для видимости в классе
            String nameLibrary;     // Опорное поле для свойства с именем библиотеки
            int count;              // Поле под число рисунков
            string[] names;         // Поле под имена рисунков
            Properties.Settings settings; // Параметры
        
            // Конструктор
            public Window5()
            {
                InitializeComponent();
        
                // Запускаем диалог выбора библиотеки
                nameLibrary = SelectLibrary();
                // При отказе от выбора ничего не запускаем,
                // а в главном окне Window1 закрываем это окно 
                if (nameLibrary != String.Empty)
                    ExecuteWindow();
            }
        
            // Извлекаем рисунки и управляем показом
            private void ExecuteWindow()
            {
            }
        
            // При смене индекса списка показывает выбранный рисунок
            private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
            }
        
            // Свойство доступа, проверяется в Window1
            public String NameLibrary   
            {
                get { return nameLibrary; }
            }
        
            // Диалог выбора библиотеки. Возвращает
            // пусто, если библиотека не выбрана
            private String SelectLibrary()
            {
                settings = new App7.Properties.Settings();
                String path = settings.dialogOpen;
                String fileName = IO.Path.Combine(path, "Pictures.my1.lib");
                  
                // Создаем и настраиваем диалог выбора файла
                FORMS.OpenFileDialog openFileDialog = new FORMS.OpenFileDialog();
                openFileDialog.InitialDirectory = path;
                openFileDialog.FileName = "";
                openFileDialog.Filter = "Library Files(*.my1.lib)|*.my1.lib";
                FORMS.DialogResult result = openFileDialog.ShowDialog();
                if (result != FORMS.DialogResult.Cancel)
                    fileName = openFileDialog.FileName; // Полное имя
                else
                    fileName = String.Empty;
        
                return fileName;
            }
        
            // Обработчик события Closing, срабатывает перед закрытием приложения
            private void Window_Closing(object sender, 
                System.ComponentModel.CancelEventArgs e)
            {
                // Закрываем библиотечный поток
                if (fs != null)
                    fs.Close(); 
        
                // Сохраняем параметры 
                if (nameLibrary != String.Empty)
                {
                    settings.dialogOpen = 
                        nameLibrary.Substring(0, nameLibrary.LastIndexOf('\\'));
                    settings.Save();
                }
            }
        }
    }
  • В файле Window1.xaml измените заголовок списка для ' Пример 5 ' на ' Пример 5. Использование БД + файловой библиотеки '
  • Заполните в файле Window1.xaml.cs функцию Example5() следующим кодом (окончательный, приводится полностью)
  • void Example5()
            {
                // Предотвращение повторного создания дочернего окна
                bool existsWindow = false;
                foreach (Window window in Application.Current.Windows)// Перебираем коллекцию
                {
                    if (window.Name == "_Window5")
                    {
                        // Нашли среди существующих 
                        existsWindow = true;
                        window.Activate();// Сдвинуть на передний план
                        break;// Прервать перебор
                    }
                }
        
                if (!existsWindow)// Если не существует, то создать и показать
                {
                    Window5 wnd = new Window5();
                    // Перед показом проверяем, выбрана ли библиотека
                    if (wnd.NameLibrary != String.Empty)
                        wnd.Show();    // Показываем окно Window5
                    else
                        wnd.Close();   // Закрываем окно Window5
                }
            }
  • Запустите приложение - окно Window5 запускается, но пока не функционирует
  • Добавьте в файл Window5.xaml.cs к членам класса Window5 функцию, возвращающую строку соединения с БД
  • public partial class Window5 : Window
        {
            ........................................................
            // Строка соединения с БД 
            private String ConnectionString(String fileName)
            {
                //string pathToFile = Application.StartupPath.ToString();
                string JetEngineString = "Provider=Microsoft.Jet.OLEDB.4.0;" +
                    "Jet OLEDB:Engine Type=5;Data Source=";
                return JetEngineString + fileName;
            }
            ........................................................
        }
  • Заполните функцию ExecuteWindow() следующим кодом
  • // Извлекаем рисунки и управляем показом 
            private void ExecuteWindow()
            {
                // Формируем имя БД из имени файловой библиотеки
                string nameDB = nameLibrary.Substring(0, nameLibrary.LastIndexOf('.')) + ".mdb";
        
                // Читаем некоторые данные из БД
                OleDbConnection connection = new OleDbConnection(ConnectionString(nameDB));
                OleDbCommand command = new OleDbCommand("SELECT COUNT(*) FROM MyTable");
                command.Connection = connection;
                connection.Open();
                // Извлекаем число рисунков
                count = (int)command.ExecuteScalar(); 
                // Меняем команду и извлекаем имена рисунков
                command.CommandText = "SELECT FileName FROM MyTable";
                OleDbDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection);
                names = new string[count];
                int i = 0;
                foreach (DbDataRecord rec in reader)// Равносильно while(dataReader.Read())
                    names[i++] = ((string)rec[0]).Trim();// Сразу убираем пробелы
                // Соединение здесь закроет сам объект DataReader после прочтения всех данных
                // в соответствии с соглашением при его создании CommandBehavior.CloseConnection
        
                // Привязываем массив имен рисунков к списку
                listBox.ItemsSource = names;
                // Выделяем первый элемент списка, чтобы вызвать SelectionChanged
                listBox.SelectedIndex = 0;
                listBox.Focus();
            }
  • Разберитесь с кодом метода ExecuteWindow() и запустите пример - результат для ранее созданной БД Pictures.my1.mdb пока будет таким
  • Список с именами, полученными из БД Pictures.my1.mdb, функционирует нормально, но сами рисунки еще нужно извлечь из библиотеки Pictures.my1.lib и присоединить к элементу отображения Image.

  • Заполните заготовку обработчика listBox_SelectionChanged() следующим кодом
  • // При смене индекса списка показывает выбранный рисунок
            private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                // Формируем имя БД из имени файловой библиотеки
                string nameDB = nameLibrary.Substring(0, nameLibrary.LastIndexOf('.')) + ".mdb";
        
                // Создаем и настраиваем объект соединения с БД
                OleDbConnection connection = new OleDbConnection();
                connection.ConnectionString = ConnectionString(nameDB);
        
                // Создаем и настраиваем объект команды, параметризованной по имени рисунка
                OleDbCommand command = new OleDbCommand();
                command.Connection = connection;
                command.CommandType = CommandType.Text; // Необязательно! Установлено по умолчанию
                command.CommandText = "SELECT Offset, Length FROM MyTable WHERE FileName=?";
                command.Parameters.Add(new OleDbParameter());
                command.Parameters[0].Value = ((ListBox)sender).SelectedValue.ToString();// Имя рисунка
        
                // Извлекаем данные из БД 
                connection.Open();
                OleDbDataReader reader = command.ExecuteReader();
                reader.Read();  // Читаем только одну строку (она и будет всего одна)
                long offset = (int)reader["Offset"];
                int length = (int)reader["Length"];
                connection.Close(); // Раньше этого места закрывать нельзя!
        
                // Создаем и открываем один раз файловый поток библиотеки с рисункам
                if (fs == null)
                    fs = new FileStream(
                        nameLibrary, FileMode.Open, FileAccess.Read, FileShare.Read);
        
                // Извлекаем рисунок из потока
                byte[] bytes = new byte[length];
                fs.Seek(offset, SeekOrigin.Begin);
                for (int i = 0; i < length; i++)
                    bytes[i] = (byte)fs.ReadByte();
        
                // Отображаем рисунок пользователю
                BitmapImage bi = new BitmapImage();
                bi.BeginInit();
                bi.StreamSource = new MemoryStream(bytes);
                bi.EndInit();
                image.Source = bi;
            }
  • Разберитесь с кодом обработчика и запустите пример - результат для ранее созданной БД Pictures.my1.mdb и библиотеки Pictures.my1.lib соответствует ожидаемому
  • Пример 6. Создание базы данных OLE DB и заполнение ее рисунками

    Если уж мы ранее решили использовать базу данных для управления рисунками, то более конструктивным будет подход, когда сами рисунки тоже хранятся в базе данных. Чтобы его реализовать, по прежнему нужно разработать две утилиты: одна создает БД и заполняет ее рисунками из выбранного каталога; вторая извлекает эти рисунки из БД и показывает пользователю. В данном примере мы создадим первую утилиту (правда, без потоков), а в следующем - вторую (с потоком). Итак, приступим...

    Начнем с создания реляционной БД программным способом. База данных будет иметь имя Pictures.my2.md b и содержать одну таблицу с именем MyTable, структура которой в типах OLE D B и объектной модели C # будет такой

    Структура таблицы MyTable
    Имя поля Тип OLE DB Тип объектной модели C# Размер
    Name Type Type DefinedSize
    FileName (уникальное) adWChar string 20 (определим сами, с запасом)
    Picture adLongVarBinary (BLOB) byte[] 0 (неопределено)
  • В файле Window1.xaml измените заголовок списка для элемента ' Пример 6 ' на ' Пример 6. Создание базы данных с рисунками '
  • В панели Solution Explorer добавьте к узлу App7 командой контекстного меню Add/Class новый файл с именем CreateDB.cs и заполните его следующим кодом (приводится полностью)
  • using System;
    using System.Windows.Forms;
    using System.IO;
        
    // Пространства имен для ADO.NET
    using System.Data;
    using System.Data.OleDb;
    using System.Data.Common;
        
    namespace App7
    {
        class CreateDB
        {
            // Локальные поля
            String nameDB = "Pictures.my2.mdb";
            string path, fileName,
                tableName = "MyTable";// Нельзя давать имя "Local"!!!
            String extensionPictures = "jpg";
        
            // Параметризованный конструктор
            public CreateDB(string path)
            {
                // Инициализируем поля
                this.path = path;
                fileName = Path.Combine(path, nameDB);
        
                // Удалим предыдущую базу данных, если она есть
                if (new FileInfo(fileName).Exists)
                    File.Delete(fileName);
            }
        
            // Строка соединения с БД
            private String ConnectionString(String fileName)
            {
                //string pathToFile = Application.StartupPath.ToString();
                string JetEngineString = "Provider=Microsoft.Jet.OLEDB.4.0;" +
                    "Jet OLEDB:Engine Type=5;Data Source=";
                return JetEngineString + fileName;
            }
        
            // Создание БД
            public bool CreateDatabase()
            {
                Object adoConnection = null;
                ADOX.CatalogClass catalog = new ADOX.CatalogClass();
        
                try
                {
                    catalog.Create(ConnectionString(fileName));
                    adoConnection = catalog.ActiveConnection;
                }
                catch (Exception ex)
                {
                    MessageBox.Show(String.Format("Ошибка в создании БД\n{0}", ex.Message),
                        "Ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return false;
                }
                finally
                {
                    if (catalog != null  adoConnection != null)
                    {
                        ((ADODB.Connection)adoConnection).Close();// Приводим к интерфейсу
                        /***********  Вариант  *********
                        adoConnection.GetType().InvokeMember(
                            "Close", System.Reflection.BindingFlags.InvokeMethod,
                            null, adoConnection, new object[0]);
                        //*****************************/
        
                        catalog = null;
                        adoConnection = null;
                    }
                }
        
                return true;// Успех, если дошли до этого места
            }
        
            // Создание таблицы
            public bool CreateTable()
            {
                ADODB.Connection conn = new ADODB.ConnectionClass();
                conn.Open(ConnectionString(fileName), null, null, 0);
                ADOX.Catalog catalog = new ADOX.CatalogClass();
                catalog.ActiveConnection = conn;
        
                try
                {
                    ADOX.TableClass table = new ADOX.TableClass();// Объект таблицы
                    table.ParentCatalog = catalog;  // Указали родителя для таблицы
                    table.Name = tableName;         // Имя таблицы
                    catalog.Tables.Append(table);   // Добавили в коллекцию таблиц
        
                    // Задаем параметры настройки таблицы
                    string[] columnNames =
                    {
                        "FileName",
                        "Picture"
                    };
                    // Неопределенный размер заменяем нулем
                    int[] columnSizes = new int[] { 20, 0 };
                    ADOX.DataTypeEnum[] columnTypes = new ADOX.DataTypeEnum[] 
                        {
                            ADOX.DataTypeEnum.adWChar,          // До 255 символов fixed
                            ADOX.DataTypeEnum.adLongVarBinary   // BLOB
                        };
        
                    // Настраиваем структуру таблицы 
                    ADOX.ColumnClass column;
                    for (int i = 0; i < columnSizes.Length; i++)
                    {
                        column = new ADOX.ColumnClass();
                        column.ParentCatalog = catalog;  // Указали родителя для столбца
                        column.Name = columnNames[i];
                        column.DefinedSize = columnSizes[i];
                        column.Type = columnTypes[i];
                        column.Attributes = ADOX.ColumnAttributesEnum.adColNullable;// Позволить пусто
                        //column.Attributes = ADOX.ColumnAttributesEnum.adColFixed;// Фиксированная длина
                        table.Columns.Append(column, column.Type, column.DefinedSize);
                    }
        
                    // Делаем первый столбец уникальным (совпадения не допускаются)
                    // http://www.sqldev.org/sql-server-data-access/
                    // multi-column-primary-keys-in-access-using-adox-c-60766.shtml
                    ADOX.Table tbl = catalog.Tables[tableName];
                    tbl.Keys.Append(
                        "UniqueKeyLocal", ADOX.KeyTypeEnum.adKeyUnique, "FileName", null, null);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message.ToString());
                    return false;
                }
                finally
                {
                    conn.Close();// Приводим к интерфейсу
                }
        
                return true;// Успех, если дошли до этого места
            }
        
            // Наполняем файловую БД данными
            public bool CreateData()
            {
                // Читаем информацию о всех файлах выбранного каталога
                FileInfo[] filesInfo = new DirectoryInfo(path).
                    GetFiles("*." + extensionPictures);
                if (filesInfo.Length == 0)
                {
                    MessageBox.Show(
                        "В выбранном каталоге нет файлов для упаковки в БД",
                        "Предупреждение",
                        MessageBoxButtons.OK,
                        MessageBoxIcon.Information);
                    return false;
                }
        
                // Сортируем объекты массива FileInfo по именам файлов
                Array.Sort<FileInfo>(filesInfo, 
                    delegate(FileInfo x, FileInfo y)// Анонимный делегат вместо функции обратного вызова
                    {
                        return String.Compare(x.Name, y.Name);
                    });
        
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection(ConnectionString(fileName));
                OleDbCommand cmd = new OleDbCommand();
                cmd.CommandType = CommandType.Text;  // Необязательно!
                cmd.CommandText = "SELECT * FROM " + tableName;
                cmd.Connection = conn;
                OleDbDataAdapter adapter = new OleDbDataAdapter();
                adapter.SelectCommand = cmd;
                new OleDbCommandBuilder(adapter);   // Донастраиваем адаптер на запись
        
                // Пишем построчно в объектную таблицу и затем сразу в БД,
                // чтобы экономить оперативную память, если рисунков много
                DataTable table = null;
                conn.Open();// Метод FillSchema() теперь уже не закроет!!!
                for (int i = 0; i < filesInfo.Length; i++)
                {
                    // Создаем и настраиваем объектную модель таблицы
                    if (table == null)
                    {
                        // Только один раз
                        table = new DataTable(tableName);
                        adapter.FillSchema(table, SchemaType.Source);
                    }
                    else   
                        table.Clear(); // Теперь только чистим, структура таблицы остается
        
                    // Получаем сокращенное имя файла без пути и расширения
                    String shortName = filesInfo[i].Name; // Уже без пути и есть!!!
                    int len = shortName.LastIndexOf('.');
                    shortName = shortName.Substring(0, len);// Убрали расширение
        
                    // Добавляем в строку имя рисунка (и порядковый номер, чтобы було!)
                    DataRow row = table.NewRow();
                    row["FileName"] = (i + 1).ToString() + ") " + shortName;
        
                    // Читаем файл с рисунком
                    byte[] bytes = File.ReadAllBytes(
                        Path.Combine(filesInfo[i].DirectoryName, filesInfo[i].Name));
                    /***********  Вариант  *********
                    FileStream fileStream = filesInfo[i].OpenRead();
                    byte[] bytes = new byte[fileStream.Length];
                    int readByte, count = 0;
                    while ((readByte = fileStream.ReadByte()) != -1)
                        bytes[count++] = (byte)readByte;
                    fileStream.Close();
                    //*****************************/
        
                    // Добавляем в строку сам рисунок, адресуясь по имени столбца
                    row["Picture"] = bytes;
                    table.Rows.Add(row);    // Скидываем в объектную таблицу
                    adapter.Update(table);  // Записываем эту одну строку в БД
                }
                // Удаляем объектную таблицу (Необязательно!)
                table.Dispose();
                // Закрываем соединение
                conn.Close();
        
                return true;// Успех, если дошли до этого места
            }
        }
    }

    Учитывая, что открытие соединения с БД является очень медленной операцией, в методе CreateData() перед вхождением в цикл заполнения БД рисунками мы открываем соединение только один раз. Когда соединение открыто, то методы адаптера FillSchema() и Fill() его не закрывают, поэтому после цикла нужно его закрыть самим. Экземпляр объекта построителя команд OleDbCommandBuilder нам нужен только для настройки адаптера на обновление данных в БД (по умолчанию последний настроен только на чтение), для этого нет необходимости создавать на него ссылку.

    Желательно, чтобы рисунки попадали в БД уже отсортированными по именам. Поэтому после получения из выбранного каталога всех объектов FileInfo, мы их предварительно сортируем и только потом считываем с диска файлы в установленном порядке. Для сортировки используем перегрузку типизированного статического метода с делегатом, чтобы не выносить отдельно другую статическую функцию - обратного вызова (компаратор). В ходе сортировки метод Sort() автоматически будет обращаться к этой функции (анонимному делегату) при оценке весов Name очередных сравниваемых пар элементов массива.

  • В файле Window1.xaml.cs найдите функцию Example6() и заполните ее следующим вызывающим кодом (приводится полностью)
  • void Example6()
            {
                // Диалог выбора каталога
                dialogFolder =
                    new System.Windows.Forms.FolderBrowserDialog();
                dialogFolder.Description = "Выберите каталог для создания БД";
                dialogFolder.ShowNewFolderButton = false;// Отключить кнопку создания новой папки
                dialogFolder.RootFolder = Environment.SpecialFolder.MyComputer;// Компьютер
                dialogFolder.SelectedPath = settings.dialogFolder;
                System.Windows.Forms.DialogResult result = dialogFolder.ShowDialog();
                // Если был отказ или диалог закрыт системной кнопкой
                if (result == System.Windows.Forms.DialogResult.Cancel)
                    return;
        
                // Извлекаем новый каталог
                String path = dialogFolder.SelectedPath;// Полный путь
                // Создаем структуру БД
                CreateDB create = new CreateDB(path);
                bool success;
                if (success = create.CreateDatabase())
                    success = create.CreateTable();
                if (!success)
                {
                    MessageBox.Show("Структура БД не создана");
                    return;
                }
        
                success = create.CreateData();
                if (success)
                    MessageBox.Show("База данных создана");
                else
                    MessageBox.Show("База данных не создана");
        
                // Принудительная перерисовка окна WPF
                this.Height = this.ActualHeight + 1;
                this.Height = this.ActualHeight - 1;
            }
  • Запустите пример и зайдите в папку Images, расположенную в прилагаемом к работе каталоге Source
  • Мы видим, что файловая БД Pictures.my2.mdb создается и наполняется данными. Если открыть эту БД в Office Access, то она (с учетом автоматической сортировки Office Access ) будет такой

    Если проверить разработанную утилиту на создании БД с большим количеством рисунков (1000), то в сравнении с формированием файловой библиотеки она работает ничуть не медленнее. Основная выгода заключается в том, что при записи каждого рисунка мы пользуемся уже открытым соединением. Точно также для библиотечного файла поток создается только один раз. Мы дальше убедимся, что при чтении файловой библиотеки и БД скорость тоже мало различима (и это хорошо), поскольку там и там извлекается только один рисунок и скорость открытия примерно равна.

    Можно было бы, конечно, сначала прочитать все рисунки с диска, а потом сразу записать их в БД, и это было бы еще быстрее. Но рисунков может оказаться много и большого размера. В этом случае оперативной памяти компьютера просто нехватит и он вынужден будет создавать на жестком диске временные файлы промежуточного хранения данных (такой механизм называется спуллинг - Simultaneous Peripherial Operation Off Line ). Это тоже существенно замедлит работу.

    Базы данных - это большое достижение 'всего прогрессивного человечества' (и нас с вами!): они универсальны, надежны, элегантны, незаменимы, а программировать их просто приятно и 'просто просто' (это элементарно, Ватсон).

    Пример 7. Использование базы данных с рисунками

    Теперь создадим утилиту для просмотра рисунков из полученной ранее БД.

  • В файле Window1.xaml измените заголовок списка для элемента ' Пример 7 ' на ' Пример 7. Использование БД с рисунками '
  • В панели Solution Explorer добавьте к узлу App7 командой контекстного меню Add/Window новый файл с именем Window7.xaml
  • Заполните дескрипторную часть окна Window7 (файл Window7.xaml ) следующим кодом (приводится полностью)
  • <Window x:Class="App7.Window7"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Пример 7. Использование БД с рисунками" 
        Height="300"
        Width="470"
        MinHeight="300"
        MinWidth="450" 
        Name="_Window7" 
        Closing="Window_Closing"
        >
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="110" />
            </Grid.ColumnDefinitions>
            <Border Background="Black">
                <Image Name="image" Stretch="Fill" />
            </Border>
            <ListBox Name="listBox" Grid.Column="1" Padding="5,0"
                     ScrollViewer.VerticalScrollBarVisibility="Visible"
                     SelectionChanged="listBox_SelectionChanged">
            </ListBox>
        </Grid>
    </Window>
  • Заполните процедурную часть окна Window7 (файл Window7.xaml.cs ) следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
        
    // Дополнительные пространства имен
    using FORMS = System.Windows.Forms; // Псевдоним
    using System.IO;
    using System.Configuration;         // Для SettingsProperty
    using IO = System.IO;               // Псевдоним для адресации Path
        
    // Дополнительные пространства имен для ADO.NET
    using System.Data;
    using System.Data.OleDb;
    using System.Data.Common;
        
    namespace App7
    {
        public partial class Window7 : Window
        {
            String nameDB;     // Опорное поле для свойства с именем БД
            int count;              // Поле под число рисунков
            string[] names;         // Поле под имена рисунков
            Properties.Settings settings; // Параметры
        
            // Конструктор
            public Window7()
            {
                InitializeComponent();
        
                // Запускаем диалог выбора БД
                nameDB = SelectDB();
                // При отказе от выбора ничего не запускаем,
                // а в главном окне Window1 закрываем это окно 
                if (nameDB != String.Empty)
                    ExecuteWindow();
            }
        
            // Извлекаем рисунки и управляем показом 
            private void ExecuteWindow()
            {
                // Читаем некоторые данные из БД
                OleDbConnection conn = new OleDbConnection(ConnectionString(nameDB));
                OleDbCommand cmd = new OleDbCommand("SELECT COUNT(*) FROM MyTable");
                cmd.Connection = conn;
                conn.Open();
                // Извлекаем число рисунков
                count = (int)cmd.ExecuteScalar();
                // Меняем команду и извлекаем имена рисунков
                cmd.CommandText = "SELECT FileName FROM MyTable";
                OleDbDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
                names = new string[count];
                int i = 0;
                while (reader.Read())// Равносильно foreach (DbDataRecord rec in reader)
                    names[i++] = ((string)reader["FileName"]).Trim();// Сразу убираем пробелы
                // Соединение здесь закроет сам объект DataReader после прочтения всех данных
                // в соответствии с соглашением при его создании CommandBehavior.CloseConnection
        
                // Привязываем массив имен рисунков к списку
                listBox.ItemsSource = names;
                // Выделяем первый элемент списка, чтобы вызвать SelectionChanged
                listBox.SelectedIndex = 0;
                listBox.Focus();
            }
        
            // Строка соединения с БД 
            private String ConnectionString(String fileName)
            {
                //string pathToFile = Application.StartupPath.ToString();
                string JetEngineString = "Provider=Microsoft.Jet.OLEDB.4.0;" +
                    "Jet OLEDB:Engine Type=5;Data Source=";
                return JetEngineString + fileName;
            }
        
            // При смене индекса списка показывает выбранный рисунок
            private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection();
                conn.ConnectionString = ConnectionString(nameDB);
        
                // Создаем и настраиваем объект команды, параметризованной по имени рисунка
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = conn;
                cmd.CommandType = CommandType.Text; // Необязательно! Установлено по умолчанию
                cmd.CommandText = "SELECT Picture FROM MyTable WHERE FileName=?";
                cmd.CommandTimeout = 1;// Пытаться X секунд, потом сгенерировать ошибку (=30 сек)
                cmd.Parameters.Add(new OleDbParameter());
                cmd.Parameters[0].Value = ((ListBox)sender).SelectedValue.ToString();// Имя рисунка
        
                OleDbDataAdapter adapter = new OleDbDataAdapter(cmd);
        
                // Извлекаем рисунок из БД 
                DataTable table = new DataTable();
                adapter.Fill(table);
                byte[] bytes = (byte[])table.Rows[0]["Picture"]; // Подключаемся к рисунку
        
                // Отображаем рисунок пользователю
                BitmapImage bi = new BitmapImage();
                bi.BeginInit();
                bi.StreamSource = new MemoryStream(bytes);// Обертываем извлеченный рисунок в поток
                bi.EndInit();
                image.Source = bi;
            }
        
            // Свойство доступа, проверяется в Window1
            public String NameDB
            {
                get { return nameDB; }
            }
        
            // Диалог выбора БД. Возвращает
            // пусто, если БД не выбрана
            private String SelectDB()
            {
                settings = new App7.Properties.Settings();
                String path = settings.dialogOpen;
                String fileName = IO.Path.Combine(path, "Pictures.my2.mdb");
        
                // Создаем и настраиваем диалог выбора файла
                FORMS.OpenFileDialog openFileDialog = new FORMS.OpenFileDialog();
                openFileDialog.InitialDirectory = path;
                openFileDialog.FileName = "";
                openFileDialog.Filter = "DB Files(*.my2.mdb)|*.my2.mdb";
                FORMS.DialogResult result = openFileDialog.ShowDialog();
                if (result != FORMS.DialogResult.Cancel)
                    fileName = openFileDialog.FileName; // Полное имя
                else
                    fileName = String.Empty;
        
                return fileName;
            }
        
            // Обработчик события Closing, срабатывает перед закрытием приложения
            private void Window_Closing(object sender,
                System.ComponentModel.CancelEventArgs e)
            {
                // Сохраняем путь в параметрах 
                if (nameDB != String.Empty)
                {
                    settings.dialogOpen =
                        nameDB.Substring(0, nameDB.LastIndexOf('\\'));
                    settings.Save();
                }
            }
        }
    }

    Свойство CommandTimeout объекта OleDbCommand получает или задает время ожидания (в секундах) перед завершением попытки выполнения команды и созданием ошибки. По умолчанию установлено 30 секунд, что соответствует большой сети и сложным запросам. Для нашей задачи и 1 секунды очень много, но меньше нельзя, поскольку это свойство типа int.

  • Заполните в файле Window1.xaml.cs функцию Example7() следующим кодом (окончательный, приводится полностью)
  • void Example7()
            {
                // Предотвращение повторного создания дочернего окна
                bool existsWindow = false;
                foreach (Window window in Application.Current.Windows)// Перебираем коллекцию
                {
                    if (window.Name == "_Window7")
                    {
                        // Нашли среди существующих 
                        existsWindow = true;
                        window.Activate();// Сдвинуть на передний план
                        break;// Прервать перебор
                    }
                }
        
                if (!existsWindow)// Если не существует, то создать и показать
                {
                    Window7 wnd = new Window7();
                    // Перед показом проверяем, выбрана ли БД
                    if (wnd.NameDB != String.Empty)
                        wnd.Show();    // Показываем окно Window7
                    else
                        wnd.Close();   // Закрываем окно Window7
                }
            }
  • Разберитесь с кодом и запустите пример - результат для ранее созданной БД Pictures.my2.mdb соответствует ожидаемому и будет таким
  • Все, иллюстрацию потока MemoryStream мы закончили и попутно еще кое в чем потренировались. Остальные неиспользованные пункты списка главного окна в файле Window1.xaml можно удалить, как и соответствующий им процедурный код в файле Window1.xaml.cs. Или пусть так и болтаются.

    Задание к упражнению

    Напоследок можно сделать еще одно замечание к приведенному коду Упражнения 7. В окнах просмотра Window3, Window5 и Window7 мы создавали обработчик события Closing и старательно сохраняли переменную dialogOpen в параметрах приложения. Переменную dialogFolder мы тоже сохраняем, но только при закрытии главного окна. Отсюда дизгармония (физгармоника): пока не закрыли главное окно, при выполнении четных примеров диалог выбора каталогов всегда стартует с того места, которое было сохранено при закрытии главного окна в последнем запуске.

  • Исправьте этот недостаток, чтобы пока длится сеанс, приемственность диалога выбора папки в четных примерах соблюдалась
  • Сохранять путь и переменную dialogFolder можно сразу после выдачи сообщения MessageBox.Show() об удачном создании файловой библиотеки или БД.

    Упражнение 8. Сетевые приложения на базе потока NetworkStream

    Еще одним потомком абстрактного класса Stream является сетевой поток NetworkStream пространства имен System.Net.Sockets библиотеки System.dll. Можно использовать класс NetworkStream для выполнения как синхронной, так и асинхронной передачи данных. В синхронном режиме перед возвращением управления вызывающей программе вызовы функций, которые выполняют сетевые операции, ожидают окончания выполнения этих операции. В асинхронном режиме вызовы немедленно возвращаются инициировавшему их коду.

    Рассмотрим применение NetworkStream на примере совместной работы нескольких запущенных копий клиентского приложения и одной копии серверного. Работу построим на основе созданной ранее БД Pictures.my2.mdb с рисунками. Сервер будет иметь доступ к этой базе и посылать все необходимые данные клиентам по их запросу. Интерфейсное окно клиента реализуем по технологии Windows Forms. А вот сервер создадим как резидентную программу ( Службу Windows ) и запустим в фоновом режиме. Протокол обмена для контроля будем выводить в текстовый файл. Так мы максимально разнообразим демонстрацию применения потоков.

    Шаги выполнения упражнения оформим в виде примеров. Вначале познакомимся с некоторыми понятиями, взятыми из Википедии - свободной энциклопедии.

    Сокет ( Socket ) - программный интерфейс ( API - набор программ) для обеспечения информационного обмена между процессами (загрущенными в ОЗУ приложениями, которые уже выполняются процессором). Потоковые сокеты работают с установкой соединения. Они обеспечивают надежную идентификацию обеих сторон, гарантируя целостность и успешность доставки данных, и опираются на протокол TCP. Условно можно представить себе сокеты в виде двух разъемов, в которые включен канал связи, предназначенный для передачи данных через сеть (поток данных). При передачи данных сокет можно считать резервным хранилищем - аналогом файла. Данные между сокетами перекачивает поток.

    TCP ( Transmission Control Protocol - протокол управления передачей) - один из основных сетевых протоколов Интернета, предназначенный для управления передачей данных в сетях и подсетях TCP/IP. Это транспортный механизм, предоставляющий поток данных, с предварительной установкой соединения, за счёт этого дающий уверенность в достоверности получаемых данных. TCP осуществляет повторный запрос данных в случае потери данных и устраняет дублирование при получении двух копий одного пакета. TCP гарантирует, что приложение получит данные точно в такой же последовательности, в какой они были отправлены, и без потерь. TCP устанавливает соединение, передает данные и завершает соединение.

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

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

    Порт - в протоколах TCP и UDP (семейства TCP/IP ) порт представляет собой идентифицируемый номером системный ресурс, выделяемый приложению, выполняемому на некотором сетевом хосте, для связи с приложениями, выполняемыми на других сетевых хостах (в том числе c другими приложениями на этом же хосте).

    Для каждого из протоколов TCP и UDP стандарт определяет возможность одновременного выделения на хосте до 65536 уникальных портов, идентифицирующихся номерами от 0 до 65535. При передаче по сети номер порта в заголовке пакета используется (вместе с IP -адресом хоста) для адресации конкретного приложения (и конкретного, принадлежащего ему, сетевого соединения).

    В обычной клиент-серверной модели одно приложение ожидает входящих данных или запроса на соединение (сервер "слушает порт"), другое посылает данные или запрос на соединение в известный порт, открытый приложением-сервером (клиент).

    По умолчанию приложению выдается порт с произвольным (например, ближайшим свободным и большим 1024) номером. При необходимости приложение может запросить конкретный (предопределенный) номер порта. Так, веб-серверы обычно открывают для ожидания соединения предопределенный порт 80 протокола TCP.

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

    Очень хорошо вопросы сетевого программирования описаны в книге (скорее, единственной такой):

    ".NET - Сетевое программирование для профессионалов"

    Авторы: Эндрю Кровчик, Винод Кумар, Номан Лагари, Аджит Мунгале, Кристиан Нагел, Тим Паркер,

    Шриниваса Шивакумар

    Издательство: Лори

    Год: 2005

    Пример 1. Создание простой тестовой Службы Windows

    Сервером в C# может служить приложение любого из существующих исполнимых типов: Console Application, Windows Forms Application, WPF Application или Windows Service. Для разнообразия, вначале построим сервер на основе Windows Service. Такие программы не имеют пользовательского интерфейса (он им и не нужен - висят в оперативной памяти и делают свое дело) и раньше назывались резидентными, а теперь Службами.

    Позже мы создадим сервер и как Console Application, в котором консольное окно может быть использовано для запуска, настроек и контроля состояния сервера. А пока немного отвлечемся и на простом примере научимся создавать Службу Windows как прототип нашего будущего сервера, затем опять продолжим рассмотрение темы потоков.

  • Командой File/New/Project создайте в каталоге решений Stream новое решение с именем NetworkStream и новый проект с именем TestService типа Windows Service
  • Оболочка создаст новый проект и покажет окно графического конструктора сервисов.

  • Закройте окно конструктора сервисов и через панель Solution Explorer в узле Service1.cs удалите подчиненный файл Service1.Designer.cs за ненадобностью, поскольку компоненты в этом примере мы использовать не будем
  • В панели Solution Explorer вызовите контекстное меню для узла Service1.cs и выполните команду View Code
  • Заполните файл Service1.cs следующим кодом ('http://msdn.microsoft.com/ru-ru/library/9k985bc9(VS.90).aspx')
  • using System;
    using System.ServiceProcess;
    using System.Timers;
    using System.IO;
        
    namespace TestService
    {
        public partial class Service1 : ServiceBase
        {
            Timer timer;// Системный таймер
            String logFile = "C:\\Report.txt";
        
            // Конструктор
            public Service1()
            {
                // Создаем и настраиваем системный таймер
                timer = new Timer();
                timer.Elapsed += timer_Elapsed;// Регистрируем обработчик
                timer.Interval = 5000; // 5 секунд
            }
        
            // Обработчик события таймера
            void timer_Elapsed(object sender, ElapsedEventArgs e)
            {
                StreamWriter writer = File.AppendText(logFile);
                writer.WriteLine(DateTime.Now.ToString());
                writer.Close(); // Закрываем поток
            }
        
            // Метод срабатывает при запуске Службы
            protected override void OnStart(string[] args)
            {
                timer.Start();
            }
        
            // Метод срабатывает при остановке Службы
            protected override void OnStop()
            {
                timer.Stop();
            }
        }
    }

    Запускать на выполнение через оболочку эту Службу нельзя. Она запускается операционной системой через Диспетчер Служб (или через Диспетчер задач Windows по жесту Ctrl-Alt-Del). Мы ее можем пока только откомпилировать, но и это преждевременно. Еще нужно зарегистрировать Службу на текущем компьютере, чтобы она была видна диспетчеру Служб, а для этого нужно к проекту добавить класс настроек регистрации Службы.

  • В панели Solution Explorer вызовите на узле TestService проекта контекстное меню и командой Add/New Item в появишемся окне мастера выберите элемент Installer Class
  • Закройте окно графического конструктора и через панель Solution Explorer в узле Installer1.cs удалите подчиненный файл Installer1.Designer.cs за ненадобностью
  • В панели Solution Explorer вызовите контекстное меню для узла Installer1.cs и выполните команду View Code
  • Заполните файл Installer1.cs следующим кодом настроек регистрации Службы ('http://msdn.microsoft.com/ru-ru/library/ddhy0byf(v=VS.90).aspx')
  • using System;
    using System.ComponentModel;
    using System.Configuration.Install;
    using System.ServiceProcess;
        
    namespace TestService
    {
        [RunInstaller(true)]// Во время установки сборки следует вызвать установщик
        public partial class Installer1 : Installer
        {
            private ServiceInstaller serviceInstaller;
            private ServiceProcessInstaller serviceProcessInstaller;
        
            public Installer1()
            {
                // Создаем объекты настройки для Службы
                serviceInstaller = new ServiceInstaller();
                serviceProcessInstaller = new ServiceProcessInstaller();
        
                // Имя Службы для машины и пользователя
                serviceInstaller.ServiceName = "TestServiceServiceName";
                serviceInstaller.DisplayName = "TestServiceDisplayName";
        
                // Контекст безопасности - как будет запускаться Служба
                this.serviceProcessInstaller.Account = ServiceAccount.LocalSystem;
                this.serviceProcessInstaller.Password = null;
                this.serviceProcessInstaller.Username = null;
        
                // Добавляем настройки в коллекцию текущего объекта
                this.Installers.AddRange(
                    new Installer[]         
                    {
                        serviceInstaller,
                        serviceProcessInstaller
                    });
            }
        }
    }
  • Откомпилируйте проект командой Build/Build Solution
  • Мы создали Службу, которая должна быть зарегистрирована в операционной системе. Как это делается, описано в MSDN ('http://msdn.microsoft.com/ru-ru/library/sd8zc8ha(VS.90).aspx') и мы это сейчас выполним. Для регистрации Служб приложений .NET используется утилита InstallUtil.exe, которая находится в каталоге " C:\Windows\Microsoft.NET\Framework\v2.0.50727 ". Для удаления регистрации Службы используется та же самая утилита, но с опцией /u.

  • В меню Пуск/Выполнить введете в текстовое поле имя запускаемой программы cmd и откройте окно команд Windows
  • Следующей командой перейдите в каталог с утилитой InstallUtil.exe >cd C:\Windows\Microsoft.NET\Framework\v2.0.50727
  • В панели Solution Explorer щелкните на пиктограмме Show All Files, раскройте узел bin/Debug и скопируйте полное имя сборки E:\Tmp\Stream\NetworkStream\TestService\bin\Debug\TestService.exe
  • В окне команд Windows введите команду >InstallUtil.exe и добавьте мышью скопированное имя сборки, должно получиться >InstallUtil.exe E:\Tmp\Stream\NetworkStream\TestService\bin\Debug\TestService.exe
  • После выполнения этой команды откройте окно Пуск/Настройка/Панель управления/Администрирование/Службы и запустите нашу Службу (или жестом Ctrl-Alt-Del запустите Диспетчер задач и откройте вкладку Службы)
  • Убедитесь, что в файле Report.txt стала накапливаться информация, остановите Службу командой Остановить и удалите этот пробный файл
  • Замечания

  • Вызвать окно со списком Служб можно и через Диспетчер задач Windows комбинацией клавиш Ctrl-Alt-Del (когда-то заветная комбинация для программистов DOS!)
  • Созданная нами Служба запускается вручную, но через команду Свойства контекстного меню ее можно сделать запускаемой автоматически при загрузке компьютера
  • После регистрации Службы в системе нельзя менять место расположения созданной нами сборки, иначе при запуске система эту Службу просто не найдет. При необходимости Службу нужно вначале разрегистрировать, перенести и вновь зарегистрировать для нового места
  • В каталоге сборки утилита инсталляции InstallUtil.exe создала два протокольных текстовых файла (без расширения): TestService.InstallLog и TestService.InstallState, которые не влияют на дальнейшую работу Службы и их можно спокойно удалить
  • В окне команд Windows разрегистрируйте Службу командой >InstallUtil.exe /u E:\Tmp\Stream\NetworkStream\TestService\bin\Debug\TestService.exe
  • Перейдите в окно Службы, обновите его командой Действие/Обновить и убедитесь, что Служба была удалена
  • Итак, на этом простом примере мы научились создавать Службы Windows (не путайте с Web -Службами) и теперь можно бы приступить к созданию серверной программы, которая по запросу клиентов будет извлекать рисунки из БД и посылать им (их) для просмотра. Но не будем торопиться и рассмотрим еще один тестовый пример, теперь уже иллюстрирующий взаимодействие клиента и сервера.

    Пример 2. Создание простого клиент-серверного приложения

    В этом примере мы разработаем простой сервер и простую клиентскую программу, ведущих между собой 'неспешный' диалог. Клиента построим по технике Windows Forms, а сервер - Windows Service. Сервер будет иметь набор готовых маркированных ответов, ждать маркированных запросов клиентов и отвечать им соответствующими сообщениями. Это настроит нас на создание еще более сложной системы - просмотру дистанционных рисунков из БД, которой мы займемся позже.

    Создание клиента

    Начнем с клиентской программы, которая может запускаться во многих экземплярах ('http://msdn.microsoft.com/ru-ru/library/system.net.sockets.tcplistener.accepttcpclient.aspx')

  • Командой File/Add/New Project добавьте к решению NetworkStream новый проект с именем SimpleClient и назначьте его стартовым
  • Разместите на форме элементы ListBox, Button, TextBox и настройте их в соответствии с таблицей свойств
  • Элемент Свойство Значение
    Form Text Client
    Size 300; 300
    ListBox (Name) listBox
    Dock Top
    Font Arial; 12pt
    Items
  • Привет!
  • Лелик
  • Как жизнь
  • Оттопыремся сегодня?
  • Ну тогда пока!
  • SelectionMode One
    Size 292; 119
    Button (Name) btnSubmit
    AutoSize True
    Font Arial; 10pt
    Location 96; 127
    Size 101; 29
    Text Отправить
    TextBox (Name) textBox
    Dock Bottom
    Location 0; 162
    Multiline True
    ScrollBars Vertical
    Size 292; 105

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

  • Откройте файл Form1.cs и сделайте его таким (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
        
    namespace SimpleClient
    {
        public partial class Form1 : Form
        {
            int port = 12000;
            String hostName = "127.0.0.1";// local
            TcpClient client = null;// Ссылка на клиента
        
            public Form1()
            {
                InitializeComponent();
        
                // Выделить первый элемент списка
                listBox.SelectedIndex = 0;
                listBox.Focus();
        
                // Контекстное меню для очистки TextBox
                ContextMenuStrip contextMenu = new ContextMenuStrip();
                textBox.ContextMenuStrip = contextMenu;
                ToolStripMenuItem item = new ToolStripMenuItem("Очистить");
                contextMenu.Items.Add(item);
                item.MouseDown += new MouseEventHandler(item_MouseDown);
            }
        
            // Отослать запрос и получить ответ
            private void btnSubmit_Click(object sender, EventArgs e)
            {
                if (listBox.SelectedIndices.Count == 0)
                {
                    MessageBox.Show("Выделите сообщение");
                    return;
                }
        
                try
                {
                    // Создаем клиента, соединенного с сервером
                    client = new TcpClient(hostName, port);
                    // Сами задаем размеры буферов обмена (Необязательно!)
                    client.SendBufferSize = client.ReceiveBufferSize = 1024;
                }
                catch
                {
                    MessageBox.Show("Сервер не готов!");
                    return;
                }
        
                // Записываем запрос в протокол
                AddString("Клиент: " + listBox.SelectedItem.ToString());
        
                // Создаем потоки NetworkStream, соединенные с сервером
                NetworkStream streamIn = client.GetStream();
                NetworkStream streamOut = client.GetStream();
                StreamReader readerStream = new StreamReader(streamIn);
                StreamWriter writerStream = new StreamWriter(streamOut);
        
                // Отсылаем запрос серверу
                writerStream.WriteLine(listBox.SelectedItem.ToString());
                writerStream.Flush();
        
                // Читаем ответ
                String receiverData = readerStream.ReadLine();
        
                // Записываем ответ в протокол 
                AddString("Сервер: " + receiverData);
        
                // Закрываем соединение и потоки, порядок неважен
                client.Close();
                writerStream.Close();
                readerStream.Close();
            }
        
            // Добавление строки, когда TextBox включен в режиме Multiline
            private void AddString(String line)
            {
                StringBuilder sb = new StringBuilder(textBox.Text);
                StringWriter sw = new StringWriter(sb);
                sw.WriteLine(line);
                textBox.Text = sw.ToString();
            }
        
            // Очистка списка через контекстное меню
            void item_MouseDown(object sender, MouseEventArgs e)
            {
                textBox.Clear();
                listBox.Focus();
            }
        }
    }

    Получив соединение с сервером, мы создаем два потока NetworkStream и упаковываем их в оболочки, удобные для управления чтением/записью. Обмен с сервером отображаем в протоколе TextBox. Для очистки протокола динамически создали контекстное меню.

    Класс TcpClient, который мы использовали в коде, является высокоуровневой (и упрощенной) оболочкой сокета (класса Socket ). Если потребуется более низкоуровневое управление сокетом (более детальное), то ссылка на него хранится в свойстве TcpClient.Client. Но поскольку это свойство защищенное ( protected ), то доступ к нему возможен только из производного от TcpClient класса.

  • Разберитесь с кодом клиента
  • Если сейчас запустить приложение SimpleClient, то оно будет работать, но при попытке что-то отослать на сервер, будет выдаваться сообщение, что сервер не готов. Сервера-то еще пока вообще нет, создадим его.

    Создание сервера

    Серверную программу сделаем резидентной по шаблону Windows Service, как мы это делали в предыдущем примере, хотя можно сделать и с интерфейсом, главное, чтобы он был запущен в единственном экземпляре на локальном компьютере. Если программа-сервер включена в глобальную сеть, то с используемым IP и портом она должна быть единственной в этой сети. Поэтому на использование сетевого IP для глобальной сети нужно получать разрешение.

  • Командой File/Add/New Project добавьте к решению NetworkStream новый проект с именем SimpleServer по шаблону Windows Service
  • В панели Solution Explorer вызовите на узле SimpleServer проекта контекстное меню и командой Add/New Item в появишемся окне мастера выберите элемент Installer Class
  • Закройте окно графического конструктора и через панель Solution Explorer в узле Installer1.cs удалите подчиненный файл Installer1.Designer.cs за ненадобностью
  • В панели Solution Explorer вызовите контекстное меню для узла Installer1.cs и выполните команду View Code
  • Заполните файл Installer1.cs проекта SimpleServer следующим кодом настроек регистрации Службы (приводится полностью)
  • using System;
    using System.ComponentModel;
    using System.Configuration.Install;
    using System.ServiceProcess;
        
    namespace SimpleServer
    {
        [RunInstaller(true)]// Во время установки сборки следует вызвать установщик
        public partial class Installer1 : Installer
        {
            private ServiceInstaller serviceInstaller;
            private ServiceProcessInstaller serviceProcessInstaller;
        
            public Installer1()
            {
                // Создаем настройки для Службы
                serviceInstaller = new ServiceInstaller();
                serviceProcessInstaller = new ServiceProcessInstaller();
        
                // Имя Службы для машины и пользователя
                serviceInstaller.ServiceName = "SimpleServerServiceName";
                serviceInstaller.DisplayName = "SimpleServer";
                serviceInstaller.StartType = ServiceStartMode.Manual;// Запуск вручную
        
                // Как будет запускаться Служба
                this.serviceProcessInstaller.Account = ServiceAccount.LocalService;
                this.serviceProcessInstaller.Password = null;
                this.serviceProcessInstaller.Username = null;
        
                // Добавляем настройки в коллекцию текущего объекта
                this.Installers.AddRange(
                    new Installer[]         
                    {
                        serviceInstaller,
                        serviceProcessInstaller
                    });
            }
        }
    }
  • Через панель Solution Explorer в узле Service1.cs удалите подчиненный файл Service1.Designer.cs за ненадобностью, поскольку компоненты в этом примере мы использовать не будем
  • В панели Solution Explorer вызовите контекстное меню для узла Service1.cs и выполните команду View Code
  • Заполните файл Service1.cs проекта SimpleServer следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.ServiceProcess;
    using System.Collections;
        
    namespace SimpleServer
    {
        class Service1 : ServiceBase
        {
            TcpListener server = null;// Ссылка на сервер
            int port = 12000;
            String hostName = "127.0.0.1";// local
            IPAddress localAddr;
            String[] answers = {
                                   "1. Ты кто?",
                                   "2. Привет, Лелик!",
                                   "3. Лучше всех!",
                                   "4. Конечно, на полную катушку",
                                   "5. До вечера!"
                               };
        
            // Конструктор
            public Service1()
            {
                localAddr = IPAddress.Parse(hostName);// Конвертируем в другой формат
        
                Thread thread = new Thread(ExecuteLoop);
                thread.IsBackground = true;
                thread.Start();
            }
        
            private void ExecuteLoop()
            {
                try
                {
                    server = new TcpListener(localAddr, port);// Создаем сервер-слушатель
                    server.Start();// Запускаем сервер
        
                    String data;
        
                    // Бесконечный цикл прослушивания клиентов
                    while (true)
                    {
                        if (!server.Pending())// Очередь запросов пуста
                            continue;
                        TcpClient client = server.AcceptTcpClient();// Текущий клиент
                        // Сами задаем размеры буферов обмена (Необязательно!)
                        // По умолчанию оба буфера установлены размером по 8192 байта
                        client.SendBufferSize = client.ReceiveBufferSize = 1024;
        
                        // Подключаем NetworkStream и погружаем для удобства в оболочки
                        NetworkStream streamIn = client.GetStream();
                        NetworkStream streamOut = client.GetStream();
                        StreamReader readerStream = new StreamReader(streamIn);
                        StreamWriter writerStream = new StreamWriter(streamOut);
        
                        // Читаем запрос
                        data = readerStream.ReadLine();
                        // Отправляем ответ
                        int index;
                        if (int.TryParse(data.Substring(0, data.IndexOf('.')), out index))
                            data = answers[index - 1];
                        else
                            data = data.ToUpper();
                        writerStream.WriteLine(data);
                        writerStream.Flush();
        
                        // Закрываем соединение и потоки, порядок неважен
                        client.Close();
                        readerStream.Close();
                        writerStream.Close();
                    }
                }
                catch (SocketException)
                {
                }
                finally
                {
                    // Останавливаем сервер
                    server.Stop();
                }
            }
        }
    }

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

    На другой стороне соединения сервер TcpListener в бесконечном цикле прослушивает очередь соединений с клиентами. Если какой-то клиент с ним соединился ( server.Pending()!=false ), то сервер извлекает этого клиента методом AcceptTcpClient() - создает сокет для приема/передачи с готовым обратным адресом, создает двунаправленный поток (или два однонаправленных), затем читает запрос и передает ответ.

  • Откомпилируйте проект командой Build/Build Solution
  • В меню Пуск/Выполнить введете в текстовое поле имя запускаемой программы cmd и откройте окно команд Windows
  • Следующей командой перейдите в каталог с утилитой InstallUtil.exe >cd C:\Windows\Microsoft.NET\Framework\v2.0.50727
  • В панели Solution Explorer выделите проект SimpleServer, щелкните на пиктограмме Show All Files, раскройте узел bin/Debug и командой Copy контекстного меню скопируйте полное имя сборки E:\Tmp\Stream\NetworkStream\SimpleServer\bin\Debug\SimpleServer.exe
  • В окне команд Windows введите команду >InstallUtil.exe и добавьте мышью скопированное имя сборки, должно получиться >InstallUtil.exe E:\Tmp\Stream\NetworkStream\SimpleServer\bin\Debug\SimpleServer.exe
  • После выполнения этой команды откройте окно Пуск/Настройка/Панель управления/Администрирование/Службы (или Диспетчер задач жестом Ctrl-Alt-Del) и запустите программу SimpleServer
  • Запустите проект SimpleClient - все работает и результат такой
  • Через Проводник Windows зайдите в каталог SimpleClient\bin\Debug размещения сборки SimpleClient.exe и запустите несколько копий приложения (несколько клиентов) - все они работают нормально
  • Разберитесь в файле Service1.cs с кодом построенной нами серверной программы, сравните ее с клиентской частью
  • Остановите службу SimpleServer и разрегистрируйте ее, как мы это делали в предыдущем примере
  • Обратите внимание, что если код работы нашей серверной программы не упаковать в отдельную нить Thread (поток выполнения), то в окне служб эта программа операционной системой запускаться не будет (попробуйте!). Причина в том, что в коде метода ExecuteLoop() в сервере используется бесконечный цикл прослушивания очереди запросов клиентов. Если этот цикл оставить в основном потоке выполнения ( Thread ) приложения, то оно просто зациклится и не сможет само нормально завершиться. Поэтому код с циклом мы помещаем в отдельный поток (трэд) и делаем его фоновым, чтобы он закрывался вместе с основным потоком приложения (трэдом сервера).

    Важное замечание

    Поток NetworkStream является двухсторонним фиксированной длины. Методом GetStream() он только устанавливает адресное соединение между сокетами клиента и сервера. Но реальная его длина определяется сообщением отправляющей стороны. Можно для приема/передачи использовать один поток, но тогда длина сообщения, отправляемого сервером, не должна превышать длину сообщения, принятого им от клиента (чуть глаза не отсидел!). Поэтому мы и используем на каждой стороне два потока для раздельной однонаправленной передачи между двумя узлами сетевого соединения.

    Пример 3. Клиент-серверное приложение просмотра рисунков из БД

    На предыдущем простом примере мы познакомились (чуть-чуть) с пронципами создания сетевых приложений. А теперь построим более сложный пример, когда клиент запрашивает рисунки, а сервер извлекает их из хранилища и посылает клиенту. В Упражнении 7 нами было разработано три разных хранилища рисунков и три программы просмотра. В данном примере воспользуемся БД Pictures.my2.mdb с готовыми рисунками и на ее основе создадим сетевое приложение (для тех, кто не делал Упражнение 7, БД прилагается в каталоге Source/Data ).

    Построение клиента

    Для клиента построим оконное приложение типа WPF с пользовательским интерфейсом, частично заимствованным из Примера 6 Упражнения 7.

  • Командой File/Add/New Project добавьте к решению NetworkStream новый проект с именем PicturesClientDB типа WPF и назначьте его стартовым
  • Заполните дескрипторную часть окна Window1 (файл Window1.xaml ) следующим кодом (приводится полностью)
  • <Window x:Class="PicturesClientDB.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Клиентское приложение просмотра рисунков из БД " 
        Height="300"
        Width="470"
        MinHeight="300"
        MinWidth="450" 
        xmlns:local="clr-namespace:PicturesClientDB"
        >
        <Window.Resources>
            <local:Pictures x:Key="promptImage" />
        </Window.Resources>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="110" />
            </Grid.ColumnDefinitions>
            <Border Name="border"
                    Background="{Binding Source={StaticResource promptImage},
                    Path=Picture, Mode=OneWay}">
                <Viewbox Name="Prompt">
                    <Border 
                        BorderBrush="Black" 
                        BorderThickness="5" 
                        Background="Red" Margin="15,0">
                        <TextBlock
                            TextAlignment="Center"
                            FontWeight="Bold"
                            Padding="5,5"
                            >
                        <TextBlock.Foreground>
                            <SolidColorBrush Color="Yellow" />
                        </TextBlock.Foreground>
                        Сервер не готов, ждите!
                        <LineBreak />
                        Мы пытаемся связаться,
                        <LineBreak />
                        извините за неудобства...
                        </TextBlock>
                    </Border>
                </Viewbox>
            </Border>
            <ListBox Name="listBox" Grid.Column="1" Padding="5,0"
                     ScrollViewer.VerticalScrollBarVisibility="Visible"
                     SelectionChanged="listBox_SelectionChanged">
            </ListBox>
        </Grid>
    </Window>

    Для вывода заставки с текстом о неготовности сервера мы применили элемент Viewbox, в который поместили еще один элемент Border с текстовым содержимым. Такой 'огород' позволит увеличивать заставку пропорционально размеру окна. Однако введение элемента Viewbox начинает заметно притормаживать перерисовку интерфейса при перемещениях окна, потому что он пытается постоянно пересчитывать масштабы своих дочерних элементов. Имена мы присвоили только тем интерфейсным элементам, которыми собираемся управлять в процедурном коде.

  • Заполните файл Window1.xaml.cs следующим процедурным кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Data;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    using System.Windows.Media.Imaging;
    using System.Windows.Shapes;
        
    // Дополнительные пространства имен для Stream
    using System.IO;
    using IO = System.IO;               // Псевдоним для адресации Path
    using System.Windows.Threading;     // Для DispatcherTimer
        
    // Дополнительные пространства имен для Socket
    //using System.Net;
    using System.Net.Sockets;
    using System.Collections;   // List<byte>
        
    namespace PicturesClientDB
    {
        public partial class Window1 : Window
        {
            int port = 12000;
            String hostName = "127.0.0.1";  // local
            TcpClient client = null;        // Ссылка на клиента
            String sendMessage = "!!!GetNames!!!";    // Запрос на список (позаковыристей)
            Char[] separator = { '#' };     // Для преобразования ответа в массив имен
            DispatcherTimer timer;          // Таймер  
        
            // Конструктор
            public Window1()
            {
                InitializeComponent();
        
                // Создаем и запускаем таймер
                timer = new DispatcherTimer();
                timer.Tick += new EventHandler(timer_Tick);
                timer.Interval = TimeSpan.FromSeconds(1);
                timer.Start();
            }
        
            // Инициирует обращение к серверу
            void timer_Tick(object sender, EventArgs e)
            {
                Execute(listBox);
            }
        
            private void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
            {
                Execute((ListBox)sender);
            }
        
            void Execute(ListBox lst)
            {
                // Заполняем список именами рисунков
                try
                {
                    // Если сервер доступен, создаем клиента
                    client = new TcpClient(hostName, port);
                }
                catch
                {
                    // Сервер не готов, запускаем таймер и выходим
                    if (Prompt.Visibility != Visibility.Visible)
                    {
                        Prompt.Visibility = Visibility.Visible;
                        timer.Start();
                    }
                    return;
                }
        
                switch (sendMessage)
                {
                    case "!!!GetNames!!!":
                        // Получаем и привязываем имена рисунков к списку
                        lst.ItemsSource = GetNames();
                        // Выделяем первый элемент списка, чтобы вызвать SelectionChanged
                        lst.SelectedIndex = 0;
                        lst.Focus();
                        sendMessage = "";
                        break;
                    default:
                        // Скрываем сообщение и останавливаем таймер
                        if (Prompt.Visibility == Visibility.Visible)
                        {
                            Prompt.Visibility = Visibility.Hidden;
                            timer.Stop();
                        }
        
                        // Получаем рисунок и отображаем пользователю кистью
                        String name = lst.SelectedValue.ToString();
                        BitmapImage bi = new BitmapImage();
                        bi.BeginInit();
                        // Получаем от сервера рисунок и обертываем его в поток памяти
                        bi.StreamSource = new MemoryStream(GetPicture(name));
                        bi.EndInit();
                        Pictures.picture.ImageSource = bi;// Передаем рисунок фону Border
                        break;
                }
            }
        
            private String[] GetNames()
            {
                String[] names;
        
                // Создаем потоки сетевых соединений
                StreamReader readerStream = new StreamReader(client.GetStream());
                StreamWriter writerStream = new StreamWriter(client.GetStream());
                // Отсылаем запрос серверу
                writerStream.WriteLine(sendMessage);
                writerStream.Flush();
        
                // Читаем ответ
                String receiverData = readerStream.ReadLine();
                names = receiverData.Split(separator);// Преобразуем в строковый массив
        
                // Закрываем соединение и потоки, порядок неважен
                client.Close();
                writerStream.Close();
                readerStream.Close();
        
                return names;
            }
        
            Byte[] GetPicture(String name)
            {
                // Создаем потоки сетевых соединений
                NetworkStream readerStream = client.GetStream();
                StreamWriter writerStream = new StreamWriter(client.GetStream());
        
                // Отсылаем запрос серверу
                writerStream.WriteLine(name);
                writerStream.Flush();
        
                // Читаем ответ 
                // ReceiveBufferSize - размер буфера для входящих данных
                // SendBufferSize - размер буфера для исходящих данных
                List<byte> list = new List<byte>(client.ReceiveBufferSize);// С приращением capacity
                Byte[] bytes = new Byte[client.ReceiveBufferSize]; // Размер буфера сокета
                int count = 0;  // Порции входящих данных
                while ((count = readerStream.Read(bytes, 0, bytes.Length)) != 0)
                    for (int i = 0; i < count; i++)
                        list.Add(bytes[i]);
        
                // Преобразуем в массив результата
                bytes = new Byte[list.Count];
                list.CopyTo(bytes);
        
                // Закрываем соединение и потоки, порядок неважен
                client.Close();
                writerStream.Close();
                readerStream.Close();
        
                return bytes;
            }
        }
        
        // Для привязки к ресурсу
        class Pictures
        {
            // Поле
            public static ImageBrush picture = new ImageBrush();
        
            static Pictures()
            {
                // Дежурный рисунок заставки
                picture.ImageSource = new BitmapImage(
                    new Uri(@"flower2.jpg", UriKind.Relative));
                picture.Stretch = Stretch.Fill;
                picture.Opacity = 1.0D;
            }
        
            // Привязываемое в интерфейсе свойство
            public static ImageBrush Picture { get { return picture; } }
        }
    }

    Обратите внимание, что при отображении рисунков мы отказались от традиционного элемента Image, как это делали в предыдущем упражнении. А для разнообразия поступили совершенно нетрадиционно (по турецки). Теперь мы рисунки будем отображать кистью ImageBrush в фоне прямоугольника Border через привязанный к нему объект Pictures. Конечно, в жизни так извращаться вряд ли придется, но и такой вариант где-нибудь может пригодиться.

  • В панели Solution Explorer контекстной командой Add/Existing Item добавьте в корень проекта из прилагаемой к лабораторной работе папки Source/Images рисунок для заставки flower2.jpg, проверьте его свойства
  • Build Action=None
  • Copy to Output Directory=Copy if newer
  • Запустите приложение - пока получилась пустое окно с заставкой и траурным сообщением, что сервер не готов
  • Заставка появится сразу же, как будет обнаружен факт отсутствия или остановки сервера. А после обнаружения сервера заставка исчезнет. Этот механизм немедленно сработает благодаря используемому нами системному таймеру. Однако, сервера пока еще совсем нет и следует его изготовить.

    Построение сервера БД как службу

  • Командой File/Add/New Project добавьте к решению NetworkStream новый проект с именем PicturesServerDB типа Windows Service
  • Закройте окно конструктора сервисов и через панель Solution Explorer в узле Service1.cs удалите подчиненный файл Service1.Designer.cs за ненадобностью, поскольку компоненты в этом примере мы использовать не будем
  • В панели Solution Explorer вызовите контекстное меню для узла Service1.cs и выполните команду View Code
  • Заполните файл Service1.cs следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Diagnostics;
    using System.ServiceProcess;
    using System.Text;
        
    // Дополнительные пространства имен для ADO.NET
    using System.Data.OleDb;
    using System.Data.Common;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.Collections;
        
    namespace PicturesServerDB
    {
        public partial class Service1 : ServiceBase
        {
            int port = 12000;
            String hostName = "127.0.0.1";  // local
            IPAddress localAddr;
            TcpListener server = null;      // Ссылка на сервер
            String separator = "#";         // Разделитель имен в строке ответа
            String connectionString;        // Строка соединения с БД
        
            public Service1()
            {
                // Извлекаем в поле строку соединения с БД из файла App.config
                connectionString = System.Configuration.ConfigurationManager.
                    ConnectionStrings["PicturesDB"].ConnectionString;
        
                // Конвертируем IP в другой формат
                localAddr = IPAddress.Parse(hostName);
        
                // Запускаем в новом потоке (ните)
                Thread thread = new Thread(ExecuteLoop);
                thread.IsBackground = true;
                thread.Start();
            }
        
            private void ExecuteLoop()
            {
                try
                {
                    server = new TcpListener(localAddr, port);// Создаем сервер-слушатель
                    server.Start();// Запускаем сервер
        
                    // Бесконечный цикл прослушивания клиентов
                    while (true)
                    {
                        // Проверяем очередь соединений
                        if (!server.Pending())// Очередь запросов пуста
                            continue;
                        TcpClient client = server.AcceptTcpClient();// Текущий клиент
        
                        // Создаем потоки сетевых соединений
                        StreamReader readerStream = new StreamReader(client.GetStream());
                        NetworkStream streamOut = client.GetStream();
                        StreamWriter writerStream = new StreamWriter(streamOut);
        
                        // Читаем команду клиента 
                        String receiverData = readerStream.ReadLine();
        
                        // Распознаем и исполняем
                        switch (receiverData)
                        {
                            case "!!!GetNames!!!":// Посылаем имена, разделенные сепаратором
                                String names = GetNames();
                                writerStream.WriteLine(names);  // Используем через оболочку
                                writerStream.Flush();
                                break;
                            default:// Посылаем рисунок
                                Byte[] bytes = GetPicture(receiverData);
                                streamOut.Write(bytes, 0, bytes.Length);// Используем напрямую
                                streamOut.Flush();
                                break;
                        }
        
                        // Закрываем соединение и потоки, порядок неважен
                        client.Close();
                        readerStream.Close();
                        writerStream.Close();
                    }
                }
                finally
                {
                    // Останавливаем сервер
                    server.Stop();
                }
            }
        
            // Извлечение из БД имен рисунков и упаковка их в одну строку для пересылки клиенту
            string GetNames()
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection(connectionString);
                OleDbCommand cmd = new OleDbCommand("SELECT FileName FROM MyTable");
                cmd.Connection = conn;
                conn.Open();
        
                // Извлекаем имена рисунков
                OleDbDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
        
                // Формируем строку исходящих данных
                StringBuilder sb = new StringBuilder();
                foreach (DbDataRecord record in reader)// Равносильно чтению reader.Read()
                    sb.Append(((string)record["FileName"]).Trim() + separator);
                // Соединение здесь закроет сам объект DataReader после прочтения всех данных
                // в соответствии с соглашением при его создании CommandBehavior.CloseConnection
        
                // Удаляем лишний последний символ сепаратора
                sb.Replace(separator, String.Empty, sb.ToString().
                    LastIndexOf(separator), separator.Length);
        
                return sb.ToString();
            }
        
            // Извлечение из БД самого рисунка для отправки клиенту
            byte[] GetPicture(String name)
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection();
                conn.ConnectionString = connectionString;
        
                // Создаем и настраиваем объект команды, параметризованной по имени рисунка
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = conn;
                cmd.CommandType = CommandType.Text; // Необязательно! Установлено по умолчанию
                cmd.CommandText = "SELECT Picture FROM MyTable WHERE FileName=?";
                cmd.Parameters.Add(new OleDbParameter());
                cmd.Parameters[0].Value = name;// Имя рисунка
                OleDbDataAdapter adapter = new OleDbDataAdapter(cmd);
        
                // Извлекаем рисунок из БД 
                DataTable table = new DataTable();
                adapter.Fill(table);
                byte[] bytes = (byte[])table.Rows[0]["Picture"]; // Подключаемся к рисунку
        
                return bytes;
            }
        }
    }
  • В панели Solution Explorer контекстной командой Add/New Item для корня проекта добавьте в него конфигурационный файл App.config
  • Заполните файл App.config следующим кодом (приводится полностью)
  • <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
      <connectionStrings>
        <add name="PicturesDB"
            connectionString="Provider=Microsoft.Jet.OLEDB.4.0;
            Jet OLEDB:Engine Type=5;
            Data Source=|DataDirectory|\Data\Pictures.my2.mdb"
            providerName="System.Data.OleDb" />
      </connectionStrings>
    </configuration>
  • В панели Solution Explorer контекстной командой Add/References для узла References добавьте к проекту ссылку на библиотеку System.Configuration.dll
  • В панели Solution Explorer контекстной командой Add/New Folder для корня проекта добавьте новую папку с именем Data
  • В панели Solution Explorer контекстной командой Add/New Item для папки Data добавьте в нее ранее созданную базу данных Pictures.my2.mdb (или возьмите готовую из прилагаемой к лабораторной работе папки Source/Data ). На предложение мастера создать для БД типизированный DataSet ответьте Cancel
  • Выделите базу данных Pictures.my2.mdb и в панели Properties установите (или проверьте) ее свойства, которые должны быть такими
  • Build Action=None
  • Copy to Output Directory=Copy if newer
  • Теперь создадим для сервиса инсталляционный файл.

  • В панели Solution Explorer вызовите на узле PicturesServerDB проекта контекстное меню и командой Add/New Item в появишемся окне мастера выберите элемент Installer Class
  • Закройте окно графического конструктора и через панель Solution Explorer в узле Installer1.cs удалите подчиненный файл Installer1.Designer.cs за ненадобностью
  • В панели Solution Explorer вызовите контекстное меню для узла Installer1.cs и выполните команду View Code
  • Заполните файл Installer1.cs следующим кодом настроек регистрации сервиса (приводится полностью)
  • using System;
    using System.ComponentModel;
    using System.Configuration.Install;
    using System.ServiceProcess;
        
    namespace SimpleServer
    {
        [RunInstaller(true)]// Во время установки сборки следует вызвать установщик
        public partial class Installer1 : Installer
        {
            private ServiceInstaller serviceInstaller;
            private ServiceProcessInstaller serviceProcessInstaller;
        
            public Installer1()
            {
                // Создаем настройки для Службы
                serviceInstaller = new ServiceInstaller();
                serviceProcessInstaller = new ServiceProcessInstaller();
        
                // Имя Службы для машины и пользователя
                serviceInstaller.ServiceName = "PicturesServerDB";
                serviceInstaller.DisplayName = "PicturesServerDB";
                serviceInstaller.StartType = ServiceStartMode.Manual;// Запуск вручную
        
                // Как будет запускаться Служба
                this.serviceProcessInstaller.Account = ServiceAccount.LocalService;
                this.serviceProcessInstaller.Password = null;
                this.serviceProcessInstaller.Username = null;
        
                // Добавляем настройки в коллекцию текущего объекта
                this.Installers.AddRange(
                    new Installer[]         
                    {
                        serviceInstaller,
                        serviceProcessInstaller
                    });
            }
        }
    }
  • Откомпилируйте проект командой Build/Build Solution
  • Теперь нужно зарегистрировать созданный сервис (для нас - сервер) в операционной системе.

  • В меню Пуск/Выполнить введете в текстовое поле имя запускаемой программы cmd и откройте окно команд Windows
  • Следующей командой перейдите в каталог с утилитой InstallUtil.exe >cd C:\Windows\Microsoft.NET\Framework\v2.0.50727
  • В панели Solution Explorer выделите узел PicturesServerDB, щелкните на пиктограмме Show All Files, раскройте узел bin/Debug и скопируйте полное имя созданной сборки E:\Tmp\Stream\NetworkStream\PicturesServerDB\bin\Debug\PicturesServerDB.exe
  • В окне команд Windows введите команду >InstallUtil.exe и добавьте мышью скопированное имя сборки, должно получиться >InstallUtil.exe E:\Tmp\Stream\NetworkStream\PicturesServerDB\bin\Debug\PicturesServerDB.exe
  • После выполнения этой команды откройте окно Пуск/Настройка/Панель управления/Администрирование/Службы (или жестом Ctrl-Alt-Del запустите Диспетчер задач Windows и откройте вкладку Службы) и запустите созданный сервис (для нас - сервер)
  • Запустите одного или нескольких клиентов PicturesClientDB.exe и испытайте работу полученного клиент-серверного приложения
  • Понаблюдайте за поведением клиентов при отключении и включении серверной программы, разберитесь с кодом (по возможности)
  • Вот одна из картинок, когда работа клиента (или всех клиентов) была временно прекращена остановкой сервера

    Чтобы созданный сервис не загружал компьютер, его можно деинсталлировать той же утилитой InstallUtil.exe, только с опцией /u.

    В последнем примере мы немного перегорячились и сразу стали делать сервер как службу Windows, чтобы не было видно пользовательского интерфейса (даже консольного окна). Это совершенно необязательно, сервер можно сделать и как обычное приложение с интерфейсом пользователя и запускать его по мере необходимости. Главное, что нужно помнить и что послужило причиной создания нами сервера как службы, - на одном IP -адресе не может сидеть сразу несколько запущенных серверов. Иначе бы получилась неопределенность, с каким именно сервером должны общаться клиенты. А службы как раз и запускаются в одном экземпляре.

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

    Пример 4. Создание сервера БД как Console Application

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

  • Добавьте к решению командой File/Add/New Project консольное приложение с именем ConsoleServerDB
  • В панели Solution Explorer в корень нового проекта скопируйте из предыдущего проекта PicturesServerDB файл App.config и папку Data. На предложение мастера создать типизированный DataSet скажите Cancel
  • В панели Solution Explorer через контекстное меню для узла References добавьте к проекту ссылку на System.Configuration.dll
  • Откройте файл Program.cs проекта консольного приложения и наполните его следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен для ADO.NET
    using System.Data;
    using System.Data.OleDb;
    using System.Data.Common;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.Collections;
    using System.Configuration;
        
    namespace ConsoleServerDB
    {
        class Program
        {
            int port = 12000;
            String hostName = "127.0.0.1";  // local
            IPAddress localAddr;
            TcpListener server = null;      // Ссылка на сервер
            String separator = "#";     // Разделитель имен в строке ответа
            String connectionString;        // Строка соединения с БД
        
            // Добавили конструктор
            public Program()
            {
                // Извлекаем в поле строку соединения с БД из файла App.config
                connectionString = System.Configuration.ConfigurationManager.
                    ConnectionStrings["PicturesDB"].ConnectionString;
        
                // Конвертируем IP в другой формат
                localAddr = IPAddress.Parse(hostName);
        
                // Запускаем в новом потоке (ните)
                Thread thread = new Thread(ExecuteLoop);
                thread.IsBackground = true;
                thread.Start();
            }
        
            static void Main(string[] args)
            {
                Console.WindowWidth = 20;
                Console.WindowHeight = 5;
                new Program();
        
                Console.ReadLine();// Держим основной поток (нить) приложения
            }
        
            private void ExecuteLoop()
            {
                try
                {
                    server = new TcpListener(localAddr, port);// Создаем сервер-слушатель
                    server.Start();// Запускаем сервер
        
                    // Бесконечный цикл прослушивания клиентов
                    while (true)
                    {
                        // Проверяем очередь соединений
                        if (!server.Pending())// Очередь запросов пуста
                            continue;
                        TcpClient client = server.AcceptTcpClient();// Текущий клиент
        
                        // Создаем потоки сетевых соединений
                        StreamReader readerStream = new StreamReader(client.GetStream());
                        NetworkStream streamOut = client.GetStream();
                        StreamWriter writerStream = new StreamWriter(streamOut);
        
                        // Читаем команду клиента 
                        String receiverData = readerStream.ReadLine();
        
                        // Распознаем и исполняем
                        switch (receiverData)
                        {
                            case "!!!GetNames!!!":// Посылаем имена, разделенные сепаратором
                                String names = GetNames();
                                writerStream.WriteLine(names);  // Используем через оболочку
                                writerStream.Flush();
                                break;
                            default:// Посылаем рисунок
                                Byte[] bytes = GetPicture(receiverData);
                                streamOut.Write(bytes, 0, bytes.Length);// Используем напрямую
                                streamOut.Flush();
                                break;
                        }
        
                        // Закрываем соединение и потоки, порядок неважен
                        client.Close();
                        readerStream.Close();
                        writerStream.Close();
                    }
                }
                finally
                {
                    // Останавливаем сервер
                    server.Stop();
                }
            }
        
            // Извлечение из БД имен рисунков и упаковка их в одну строку для пересылки клиенту
            string GetNames()
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection(connectionString);
                OleDbCommand cmd = new OleDbCommand("SELECT FileName FROM MyTable");
                cmd.Connection = conn;
                conn.Open();
        
                // Извлекаем имена рисунков
                OleDbDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
        
                // Формируем строку исходящих данных
                StringBuilder sb = new StringBuilder();
                foreach (DbDataRecord record in reader)// Равносильно чтению reader.Read()
                    sb.Append(((string)record["FileName"]).Trim() + separator);
                // Соединение здесь закроет сам объект DataReader после прочтения всех данных
                // в соответствии с соглашением при его создании CommandBehavior.CloseConnection
        
                // Удаляем лишний последний символ сепаратора
                sb.Replace(separator, String.Empty, sb.ToString().
                    LastIndexOf(separator), separator.Length);
        
                return sb.ToString();
            }
        
            // Извлечение из БД самого рисунка для отправки клиенту
            byte[] GetPicture(String name)
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection();
                conn.ConnectionString = connectionString;
        
                // Создаем и настраиваем объект команды, параметризованной по имени рисунка
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = conn;
                cmd.CommandType = CommandType.Text; // Необязательно! Установлено по умолчанию
                cmd.CommandText = "SELECT Picture FROM MyTable WHERE FileName=?";
                cmd.Parameters.Add(new OleDbParameter());
                cmd.Parameters[0].Value = name;// Имя рисунка
                OleDbDataAdapter adapter = new OleDbDataAdapter(cmd);
        
                // Извлекаем рисунок из БД 
                DataTable table = new DataTable();
                adapter.Fill(table);
                byte[] bytes = (byte[])table.Rows[0]["Picture"]; // Подключаемся к рисунку
        
                return bytes;
            }
        }
    }

    Приведенный код в точности соответствует серверу, созданному нами ранее как сервис, только дополнительно добавилась точка входа Main() и класс стал называться по другому.

  • Откомпилируйте проект командой Build/Build Solution
  • Через Проводник Windows зайдите в каталоги bin/Debug проектов ConsoleServerDB и PicturesClientDB
  • Для удобства запуска создайте для исполнимых сборок ConsoleServerDB.exe и PicturesClientDB.exe ярлыки на рабочем столе компьютера, попробуйте запустить несколько копий каждого из приложений
  • Мы видим, что копии клиентских приложений запускаются нормально, а для консольного сервера дублирующие копии вылетают с выбросом исключения операционной системы. А если запустить ранее созданный сервисный сервер (в Службах Windows ), то консольный сервер будет вылетать даже при первом запуске.

  • Поэкспериментируйте с построенными приложениями, осмыслите происходящее и разберитесь с кодом
  • Пример 5. Создание сервера БД как Windows Forms

    Чтобы показать, что сервером может выступать приложение любого типа, повторим предыдущую задачу на приложении с оконным интерфейсом. Учитывая, что в сети прослушивать клиентов по одному и тому же адресу может только один запущенный экземпляр сервера, заодно покажем, как блокировать повторный его запуск. Нового здесь будет мало, только создадим оконную заготовку приложения Windows Forms и просто скопируем код сервера в это приложение, изменив самую малость. А потом еще добавим в файл Program.cs код блокирования повторного запуска, - вот и все.

    Создание оконного сервера

  • Добавьте к проекту командой File/Add/New Project оболочки новый проект с именем WindowServerDB (стартовым его не делайте)
  • В панели Solution Explorer вызовите для узла WindowServerDB контекстное меню и выполните команду Build
  • В Проводнике Windows зайдите в каталог исполнимой сборки проекта bin\Debug и отправьте ярлык исполнимой сборки WindowServerDB.exe на рабочий стол контекстной командой Отправить
  • Заполните файл Form1.cs следующим кодом (приводится полностью и почти в точности повторяет код файла Program.cs проекта ConsoleServerDB )
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен для ADO.NET
    using System.Data.OleDb;
    using System.Data.Common;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.Collections;
    using System.Configuration;
        
    namespace WindowServerDB
    {
        public partial class Form1 : Form
        {
            int port = 12000;
            String hostName = "127.0.0.1";  // local
            IPAddress localAddr;
            TcpListener server = null;      // Ссылка на сервер
            String separator = "#";     // Разделитель имен в строке ответа
            String connectionString;        // Строка соединения с БД
        
            public Form1()
            {
                InitializeComponent();
                // Извлекаем в поле строку соединения с БД из файла App.config
                connectionString = System.Configuration.ConfigurationManager.
                    ConnectionStrings["PicturesDB"].ConnectionString;
        
                // Конвертируем IP в другой формат
                localAddr = IPAddress.Parse(hostName);
        
                // Запускаем в новом потоке (ните)
                Thread thread = new Thread(ExecuteLoop);
                thread.IsBackground = true;
                thread.Start();
            }
        
            private void ExecuteLoop()
            {
                try
                {
                    server = new TcpListener(localAddr, port);// Создаем сервер-слушатель
                    server.Start();// Запускаем сервер
        
                    // Бесконечный цикл прослушивания клиентов
                    while (true)
                    {
                        // Проверяем очередь соединений
                        if (!server.Pending())// Очередь запросов пуста
                            continue;
                        TcpClient client = server.AcceptTcpClient();// Текущий клиент
        
                        // Создаем потоки сетевых соединений
                        StreamReader readerStream = new StreamReader(client.GetStream());
                        NetworkStream streamOut = client.GetStream();
                        StreamWriter writerStream = new StreamWriter(streamOut);
        
                        // Читаем команду клиента 
                        String receiverData = readerStream.ReadLine();
        
                        // Распознаем и исполняем
                        switch (receiverData)
                        {
                            case "!!!GetNames!!!":// Посылаем имена, разделенные сепаратором
                                String names = GetNames();
                                writerStream.WriteLine(names);  // Используем через оболочку
                                writerStream.Flush();
                                break;
                            default:// Посылаем рисунок
                                Byte[] bytes = GetPicture(receiverData);
                                streamOut.Write(bytes, 0, bytes.Length);// Используем напрямую
                                streamOut.Flush();
                                break;
                        }
        
                        // Закрываем соединение и потоки, порядок неважен
                        client.Close();
                        readerStream.Close();
                        writerStream.Close();
                    }
                }
                finally
                {
                    // Останавливаем сервер
                    server.Stop();
                }
            }
        
            // Извлечение из БД имен рисунков и упаковка их в одну строку для пересылки клиенту
            string GetNames()
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection(connectionString);
                OleDbCommand cmd = new OleDbCommand("SELECT FileName FROM MyTable");
                cmd.Connection = conn;
                conn.Open();
        
                // Извлекаем имена рисунков
                OleDbDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);
        
                // Формируем строку исходящих данных
                StringBuilder sb = new StringBuilder();
                foreach (DbDataRecord record in reader)// Равносильно чтению reader.Read()
                    sb.Append(((string)record["FileName"]).Trim() + separator);
                // Соединение здесь закроет сам объект DataReader после прочтения всех данных
                // в соответствии с соглашением при его создании CommandBehavior.CloseConnection
        
                // Удаляем лишний последний символ сепаратора
                sb.Replace(separator, String.Empty, sb.ToString().
                    LastIndexOf(separator), separator.Length);
        
                return sb.ToString();
            }
        
            // Извлечение из БД самого рисунка для отправки клиенту
            byte[] GetPicture(String name)
            {
                // Создаем и настраиваем инфраструктуру ADO.NET
                OleDbConnection conn = new OleDbConnection();
                conn.ConnectionString = connectionString;
        
                // Создаем и настраиваем объект команды, параметризованной по имени рисунка
                OleDbCommand cmd = new OleDbCommand();
                cmd.Connection = conn;
                cmd.CommandType = CommandType.Text; // Необязательно! Установлено по умолчанию
                cmd.CommandText = "SELECT Picture FROM MyTable WHERE FileName=?";
                cmd.Parameters.Add(new OleDbParameter());
                cmd.Parameters[0].Value = name;// Имя рисунка
                OleDbDataAdapter adapter = new OleDbDataAdapter(cmd);
        
                // Извлекаем рисунок из БД 
                DataTable table = new DataTable();
                adapter.Fill(table);
                byte[] bytes = (byte[])table.Rows[0]["Picture"]; // Подключаемся к рисунку
        
                return bytes;
            }
        }
    }
  • В панели Solution Explorer раскройте узел проекта ConsoleServerDB и, удерживая клавишу Ctrl, выделите папку Data и файл App.config, затем перетащите их курсором в корень проекта WindowServerDB. На предложение мастера создать набор данных скажите Cancel
  • Убедитесь, чтобы скопированная база данных Data/Pictures.my2.mdb имела в панели Properties значение:
  • Copy to Output Directory=Copy if newer
  • В панели Solution Explorer выделите любой элемент проекта WindowServerDB (например, корень), откройте через меню оболочки панель View/Object Browser, задайте в ней режим Browse: All Components, а в поле поиска - пространство имен System.Configuration. Для найденного пространства имен выделите родительскую сборку System.configuration.dll и кнопкой с пиктограммой Add to References in Selected Project in Solution Explorer добавьте ссылку в папку References текущего проекта (таким способом можно добавлять сразу несколько ссылок) для видимости компилятором класса ConfigurationManager
  • Откомпилируйте проект командой Rebuild и запустите этот оконный сервер через ярлык рабочего стола компьютера - сервер хоть и с пустым окном, но работает
  • Проверьте работу сервера WindowServerDB.exe совместно с клиентской сборкой PicturesClientDB.exe - все должно работать исправно!
  • Блокирование дублирующего запуска оконного сервера

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

    Не всегда у управляемой среды исполнения CLR хватает своих средств для решения любых задач и в некоторых случаях приходится напрямую обращаться к библиотекам и функциям операционной системы. Для таких целей предусмотрен атрибут DllImport, с помощью которого можно вызвать любую функцию API Windows. Это относится и к нашей задаче - когда требуется блокировать повторный запуск приложения, то можно воспользоваться таблицей глобальных атомов самой операционной системы. А к ней из управляемого кода C# доступ невозможен.

  • Измените код файла Program.cs проекта WindowServerDB на следующий (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.Windows.Forms;
        
    // Дополнительное пространство имен импорта типов API
    using System.Runtime.InteropServices;
        
    namespace WindowServerDB
    {
        static class Program
        {
            [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            static extern ushort GlobalAddAtom(string lpString);
            [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            static extern ushort GlobalFindAtom(string lpString);
            [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            static extern ushort GlobalDeleteAtom(ushort atom);
        
            [STAThread]
            static void Main()
            {
                string strAtom = Application.ProductName
                    + Application.ProductVersion;
                ushort atom = GlobalFindAtom(strAtom);
        
                if (atom != 0)
                {
                    MessageBox.Show("Прежде чем создать новый\n"
                        + "экземпляр этого приложения,\n"
                        + "сначала следует закрыть текущий!");
                }
                else
                {
                    // Атом не найден в таблице атомов, создаем его
                    atom = GlobalAddAtom(strAtom);
        
                    // Настройка приложения 
                    Application.EnableVisualStyles();
                    Application.SetCompatibleTextRenderingDefault(false);
                    try
                    {
                        // Рабочий цикл Windows 
                        Application.Run(new Form1());
                    }
                    finally
                    {
                        // Удаляем атом в любом случае
                        GlobalDeleteAtom(atom);
                    }
                }
            }
        }
    }

    Вопросы взаимодействия CLR с операционной системой, потоки выполнения Thread, атрибуты и многое другое прекрасно описаны в:

    Дубовцев А.В. Microsoft .NET в подлиннике. - СПб.: БХВ-Петербург, 2004. - 704 с.

  • Запустите оконный сервер WindowServerDB и убедитесь, что дублирующий запуск приложения теперь заблокирован
  • Сокрытие окна сервера

    Сейчас нам стыдно смотреть на окно сервера - оно просто пустое. В дальнейшем мы можем его задействовать. Например, для регистрации администратором клиентов, редактирования настроек сервера и другими задачами, связанными с нашим приложением. А пока для тренировки его отображение на экране можно просто отключить: оно есть и работает, просто его нигде не видно (почти нигде). Для этого воспользуемся событием Shown объекта окна.

  • Дополните класс формы в файле Form1.cs следующим кодом
  • namespace WindowServerDB
    {
        public partial class Form1 : Form
        {
            ................................................
        
            public Form1()
            {
                ................................................
        
                // Подписываемся на событие для сокрытия окна
                this.Shown += Form1_Shown;
            }
        
            void Form1_Shown(object sender, EventArgs e)
            {
                this.Hide();
            }
        
            ................................................
        }
    }
  • Откомпилируйте сервер WindowServerDB и запустите через ярлыки его и клиента PicturesClientDB - все работает, но сам сервер невидно и как его остановить!
  • Вызовите жестом Ctrl-Alt-Del Диспетчер задач Windows, откройте вкладку Процессы - сервер работает и его можно даже закрыть
  • Замечания

    Если сейчас закрыть невидимый сервер через Диспетчер задач Windows, то он повторно не запустится - сработает наша блокировка. Причина в том, что в данном случае приложение завершится нестандартно, а именно: не через среду CLR, а напрямую через Диспетчер задач Windows. Соответственно, и атом (наша метка блокировки) в таблице глобальных атомов останется. Он будет удален только при перезагрузке Windows. Здесь нужно выбирать что-то одно: либо оставить блокировку и не скрывать сервер, либо убрать блокировку и сделать сервер невидимым для пользователя.

    Теперь мы знаем, что необязательно делать сервер как Службу Windows ( Windows Service ), чтобы скрыть его от пользователя. Но можно сделать сервер как приложение Windows Forms, пусть даже с пустой формой, скрыть его и включить в список программ, автоматически запускаемых при загрузке Windows. Это уже дело вкуса - наша задача состояла в том, чтобы проверить такую возможность.

    Кстати, консольное приложение не имеет события, подобного событию Shown объекта окна. Оно показывается самой CLR при запуске и скрыть его программно невозможно, хотя мы его и применяли при создании сервера.

    Есть еще один вариант: установить состояние окна при запуске свернутым, тогда его можно завершить через среду CLR естественным образом.

  • Внесите в конструктор формы следующие очевидные изменения
  • namespace WindowServerDB
    {
        public partial class Form1 : Form
        {
            ................................................
        
            public Form1()
            {
                ................................................
        
                // Подписываемся на событие для сокрытия окна
                //this.Shown += Form1_Shown;
                // Минимизируем окно при запуске
                this.WindowState = FormWindowState.Minimized;
            }
        
            void Form1_Shown(object sender, EventArgs e)
            {
                this.Hide();
            }
        
            ................................................
        }
    }
  • Запустите сервер - теперь и блокировка работает, и сервер можно закрыть, и миокард целый!
  • Пример 6. Сетевой обмен сериализованными объектами данных

    Мы уже рассматривали сериализацию объектов данных. Она позволяет легко упаковывать и восстанавливать информацию при передаче ее в потоках. Сейчас мы построим клиент-серверное приложение, на примере которого убедимся, насколько удобным является сетевой обмен данным с использованием сериализации. Нам не надо будет ничего подсчитывать и учитывать, вводя какие-то специальные метки для данных, их типе и длине, все будет гораздо проще. Мы просто создадим объект необходимой нам структуры, тип которого будет известен и серверу, и клиентам, и который будет служить контейнером данных. Этот объект стороны будут наполнять своими данными и отправлять по сети в сериализованной форме (в блестящей и хрустящей упаковке!).

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

    Но клиенты бывают разные. Злонамеренный пользователь (одушевленный клиент) может потребовать от сервера такие действия, которые ему заведомо навредят (например, скомандует удалить или прислать какой-то важный файл). Поэтому естественно, что права у клиентов на команды (на действия) должны быть разными. Защищенный сервер должен регистрировать клиентов, присваивать им статус и при каждом запросе проверять их полномочия на поступившую команду или затребованную информацию. Отсюда и берутся учетные записи, пароли и их шифрование, администраторы и т.д. Стараются защититься как программными методами, так и аппаратно.

    До этого момента нашей главной задачей было определение типа пересылаемых данных, чтобы правильно использовать инструменты их чтения и записи. Теперь такая задача станет проще, поскольку тип данных будет назначаться при конструировании объекта транспортировки. А сама транспортировка будет выполняться оболочкой сериализации унифицированным образом.

    Чтобы убедительно продемонстрировать сказанное, будем сериализовать и пересылать разнородные типы данных: текст, рисунки, звук и видео. Все это, в конечном итоге, будет представлять собой потоки байтов с хорошо распознаваемыми типами при десериализации. Работа предстоит большая, но чрезвычайно полезная, поэтому прошу ее не игнорировать, а выполнить внимательно. Постараемся код сделать интересным и разнообразным (да простят меня классики!). Итак, приступим...

    Создание объекта транспортировки

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

  • Командой оболочки File/Add/New Project добавьте к решению NetworkStream новый проект с именем MyLibrary типа Class Library
  • Заполните созданный оболочкой файл Class1.cs следующим кодом (приводится полностью)
  • using System;
    using System.IO;
        
    namespace MyLibrary.MyDataSerialization
    {
        /// <summary>
        /// Перечисление команд, передаваемых 
        /// серверу для исполнения на нем
        /// </summary>
        [Serializable()]    // Атрибут
        public enum Command
        {
            /// <summary>Добавить сообщение в журнал</summary>
            AddTextInLog,
            /// <summary>Прислать журнал</summary>
            Log,
            /// <summary>Удалить журнал</summary>
            DeleteLog,
            /// <summary>Прислать сообщение</summary>
            Text,
            /// <summary>Прислать рисунок</summary>
            Image,
            /// <summary>Прислать звук</summary>
            Sound,
            /// <summary>Прислать видео</summary>
            Video
        }
        
        /// <summary>
        /// Сериализуемый объект для транспортировки
        /// </summary>
        [Serializable()]    // Атрибут
        public class DataObject : ICloneable
        {
            // Поля с сохраняемыми данными
            Command command;        // Идентификатор команды
            string displayCommand;  // Представление команды для пользователя
            string text;            // Для передачи текстовых данных
            string logName;         // Имя файла с журналом
            string imageName;       // Имя файла с рисунком
            string soundName;       // Имя файла со звуком
            string videoName;       // Имя файла с видео
            Stream image;           // Сам рисунок
            Stream sound;           // Сам звук
            Stream video;           // Само видео
        
            // Свойства доступа к полям
            public Command Command
            {
                get { return command; }
                set { command = value; }
            }
            // Строковый тип
            public String DisplayCommand
            {
                get { return displayCommand; }
                set { displayCommand = value; }
            }
            public String Text
            {
                get { return text; }
                set { text = value; }
            }
            public String LogName
            {
                get { return logName; }
                set { logName = value; }
            }
            public String ImageName
            {
                get { return imageName; }
                set { imageName = value; }
            }
            public String SoundName
            {
                get { return soundName; }
                set { soundName = value; }
            }
            public String VideoName
            {
                get { return videoName; }
                set { videoName = value; }
            }
            // Потоковый тип
            public Stream Image
            {
                get { return image; }
                set { image = value; }
            }
            public Stream Sound
            {
                get { return sound; }
                set { sound = value; }
            }
            public Stream Video
            {
                get { return video; }
                set { video = value; }
            }
        
            /// <summary>
            /// Оригинальный метод C#
            /// Реализация интерфейса ICloneable для создания копии объекта
            /// </summary>
            public Object Clone()
            {
                // Создаем клон
                DataObject clone = new DataObject();
        
                // Копируем значимые свойства
                clone.command = this.command;
                clone.displayCommand = this.displayCommand;
                clone.text = this.text;
                clone.logName = this.logName;
                clone.imageName = this.imageName;
                clone.soundName = this.soundName;
                clone.videoName = this.videoName;
        
                // Значения ссылок не копируем
                clone.image = null;
                clone.sound = null;
                clone.video = null;
        
                return clone;
            }
        
            /// <summary>
            /// Альтернативный метод создания копии объекта 
            /// на основе шаблона, потока памяти и сериализации.
            /// А он мне нравится, нравится, нравится...!!!!!!!
            /// </summary>
            public T BinaryClone<T>()
            {
                using (var stream = new System.IO.MemoryStream())
                {
                    var binaryFormatter = new System.Runtime.Serialization.
                        Formatters.Binary.BinaryFormatter();
                    binaryFormatter.Serialize(stream, this);
                    stream.Position = 0;
        
                    return (T)binaryFormatter.Deserialize(stream);
                }
            }
        }
    }

    Метод Clone() этого класса является реализацией наследуемого библиотечного интерфейса ICloneable. Этот метод нам понадобится в коде сервера для создания объекта отправки как копии присланного объекта. Интерфейс ICloneable языка C# является альтернативой конструктора копий языка C++. Типизированный метод BinaryClone() является еще более мощной этому заменой (предложен в ' http://weblogs.asp.net/esanchez/archive/2008/05/18/cloning-objects-in-net.aspx'). Он создает копии не только значимых данных, но и копии ссылок вместе с копиями адресуемых этими ссылками данных. И все это - с использованием рассматриваемых нами интруменов. Именно его (но попробуйте и Clone ) мы включим в код сервера.

    Ключевое слово var в описании метода BinaryClone() предназначено для ленивых и означает объявление типа ссылки, определяемого типом адресуемого объекта. На более превычном нам синтаксисе метод должен выглядеть так

    public T BinaryClone<T>()
            {
                using (MemoryStream stream = new System.IO.MemoryStream())
                {
                    BinaryFormatter binaryFormatter = new System.Runtime.Serialization.
                        Formatters.Binary.BinaryFormatter();
                    binaryFormatter.Serialize(stream, this);
                    stream.Position = 0;
        
                    return (T)binaryFormatter.Deserialize(stream);
                }
            }
  • Откомпилируйте проект MyLibrary командой Build - теперь это полноправный библиотечный объект и его можно подключать к любому проекту в узле References
  • Создание клиента транспортировки

    Клиента построим с пользовательским интерфейсом по технике Windows Forms. Сервер должен 'сидеть' на компьютере с данными и по запросам клиентов отправлять им эти данные. Клиенты должны иметь интерфейс для формирования запроса и отображения присланных данных. Для разнообразия, видеоданные будем отображать с помощью плеера из WPF, хотя можно было бы воспользоваться COM -объектом " Windows Media Player ", как мы это делали в лабораторной работе №45. Поскольку в пределах одного окна интерфейсные элементы двух техник ( Windows Forms и WPF ) несовместимы без специального приема, используем для совместимости переходник ElementHost из пространства имен System.Windows.Forms.Integration 'http://msdn.microsoft.com/ru-ru/library/system.windows.forms.integration.elementhost.aspx'

  • Добавьте к решению NetworkStream новый проект с именем SerializeClient и назначьте его стартовым
  • В панели Solution Explorer выделите узел проекта SerializeClient и командой Project/Add Reference добавьте к проекту ссылки на библиотеки:
  • из вкладки .NET для видеоплеера WPF:
  • PresentationCore.dll
  • PresentationFramework.dll
  • WindowsBase.dll
  • WindowsFormsIntegration.dll
  • из вкладки Projects для нашего объекта транспортировки:
  • MyLibrary.dll
  • Настройте элементы формы в соответствии с таблицей свойств
  • Элемент Свойство Значение
    Form Text Simple SerializeClient
    Size 300; 300
    StartPosition CenterScreen
    MaximizeBox False
    Label Text Command:
    Location 3; 16
    Size 75; 17
    AutoSize True
    ComboBox (Name) cbCommand
    Location 79; 13
    Size 201; 24
    Label Text Message:
    Location 9; 53
    Size 69; 17
    AutoSize True
    TextBox (Name) txtMessage
    Location 79; 50
    Size 201; 22
    Label Text Display:
    Location 20; 144
    Size 58; 17
    AutoSize True
    ElementHost Переходник WPF->WindowsForms. Ищите в панели Toolbox на вкладке 'WPF Interoperability', если там нет, добавьте в нее контекстной командой Choose Items или создайте объект динамически в процедурном коде
    (Name) videoHost
    Location 79; 85
    Size 201; 127
    PictureBox (Name) pictureBox
    Location 79; 85
    Size 201; 127
    TextBox (Name) txtReceived
    Location 79; 85
    Size 201; 127
    Multiline True
    Button (Name) btnSend
    AutoSize True
    Location 142; 228
    Size 75; 27
    Text Send

    В итоге, пользовательский интерфейс формы должен выглядеть так

    Обратите внимание, что мы совместили позиции элементов ElementHost, PictureBox и TextBox, причем видимым на верхнем слое будет элемент TextBox, добавленный на форму последним. Естественно, что из них одновременно мы будем показывать только один элемент, делая в коде остальные невидимыми.

  • Заполните файл Form1.cs клиента SerializeClient следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.Runtime.Serialization.Formatters.Binary;
    using System.Media; // Для проигрывателя wav-файлов SoundPlayer
        
    // Своя библиотека
    using Data = MyLibrary.MyDataSerialization.DataObject;// Псевдоним класса
    using MyLibrary.MyDataSerialization;
        
    namespace SerializeClient
    {
        public partial class Form1 : Form
        {
            int port = 12000;
            String hostName = "127.0.0.1";  // local
            TcpClient client;               // Ссылка на клиента
            NetworkStream readerStream, writerStream;// Объявили ссылки на транспортный поток
            DataTable table;// Для привязки к списку ComboBox
        
            System.Windows.Controls.MediaElement player;// Для просмотра видео
            bool playerReady = true;// Блокировка повторного запуска на время проигрывания
        
            public Form1()
            {
                InitializeComponent();
        
                // Формируем объектную таблицу и привязываем к списку
                cbCommand.DataSource = DataCommand();
                cbCommand.DisplayMember = "CommandName";
                cbCommand.ValueMember = "CommandValue";
                //cbCommand.SelectedIndex = 0;// Первый
                // Устанавливаем курсор списка в конец
                cbCommand.SelectedIndex = cbCommand.Items.Count - 1;// Последний
            }
        
            DataTable DataCommand()
            {
                table = new DataTable();// Таблица, привязываемая к ComboBox
        
                // Задаем структуру таблицы
                DataColumn[] cols = new DataColumn[2];
                cols[0] = new DataColumn("CommandName", typeof(String));
                cols[1] = new DataColumn("CommandValue", typeof(Command));
                table.Columns.AddRange(cols);
        
                // Формируем строки таблицы
                DataRow row;
                String[] names ={   // Видимое в ComboBox
                                    "Добавить в журнал",
                                    "Прислать журнал",
                                    "Удалить журнал",
                                    "Прислать сообщение",
                                    "Прислать рисунок",
                                    "Прислать звук",
                                    "Прислать видео"
                                };
                //String[] commands = Enum.GetNames(typeof(Command));   // Сортированы по значению
                int[] values = (int[])Enum.GetValues(typeof(Command));  // Сортированы по значению
                for (int i = 0; i < values.Length; i++)
                {
                    row = table.NewRow();
                    row[0] = names[i];
                    row[1] = (Command)values[i];
                    table.Rows.Add(row);
                }
        
                return table;// Возвращаем заполненную таблицу для привязки к ComboBox
            }
        
            // По щелчку кнопки выполняет обмен данными с сервером
            private void btnSend_Click(object sender, EventArgs e)
            {
                try
                {
                    // Создать клиента
                    client = new TcpClient(hostName, port);
                }
                catch
                {
                    MessageBox.Show("Сервер не готов!");
                    return;
                }
        
                //
                //********************* Отправка *********************
                //
                // Собираем данные с элементов управления для отправки через объект
                Data dataSend = new Data();
                dataSend.Command = (Command)cbCommand.SelectedValue;
                dataSend.DisplayCommand =
                    table.Rows[cbCommand.SelectedIndex]["CommandName"].ToString(); ;
                dataSend.Text = txtMessage.Text.Trim();// Берем из текстового поля Message
                // Обнуляем ссылки на потоки
                dataSend.Image = null;
                dataSend.Sound = null;
                dataSend.Video = null;
                // Имена зададим жестко в коде, хотя можно 
                // сделать диалог опроса пользователя
                dataSend.LogName = "Log.txt";               // Имя журнала, локализация на сервере C:\server
                dataSend.ImageName = @"Data\flower2.jpg";   // Разместить в каталоге сборки сервера
                dataSend.SoundName = @"Data\Trumpet1.wav";  // Разместить в каталоге сборки сервера
                dataSend.VideoName = @"Data\tits.mpe";      // Разместить в каталоге сборки сервера
        
                // Подключаем транспортный поток для отправки
                writerStream = client.GetStream();
                // Сериализуем объект в транспортный поток и отправляем 
                BinaryFormatter formatter = new BinaryFormatter();
                formatter.Serialize(writerStream, dataSend);// Отправленный объект
        
                //
                //********************* Получение *********************
                //
                // Подключаем транспортный поток для получения данных
                readerStream = client.GetStream();
                // Прочитать и десериализовать в объект Data
                formatter = new BinaryFormatter();
                Data dataReceived = (Data)formatter.Deserialize(readerStream);// Полученный объект
        
                readerStream.Close();// Освобождаем поток
                writerStream.Close();// Освобождаем поток
                client.Close();// Освобождаем сокет клиента
        
                // Анализ и отображение присланного
                // Сервер может поменять команду, поэтому проверяем
                // не по cbCommand.SelectedIndex, а по присланному факту
                String dir = @"c:\Client";
                String path = Path.Combine(dir, dataReceived.LogName);
                if (!Directory.Exists(dir))
                    Directory.CreateDirectory(dir);// Создаем каталог, если не существует
        
                // Разбор и исполнение вариантов
                switch (dataReceived.Command)
                {
                    // Прислали текст, покажем на форме
                    case Command.Text:
                        if (!String.IsNullOrEmpty(dataReceived.Text))
                        {
                            txtReceived.Visible = true;
                            pictureBox.Visible = false;
                            videoHost.Visible = false;
                            txtReceived.Text = dataReceived.Text;
                        }
                        break;
                    // Прислали журнал, покажем через Блокнот
                    case Command.Log:
                        if (!String.IsNullOrEmpty(dataReceived.Text))
                        {
                            // Сохраним содержимое журнала в файл
                            FileStream fs = File.Create(path);
                            StreamWriter sw = new StreamWriter(fs);
                            sw.Write(dataReceived.Text);
                            sw.Close();
        
                            // Покажем журнал через блокнот
                            System.Diagnostics.Process exe = new System.Diagnostics.Process();
                            exe.StartInfo.FileName = "Notepad.exe"; //Имя программы для запуска
                            exe.StartInfo.Arguments = path;         //Аргументы
                            exe.Start();        // Запускаем внешний процесс
                            //exe.WaitForExit();// Останавливаем наш процесс до закрытия блокнота
                            exe.Close();        // Освобождаем связанные с процессом ресурсы
                        }
                        break;
                    // Прислали рисунок, покажем на форме
                    case Command.Image:
                        if (dataReceived.Image != null)
                        {
                            txtReceived.Visible = false;
                            pictureBox.Visible = true;
                            videoHost.Visible = false;
                            pictureBox.SizeMode = PictureBoxSizeMode.StretchImage;
                            txtMessage.Text = dataReceived.Text;// Сообщение
                            pictureBox.Image = new Bitmap(dataReceived.Image);
                        }
                        break;
                    // Прислали звук, проиграем (можно и в карты!)
                    case Command.Sound:
                        if (dataReceived.Sound != null)
                        {
                            txtReceived.Visible = true;
                            pictureBox.Visible = false;
                            videoHost.Visible = false;
                            txtReceived.Text = dataReceived.Text;
                            txtMessage.Text = dataReceived.Text;// Сообщение
                            SoundPlayer soundPlayer = new SoundPlayer(dataReceived.Sound);
                            //soundPlayer.LoadAsync(); //Играть фоново (Необязательно!)
                            soundPlayer.Play();
                        }
                        break;
                    // Прислали видео, посмотрим
                    case Command.Video:
                        if (dataReceived.Video != null  playerReady)
                        {
                            txtReceived.Visible = false;
                            pictureBox.Visible = false;
                            videoHost.Visible = true;
                            txtMessage.Text = dataReceived.Text;// Сообщение
        
                            // Создаем WPF-проигрыватель
                            player = new System.Windows.Controls.MediaElement();
                            playerReady = false;// Затычка от повторного запуска, пока играет
                            // Передаем переходнику как контейнеру
                            videoHost.Child = player;
                            // player.SetStream(dataReceived.Video); // Не работает, хотя есть в MSDN
        
                            // Поэтому по-турецки: загружаем видео через файл на клиенте
                            // Получили поток памяти и пишем его в файл на диске клиента
                            String[] tmp = dataReceived.VideoName.Split('\\');
                            string shortName = tmp[tmp.Length - 1];// Короткое имя видеофайла  
                            string fullName = Path.Combine(dir, shortName);// Имя с путем
                            FileStream fs = File.Create(fullName);// Создаем или усекаем
                            MemoryStream ms = dataReceived.Video as MemoryStream;// Приводим к исходному
                            ms.WriteTo(fs);// Сливаем в файл
                            ms.Flush();// Необязательно! Сбросится при закрытии потока
                            fs.Close();
                            ms.Close();
        
                            // Настраиваем WPF-проигрыватель
                            player.Source = new Uri(fullName, UriKind.Relative);// Держит файл!!!
                            player.Stretch = System.Windows.Media.Stretch.Fill;// На весь контейнер
                            // Сработает, когда проигрывание закончится
                            player.MediaEnded += new System.Windows.RoutedEventHandler(player_MediaEnded);
                            // Запускать вручную - только после команды Play()
                            player.LoadedBehavior = System.Windows.Controls.MediaState.Manual;
                            // Останавливать вручную - только после команды Stop()
                            player.UnloadedBehavior = System.Windows.Controls.MediaState.Stop;
                            player.IsMuted = true; // Проигрывать с отлюченным звуком
                            player.Play();// Запустить проигрывание
        
                            /*******************************************************************
                            // Это альтернативный код. Он только для WPF и не конвертируется в Windows Forms
                            System.Windows.Media.MediaPlayer player =
                                new System.Windows.Media.MediaPlayer();
                            player.Open(new Uri("WellWait - 02.AVI", UriKind.Relative));
                            System.Windows.Media.VideoDrawing aVideoDrawing =
                                new System.Windows.Media.VideoDrawing();
                            aVideoDrawing.Player = player;
                            System.Windows.Rect rect = new System.Windows.Rect(0, 0, 100, 100);
                            aVideoDrawing.Rect = rect;// Здесь несоответствие типов!!!
                            player.Play();    
                            //******************************************************************/
                        }
        
                        break;
                }
            }
        
            // Плеер блокирует файл на время жизни, поэтому ждем 
            // окончания проигрывания и полность уничтожаем объект
            void player_MediaEnded(object sender, System.Windows.RoutedEventArgs e)
            {
                player.Close();
                player = null;// Необязательно, итак работает!
                GC.WaitForFullGCComplete();// Остановить основной процесс до конца уборки
                GC.Collect();// Запускаем сборщик мусора
                // Здесь уже плеер полность удален из памяти и отпустил файл
                playerReady = true; // Открываем доступ к коду создания нового проигрывателя
            }
        }
    }
  • Запустите проект на выполнение - ошибок компиляции быть не должно и клиент выдаст сообщение, что сервер не готов
  • Создание сервера транспортировки

  • Добавьте к решению NetworkStream новый проект с именем SerializeServer типа консольного приложения (стартовым не назначайте, стартовым будет приложение SerializeClient )
  • В панели Solution Explorer для узла References проекта SerializeServer вызовите контекстное меню и командой Add Reference через вкладку Projects добавьте к проекту ссылку на библиотеку MyLibrary.dll для нашего объекта транспортировки
  • В панели Solution Explorer вызовите контекстное меню для корня проекта SerializeServer и командой Add/New Folder создайте папку Data
  • В панели Solution Explorer вызовите контекстное меню для папки Data и командой Add/Existing Item добавьте к проекту из прилагаемого каталога Source файлы:
  • flower2.jpg
  • Trumpet1.wav
  • tits.mpe
  • Выделите все три скопированных в проект файла и через панель Properties установите для них свойства:
  • Build Action=None
  • Copy to Output Directory=Copy if newer
  • Откройте файл Program.cs проекта SerializeServer и заполните его следующим процедурным кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    // Дополнительные пространства имен для ADO.NET
    using System.Data;
        
    // Дополнительные пространства имен
    using System.IO;
    using System.Net;
    using System.Net.Sockets;
    using System.Threading;
    using System.Collections;
    using System.Configuration;
    using System.Runtime.Serialization.Formatters.Binary;
        
    // Своя библиотека
    using Data = MyLibrary.MyDataSerialization.DataObject;// Псевдоним класса
    using MyLibrary.MyDataSerialization;// Пространство имен в библиотеке
        
    namespace SerializeServer
    {
        class Program
        {
            int port = 12000;
            String hostName = "127.0.0.1";  // local
            IPAddress localAddr;
            TcpListener server = null;      // Ссылка на сервер
            NetworkStream readerStream, writerStream;// Объявили ссылку на транспортный поток
        
            // Добавили конструктор
            public Program()
            {
                // Конвертируем IP в другой формат
                localAddr = IPAddress.Parse(hostName);
        
                // Запускаем в новом потоке (ните)
                Thread thread = new Thread(ExecuteLoop);
                thread.IsBackground = true;
                thread.Start();
            }
        
            static void Main(string[] args)
            {
                Console.WindowWidth = 20;
                Console.WindowHeight = 5;
                new Program();
        
                Console.ReadLine();// Держим основной поток (Thread) приложения
            }
        
            private void ExecuteLoop()
            {
                try
                {
                    server = new TcpListener(localAddr, port);// Создаем сервер-слушатель
                    server.Start();// Запускаем сервер
        
                    // Бесконечный цикл прослушивания очереди клиентов
                    while (true)
                    {
                        // Проверяем очередь соединений
                        if (!server.Pending())// Очередь запросов пуста
                            continue;
                        TcpClient client = server.AcceptTcpClient();// Текущий клиент
        
                        //
                        //********************* Получение *********************
                        //
                        // Подключаемся к сокету для чтения
                        readerStream = client.GetStream();
        
                        // Получить объект данных и десериализовать
                        BinaryFormatter formatter = new BinaryFormatter();
                        Data dataReceived = (Data)formatter.Deserialize(readerStream);
        
                        //
                        //************ Выполнение действий и отправка **********
                        //
                        // Создаем объект-клон для отправки и постепенно заполняем данными
                        //Data dataSend = (Data)dataReceived.Clone();    // Оригинальный метод
                        Data dataSend = dataReceived.BinaryClone<Data>();// Альтернативный метод
        
                        // Распознаем присланное, исполняем и формируем данные для отправки
                        String dir = @"c:\Server";
                        String path = Path.Combine(dir, dataReceived.LogName);
                        if (!Directory.Exists(dir))
                            Directory.CreateDirectory(dir);
                        String message = "";
                        FileStream fs;
                        Byte[] bytes;
                        switch (dataReceived.Command)
                        {
                            case Command.AddTextInLog:// Клиент хочет сообщение добавить в журнал
                                fs = new FileStream(path, FileMode.Append);
                                StreamWriter sw = new StreamWriter(fs);
                                sw.WriteLine("Сервер: " + DateTime.Now.ToString());
                                message = "Получил команду: \"" + dataReceived.DisplayCommand;
                                sw.WriteLine(message + "\"");// Три конкатенации сразу нельзя
                                sw.WriteLine("Идентификатор команды: " +
                                    Enum.GetName(typeof(Command), dataReceived.Command));
                                sw.WriteLine("Получил сообщение: " + dataReceived.Text);
                                message = "Добавлено сообщение в журнал!";
                                sw.WriteLine(message);
                                sw.WriteLine();
                                sw.Close();
                                dataSend.Text = message;// Отчет для отправки
                                dataSend.Command = Command.Text;// Изменили команду
                                break;
                            case Command.DeleteLog:// Клиент хочет удалить журнал с диска сервера
                                if (File.Exists(path))
                                    File.Delete(path);
                                dataSend.Text = "Журнал удален";// Отчет для отправки
                                dataSend.Command = Command.Text;// Изменили команду
                                break;
                            case Command.Text:// Клиент хочет получить сообщение 
                                message = String.Format(
                                    "Получил сообщение \"{0}\"\r\nОчень рад!", dataReceived.Text);
                                dataSend.Text = message;// Отчет для отправки
                                break;
                            case Command.Image:// Клиент хочет получить рисунок
                                // Файл должен быть в каталоге сборки, иначе надо дополнить путем
                                if (!File.Exists(dataReceived.ImageName))
                                {
                                    dataSend.Text = "Рисунок не существует";// Отчет для отправки
                                    dataSend.Command = Command.Text;// Изменили команду
                                    break;
                                }
                                // Читаем файл с рисунком
                                bytes = File.ReadAllBytes(dataReceived.ImageName);
                                // Подключаем поток к свойству объекта
                                dataSend.Image = new MemoryStream(bytes);
                                dataSend.Text = "Посылаю рисунок";// Отчет для отправки
                                break;
                            case Command.Log:// Клиент требует содержимое журнала
                                if (!File.Exists(path))
                                {
                                    dataSend.Text = "Журнал еще не создан";// Отчет для отправки
                                    dataSend.Command = Command.Text;// Изменили команду
                                    break;
                                }
                                fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
                                StreamReader sr = new StreamReader(fs);
                                dataSend.Text = sr.ReadToEnd();
                                sr.Close();
                                break;
                            case Command.Sound:// Клиент хочет музычку
                                // Файл должен быть в каталоге сборки, иначе надо дополнить путем
                                if (!File.Exists(dataReceived.SoundName))
                                {
                                    dataSend.Text = "Файл с музычкой не существует";// Отчет для отправки
                                    dataSend.Command = Command.Text;// Изменили команду
                                    break;
                                }
                                // Читаем файл с музычкой
                                bytes = File.ReadAllBytes(dataReceived.SoundName);
                                // Подключаем поток к свойству объекта
                                dataSend.Sound = new MemoryStream(bytes);
                                dataSend.Text = "Посылаю музычку";// Отчет для отправки
                                break;
                            case Command.Video:// Клиент хочет видео
                                // Файл должен быть в каталоге сборки, иначе надо дополнить путем
                                if (!File.Exists(dataReceived.VideoName))
                                {
                                    dataSend.Text = "Файл с video не существует";// Отчет для отправки
                                    dataSend.Command = Command.Text;// Изменили команду
                                    break;
                                }
                                // Читаем файл с video
                                bytes = File.ReadAllBytes(dataReceived.VideoName);
                                // Подключаем поток к свойству объекта
                                dataSend.Video = new MemoryStream(bytes);
                                dataSend.Text = "Там у них другие мерки...";// Отчет для отправки
                                break;
                        }
        
                        // Отправляем данные
                        writerStream = client.GetStream();
                        formatter = new BinaryFormatter();
                        formatter.Serialize(writerStream, dataSend);// Отправленный объект
        
                        readerStream.Close();// Освобождаем поток
                        writerStream.Close();// Освобождаем поток
                        client.Close();// Освобождаем сокет текущего клиента
                    }
                }
                finally
                {
                    // Останавливаем сервер
                    server.Stop();
                }
            }
        }
    }
  • Откомпилируйте проект SerializeServer и через Windows Explorer выведите на рабочий стол ярлык для запуска консольной сборки сервера
  • Запустите оба созданных приложения и испытайте их работу
  • Если запустить сразу несколько клиентов (не через оболочку, а через сборку), то наша защелка playerReady помогает только тогда, когда видеопроигрыватель в данный момент активен только на одном клиенте. При одновременном запуске в нескольких сборках WPF -проигрыватель MediaElement дает системное исключение. Да к тому же MediaElement еще требует наличия на компьютере установленного проигрывателя Windows Media версии 10 и выше.

    На сегодняшний день только Windows Vista поставляется вместе с проигрывателем Windows Media 11 (у меня установлен и пример работает). Если на вашем компьютере такого проигрывателя нет (хотя и установлен WPF ), замените его на COM -объект, как в работе №45. Суть примера не в проигрывании видео, а в транспортировке данных, упакованных в сериализованный объект.

  • Попытайтесь осмыслить код примера - в нем много поучительного и полезного (как и в других предыдущих примерах!)
  • Клиент SerializeClient при запущенном сервере SerializeServer может выглядеть так

    Упражнение 9. Защита данных с помощью потока CryptoStream

    Часто встречаются ситуации, когда при хранении и передаче данных их защита является основополагающим требованием к приложению. Для защиты данных обычно применяют шифрование с секретным ключем. Одним из способов решения задачи шифрования является применение класса CryptoStream из пространства имен System.Security.Cryptography библиотеки mscorlib.dll. Этот класс служит оболочкой над другими потоками и выполняет криптографические операции над передаваемыми данными.

    В основе шифрования .NET лежат три абстрактных класса, которые порождают экземплярные классы для реализации следующих криптографических алгоритмов:

  • SymmetricAlgorithm - симметричные алгоритмы, которые для шифрования и дешифрования информации используют один и тот же ключ. Производными от класса SymmetricAlgorithm являются тоже абстрактные классы:
  • DES
  • RC2
  • Rijndael
  • TripleDES
  • AsymmetricAlgorithm - асимметричные алгоритмы, которые для шифрования и дешифрования применяют разные ключи. Асимметричные криптографические алгоритмы также называют инфраструктурой открытого ключа ( PKI - Public Key Infrastructure ) Производными от класса AsymmetricAlgorithm являются тоже абстрактные классы:
  • DSA
  • ECDiffieHellman
  • ECDsa
  • RSA
  • HashAlgorithm - алгоритмы хеширования или дайджеста сообщения, преобразующие первоначальный текст любой длины в шифрованный текст фиксированной длины. Они выполняют односторонне шифрование, а это значит, что из полученного хешированием шифротекста нельзя восстановить первоначальный документ. Фиксированная длина хеша зависит от применяемого алгоритма и находится в пределах от 16 до 32 байтов. Производными от класса HashAlgorithm являются тоже абстрактные классы:
  • KeyedHashAlgorithm
  • MD5
  • RIPEMD160
  • SHA1
  • SHA256
  • SHA384
  • SHA512
  • Поскольку перечисленные классы являются абстрактными, то для создания экземплярных объектов они используют классы провайдеров ( Provider - поставщик) или менеджеров ( Manager - управленец).

    Криптография - это целая ветвь математики, создающая модели и алгоритмы для шифрования и дешифрования сообщений. Не вдаваясь в подробности криптографии мы просто проиллюстрируем работу потока CryptoStream на нескольких примерах.

    Введение в симметричные алгоритмы .NET

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

    Симметричные алгоритмы обычно намного быстрее асимметричных и они хорошо выполняют операции шифрования-дешифрования объемных файлов. Пространство имен System.Security.Cryptography поддерживает симметричные алгоритмы DES, RC2, Rijndael и TripleDES. Только у алгоритма Rijndael есть своя управляемая реализация, остальные поставляются из Microsoft Crypto API готовыми с помощью провайдеров.

    Все классы симметричных алгоритмов наследуют от своего общего предка SymmetricAlgorithm, который предоставляет им ряд общих методов и свойств, некоторые из которых приведены в таблице

    Некоторые члены класса SymmetricAlgorithm
    Член Описание
    Key Свойство возвращает или устанавливает секретный ключ, представляющий собой байтовый массив ( bytes )
    KeySize Свойство возвращает или устанавливает размер секретного ключа в битах ( bits )
    LegalKeySize Свойство возвращает целые величины диапазона корректных размеров секретного ключа в битах: MinSize, MaxSize и допустимый для выбранного алгоритма шаг дискретизации SkipSize
    IV Свойство возвращает или устанавливает байтовый вектор инициализации симметричного алгоритма ( Initialization Vector ). Поскольку алгоритмы прокручивают цепочку кодирования несколько раз, беря в качестве исходного результат предыдущего сдвига, то для самой первой итерации требуется начальная информация, которую и содержит IV
    BlockSize Свойство возвращает или устанавливает размер блока данных в битах ( bits ), обрабатываемых за один проход. Длинные данные делятся на блоки установленного размера и если для последнего блока данных не хватает, он будет дополнен значениями по умолчанию
    LegalBlockSize Свойство возвращает целые величины диапазона корректных размеров блока в битах: MinSize, MaxSize и допустимый для выбранного алгоритма шаг дискретизации SkipSize
    Mode Устанавливает режим работы симметричного алгоритма и принимает значения от перечисления CipherMode:
  • CBC ( Cipher Block Chaining ) - сцепление блоков шифра. Каждый следующий блок исходного текста шифруется с учетом результатов шифрования предыдущего блока. Здесь для самого первого блока используется IV
  • ECB ( Electronic Codebook ) - электронная книга шифров. Режим шифрует каждый блок индивидуально. Это означает, что любые одинаковые блоки исходного текста, зашифрованные с тем же самым ключом, будут преобразованы в идентичные блоки шифрованного текста. Такой режим не очень устойчив к взлому, но быстрый и не требует IV
  • CFB ( Cipher Feedback ) - шифр с обратной связью. Режим делит каждый блок на маленькие разделы и обрабатывает их индивидуально со сдвигом, вместо того, чтобы обработать весь блок сразу
  • CTS ( Cipher Text Stealing ) - режим обрабатывает исходный текст любой длины и производит шифрованный текст той же длины
  • OFB ( Output Feedback ) - выходная обратная связь. Режим похож на CFB, но отличается способом заполнения сдвигового регистра
  • ICryptoTransform CreateEncryptor() Метод создает объект симметричного шифрования и возвращает ссылку на него. Принимает вектор ключа и вектор инициализации
    ICryptoTransform CreateDecryptor() Метод создает объект симметричного дешифрования и возвращает ссылку на него. Принимает вектор ключа и вектор инициализации
    void GenerateKey() Метод изменяет значение свойства Key случайным образом
    void GenerateIV() Метод изменяет значение свойства IV случайным образом
    bool ValidKeySize(int bitLength) Проверяет, подходит ли выбранный размер ключа для работы алгоритма

    Рассмотрим работу семметричных алгоритмов на примерах.

    Пример 1. Поток CryptoStream и шифрование в память

  • Создайте новое решение CryptoStream с проектом Demo1 так
  • Настройте форму в соответствии со следующей таблице свойств
  • Элемент Свойство Значение
    Сама форма
    Form Text Demo1 CryptoStream
    Size 563; 438
    StartPosition CenterScreen
    MaximizeBox False
    Метки и тектовые блоки для шифруемой информации
    Label Text Source Text:
    Location 95; 9
    Label Text Encrypted Text:
    Location 86; 128
    Label Text Decrypted Text:
    Location 85; 247
    TextBox (Name) txtSourceData
    Location 2; 31
    Size 284; 88
    Multiline True
    Lines

    Привет всем!

    Пусть классики не волнуются:

    это не тема по криптографии, это тема про поток CryptoStream

    ScrollBars Vertical
    TextBox (Name) txtEncryptedData
    Location 2; 150
    Size 284; 88
    Multiline True
    ScrollBars Vertical
    TextBox (Name) txtDecryptedData
    Location 2; 269
    Size 284; 88
    Multiline True
    ScrollBars Vertical
    Блок радиокнопок для выбора алгоритма шифрования
    GroupBox Text SymmetricAlgorithm
    Location 291; 9
    Size 259; 136
    RadioButton Checked True
    Location 8; 16
    Size 199; 21
    Tag DES
    Text DESCryptoServiceProvider
    RadioButton Location 8; 40
    Size 194; 21
    Tag RC2
    Text RC2CryptoServiceProvider
    RadioButton Location 8; 64
    Size 136; 21
    Tag Rijndael
    Text RijndaelManaged
    RadioButton Location 8; 88
    Size 231; 21
    Tag TripleDES
    Text TripleDESCryptoServiceProvider
    RadioButton Location 8; 112
    Size 175; 21
    Tag SymmetricAlgorithm
    Text DefaultServiceProvider
    Блок с характеристиками выбранного алгоритма шифрования
    GroupBox Text Properties SymmetricAlgorithm
    Location 291; 151
    Size 259; 206
    Label Text Key:
    Location 5; 26
    Label Text IV:
    Location 17; 63
    Label Text KeySize (bits):
    Location 5; 99
    Label Text BlockSize:
    Location 155; 100
    Label Text LegalKeySizes:
    Location 15; 137
    Label Text LegalBlockSizes:
    Location 5; 174
    TextBox (Name) txtKey
    Location 42; 23
    Size 209; 22
    TextBox (Name) txtIV
    Location 42; 60
    Size 209; 22
    TextBox (Name) txtKeySize
    Location 102; 97
    Size 25; 22
    TextBox (Name) txtBlockSize
    Location 226; 98
    Size 25; 22
    TextBox (Name) txtLegalKeySizes
    Location 124; 134
    Size 127; 22
    TextBox (Name) txtLegalBlockSizes
    Location 124; 171
    Size 128; 22
    Кнопка для выполнения шифрования
    Button (Name) btnExecute
    Text Execute
    Location 240; 368
  • Выделите все элементы формы командой Edit/Select All и замкните созданный интерфейс командой Format/Lock Controls
  • Выделите все текстовые блоки, кроме txtSourceData, и задайте для них
  • BorderStyle=FixedSingle
  • Выделите все элементы, кроме TextBox и GroupBox, и задайте для них
  • AutoSize=True
  • Созданный интерфейс должен получиться таким

  • Выделите все текстовые блоки, кроме txtSourceData, и создайте для них общие обработчики событий KeyDown и KeyPress для блокировки редактирования (имена обработчиков введите вручную)
  • Командой Project/Add Class добавьте новый класс с именем ExecuteCryptography.cs и заполните его следующим кодом (приводится полностью)
  • using System;
    using System.IO;
    using System.Text;
    using System.Security.Cryptography;
        
    namespace Demo1
    {
        class ExecuteCryptography
        {
            // Создаем кодировщик символов
            private UTF7Encoding utf7 = new UTF7Encoding();
        
            // Объявляем ссылки на шифровальщик,
            // на массивы Key и IV (Initialization Vector) 
            private SymmetricAlgorithm codec;
            private byte[] key;
            private byte[] initVector;// IV
        
            // Конструктор
            public ExecuteCryptography(SymmetricAlgorithm codec)
            {
                this.key = codec.Key;
                this.initVector = codec.IV;
                this.codec = codec;
            }
        
            public string Encrypt(string text)
            {
                byte[] input = utf7.GetBytes(text);
                byte[] output = Transform(input,
                                codec.CreateEncryptor(key, initVector));
                return Convert.ToBase64String(output);
            }
            public string Decrypt(string text)
            {
                byte[] input = Convert.FromBase64String(text);
                byte[] output = Transform(input,
                                codec.CreateDecryptor(key, initVector));
                return utf7.GetString(output);
            }
        
            private byte[] Transform(byte[] input, ICryptoTransform cryptoTransform)
            {
                // Создаем потоки
                MemoryStream memoryStream = // Поток памяти
                    new MemoryStream();
                CryptoStream cryptoStream = // Шифропоток-оболочка
                    new CryptoStream(memoryStream, 
                        cryptoTransform, CryptoStreamMode.Write);
        
                // Кодируем в поток памяти через шифропоток
                cryptoStream.Write(input, 0, input.Length);
                cryptoStream.FlushFinalBlock();
        
                // Преобразуем заполненный поток памяти в массив байтов
                byte[] result = memoryStream.ToArray();
        
                // Останавливаем потоки
                memoryStream.Close();
                cryptoStream.Close();
        
                // Возвращаем кодированное/раскодированное
                return result;
            }
        
            // Перегруженные методы
            public byte[] Encrypt(byte[] input)
            {
                return Transform(input,
                       codec.CreateEncryptor(key, initVector));
            }
            public byte[] Decrypt(byte[] input)
            {
                return Transform(input,
                       codec.CreateDecryptor(key, initVector));
            }
        }
    }

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

    MemoryStream memoryStream = // Поток памяти
                    new MemoryStream();
                CryptoStream cryptoStream = // Шифропоток-оболочка
                    new CryptoStream(memoryStream, 
                        cryptoTransform, CryptoStreamMode.Write);
                StreamWriter streamWriter = new StreamWriter(cryptoStream);
                streamWriter.Write(txtSourceData.Text);
                streamWriter.Flush();            
                cryptoStream.FlushFinalBlock();
  • Заполните файл Form1.cs следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.Security.Cryptography;
    using System.IO;
        
    namespace Demo1
    {
        public partial class Form1 : Form
        {
            public Form1()
            {
                InitializeComponent();
        
                // Подписываемся на событие Click кнопки
                btnExecute.Click += btnExecute_Click;
            }
               
            // Обработчик кнопки 
            private void btnExecute_Click(object sender, EventArgs e)
            {
                // Создаем объект симметричного шифрования
                SymmetricAlgorithm codec = null;// Тип общего предка 
                switch (SelectedRadio().Trim())
                {
                    case "DES":
                        codec = new DESCryptoServiceProvider();
                        break;
                    case "RC2":
                        codec = new RC2CryptoServiceProvider();
                        break;
                    case "Rijndael":
                        codec = new RijndaelManaged();
                        break;
                    case "TripleDES":
                        codec = new TripleDESCryptoServiceProvider();
                        break;
                    case "SymmetricAlgorithm":
                        codec = SymmetricAlgorithm.Create();
                        break;
                    default:
                        MessageBox.Show("Выделите провайдер шифрования");
                        return;
                }
        
                // Создаем экземпляр нашего класса
                // и передаем ему выбранный шифровальшик
                ExecuteCryptography executeCryptography =
                    new ExecuteCryptography(codec);
        
                // Шифруем текст
                string sourceData = txtSourceData.Text;
                string encryptedData = executeCryptography.Encrypt(sourceData);
                txtEncryptedData.Text = encryptedData;
        
                // Расшифровываем текст
                string decryptedData = executeCryptography.Decrypt(encryptedData);
                txtDecryptedData.Text = decryptedData;
        
                //////////////////////////////////////////////////
                // Показываем характеристики алгоритма шифрования
                //////////////////////////////////////////////////
                {
                    txtKey.Text = Encoding.UTF7.GetString(codec.Key);// Key
                    txtIV.Text = Encoding.UTF7.GetString(codec.IV);// Initialization Vector
        
                    txtKeySize.Text = codec.KeySize.ToString();// KeySize
                    StringBuilder sb = new StringBuilder();
                    foreach (KeySizes sizes in codec.LegalKeySizes)
                    {
                        sb.Append(sizes.MinSize.ToString() + "-" +
                            sizes.MaxSize.ToString() + 
                            " (" + sizes.SkipSize.ToString() + ") ");
                    }
                    txtLegalKeySizes.Text = sb.ToString();// LegalKeySizes
        
                    txtBlockSize.Text = codec.BlockSize.ToString();// BlockSize
                    sb = new StringBuilder();
                    foreach (KeySizes sizes in codec.LegalBlockSizes)
                    {
                        sb.Append(sizes.MinSize.ToString() + "-" +
                            sizes.MaxSize.ToString() + 
                            " (" + sizes.SkipSize.ToString() + ") ");
                    }
                    txtLegalBlockSizes.Text = sb.ToString();// LegalBlockSizes
                }
            }
        
            // Динамически ищем включенный RadioButton
            string SelectedRadio()
            {
                string tag = "";
                foreach (Control ctrl in groupBox1.Controls)
                {
                    RadioButton radio = ctrl as RadioButton;// Приводим ссылку
                    if (radio.Checked)
                    {
                        tag = (String)radio.Tag;// Приводим ссылку
                        break;
                    }
                }
        
                return tag;
            }
        
            // Блокировка редактирования всех целевых TextBox
            private void txtTarget_KeyDown(object sender, KeyEventArgs e)
    
            {
                e.Handled = true;
            }
            private void txtTarget_KeyPress(object sender, KeyPressEventArgs e)
            {
                e.Handled = true;
            }
        }
    }
  • Запустите приложение и испытайте его работу - результат может быть таким
  • Приложение при каждом новом создании объекта шифрования создает для него новый ключ и вектор инициализации. Если шифрованную информацию сохранять в файле, то одновременно нужно где-то запомнить имя алгоритма и его характеристики (можно только имя и секретный ключ). Это необходимо для последующей настройки объекта симметричного алгоритма шифрования на корректное восстановление информации.

  • Разберитесь с кодом примера
  • Пример 2. Поток CryptoStream и шифрование в файл

    В данном примере мы разработаем диалоговое приложение Windows Forms, иллюстрирующее работу алгоритмов шифрования/дешифрования в файл. Вернее, приложение уже разработано и ниже приводятся его составляющие, с которыми нужно разобраться самостоятельно. Но прежде нужно собрать приложение полностью и только потом испытывать, иначе посыпятся ошибки! Итак, собираем...

  • Добавьте к решению CryptoStream новый проект с именем Demo2 и назначьте его стартовым
  • В панели Solution Explorer откройте интерфейсный файл Form1.Designer.cs и заполните его следующим кодом (приводится полностью)
  • namespace Demo2
    {
        partial class Form1
        {
            private System.ComponentModel.IContainer components = null;
        
            protected override void Dispose(bool disposing)
            {
                if (disposing  (components != null))
                {
                    components.Dispose();
                }
                base.Dispose(disposing);
            }
        
            #region Windows Form Designer generated code
        
            private void InitializeComponent()
            {
                this.cmbCodec = new System.Windows.Forms.ComboBox();
                this.label1 = new System.Windows.Forms.Label();
                this.label2 = new System.Windows.Forms.Label();
                this.txtSourceFile = new System.Windows.Forms.TextBox();
                this.btnSelectFile = new System.Windows.Forms.Button();
                this.txtDestinationDir = new System.Windows.Forms.TextBox();
                this.btnDestinationDir = new System.Windows.Forms.Button();
                this.label4 = new System.Windows.Forms.Label();
                this.txtKey = new System.Windows.Forms.TextBox();
                this.btnClose = new System.Windows.Forms.Button();
                this.numKeySize = new System.Windows.Forms.NumericUpDown();
                this.numBlockSize = new System.Windows.Forms.NumericUpDown();
                this.label5 = new System.Windows.Forms.Label();
                this.label6 = new System.Windows.Forms.Label();
                this.label3 = new System.Windows.Forms.Label();
                this.btnEncrypt = new System.Windows.Forms.Button();
                this.btnDecrypt = new System.Windows.Forms.Button();
                this.btnShowFile = new System.Windows.Forms.Button();
                this.btnNewFile = new System.Windows.Forms.Button();
                ((System.ComponentModel.ISupportInitialize)(this.numKeySize)).BeginInit();
                ((System.ComponentModel.ISupportInitialize)(this.numBlockSize)).BeginInit();
                this.SuspendLayout();
                // 
                // cmbCodec
                // 
                this.cmbCodec.FormattingEnabled = true;
                this.cmbCodec.Location = new System.Drawing.Point(94, 21);
                this.cmbCodec.Name = "cmbCodec";
                this.cmbCodec.Size = new System.Drawing.Size(280, 24);
                this.cmbCodec.TabIndex = 0;
                this.cmbCodec.SelectedIndexChanged += new System.EventHandler(this.cmbCodec_SelectedIndexChanged);
                // 
                // label1
                // 
                this.label1.AutoSize = true;
                this.label1.Location = new System.Drawing.Point(17, 24);
                this.label1.Name = "label1";
                this.label1.Size = new System.Drawing.Size(71, 17);
                this.label1.TabIndex = 1;
                this.label1.Text = "Algorithm:";
                // 
                // label2
                // 
                this.label2.AutoSize = true;
                this.label2.Location = new System.Drawing.Point(5, 156);
                this.label2.Name = "label2";
                this.label2.Size = new System.Drawing.Size(83, 17);
                this.label2.TabIndex = 2;
                this.label2.Text = "Source File:";
                // 
                // txtSourceFile
                // 
                this.txtSourceFile.Location = new System.Drawing.Point(94, 155);
                this.txtSourceFile.Name = "txtSourceFile";
                this.txtSourceFile.Size = new System.Drawing.Size(280, 22);
                this.txtSourceFile.TabIndex = 3;
                this.txtSourceFile.KeyDown += new System.Windows.Forms.KeyEventHandler(this.EditKeyDown);
                this.txtSourceFile.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.EditKeyPress);
                // 
                // btnSelectFile
                // 
                this.btnSelectFile.Font = new System.Drawing.Font("Agency FB", 7.2F, System.Drawing.FontStyle.Bold, 
                	System.Drawing.GraphicsUnit.Point, ((byte)(0)));
                this.btnSelectFile.Location = new System.Drawing.Point(380, 155);
                this.btnSelectFile.Name = "btnSelectFile";
                this.btnSelectFile.Size = new System.Drawing.Size(25, 23);
                this.btnSelectFile.TabIndex = 4;
                this.btnSelectFile.Text = "...";
                this.btnSelectFile.UseVisualStyleBackColor = true;
                this.btnSelectFile.Click += new System.EventHandler(this.btnSelectFile_Click);
                // 
                // txtDestinationDir
                // 
                this.txtDestinationDir.Location = new System.Drawing.Point(94, 198);
                this.txtDestinationDir.Name = "txtDestinationDir";
                this.txtDestinationDir.Size = new System.Drawing.Size(280, 22);
                this.txtDestinationDir.TabIndex = 6;
                // 
                // btnDestinationDir
                // 
                this.btnDestinationDir.Font = new System.Drawing.Font("Agency FB", 7.2F, System.Drawing.FontStyle.Bold, 
                	System.Drawing.GraphicsUnit.Point, ((byte)(0)));
                this.btnDestinationDir.Location = new System.Drawing.Point(380, 198);
                this.btnDestinationDir.Name = "btnDestinationDir";
                this.btnDestinationDir.Size = new System.Drawing.Size(25, 23);
                this.btnDestinationDir.TabIndex = 7;
                this.btnDestinationDir.Text = "...";
                this.btnDestinationDir.UseVisualStyleBackColor = true;
                this.btnDestinationDir.Click += new System.EventHandler(this.btnDestinationDir_Click);
                // 
                // label4
                // 
                this.label4.AutoSize = true;
                this.label4.Location = new System.Drawing.Point(52, 67);
                this.label4.Name = "label4";
                this.label4.Size = new System.Drawing.Size(36, 17);
                this.label4.TabIndex = 8;
                this.label4.Text = "Key:";
                // 
                // txtKey
                // 
                this.txtKey.Location = new System.Drawing.Point(94, 66);
                this.txtKey.Name = "txtKey";
                this.txtKey.Size = new System.Drawing.Size(280, 22);
                this.txtKey.TabIndex = 9;
                // 
                // btnClose
                // 
                this.btnClose.AutoSize = true;
                this.btnClose.Location = new System.Drawing.Point(353, 256);
                this.btnClose.Name = "btnClose";
                this.btnClose.Size = new System.Drawing.Size(75, 27);
                this.btnClose.TabIndex = 12;
                this.btnClose.Text = "Close";
                this.btnClose.UseVisualStyleBackColor = true;
                this.btnClose.Click += new System.EventHandler(this.btnClose_Click);
                // 
                // numKeySize
                // 
                this.numKeySize.Location = new System.Drawing.Point(111, 110);
                this.numKeySize.Name = "numKeySize";
                this.numKeySize.Size = new System.Drawing.Size(55, 22);
                this.numKeySize.TabIndex = 13;
                this.numKeySize.ValueChanged += new System.EventHandler(this.Numeric_ValueChanged);
                this.numKeySize.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.EditKeyPress);
                this.numKeySize.KeyDown += new System.Windows.Forms.KeyEventHandler(this.EditKeyDown);
                // 
                // numBlockSize
                // 
                this.numBlockSize.Location = new System.Drawing.Point(299, 110);
                this.numBlockSize.Name = "numBlockSize";
                this.numBlockSize.Size = new System.Drawing.Size(55, 22);
                this.numBlockSize.TabIndex = 14;
                this.numBlockSize.ValueChanged += new System.EventHandler(this.Numeric_ValueChanged);
                this.numBlockSize.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.EditKeyPress);
                this.numBlockSize.KeyDown += new System.Windows.Forms.KeyEventHandler(this.EditKeyDown);
                // 
                // label5
                // 
                this.label5.AutoSize = true;
                this.label5.Location = new System.Drawing.Point(38, 111);
                this.label5.Name = "label5";
                this.label5.Size = new System.Drawing.Size(67, 17);
                this.label5.TabIndex = 15;
                this.label5.Text = "Key Size:";
                // 
                // label6
                // 
                this.label6.AutoSize = true;
                this.label6.Location = new System.Drawing.Point(216, 111);
                this.label6.Name = "label6";
                this.label6.Size = new System.Drawing.Size(77, 17);
                this.label6.TabIndex = 16;
                this.label6.Text = "Block Size:";
                // 
                // label3
                // 
                this.label3.AutoSize = true;
                this.label3.Location = new System.Drawing.Point(8, 186);
                this.label3.Name = "label3";
                this.label3.Size = new System.Drawing.Size(79, 34);
                this.label3.TabIndex = 5;
                this.label3.Text = "Destination\r\ndirectory:";
                this.label3.TextAlign = System.Drawing.ContentAlignment.TopRight;
                // 
                // btnEncrypt
                // 
                this.btnEncrypt.AutoSize = true;
                this.btnEncrypt.Location = new System.Drawing.Point(98, 256);
                this.btnEncrypt.Name = "btnEncrypt";
                this.btnEncrypt.Size = new System.Drawing.Size(75, 27);
                this.btnEncrypt.TabIndex = 19;
                this.btnEncrypt.Text = "Encrypt";
                this.btnEncrypt.UseVisualStyleBackColor = true;
                this.btnEncrypt.Click += new System.EventHandler(this.btnEncrypt_Click);
                // 
                // btnDecrypt
                // 
                this.btnDecrypt.AutoSize = true;
                this.btnDecrypt.Location = new System.Drawing.Point(182, 256);
                this.btnDecrypt.Name = "btnDecrypt";
                this.btnDecrypt.Size = new System.Drawing.Size(75, 27);
                this.btnDecrypt.TabIndex = 20;
                this.btnDecrypt.Text = "Decrypt";
                this.btnDecrypt.UseVisualStyleBackColor = true;
                this.btnDecrypt.Click += new System.EventHandler(this.btnDecrypt_Click);
                // 
                // btnShowFile
                // 
                this.btnShowFile.AutoSize = true;
                this.btnShowFile.Location = new System.Drawing.Point(266, 256);
                this.btnShowFile.Name = "btnShowFile";
                this.btnShowFile.Size = new System.Drawing.Size(78, 27);
                this.btnShowFile.TabIndex = 21;
                this.btnShowFile.Text = "Show File";
                this.btnShowFile.UseVisualStyleBackColor = true;
                this.btnShowFile.Click += new System.EventHandler(this.btnShowFile_Click);
                // 
                // btnNewFile
                // 
                this.btnNewFile.AutoSize = true;
                this.btnNewFile.Location = new System.Drawing.Point(14, 256);
                this.btnNewFile.Name = "btnNewFile";
                this.btnNewFile.Size = new System.Drawing.Size(75, 27);
                this.btnNewFile.TabIndex = 22;
                this.btnNewFile.Text = "New File";
                this.btnNewFile.UseVisualStyleBackColor = true;
                this.btnNewFile.Click += new System.EventHandler(this.btnNewFile_Click);
                // 
                // Form1
                // 
                this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
                this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
                this.ClientSize = new System.Drawing.Size(442, 296);
                this.Controls.Add(this.btnNewFile);
                this.Controls.Add(this.btnShowFile);
                this.Controls.Add(this.btnDecrypt);
                this.Controls.Add(this.btnEncrypt);
                this.Controls.Add(this.label6);
                this.Controls.Add(this.label5);
                this.Controls.Add(this.numBlockSize);
                this.Controls.Add(this.numKeySize);
                this.Controls.Add(this.btnClose);
                this.Controls.Add(this.txtKey);
                this.Controls.Add(this.label4);
                this.Controls.Add(this.btnDestinationDir);
                this.Controls.Add(this.txtDestinationDir);
                this.Controls.Add(this.label3);
                this.Controls.Add(this.btnSelectFile);
                this.Controls.Add(this.txtSourceFile);
                this.Controls.Add(this.label2);
                this.Controls.Add(this.label1);
                this.Controls.Add(this.cmbCodec);
                this.MaximizeBox = false;
                this.Name = "Form1";
                this.Text = "SymmetricAlgorithm with Files";
                this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
                ((System.ComponentModel.ISupportInitialize)(this.numKeySize)).EndInit();
                ((System.ComponentModel.ISupportInitialize)(this.numBlockSize)).EndInit();
                this.ResumeLayout(false);
                this.PerformLayout();
            }
        
            #endregion
        
            private System.Windows.Forms.ComboBox cmbCodec;
            private System.Windows.Forms.Label label1;
            private System.Windows.Forms.Label label2;
            private System.Windows.Forms.TextBox txtSourceFile;
            private System.Windows.Forms.Button btnSelectFile;
            private System.Windows.Forms.TextBox txtDestinationDir;
            private System.Windows.Forms.Button btnDestinationDir;
            private System.Windows.Forms.Label label4;
            private System.Windows.Forms.TextBox txtKey;
            private System.Windows.Forms.Button btnClose;
            private System.Windows.Forms.NumericUpDown numKeySize;
            private System.Windows.Forms.NumericUpDown numBlockSize;
            private System.Windows.Forms.Label label5;
            private System.Windows.Forms.Label label6;
            private System.Windows.Forms.Label label3;
            private System.Windows.Forms.Button btnEncrypt;
            private System.Windows.Forms.Button btnDecrypt;
            private System.Windows.Forms.Button btnShowFile;
            private System.Windows.Forms.Button btnNewFile;
        }
    }

    Этот код сразу определит интерфейс формы и мне не нужно будет писать (а вам - читать) таблицу свойств элементов.

  • Добавьте к текущему проекту командой Project/Add Class новый файл с именем MySettings.cs и заполните его следующим кодом (приводится полностью)
  • using System;
    using System.Configuration;
        
    namespace Demo2
    {
        // Класс для сохранения параметров приложения
        class MySettings : ApplicationSettingsBase
        {
            [UserScopedSettingAttribute()]       // Область действия - 'Пользователь' 
            [DefaultSettingValueAttribute("C:\\")]   // Значение параметра по умолчанию
            public String DestinationDir
            {
                get
                {
                    return ((String)this["DestinationDir"]);
                }
                set
                {
                    this["DestinationDir"] = (String)value;
                }
            }
        
            [UserScopedSettingAttribute()]       // Область действия - 'Пользователь' 
            [DefaultSettingValueAttribute("")]   // Значение параметра по умолчанию
            public String SourceFile
            {
                get
                {
                    return ((String)this["SourceFile"]);
                }
                set
                {
                    this["SourceFile"] = (String)value;
                }
            }
        }
    }
  • Откройте файл Form1.cs в режиме View Code и заполните его следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.Security.Cryptography;
    using System.IO;
        
    namespace Demo2
    {
        public partial class Form1 : Form
        {
            // Создаем объект симметричного шифрования
            SymmetricAlgorithm codec = null;// Тип общего предка 
            // Создаем объект сохранения параметров
            MySettings settings = new MySettings();// Это наш класс в файле MySettings.cs
        
            public Form1()
            {
                InitializeComponent();
        
                // Привязка именованных параметров к 
                // сохраняемым свойствам элементов управления
                txtDestinationDir.DataBindings.Add(new Binding("Text", settings, "DestinationDir"));
                txtSourceFile.DataBindings.Add(new Binding("Text", settings, "SourceFile"));
        
                // Настраиваем ComboBox динамически
                cmbCodec.Items.AddRange(
                    new string[]
                    {
                        "DESCryptoServiceProvider",
                        "RC2CryptoServiceProvider",
                        "RijndaelManaged",
                        "TripleDESCryptoServiceProvider",
                        "DefaultServiceProvider "
                    });
        
                cmbCodec.SelectedIndex = 0;// Выделяем первый элемент
            }
        
            // Меняем алгоритм шифрования
            private void cmbCodec_SelectedIndexChanged(object sender, EventArgs e)
            {
                // Блокируем обработчик Numeric_ValueChanged
                initFlag = false;
        
                ComboBox cmb = sender as ComboBox;
                switch (cmb.SelectedIndex)
                {
                    case 0:
                        codec = new DESCryptoServiceProvider();
                        break;
                    case 1:
                        codec = new RC2CryptoServiceProvider();
                        break;
                    case 2:
                        codec = new RijndaelManaged();
                        break;
                    case 3:
                        codec = new TripleDESCryptoServiceProvider();
                        break;
                    case 4:
                        codec = SymmetricAlgorithm.Create();
                        break;
                }
        
                // Заполняем NumericUpDown
                foreach (KeySizes sizes in codec.LegalKeySizes)
                {
                    numKeySize.Minimum = sizes.MinSize;
                    numKeySize.Maximum = sizes.MaxSize;
                    numKeySize.Increment = sizes.SkipSize;
                    break;
                }
                numKeySize.Value = codec.KeySize;
                CorrectTextKey();
                foreach (KeySizes sizes in codec.LegalBlockSizes)
                {
                    numBlockSize.Minimum = sizes.MinSize;
                    numBlockSize.Maximum = sizes.MaxSize;
                    numBlockSize.Increment = sizes.SkipSize;
                    break;
                }
                numBlockSize.Value = codec.BlockSize;
        
                // Разблокируем обработчик Numeric_ValueChanged
                initFlag = true;
            }
        
            // Устанавливаем допустимую длину ввода
            void CorrectTextKey()
            {
                // Корректируем длину поля ввода ключа
                int lenKey = codec.KeySize / 8;
                int lenTextBox= ASCIIEncoding.ASCII.GetByteCount(txtKey.Text);
                if (lenTextBox > lenKey)// Убираем лишнее
                    txtKey.Text = txtKey.Text.Substring(0, lenKey);
                txtKey.MaxLength = lenKey;
            }
        
            bool initFlag = false;
            private void Numeric_ValueChanged(object sender, EventArgs e)
            {
                if (initFlag)
                {
                    codec.KeySize = (int)numKeySize.Value;
                    codec.BlockSize = (int)numBlockSize.Value;
                    CorrectTextKey();
                }
            }
        
            // Создать новый текстовый файл через блокнот
            private void btnNewFile_Click(object sender, EventArgs e)
            {
                // Создаем новый текстовый файл через блокнот
                System.Diagnostics.Process exe = new System.Diagnostics.Process();
                exe.StartInfo.FileName = "Notepad.exe"; //Имя программы для запуска
                //exe.StartInfo.Arguments = path;         //Аргументы
                exe.Start();        // Запускаем внешний процесс
                //exe.WaitForExit();// Останавливаем наш процесс до закрытия блокнота
                exe.Close();        // Освобождаем связанные с процессом ресурсы
            }
        
            // Посмотреть через блокнот результаты шифрования
            private void btnShowFile_Click(object sender, EventArgs e)
            {
                // Покажем выбранный файл через блокнот
                System.Diagnostics.Process exe = new System.Diagnostics.Process();
                exe.StartInfo.FileName = "Notepad.exe"; //Имя программы для запуска
                exe.StartInfo.Arguments = SelectFile(); //Выбрать файл через диалог
                exe.Start();        // Запускаем внешний процесс
                //exe.WaitForExit();// Останавливаем наш процесс до закрытия блокнота
                exe.Close();        // Освобождаем связанные с процессом ресурсы
            }
        
            // Выполнить шифрование
            private void btnEncrypt_Click(object sender, EventArgs e)
            {
                if (txtKey.Text == "")
                {
                    MessageBox.Show(this, "Введите ключ для шифрования", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
        
                string destinationFile = "";
                if (Path.GetExtension(txtSourceFile.Text) != ".txt")
                {
                    MessageBox.Show(this, "Выберите файл '*.txt' для шифрования", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
                else
                {
                    destinationFile = txtSourceFile.Text.Replace(".txt", String.Empty) + ".enc";
                    destinationFile = Path.GetFileName(destinationFile);// Укороченное имя
                    destinationFile = Path.Combine(txtDestinationDir.Text, destinationFile);
                }
        
                if (encryptData(txtSourceFile.Text, destinationFile, txtKey.Text))
                    MessageBox.Show(this, "Выполнено!", "Шифрование файла",
                        MessageBoxButtons.OK, MessageBoxIcon.Information);
                else
                    MessageBox.Show(this, "Процесс шифрования закончился неудачей!", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
            }
        
            // Выполнить дешифрование
            private void btnDecrypt_Click(object sender, EventArgs e)
            {
                if (txtKey.Text == "")
                {
                    MessageBox.Show(this, "Введите ключ для дешифрования", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
        
                string destinationFile = "";
                if (Path.GetExtension(txtSourceFile.Text) != ".enc")
                {
                    MessageBox.Show(this, "Выберите файл '*.enc' для дешифрования", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
                else
                {
                    destinationFile = txtSourceFile.Text + ".txt";
                    destinationFile = Path.GetFileName(destinationFile);// Укороченное имя
                    destinationFile = Path.Combine(txtDestinationDir.Text, destinationFile);
                }
        
                if (decryptData(txtSourceFile.Text, destinationFile, txtKey.Text))
                    MessageBox.Show(this, "Выполнено!", "Дешифрование файла",
                        MessageBoxButtons.OK, MessageBoxIcon.Information);
                else
                    MessageBox.Show(this, "Процесс дешифрования закончился неудачей!", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
            }
        
            // Шифруем заданный файл
            private bool encryptData(
                string sourceFile, string destinationFile, string cryptoKey)
            {
                FileStream inFileStream = null, outFileStream = null;
                CryptoStream cryptoStream = null;
                
                // Заготавливаем пустой ключ полного размера
                byte[] key = new byte[(int)numKeySize.Value / 8];
        
                try
                {
                    // Создаем и настраиваем кодировщик
                    ASCIIEncoding.ASCII.GetBytes(cryptoKey.Trim()).CopyTo(key, 0);// Заполняем ключ
                    codec.Key = key;
                    //codec.GenerateIV();
                    ICryptoTransform encryptor = codec.CreateEncryptor();
        
                    // Открываем исходный и результирующий файлы, используя файловый поток
                    inFileStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
                    outFileStream = new FileStream(destinationFile, FileMode.Create, FileAccess.Write);
        
                    // Создаем оболочку CryptoStream для записи шифрованного файла
                    cryptoStream = new CryptoStream(outFileStream, encryptor, CryptoStreamMode.Write);
        
                    // Объявляем массив файлов с длиной входного файла 
                    byte[] bytes = new byte[inFileStream.Length];
        
                    // Считываем входной файловый поток в массив байтов 
                    // и записываем байты в CryptoStream
                    inFileStream.Read(bytes, 0, bytes.Length);
                    cryptoStream.Write(bytes, 0, bytes.Length);
        
                    inFileStream.Flush();
                    cryptoStream.FlushFinalBlock();
        
                    return true;
                }
                catch (Exception e)
                {
                    MessageBox.Show(this, e.ToString(), "Encryption Error",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
        
                    return false;
                }
                finally
                {
                    // Освобождаем ресурсы в любом случае
                    if (cryptoStream != null) cryptoStream.Close();
                    if (inFileStream != null) inFileStream.Close();
                    if (outFileStream != null) outFileStream.Close();
                }
            }
        
            // Расшифровываем указанный файл с тем же ключем и алгоритмом
            private bool decryptData(
                string sourceFile, string destinationFile, string cryptoKey)
            {
                CryptoStream cryptoStream = null;
                StreamWriter decryptedOut = null;
                FileStream decryptedFile = null;
                StreamReader streamReader = null;
        
                // Заготавливаем пустой ключ полного размера
                byte[] key = new byte[(int)numKeySize.Value / 8];
        
                try
                {
                    // Создаем и настраиваем кодировщик
                    ASCIIEncoding.ASCII.GetBytes(cryptoKey.Trim()).CopyTo(key, 0);// Заполняем ключ
                    codec.Key = key;
                    //codec.GenerateIV();
                    ICryptoTransform decryptor = codec.CreateDecryptor();
        
                    // Открываем исходный и результирующий файлы, используя файловый поток
                    decryptedFile = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
        
                    // Создаем оболочку CryptoStream для записи шифрованного файла
                    cryptoStream = new CryptoStream(decryptedFile, decryptor, CryptoStreamMode.Read);
        
                    decryptedOut = new StreamWriter(destinationFile);
                    streamReader = new StreamReader(cryptoStream, Encoding.GetEncoding("windows-1251"));
                    decryptedOut.Write(streamReader.ReadToEnd());
        
                    return true;
                }
                catch (Exception e)
                {
                    MessageBox.Show(this, e.ToString(), "Decryption Error",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
        
                    return false;
                }
                finally
                {
                    // Освобождаем ресурсы в любом случае
                    if (streamReader != null) streamReader.Close();
                    if (cryptoStream != null) cryptoStream.Close();
                    if (decryptedOut != null) decryptedOut.Close();
                    if (decryptedFile != null) decryptedFile.Close();
                }
            }
        
            // Диалог выбора файла для обработки
            OpenFileDialog openFileDialog = new OpenFileDialog();
            String SelectFile()
            {
                // Настраиваем диалог выбора файла
                if (txtSourceFile.Text == String.Empty)
                    openFileDialog.InitialDirectory = txtDestinationDir.Text;
                else
                    openFileDialog.InitialDirectory = Path.GetDirectoryName(txtSourceFile.Text);
        
                openFileDialog.FileName = "";
                openFileDialog.Filter = "Source Files(*.txt;*.enc)|*.txt;*.enc";
                DialogResult result = openFileDialog.ShowDialog();
                if (result != DialogResult.Cancel)
                    return openFileDialog.FileName; // Полное имя
                else
                    return txtSourceFile.Text;
            }
        
            // Запуск диалога выбора файла
            private void btnSelectFile_Click(object sender, EventArgs e)
            {
                txtSourceFile.Text = SelectFile();
                txtSourceFile.Focus();// Без этого не сохранится в установках приложения
            }
        
            // Запуск диалога выбора папки назначения
            FolderBrowserDialog dialogFolder = new FolderBrowserDialog();
            private void btnDestinationDir_Click(object sender, EventArgs e)
            {
                if (txtDestinationDir.Text == String.Empty ||
                    Directory.Exists(txtDestinationDir.Text) == false)
                {
                    dialogFolder.RootFolder =
                        System.Environment.SpecialFolder.MyComputer;
                    dialogFolder.SelectedPath = "";
                    txtDestinationDir.Text = String.Empty;
                }
                else
                {
                    dialogFolder.SelectedPath = txtDestinationDir.Text;
                }
        
                dialogFolder.Description =
                    "Выберите место размещения целевого файла";
        
                dialogFolder.ShowDialog();
                txtDestinationDir.Text = dialogFolder.SelectedPath;
                txtDestinationDir.Focus();
            }
        
            // Блокировка редактирования TextBox и NumericUpDown вручную
            private void EditKeyDown(object sender, KeyEventArgs e)
            {
                e.Handled = true;
            }
            private void EditKeyPress(object sender, KeyPressEventArgs e)
            {
                e.Handled = true;
            }
        
            // При закрытии формы сохраняем параметры
            private void Form1_FormClosing(object sender, FormClosingEventArgs e)
            {
                settings.Save();
            }
        
            // Закрываем форму
            private void btnClose_Click(object sender, EventArgs e)
            {
                this.Close();
            }
        }
    }

    Каждый используемый алгоритм шифрования имеет свой фиксированный размер ключа, нарушение которого генерирует исключение. Поэтому мы вначале создаем байтовый массив фиксированного размера, а потом копируем в него содержимое пользовательского ключа, не превышающего по размеру допустимого. Незаполненные байты массива остаются нулевыми, зато этот прием позволяет принимать от пользователя ключи любой длины, не превышающей допустимую. В дополнение к этому на поле ввода ключа txtKey наложено ограничение по максимальной длине текста.

  • Запустите приложение Demo2 - оно вполне работоспособно и справляется с шифрованием при правильном обращении с ним
  • Разберитесь с кодом ( кому надо!), да простят меня классики
  • Внешний вид окна будет таким

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

    Пример 3. Поток CryptoStream и электронные подписи документов

    Чтобы уж совсем завершить тему потока CryptoStream, рассмотрим еще одну сферу его применения - электронные подписи документов. Иногда получатель документа должен быть уверен, что этот документ не был искажен. В этом случае отправитель и получатель договариваются об одном и том же секретном ключе и применяют специальные алгоритмы хеширования, использующие ключ.

    Напомним, что любые хеш-алгоритмы всегда генерируют значение постоянной длины (каждый своей) независимо от длины хешируемого текста. Но алгоритмы хеширования с ключами HMAC ( Hash Message Authentication ) отличаются от обычных алгоритмов хеширования тем, что они генерируют хеш на основании ключа. Отправитель документа вычисляет его хеш на основании секретного ключа и отправляет вместе с ключем получателю. Получатель извлекает хеш из присланного документа и повторно хеширует сам оставшийся документ с применением своего дубликата ключа. Если вычисленный хеш совпадает с полученным, то подлинность документа считается установленной.

    Библиотека .NET Framework поддерживает задачи электронных подписей несколькими классами-алгоритмами пространства имен System.Security.Cryptography. Они принимают ключи разного размера и производят хеш-значение разной длины. Если длина применяемых ключей отличается от разрешенной, то любой из алгоритмов генерирует исключение.

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

  • Добавьте к решению CryptoStream новый проект с именем Demo3 и назначьте его стартовым
  • Откройте интерфейсный файл Form1.Designer.cs и заполните его следующим кодом (приводится полностью)
  • namespace Demo3
    {
        partial class Form1
        {
            private System.ComponentModel.IContainer components = null;
        
            protected override void Dispose(bool disposing)
            {
                if (disposing  (components != null))
                {
                    components.Dispose();
                }
                base.Dispose(disposing);
            }
        
            #region Windows Form Designer generated code
        
            private void InitializeComponent()
            {
                this.cmbHMAC = new System.Windows.Forms.ComboBox();
                this.label1 = new System.Windows.Forms.Label();
                this.label2 = new System.Windows.Forms.Label();
                this.txtSourceFile = new System.Windows.Forms.TextBox();
                this.btnSelectFile = new System.Windows.Forms.Button();
                this.txtDestinationDir = new System.Windows.Forms.TextBox();
                this.btnDestinationDir = new System.Windows.Forms.Button();
                this.label4 = new System.Windows.Forms.Label();
                this.txtKey = new System.Windows.Forms.TextBox();
                this.btnClose = new System.Windows.Forms.Button();
                this.lblInfo = new System.Windows.Forms.Label();
                this.label3 = new System.Windows.Forms.Label();
                this.btnSigned = new System.Windows.Forms.Button();
                this.btnChecked = new System.Windows.Forms.Button();
                this.btnShowFile = new System.Windows.Forms.Button();
                this.btnNewFile = new System.Windows.Forms.Button();
                this.SuspendLayout();
                // 
                // cmbHMAC
                // 
                this.cmbHMAC.FormattingEnabled = true;
                this.cmbHMAC.Location = new System.Drawing.Point(94, 21);
                this.cmbHMAC.Name = "cmbHMAC";
                this.cmbHMAC.Size = new System.Drawing.Size(280, 24);
                this.cmbHMAC.TabIndex = 0;
                this.cmbHMAC.SelectedIndexChanged += new System.EventHandler(this.cmbHMAC_SelectedIndexChanged);
                // 
                // label1
                // 
                this.label1.AutoSize = true;
                this.label1.Location = new System.Drawing.Point(36, 24);
                this.label1.Name = "label1";
                this.label1.Size = new System.Drawing.Size(51, 17);
                this.label1.TabIndex = 1;
                this.label1.Text = "HMAC:";
                // 
                // label2
                // 
                this.label2.AutoSize = true;
                this.label2.Location = new System.Drawing.Point(5, 156);
                this.label2.Name = "label2";
                this.label2.Size = new System.Drawing.Size(83, 17);
                this.label2.TabIndex = 2;
                this.label2.Text = "Source File:";
                // 
                // txtSourceFile
                // 
                this.txtSourceFile.Location = new System.Drawing.Point(94, 155);
                this.txtSourceFile.Name = "txtSourceFile";
                this.txtSourceFile.Size = new System.Drawing.Size(280, 22);
                this.txtSourceFile.TabIndex = 3;
                this.txtSourceFile.KeyDown += new System.Windows.Forms.KeyEventHandler(this.EditKeyDown);
                this.txtSourceFile.KeyPress += new System.Windows.Forms.KeyPressEventHandler(this.EditKeyPress);
                // 
                // btnSelectFile
                // 
                this.btnSelectFile.Font = new System.Drawing.Font("Agency FB", 7.2F, System.Drawing.FontStyle.Bold, 
                	System.Drawing.GraphicsUnit.Point, ((byte)(0)));
                this.btnSelectFile.Location = new System.Drawing.Point(380, 155);
                this.btnSelectFile.Name = "btnSelectFile";
                this.btnSelectFile.Size = new System.Drawing.Size(25, 23);
                this.btnSelectFile.TabIndex = 4;
                this.btnSelectFile.Text = "...";
                this.btnSelectFile.UseVisualStyleBackColor = true;
                this.btnSelectFile.Click += new System.EventHandler(this.btnSelectFile_Click);
                // 
                // txtDestinationDir
                // 
                this.txtDestinationDir.Location = new System.Drawing.Point(94, 198);
                this.txtDestinationDir.Name = "txtDestinationDir";
                this.txtDestinationDir.Size = new System.Drawing.Size(280, 22);
                this.txtDestinationDir.TabIndex = 6;
                // 
                // btnDestinationDir
                // 
                this.btnDestinationDir.Font = new System.Drawing.Font("Agency FB", 7.2F, System.Drawing.FontStyle.Bold, 
                	System.Drawing.GraphicsUnit.Point, ((byte)(0)));
                this.btnDestinationDir.Location = new System.Drawing.Point(380, 198);
                this.btnDestinationDir.Name = "btnDestinationDir";
                this.btnDestinationDir.Size = new System.Drawing.Size(25, 23);
                this.btnDestinationDir.TabIndex = 7;
                this.btnDestinationDir.Text = "...";
                this.btnDestinationDir.UseVisualStyleBackColor = true;
                this.btnDestinationDir.Click += new System.EventHandler(this.btnDestinationDir_Click);
                // 
                // label4
                // 
                this.label4.AutoSize = true;
                this.label4.Location = new System.Drawing.Point(52, 67);
                this.label4.Name = "label4";
                this.label4.Size = new System.Drawing.Size(36, 17);
                this.label4.TabIndex = 8;
                this.label4.Text = "Key:";
                // 
                // txtKey
                // 
                this.txtKey.Location = new System.Drawing.Point(94, 66);
                this.txtKey.Name = "txtKey";
                this.txtKey.Size = new System.Drawing.Size(280, 22);
                this.txtKey.TabIndex = 9;
                // 
                // btnClose
                // 
                this.btnClose.AutoSize = true;
                this.btnClose.Location = new System.Drawing.Point(353, 256);
                this.btnClose.Name = "btnClose";
                this.btnClose.Size = new System.Drawing.Size(75, 27);
                this.btnClose.TabIndex = 12;
                this.btnClose.Text = "Close";
                this.btnClose.UseVisualStyleBackColor = true;
                this.btnClose.Click += new System.EventHandler(this.btnClose_Click);
                // 
                // lblInfo
                // 
                this.lblInfo.AutoSize = true;
                this.lblInfo.Location = new System.Drawing.Point(104, 110);
                this.lblInfo.Name = "lblInfo";
                this.lblInfo.Size = new System.Drawing.Size(45, 17);
                this.lblInfo.TabIndex = 15;
                this.lblInfo.Text = "lblInfo";
                // 
                // label3
                // 
                this.label3.AutoSize = true;
                this.label3.Location = new System.Drawing.Point(8, 186);
                this.label3.Name = "label3";
                this.label3.Size = new System.Drawing.Size(79, 34);
                this.label3.TabIndex = 5;
                this.label3.Text = "Destination\r\ndirectory:";
                this.label3.TextAlign = System.Drawing.ContentAlignment.TopRight;
                // 
                // btnSigned
                // 
                this.btnSigned.AutoSize = true;
                this.btnSigned.Location = new System.Drawing.Point(98, 256);
                this.btnSigned.Name = "btnSigned";
                this.btnSigned.Size = new System.Drawing.Size(75, 27);
                this.btnSigned.TabIndex = 19;
                this.btnSigned.Text = "Signed";
                this.btnSigned.UseVisualStyleBackColor = true;
                this.btnSigned.Click += new System.EventHandler(this.btnSigned_Click);
                // 
                // btnChecked
                // 
                this.btnChecked.AutoSize = true;
                this.btnChecked.Location = new System.Drawing.Point(182, 256);
                this.btnChecked.Name = "btnChecked";
                this.btnChecked.Size = new System.Drawing.Size(75, 27);
                this.btnChecked.TabIndex = 20;
                this.btnChecked.Text = "Checked";
                this.btnChecked.UseVisualStyleBackColor = true;
                this.btnChecked.Click += new System.EventHandler(this.btnChecked_Click);
                // 
                // btnShowFile
                // 
                this.btnShowFile.AutoSize = true;
                this.btnShowFile.Location = new System.Drawing.Point(266, 256);
                this.btnShowFile.Name = "btnShowFile";
                this.btnShowFile.Size = new System.Drawing.Size(78, 27);
                this.btnShowFile.TabIndex = 21;
                this.btnShowFile.Text = "Show File";
                this.btnShowFile.UseVisualStyleBackColor = true;
                this.btnShowFile.Click += new System.EventHandler(this.btnShowFile_Click);
                // 
                // btnNewFile
                // 
                this.btnNewFile.AutoSize = true;
                this.btnNewFile.Location = new System.Drawing.Point(14, 256);
                this.btnNewFile.Name = "btnNewFile";
                this.btnNewFile.Size = new System.Drawing.Size(75, 27);
                this.btnNewFile.TabIndex = 22;
                this.btnNewFile.Text = "New File";
                this.btnNewFile.UseVisualStyleBackColor = true;
                this.btnNewFile.Click += new System.EventHandler(this.btnNewFile_Click);
                // 
                // Form1
                // 
                this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 16F);
                this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
                this.ClientSize = new System.Drawing.Size(442, 296);
                this.Controls.Add(this.btnNewFile);
                this.Controls.Add(this.btnShowFile);
                this.Controls.Add(this.btnChecked);
                this.Controls.Add(this.btnSigned);
                this.Controls.Add(this.lblInfo);
                this.Controls.Add(this.btnClose);
                this.Controls.Add(this.txtKey);
                this.Controls.Add(this.label4);
                this.Controls.Add(this.btnDestinationDir);
                this.Controls.Add(this.txtDestinationDir);
                this.Controls.Add(this.label3);
                this.Controls.Add(this.btnSelectFile);
                this.Controls.Add(this.txtSourceFile);
                this.Controls.Add(this.label2);
                this.Controls.Add(this.label1);
                this.Controls.Add(this.cmbHMAC);
                this.MaximizeBox = false;
                this.Name = "Form1";
                this.Text = "HMAC Algorithm with Files";
                this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.Form1_FormClosing);
                this.ResumeLayout(false);
                this.PerformLayout();
            }
        
            #endregion
        
            private System.Windows.Forms.ComboBox cmbHMAC;
            private System.Windows.Forms.Label label1;
            private System.Windows.Forms.Label label2;
            private System.Windows.Forms.TextBox txtSourceFile;
            private System.Windows.Forms.Button btnSelectFile;
            private System.Windows.Forms.TextBox txtDestinationDir;
            private System.Windows.Forms.Button btnDestinationDir;
            private System.Windows.Forms.Label label4;
            private System.Windows.Forms.TextBox txtKey;
            private System.Windows.Forms.Button btnClose;
            private System.Windows.Forms.Label lblInfo;
            private System.Windows.Forms.Label label3;
            private System.Windows.Forms.Button btnSigned;
            private System.Windows.Forms.Button btnChecked;
            private System.Windows.Forms.Button btnShowFile;
            private System.Windows.Forms.Button btnNewFile;
        }
    }
  • Откройте файл Form1.cs в режиме View Code и заполните его следующим кодом (приводится полностью)
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    // Дополнительные пространства имен
    using System.Security.Cryptography;
    using System.IO;
        
    namespace Demo3
    {
        public partial class Form1 : Form
        {
            // Объявляем ссылку на объект для хеширования документов
            KeyedHashAlgorithm hmac = null;// Тип общего предка 
            // Создаем объект сохранения параметров
            MySettings settings = new MySettings();// Это наш класс в файле MySettings.cs
        
            public Form1()
            {
                InitializeComponent();
        
                // Привязка именованных параметров к 
                // сохраняемым свойствам элементов управления
                txtDestinationDir.DataBindings.Add(new Binding("Text", settings, "DestinationDir"));
                txtSourceFile.DataBindings.Add(new Binding("Text", settings, "SourceFile"));
        
                // Настраиваем ComboBox динамически
                cmbHMAC.Items.AddRange(
                    new string[]
                    {
                        "HMACSHA1",
                        "MACTripleDES",
                        "HMACMD5",
                        "HMACRIPEMD160",
                        "HMACSHA256",
                        "HMACSHA384",
                        "HMACSHA512"
                    });
        
                cmbHMAC.SelectedIndex = 0;// Выделяем первый элемент
            }
        
            // Меняем алгоритм хеширования
            private void cmbHMAC_SelectedIndexChanged(object sender, EventArgs e)
            {
                ComboBox cmb = sender as ComboBox;
                switch (cmb.SelectedIndex)
                {
                    case 0:
                        hmac = new HMACSHA1();
                        break;
                    case 1:
                        hmac = new MACTripleDES();
                        break;
                    case 2:
                        hmac = new HMACMD5();
                        break;
                    case 3:
                        hmac = new HMACRIPEMD160();
                        break;
                    case 4:
                        hmac = new HMACSHA256();
                        break;
                    case 5:
                        hmac = new HMACSHA384();
                        break;
                    case 6:
                        hmac = new HMACSHA512();
                        break;
                }
        
                CorrectTextKey();
            }
        
            // Устанавливаем допустимую длину ввода
            void CorrectTextKey()
            {
                // Корректируем длину поля ввода ключа
                int lenKey = hmac.Key.Length;
                int lenTextBox = ASCIIEncoding.ASCII.GetByteCount(txtKey.Text);
                if (lenTextBox > lenKey)// Убираем лишнее
                    txtKey.Text = txtKey.Text.Substring(0, lenKey);
                txtKey.MaxLength = lenKey;// Ограничение в поле ввода
                // Отображаем в текстовой метке
                lblInfo.Text = String.Format("Key Size: {0} bytes   Hash Size: {1} bytes",
                    hmac.Key.Length, hmac.HashSize / 8);
            }
        
            // Создать новый текстовый файл через блокнот
            private void btnNewFile_Click(object sender, EventArgs e)
            {
                // Создаем новый текстовый файл через блокнот
                System.Diagnostics.Process exe = new System.Diagnostics.Process();
                exe.StartInfo.FileName = "Notepad.exe"; //Имя программы для запуска
                //exe.StartInfo.Arguments = path;         //Аргументы
                exe.Start();        // Запускаем внешний процесс
                //exe.WaitForExit();// Останавливаем наш процесс до закрытия блокнота
                exe.Close();        // Освобождаем связанные с процессом ресурсы
            }
        
            // Посмотреть через блокнот результаты 
            private void btnShowFile_Click(object sender, EventArgs e)
            {
                // Покажем выбранный файл через блокнот
                System.Diagnostics.Process exe = new System.Diagnostics.Process();
                exe.StartInfo.FileName = "Notepad.exe"; //Имя программы для запуска
                exe.StartInfo.Arguments = SelectFile(); //Выбрать файл через диалог
                exe.Start();        // Запускаем внешний процесс
                //exe.WaitForExit();// Останавливаем наш процесс до закрытия блокнота
                exe.Close();        // Освобождаем связанные с процессом ресурсы
            }
        
            // Подписать документ
            private void btnSigned_Click(object sender, EventArgs e)
            {
                if (txtKey.Text == "")
                {
                    MessageBox.Show(this, "Введите ключ для хеширования", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
        
                string destinationFile = "";
                if (Path.GetExtension(txtSourceFile.Text) != ".txt")
                {
                    MessageBox.Show(this, "Выберите файл '*.txt' для хеширования", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
                else
                {
                    destinationFile = txtSourceFile.Text.Replace(".txt", String.Empty) + ".sign";
                    destinationFile = Path.GetFileName(destinationFile);// Укороченное имя
                    destinationFile = Path.Combine(txtDestinationDir.Text, destinationFile);
                }
        
                if (signedData(txtSourceFile.Text, destinationFile, txtKey.Text))
                    MessageBox.Show(this, "Выполнено!", "Подписание файла",
                        MessageBoxButtons.OK, MessageBoxIcon.Information);
                else
                    MessageBox.Show(this, "Процесс подписания закончился неудачей!", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
            }
        
            // Выполнить аутентификацию документа
            private void btnChecked_Click(object sender, EventArgs e)
            {
                if (txtKey.Text == "")
                {
                    MessageBox.Show(this, "Введите ключ для аутентификации", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
        
                string destinationFile = "";
                if (Path.GetExtension(txtSourceFile.Text) != ".sign")
                {
                    MessageBox.Show(this, "Выберите файл '*.sign' для аутентификации", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    return;
                }
                else
                {
                    destinationFile = txtSourceFile.Text + ".txt";
                    destinationFile = Path.GetFileName(destinationFile);// Укороченное имя
                    destinationFile = Path.Combine(txtDestinationDir.Text, destinationFile);
                }
        
                if (checkedData(txtSourceFile.Text, destinationFile, txtKey.Text))
                    MessageBox.Show(this, "Выполнено!", "Аутентификация файла",
                        MessageBoxButtons.OK, MessageBoxIcon.Information);
                else
                    MessageBox.Show(this, "Процесс аутентификации закончился неудачей!", "Ошибка",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
            }
        
            // Подписываем заданный файл
            private bool signedData(
                string sourceFile, string destinationFile, string cryptoKey)
            {
                FileStream inFileStream = null, outFileStream = null;
        
                // Создаем заново, иначе барахлит
                switch (cmbHMAC.SelectedIndex)
                {
                    case 0:
                        hmac = new HMACSHA1();
                        break;
                    case 1:
                        hmac = new MACTripleDES();
                        break;
                    case 2:
                        hmac = new HMACMD5();
                        break;
                    case 3:
                        hmac = new HMACRIPEMD160();
                        break;
                    case 4:
                        hmac = new HMACSHA256();
                        break;
                    case 5:
                        hmac = new HMACSHA384();
                        break;
                    case 6:
                        hmac = new HMACSHA512();
                        break;
                }
        
                try
                {
                    // Настраиваем алгоритм хеширования
                    byte[] key = new byte[hmac.Key.Length];// Заготавливаем требуемый размер ключа
                    ASCIIEncoding.ASCII.GetBytes(cryptoKey.Trim()).CopyTo(key, 0);// Заполняем ключ
                    hmac.Key = key;
        
                    // Создаем оболочку CryptoStream для хеширования 'в никуда'
                    CryptoStream cryptoStream = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write);
        
                    // Открываем исходный файл, используя файловый поток
                    inFileStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
                    // Объявляем массив байтов с длиной входного файла 
                    byte[] bytes = new byte[inFileStream.Length];
        
                    // Считываем входной файловый поток в массив байтов и записываем 
                    // байты в CryptoStream, чтобы получить хеш исходного документа
                    inFileStream.Read(bytes, 0, bytes.Length);
                    cryptoStream.Write(bytes, 0, bytes.Length);
                    cryptoStream.Close();
        
                    // Извлекаем хеш и записываем все в выходной документ
                    outFileStream = new FileStream(destinationFile, FileMode.Create, FileAccess.Write);
                    BinaryWriter bw = new BinaryWriter(outFileStream);
                    bw.Write(hmac.Hash.Length);// Пишем длину хеша
                    outFileStream.Write(hmac.Hash, 0, hmac.Hash.Length);// Пишем сам хеш
                    inFileStream.Position = 0;// Указатель в начало
                    inFileStream.Read(bytes, 0, bytes.Length); // Читаем исходный файл
                    outFileStream.Write(bytes, 0, bytes.Length); // Пишем сам документ
        
                    return true;
                }
                catch (Exception e)
                {
                    MessageBox.Show(this, e.ToString(), "Hashed Error",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
        
                    return false;
                }
                finally
                {
                    // Освобождаем файловые ресурсы в любом случае
                    if (inFileStream != null) inFileStream.Close();
                    if (outFileStream != null) outFileStream.Close();
                }
            }
        
            // Хешируем указанный файл с тем же ключем и алгоритмом
            private bool checkedData(
                string sourceFile, string destinationFile, string cryptoKey)
            {
                FileStream inFileStream = null, outFileStream = null;
        
                // Создаем заново, иначе барахлит
                switch (cmbHMAC.SelectedIndex)
                {
                    case 0:
                        hmac = new HMACSHA1();
                        break;
                    case 1:
                        hmac = new MACTripleDES();
                        break;
                    case 2:
                        hmac = new HMACMD5();
                        break;
                    case 3:
                        hmac = new HMACRIPEMD160();
                        break;
                    case 4:
                        hmac = new HMACSHA256();
                        break;
                    case 5:
                        hmac = new HMACSHA384();
                        break;
                    case 6:
                        hmac = new HMACSHA512();
                        break;
                }
        
                try
                {
                    // Настраиваем алгоритм хеширования
                    byte[] key = new byte[hmac.Key.Length];// Заготавливаем требуемый размер ключа
                    ASCIIEncoding.ASCII.GetBytes(cryptoKey.Trim()).CopyTo(key, 0);// Заполняем ключ
                    hmac.Key = key;
        
                    // Создаем оболочку CryptoStream для хеширования 'в никуда'
                    CryptoStream cryptoStream = new CryptoStream(Stream.Null, hmac, CryptoStreamMode.Write);
        
                    // Открываем исходный файл и извлекаем хеш
                    inFileStream = new FileStream(sourceFile, FileMode.Open, FileAccess.Read);
                    BinaryReader br = new BinaryReader(inFileStream);// Вспомогательная оболочка
                    int hashLength = br.ReadInt32();// Читаем длину хеша
                    byte[] receivedHash = new byte[hashLength];// Место под хеш
                    inFileStream.Read(receivedHash, 0, receivedHash.Length);// Извлекаем хеш
                    long posBeginDoc = inFileStream.Position;// Начало документа
        
                    // Объявляем массив байтов тела документа
                    byte[] bytes = new byte[inFileStream.Length - posBeginDoc];
        
                    // Дочитываем документ в массив байтов и записываем байты 
                    // в CryptoStream, чтобы получить хеш исходного документа
                    inFileStream.Read(bytes, 0, bytes.Length);
                    cryptoStream.Write(bytes, 0, bytes.Length);
                    cryptoStream.Close();
        
                    // Извлекаем хеш и сравниваем с присланным
                    // Преобразование ASCIIEncoding.ASCII.GetString() сбоит!
                    String strCurrentHash = Encoding.Unicode.GetString(hmac.Hash);// byte[] -> string
                    String strReceivedHash = Encoding.Unicode.GetString(receivedHash);// Присланный хеш
                    if (strCurrentHash.CompareTo(strReceivedHash) != 0)
                        MessageBox.Show("Документ фальсифицирован!");
                    else
                    {
                        inFileStream.Position = posBeginDoc;
                        inFileStream.Read(bytes, 0, bytes.Length);
                        outFileStream = new FileStream(destinationFile, FileMode.Create, FileAccess.Write);
                        outFileStream.Write(bytes, 0, bytes.Length);
                        outFileStream.Flush();
                        MessageBox.Show("Документ подлинный и очищен от хеша!");
                    }
        
                    return true;
                }
                catch (Exception e)
                {
                    MessageBox.Show(this, e.ToString(), "Checked Error",
                        MessageBoxButtons.OK, MessageBoxIcon.Stop);
        
                    return false;
                }
                finally
                {
                    // Освобождаем файловые ресурсы в любом случае
                    if (inFileStream != null) inFileStream.Close();
                    if (outFileStream != null) outFileStream.Close();
                }
            }
        
            // Диалог выбора файла для обработки
            OpenFileDialog openFileDialog = new OpenFileDialog();
            String SelectFile()
            {
                // Настраиваем диалог выбора файла
                if (txtSourceFile.Text == String.Empty)
                    openFileDialog.InitialDirectory = txtDestinationDir.Text;
                else
                    openFileDialog.InitialDirectory = Path.GetDirectoryName(txtSourceFile.Text);
        
                openFileDialog.FileName = "";
                openFileDialog.Filter = "Source Files(*.txt;*.sign)|*.txt;*.sign";
                DialogResult result = openFileDialog.ShowDialog();
                if (result != DialogResult.Cancel)
                    return openFileDialog.FileName; // Полное имя
                else
                    return txtSourceFile.Text;
            }
        
            // Запуск диалога выбора файла
            private void btnSelectFile_Click(object sender, EventArgs e)
            {
                txtSourceFile.Text = SelectFile();
                txtSourceFile.Focus();// Без этого не сохранится в установках приложения
            }
        
            // Запуск диалога выбора папки назначения
            FolderBrowserDialog dialogFolder = new FolderBrowserDialog();
            private void btnDestinationDir_Click(object sender, EventArgs e)
            {
                if (txtDestinationDir.Text == String.Empty ||
                    Directory.Exists(txtDestinationDir.Text) == false)
                {
                    dialogFolder.RootFolder =
                        System.Environment.SpecialFolder.MyComputer;
                    dialogFolder.SelectedPath = "";
                    txtDestinationDir.Text = String.Empty;
                }
                else
                {
                    dialogFolder.SelectedPath = txtDestinationDir.Text;
                }
        
                dialogFolder.Description =
                    "Выберите место размещения целевого файла";
        
                dialogFolder.ShowDialog();
                txtDestinationDir.Text = dialogFolder.SelectedPath;
                txtDestinationDir.Focus();
            }
        
            // Блокировка редактирования TextBox вручную
            private void EditKeyDown(object sender, KeyEventArgs e)
            {
                e.Handled = true;
            }
            private void EditKeyPress(object sender, KeyPressEventArgs e)
            {
                e.Handled = true;
            }
        
            // При закрытии формы сохраняем параметры
            private void Form1_FormClosing(object sender, FormClosingEventArgs e)
            {
                settings.Save();
            }
        
            // Закрываем форму
            private void btnClose_Click(object sender, EventArgs e)
            {
                this.Close();
            }
        }
    }

    Здесь мы применили двоичные потоки BinaryReader и BinaryWriter вместо текстовых StreamReader и StreamWriter, поскольку последние оставляют в документе невидимые метки и меняют хеш (пачкают). Кодировщики Unicode тоже точнее преобразуют хеш, который может содержать отличные от ASCII символы.

  • Добавьте к текущему проекту командой Project/Add Class новый файл с именем MySettings.cs и заполните его следующим кодом (приводится полностью)
  • using System;
    using System.Configuration;
        
    namespace Demo3
    {
        // Класс для сохранения параметров приложения
        class MySettings : ApplicationSettingsBase
        {
            [UserScopedSettingAttribute()]       // Область действия - 'Пользователь' 
            [DefaultSettingValueAttribute("C:\\")]   // Значение параметра по умолчанию
            public String DestinationDir
            {
                get
                {
                    return ((String)this["DestinationDir"]);
                }
                set
                {
                    this["DestinationDir"] = (String)value;
                }
            }
        
            [UserScopedSettingAttribute()]       // Область действия - 'Пользователь' 
            [DefaultSettingValueAttribute("")]   // Значение параметра по умолчанию
            public String SourceFile
            {
                get
                {
                    return ((String)this["SourceFile"]);
                }
                set
                {
                    this["SourceFile"] = (String)value;
                }
            }
        }
    }
  • Запустите приложение Demo3 - оно вполне работоспособно, подписывает документы и проводит их аутентификацию
  • Разберитесь с кодом ( кому надо!), да пусть еще больше простят меня классики
  • Внешний вид окна будет таким

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

    Дальше пока сами... (опять по шпалам!)

    Вернуться к учебному плану