Клиент-серверная архитектура: особенности взаимодействия. Клиент-серверное приложение на потоковом сокете TCP

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

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

(Здесь опять проявляются недостатки в терминологии. Обычно, когда компания объявляет о выпуске очередного сервера баз данных, то неявно понимается, что имеется и клиентская составляющая этого продукта. Сочетание "клиентская часть сервера баз данных" кажется несколько странным, но нам придется пользоваться именно этим термином.)

Рис. 2.3. Общее представление информационной системы в архитектуре "клиент-сервер"

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

Наконец, клиентская часть сервера баз данных, используя средства сетевого доступа, обращается к серверу баз данных, передавая ему текст оператора языка SQL.

Здесь необходимо сделать еще два замечания.

  1. Обычно компании, производящие развитые серверы баз данных, стремятся к тому, чтобы обеспечить возможность использования своих продуктов не только в стандартных на сегодняшний день TCP/IP-ориентированных сетях, но в сетях, основанных на других протоколах (например, SNA или IPX/SPX). Поэтому при организации сетевых взаимодействий между клиентской и серверной частями СУБД часто используются не стандартные средства высокого уровня (например, механизмы программных гнезд или вызовов удаленных процедур), а собственные функционально подобные средства, менее зависящие от особенностей сетевых транспортных протоколов.
  2. Когда мы говорим об интерфейсе на основе языка SQL, нужно отдавать себе отчет в том, что несмотря на титанические усилия по стандартизации этого языка, нет такой реализации, в которой стандартные средства языка не были бы расширены. Необдуманное использование расширений языка приводит к полной зависимости приложения от конкретного производителя сервера баз данных.

Мы остановимся на этих вопросах более подробно в четвертой части курса.

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

  • Сервер производит компиляцию полученного оператора. Не будем здесь останавливаться на том, какой целевой язык используется конкретным компилятором; в разных реализациях применяются различные подходы (примеры см. в части 4). Главное, что в любом случае на основе информации, содержащейся в таблицах-каталогах базы данных производится преобразование непроцедурного представления оператора в некоторую процедуру его выполнения.
  • Далее (если компиляция завершилась успешно) происходит выполнение оператора. Мы снова не будем обсуждать технические детали, поскольку они различаются в реализациях. Рассмотрим возможные действия операторов SQL.
    • Оператор может относиться к классу операторов определения (или создания) объектов базы данных (точнее и правильнее было бы говорить про элементы схемы базы данных или про объекты метабазы данных). В частности, могут определяться домены, таблицы, ограничения целостности, триггеры, привилегии пользователей, хранимые процедуры. В любом случае, при выполнении оператора создания элемента схемы базы данных соответствующая информация помещается в таблицы-каталоги базы данных (в таблицы метабазы данных). Ограничения целостности обычно сохраняются в метабазе данных прямо в текстовом представлении. Для действий, определенных в триггерах, и хранимых процедур вырабатывается и сохраняется в таблицах-каталогах процедурный выполняемый код. Заметим, что ограничения целостности, триггеры и хранимые процедуры являются, в некотором смысле, представителями приложения в поддерживаемой сервером базе данных; они составляют основу серверной части приложения (см. ниже).
    • При выполнении операторов выборки данных на основе содержимого затрагиваемых запросом таблиц и, возможно, с использованием поддерживаемых в базе данных индексов формируется результирующий набор данных (мы намеренно не используем здесь термин "результирующая таблица", поскольку в зависимости от конкретного вида оператора результат может быть упорядоченным, а таблицы, т.е. отношения неупорядочены по определению). Серверная часть СУБД пересылает результат клиентской части, и окончательная обработка производится уже в клиентской части приложения.
    • При выполнении операторов модификации содержимого базы данных (INSERT, UPDATE, DELETE) проверяется, что не будут нарушены определенные к этому моменту ограничения целостности (те, которые относятся к классу немедленно проверяемых), после чего выполняется соответствующее действие (сопровождаемое модификацией всех соответствующих индексов и журнализацией изменений). Далее сервер проверяет, не затрагивает ли данное изменение условие срабатывания какого-либо триггера, и если такой триггер обнаруживается, выполняет процедуру его действия. Эта процедура может включать дополнительные операторы модификации базы данных, которые могут вызвать срабатывание других триггеров и т.д. Можно считать, что те действия, которые выполняются на сервере баз данных при проверке удовлетворенности ограничений целостности и при срабатывании триггеров, представляют собой действия серверной части приложения.

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

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

