Опубликован: 05.08.2010 | Уровень: специалист | Доступ: платный
Лекция 6:

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

< Лекция 5 || Лекция 6: 12345678910111213

Пример 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();
        }
    
        ................................................
    }
}
  • Запустите сервер - теперь и блокировка работает, и сервер можно закрыть, и миокард целый!
< Лекция 5 || Лекция 6: 12345678910111213
Алексей Бабушкин
Алексей Бабушкин

При выполнении в лабораторной работе упражнения №1 , а именно при выполнении нижеследующего кода:

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Data;

using System.Drawing;

using System.Text;

using System.Windows.Forms;

using Microsoft.Xna.Framework.Graphics;

   

namespace Application1

{

    public partial class MainForm : Form

    {

        // Объявим поле графического устройства для видимости в методах

        GraphicsDevice device;

   

        public MainForm()

        {

            InitializeComponent();

   

            // Подпишемся на событие Load формы

            this.Load += new EventHandler(MainForm_Load);

   

            // Попишемся на событие FormClosed формы

            this.FormClosed += new FormClosedEventHandler(MainForm_FormClosed);

        }

   

        void MainForm_FormClosed(object sender, FormClosedEventArgs e)

        {

            //  Удаляем (освобождаем) устройство

            device.Dispose();

            // На всякий случай присваиваем ссылке на устройство значение null

            device = null;       

        }

   

        void MainForm_Load(object sender, EventArgs e)

        {

            // Создаем объект представления для настройки графического устройства

            PresentationParameters presentParams = new PresentationParameters();

            // Настраиваем объект представления через его свойства

            presentParams.IsFullScreen = false; // Включаем оконный режим

            presentParams.BackBufferCount = 1;  // Включаем задний буфер

                                                // для двойной буферизации

            // Переключение переднего и заднего буферов

            // должно осуществляться с максимальной эффективностью

            presentParams.SwapEffect = SwapEffect.Discard;

            // Устанавливаем размеры заднего буфера по клиентской области окна формы

            presentParams.BackBufferWidth = this.ClientSize.Width;

            presentParams.BackBufferHeight = this.ClientSize.Height;

   

            // Создадим графическое устройство с заданными настройками

            device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware,

                this.Handle, presentParams);

        }

   

        protected override void OnPaint(PaintEventArgs e)

        {

            device.Clear(Microsoft.Xna.Framework.Graphics.Color.CornflowerBlue);

   

            base.OnPaint(e);

        }

    }

}

Выбрасывается исключение:

Невозможно загрузить файл или сборку "Microsoft.Xna.Framework, Version=3.0.0.0, Culture=neutral, PublicKeyToken=6d5c3888ef60e27d" или один из зависимых от них компонентов. Не удается найти указанный файл.

Делаю все пунктуально. В чем может быть проблема?

Dmitriy Ivanchenko
Dmitriy Ivanchenko
Украина, Кировоград, Виктория-П, 2011
Татьяна Ковалюк
Татьяна Ковалюк
Украина, Киев, Киевский политехнический институт, 1974