При выполнении в лабораторной работе упражнения №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" или один из зависимых от них компонентов. Не удается найти указанный файл. Делаю все пунктуально. В чем может быть проблема? |
Работа с потоками данных
Пример 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 -адресе не может сидеть сразу несколько запущенных серверов. Иначе бы получилась неопределенность, с каким именно сервером должны общаться клиенты. А службы как раз и запускаются в одном экземпляре.
Но сечас мы, для разнообразия, создадим подобный сервер, но уже как консольное приложение. Клиенты останутся теми же самыми. Вот здесь-то как раз мы и убедимся, что при запуске второй копии консольного сервера операционная система выбросит исключение.