Рис. 2.2. "Толстый" клиент и "тонкий" сервер в файл-серверной архитектуре

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

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

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

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


Таблица 19.7.
Элемент Свойство Значение
Form Text Client
Size 300; 300
ListBox (Name) listBox
Dock Top
Font Arial; 12pt
Items
  1. Привет!
  2. Лелик
  3. Как жизнь
  4. Оттопыремся сегодня?
  5. Ну тогда пока!
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

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

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 для глобальной сети нужно получать разрешение.



using System; using System.ComponentModel; using System.Configuration.Install; using System.ServiceProcess; namespace SimpleServer { // Во время установки сборки следует вызвать установщик 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 }); } } }

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; 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() - создает сокет для приема/передачи с готовым обратным адресом, создает двунаправленный поток (или два однонаправленных), затем читает запрос и передает ответ.



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

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

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

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

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

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

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


Сервер не готов, ждите! Мы пытаемся связаться, извините за неудобства...

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

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 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 list = new List(client.ReceiveBufferSize);// С приращением capacity Byte bytes = new Byte; // Размер буфера сокета 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.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 . Конечно, в жизни так извращаться вряд ли придется, но и такой вариант где-нибудь может пригодиться.


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

Построение сервера БД как службу
  • Командой File/Add/New Project добавьте к решению NetworkStream новый проект с именем PicturesServerDB типа Windows Service


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.Value = name;// Имя рисунка OleDbDataAdapter adapter = new OleDbDataAdapter(cmd); // Извлекаем рисунок из БД DataTable table = new DataTable(); adapter.Fill(table); byte bytes = (byte)table.Rows["Picture"]; // Подключаемся к рисунку return bytes; } } }

Преимущества

  • Делает возможным, в большинстве случаев, распределение функций вычислительной системы между несколькими независимыми компьютерами в сети. Это позволяет упростить обслуживание вычислительной системы. В частности, замена, ремонт, модернизация или перемещение сервера не затрагивают клиентов.
  • Все данные хранятся на сервере, который, как правило, защищён гораздо лучше большинства клиентов. На сервере проще обеспечить контроль полномочий, чтобы разрешать доступ к данным только клиентам с соответствующими правами доступа.
  • Позволяет объединить различные клиенты. Использовать ресурсы одного сервера часто могут клиенты с разными аппаратными платформами, операционными системами и т. п.

Недостатки

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

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

Частные случаи многоуровневой архитектуры:

Сеть с выделенным сервером

Сеть с выделенным сервером (англ. Client/Server network ) - это локальная вычислительная сеть (LAN) , в которой сетевые устройства централизованы и управляются одним или несколькими серверами. Индивидуальные рабочие станции или клиенты (такие, как ПК) должны обращаться к ресурсам сети через сервер(ы).

Литература

Валерий Коржов Многоуровневые системы клиент-сервер . Издательство Открытые системы (17 июня 1997). Архивировано из первоисточника 26 августа 2011. Проверено 31 января 2010.


Wikimedia Foundation . 2010 .

Клиент-серверное приложение на потоковом сокете TCP

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

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

Сервер TCP

Создание структуры сервера показано на следующей функциональной диаграмме:

Вот полный код программы SocketServer.cs:

// SocketServer.cs using System; using System.Text; using System.Net; using System.Net.Sockets; namespace SocketServer { class Program { static void Main(string args) { // Устанавливаем для сокета локальную конечную точку IPHostEntry ipHost = Dns.GetHostEntry("localhost"); IPAddress ipAddr = ipHost.AddressList; IPEndPoint ipEndPoint = new IPEndPoint(ipAddr, 11000); // Создаем сокет Tcp/Ip Socket sListener = new Socket(ipAddr.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // Назначаем сокет локальной конечной точке и слушаем входящие сокеты try { sListener.Bind(ipEndPoint); sListener.Listen(10); // Начинаем слушать соединения while (true) { Console.WriteLine("Ожидаем соединение через порт {0}", ipEndPoint); // Программа приостанавливается, ожидая входящее соединение Socket handler = sListener.Accept(); string data = null; // Мы дождались клиента, пытающегося с нами соединиться byte bytes = new byte; int bytesRec = handler.Receive(bytes); data += Encoding.UTF8.GetString(bytes, 0, bytesRec); // Показываем данные на консоли Console.Write("Полученный текст: " + data + "\n\n"); // Отправляем ответ клиенту\ string reply = "Спасибо за запрос в " + data.Length.ToString() + " символов"; byte msg = Encoding.UTF8.GetBytes(reply); handler.Send(msg); if (data.IndexOf("") > -1) { Console.WriteLine("Сервер завершил соединение с клиентом."); break; } handler.Shutdown(SocketShutdown.Both); handler.Close(); } } catch (Exception ex) { Console.WriteLine(ex.ToString()); } finally { Console.ReadLine(); } } } }

Давайте рассмотрим структуру данной программы.

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

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

Создадим IPEndPoint для сервера, комбинируя первый IP-адрес хост-компьютера, полученный от метода Dns.Resolve(), с номером порта:

IPHostEntry ipHost = Dns.GetHostEntry("localhost"); IPAddress ipAddr = ipHost.AddressList; IPEndPoint ipEndPoint = new IPEndPoint(ipAddr, 11000);

Здесь класс IPEndPoint представляет localhost на порте 11000. Далее новым экземпляром класса Socket создаем потоковый сокет. Установив локальную конечную точку для ожидания соединений, можно создать сокет:

Socket sListener = new Socket(ipAddr.AddressFamily, SocketType.Stream, ProtocolType.Tcp);

Перечисление AddressFamily указывает схемы адресации, которые экземпляр класса Socket может использовать для разрешения адреса.

В параметре SocketType различаются сокеты TCP и UDP. В нем можно определить в том числе следующие значения:

Dgram

Поддерживает дейтаграммы. Значение Dgram требует указать Udp для типа протокола и InterNetwork в параметре семейства адресов.

Raw

Поддерживает доступ к базовому транспортному протоколу.

Stream

Поддерживает потоковые сокеты. Значение Stream требует указать Tcp для типа протокола.

Третий и последний параметр определяет тип протокола, требуемый для сокета. В параметре РrotocolType можно указать следующие наиболее важные значения - Tcp, Udp, Ip, Raw.

Следующим шагом должно быть назначение сокета с помощью метода Bind() . Когда сокет открывается конструктором, ему не назначается имя, а только резервируется дескриптор. Для назначения имени сокету сервера вызывается метод Bind(). Чтобы сокет клиента мог идентифицировать потоковый сокет TCP, серверная программа должна дать имя своему сокету:

SListener.Bind(ipEndPoint);

Метод Bind() связывает сокет с локальной конечной точкой. Вызывать метод Bind() надо до любых попыток обращения к методам Listen() и Accept().

Теперь, создав сокет и связав с ним имя, можно слушать входящие сообщения, воспользовавшись методом Listen() . В состоянии прослушивания сокет будет ожидать входящие попытки соединения:

SListener.Listen(10);

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

В состоянии прослушивания надо быть готовым дать согласие на соединение с клиентом, для чего используется метод Accept() . С помощью этого метода получается соединение клиента и завершается установление связи имен клиента и сервера. Метод Accept() блокирует поток вызывающей программы до поступления соединения.

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

While (true) { Console.WriteLine("Ожидаем соединение через порт {0}", ipEndPoint); // Программа приостанавливается, ожидая входящее соединение Socket handler = sListener.Accept();

Как только клиент и сервер установили между собой соединение, можно отправлять и получать сообщения, используя методы Send() и Receive() класса Socket.

Метод Send() записывает исходящие данные сокету, с которым установлено соединение. Метод Receive() считывает входящие данные в потоковый сокет. При использовании системы, основанной на TCP, перед выполнением методов Send() и Receive () между сокетами должно быть установлено соединение. Точный протокол между двумя взаимодействующими сущностями должен быть определен заблаговременно, чтобы клиентское и серверное приложения не блокировали друг друга, не зная, кто должен отправить свои данные первым.

Когда обмен данными между сервером и клиентом завершается, нужно закрыть соединение используя методы Shutdown() и Close() :

Handler.Shutdown(SocketShutdown.Both); handler.Close();

SocketShutdown - это перечисление, содержащее три значения для остановки: Both - останавливает отправку и получение данных сокетом, Receive - останавливает получение данных сокетом и Send - останавливает отправку данных сокетом.

Сокет закрывается при вызове метода Close(), который также устанавливает в свойстве Connected сокета значение false.

Клиент на TCP

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

Системы "клиент-сервер". Часть 2

Архитектура клиент-сервер: определение, предпосылки для применения, плюсы и минусы

Что такое архитектура клиент-сервер? Варианты построения приложений

Итак, поговорим, наконец, о том,

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

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

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

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

Понятие архитектуры клиент-сервер в системах управления предприятием связано с делением любой прикладной программы на три основных компонента или слоя. Этими тремя компонентами являются

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

    Для локальных приложений, полностью работающих на ПЭВМ (например, Word или Excel), все эти компоненты собраны вместе и не могут быть распределены между различными компьютеры. Такая программа является монолитной и использует для выполнения ресурсы только того компьютера, на котором выполняется.

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

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

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

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

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

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

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

    С точки зрения количества составных частей клиент-серверные системы делятся на двухуровневые и трехуровневые

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

    В третьей части рассмотрен пример трехзвенной структуры Baikonur Server .

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

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

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

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

    Архитектуру сервера приложений часто называют так называемым "тонким" клиентом , в отличие от традиционного "толстого" клиента, реализуемого в архитектуре сервера баз данных. "Тонкий" клиент является вариантом, который может быть использован, когда ресурсов, доступных на рабочих местах пользователей, недостаточно для исполнения логики приложения. Кроме того, эта технология позволяет сократить расходы на эксплуатацию клиентских компонент системы за счет их сильного упрощения.

    Предпосылки для появления архитектуры клиент-сервер на предприятии

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

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

    Справедливости ради следует заметить, что большие ЭВМ тоже способны справиться с десятками или даже сотнями пользователей. Однако, из-за высокой стоимости аппаратных средств, дороговизны разработки, и, что немаловажно, немалыми затратами на эксплуатацию подобной техники и программ для нее, вариант использования централизованной архитектуры при внедрении новых систем в нашей стране почти никогда не рассматривается.

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

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

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

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

    Архитектура клиент-сервер: Да, но...

    Мы уже поговорили о преимуществах архитектуры клиент-сервер. Может возникнуть естественный вопрос, если она так уж хороша, то почему же до сих пор на нее не перешли все крупные пользователи информационных систем. На самом деле не все так просто.

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

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

    И, наконец, самым большим подводным камнем на пути создания КС системы на предприятии является необходимость менять структуру управления и связанные с этим организационные издержки

    .

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

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

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

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



    Статьи по теме