Create a simple chat TCP/IP Client & Server using SimpleTCP library in C#
Step 1: Click New Project, then select Visual C# on the left, then Windows and then select Windows Forms Application. Name your project «TCPIPDemo» and then click OK. Similar with «Client» project
Step 2: Right click on your project select Manage NuGet Packages -> Search simpletcp -> Install
Step 3: Design your form as below
Client
Server
You can change the host and port to connect over the network, connect through the network you should open the firewall to allow connection to your port
Add code to handle client form
namespace Client { public partial class Form1 : Form { public Form1() { InitializeComponent(); } SimpleTcpClient client; private void btnConnect_Click(object sender, EventArgs e) { btnConnect.Enabled = false; //Connect to server client.Connect(txtHost.Text, Convert.ToInt32(txtPort.Text)); } private void Form1_Load(object sender, EventArgs e) { client = new SimpleTcpClient(); client.StringEncoder = Encoding.UTF8; client.DataReceived += Client_DataReceived; } private void Client_DataReceived(object sender, SimpleTCP.Message e) { //Update message to txtStatus txtStatus.Invoke((MethodInvoker)delegate () { txtStatus.Text += e.MessageString; }); } private void btnSend_Click(object sender, EventArgs e) { client.WriteLineAndGetReply(txtMessage.Text, TimeSpan.FromSeconds(3)); } } }
Add code to handle server form
namespace TCPIPDemo { public partial class Form1 : Form { public Form1() { InitializeComponent(); } SimpleTcpServer server; private void Form1_Load(object sender, EventArgs e) { server = new SimpleTcpServer(); server.Delimiter = 0x13;//enter server.StringEncoder = Encoding.UTF8; server.DataReceived += Server_DataReceived; } private void Server_DataReceived(object sender, SimpleTCP.Message e) { //Update mesage to txtStatus txtStatus.Invoke((MethodInvoker)delegate () { txtStatus.Text += e.MessageString; e.ReplyLine(string.Format("You said: {0}", e.MessageString)); }); } private void btnStart_Click(object sender, EventArgs e) { //Start server host txtStatus.Text += "Server starting..."; System.Net.IPAddress ip = System.Net.IPAddress.Parse(txtHost.Text); server.Start(ip, Convert.ToInt32(txtPort.Text)); } private void btnStop_Click(object sender, EventArgs e) { if (server.IsStarted) server.Stop(); } } }
VIDEO TUTORIALS
Related Posts
- Windows Forms: Form Load and Button click Event in C#
- Windows Forms: How to load User control dynamically in C#
- Windows Forms: Text to speech in C#
- Windows Forms: How to insert Math Equation to RichTextBox in C#
- Windows Forms: Multiple pages on the Form using Panel control in C#
- Windows Forms: Add Combobox to DataGridView in C#
- Windows Forms: Youtube Search with Paging in C#
- Windows Forms: Printing Text example in a Windows Form using C#
Пример создания распределенного приложения, демонстрирующего удаленное взаимодействие между компьютерами в сети. Пространство имен System.Runtime.Remoting. Протокол Http
В данной теме описывается один из способов разработки простейшего распределенного приложения, которое использует удаленное взаимодействие между компьютерами в сети. По данному примеру можно научиться разрабатывать приложения, которые взаимодействуют между собой в локальной сети. Тема предусматривает наличие базовых навыков работы в Microsoft Visual Studio 2010.
Содержание
- Условие задачи
- Соображения
- Выполнение
- 1. Запустить Microsoft Visual Studio 2010
- 2. Разработка общей сборки с метаданными
- 2.1. Что такое общая сборка?
- 2.2. Создание модуля общей сборки
- 2.3. Текст модуля общей сборки
- 2.4. Объяснение к методу решения квадратного уравнения
- 2.5. Компиляция. Создание .dll — файла общей сборки
- 2.6. Закрыть решение (проект)
- 3. Создание программного кода, который будет размещаться на компьютере-сервере
- 3.1. Создание приложения типа Console Application
- 3.2. Добавление ссылок на сборки
- 3.2.1. Добавление ссылки на сборку System.Runtime.Remoting
- 3.2.2. Добавление ссылки на сборку ClassLibrary1
- 3.2.3. Подключение пространств имен сборок ClassLibrary1 и System.Runtime.Remoting в программный код файла Program.cs
- 3.3. Написание кода серверной части
- 3.3.1. Действия, которые нужно выполнить на сервере
- 3.3.2. Текст кода серверной части
- 3.4. Компиляция. Файлы серверной части
- 4. Разработка приложения клиентской части
- 4.1. Создание приложения клиентской части по шаблону Windows Forms
- 4.2. Проектирование формы
- 4.2.1. Размещение элементов управления Label, Button, TextBox на форме
- 4.2.2. Настройка свойств фомы и элементов управления
- 4.3. Текст модуля формы Form1.cs
- 4.4. Алгоритм работы программы-клиента
- 4.5. Написание программного кода клиентской части
- 4.5.1. Добавление ссылок на сборки
- 4.5.2. Подключение пространств имен
- 4.5.3. Написание кода метода Form1_Load()
- 4.5.4. Программирование события клика на кнопке «Вычислить». Текст клиентской части
- 5. Тестирование взаимодействия между клиентом и сервером на одном компьютере без использования локальной сети
- 6. Копирование серверной части на другой компьютер
- 7. Определение имени сервера в сети из компьютера-клиента
- 8. Корректирование клиентского программного кода. Замена имени localhost
- 9. Тестирование взаимодействия между клиентом и сервером на разных компьютерах, объединенных между собой в локальную сеть
- Связанные темы
Поиск на других ресурсах:
Условие задачи
1. Разработать распределенное приложение, которое содержит следующие части:
- серверная часть. Сервер должен содержать функции решения математических задач. В данном случае, в качестве примера сервер содержит метод, который решает квадратное уравнение. Серверная часть должна быть разработана как консольное приложение;
- клиентская часть. Клиент содержит программный код, который получает результат из серверной части. Клиент вызывает функцию решения квадратного уравнения, которая реализована и выполняется на сервере. Клиентская часть должна быть разработана как приложение типа Windows Forms.
2. Взаимодействие между клиентом и сервером организовать на основе протокола Http.
3. Приложение реализовать в системе Microsoft Visual Studio 2010 с использованием языка программирования C# .NET.
4. Для организации взаимодействия между компьютерами использовать средства из пространства имен System.Runtime.Remoting.
⇑
Соображения
Для проверки правильности выполнения задачи, нужно наличие как минимум двух компьютеров, которые объединенные между собой в локальную сеть.
Разработка распределенного приложения осуществляется на одном компьютере в MS Visual Studio 2010. Тестирование взаимодействия клиентской и серверной частей осуществляются также на одном компьютере.
После завершения тестирования, серверный код размещается на компьютере-сервере. Клиентский код из компьютера-клиента делает запрос на компьютер-сервер, получает ответ и выводит этот ответ на экран. В нашем случае клиентский код вызывает серверную функцию решения квадратного уравнения.
⇑
Выполнение
1. Запустить Microsoft Visual Studio 2010
⇑
2. Разработка общей сборки с метаданными
2.1. Что такое общая сборка?
Для того, чтобы клиент и сервер взаимодействовали между собой, нужно наличие еще одной части программного кода – общей сборки. Общая сборка содержит метаданные типов, которые являются допустимыми для удаленного вызова. Метаданные типов – это информация о типах данных, которые используются при удаленном вызове. Эта информация нужна как клиенту, так и серверу.
В конечном счете, после создания, программный код общей сборки должен размещаться как на компьютере клиента, так и на компьютере сервера. В противном случае, связь между клиентом и сервером невозможно будет установить.
⇑
2.2. Создание модуля общей сборки
В нашем случае общая сборка не является исполняемым файлом. Это есть библиотека, которая сохраняется в формате *.dll.
В Microsoft Visual Studio 2010 для создания библиотеки используется шаблон Class Library. В данном примере, чтобы создать модуль общей сборки нужно выполнить команду
File -> New -> Project...
Откроется окно «New Project», в котором нужно выбрать Visual C# -> Class Library. В нашем случае папка для размещения библиотеки: «D:Programs». Имя библиотеки ClassLibrary1 (по умолчанию). По желанию, можно изменить папку и имя библиотеки.
Рис. 1. Создание библиотеки по шаблону Class Library
После выбора OK, создается проект, который содержит один файл Class1.cs. Листинг файла следующий:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ClassLibrary1 { public class Class1 { } }
По умолчанию создается класс с именем Class1 в пространстве имен ClassLibrary1. В нашем случае оставляем все как есть. По желанию можно изменить название класса и пространства имен.
⇑
2.3. Текст модуля общей сборки
В модуль Class1.cs нужно ввести необходимый программный код. Этот код будет выполняться на стороне сервера. А вызов этого кода будет осуществлять клиент.
В соответствии с условием задачи, сервер может решать математические задачи. Поэтому, в класс Class1 нужно вписать функцию решения квадратного уравнения. В нашем случае функция называется CalcEquation(). По желанию, сюда можно вписать другие функции, которые решают разные задачи.
Также класс Class1 наследует данные и методы класса MarshalByRefObject. Класс MarshalByRefObject обеспечивает доступ к объектам в пределах домена приложения в приложениях, которые поддерживают удаленный доступ. Это специально разработанный класс для поддержки распределенного доступа к приложениям.
В классе MarshalRefObject есть метод CreateObjRef(), который будет использован в нашем проекте. Метод CreateObjRef() содержит всю необходимую информацию для генерирования прокси. Прокси применяется чтобы соединиться с удаленным объектом.
Листинг модуля общей сборки следующий (файл Class1.cs):
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ClassLibrary1 { // класс Class1 наследует класс MarshalByRefObject public class Class1 : MarshalByRefObject { // функция вычисления квадратного уравнения public bool CalcEquation(double a, double b, double c, out double x1, out double x2) { double d; // дискриминант // Решение квадратного уравнения // вычисление дискриминанта d = b * b - 4 * a * c; if (d > 0) { x1 = (-b - Math.Sqrt(d)) / (2 * a); x2 = (-b + Math.Sqrt(d)) / (2 * a); return true; } else if (d == 0) { x1 = x2 = (-b) / (2 * a); return true; } else { x1 = x2 = 0; return false; } } } }
⇑
2.4. Объяснение к методу решения квадратного уравнения
Метод решения квадратного уравнения CalcEquation() получает входными параметрами значения коэффициентов a, b, c. Если уравнение имеет решение, то метод возвращает true и значение корней уравнения в параметрах-переменных x1, x2. Если уравнение не имеет решения, то метод возвращает false и нулевые значения параметров-переменных x1, x2.
⇑
2.5. Компиляция. Создание .dll — файла общей сборки
На этом шаге нужно откомпилировать проект командой
Build -> Build Solution
В результате будет создан *.dll-файл общей сборки с именем ClassLibrary1.dll, которая размещается в папке
D:ProgramsClassLibrary1ClassLibrary1binDebug
Этот файл нужно будет разместить в папках с исполняемыми модулями на компьютере-клиенте и на на компьютере-сервере. Файл общей сборки готов.
Рис. 2. Файл ClassLibrary1.dll
⇑
2.6. Закрыть решение (проект)
Общая сборка разработана. Чтобы закрыть решение (Solution) нужно выполнить команду
File->Close Solution
На следующем этапе будет разрабатываться программный код серверной части распределенного приложения.
⇑
3. Создание программного кода, который будет размещаться на компьютере-сервере
После создания файла (модуля) общей сборки на одном и том же компьютере нужно разработать программный код, который будет размещаться на компьютере-сервере.
Чтобы сервер отвечал на запросы клиента, программный код серверной части должен быть запущен на выполнение в момент запроса.
3.1. Создание приложения типа Console Application
В соответствии с условием задачи, на серверной части нужно создать приложение типа Console Application. Это осуществляется стандартным способом с помощью команды
File -> New -> Project...
В результате откроется окно New Project (рисунок 3), в котором нужно:
- выбрать шаблон Visual C# -> Console Application;
- оставить имя проекта по умолчанию ConsoleApplication1. По желанию можно изменить это имя;
- выбрать папку для проекта D:Programs (или другую папку);
- установить опцию «Create directory for solution».
Рис. 3. Окно создания консольного приложения
В результате будет создано обычное консольное приложение, которое содержит файл Program.cs. В этом файле объявляется пространство имен ConsoleApplication1, в котором реализован класс Program.
Листинг файла следующий
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { } } }
⇑
3.2. Добавление ссылок на сборки
Чтобы использовать программный код общей сборки нужно подключить:
- сборку System.Runtime.Remoting. Эта сборка содержит все необходимые средства для удаленного доступа;
- общую сборку ClassLibrary1, которая была разработана в п.2. В этой сборке размещается метод решения квадратного уравнения.
⇑
3.2.1. Добавление ссылки на сборку System.Runtime.Remoting
Чтобы добавить ссылку на сборку System.Runtime.Remoting нужно выполнить следующую последовательность команд.
В окне Solution Explorer вызвать команду Add Reference… как показано на рисунке 4. Для этого нужно сделать клик правой кнопкой мыши на элементе References.
Рис. 4. Команда Add Reference…
В результате откроется диалоговое окно Add Reference (рисунок 5), в котором нужно:
- активировать вкладку .NET;
- в перечне сборок выбрать сборку System.Runtime.Remoting;
- подтвердить выбор кнопкой OK.
Рис. 5. Подключение сборки System.Runtime.Remoting.dll
После этого в Solution Explorer в проекте ConsoleApplication1 будет отображена сборка System.Runtime.Remoting (рисунок 6).
Рис. 6. Подключенная к проекту сборка System.Runtime.Remoting
⇑
3.2.2. Добавление ссылки на сборку ClassLibrary1
Добавление ссылки на сборку ClassLibrary1 осуществляется такой же командой «Add Reference…», как подключение сборки System.Runtime.Remoting (рисунок 4).
В результате откроется окно Add Reference, в котором нужно активировать вкладку Browse как показано на рисунке 7.
В окне, с помощью нисходящего меню «Папка» нужно выбрать папку, в которой размещается ранее разработанная общая сборка (см. п. 2) с именем ClassLibrary1.
В нашем случае выбирается папка (см. п. 2.4):
D:ProgramsClassLibrary1ClassLibrary1binDebug
Рис. 7. Выбор папки и файла общей сборки
После выбора OK, в окне Solution Explorer в перечне References ссылок на использованные сборки появится сборка ClassLibrary1.
Рис. 8. Сборка ClassLibrary1 в перечне сборок серверного приложения
⇑
3.2.3. Подключение пространств имен сборок ClassLibrary1 и System.Runtime.Remoting в программный код файла Program.cs
После подключения сборок к проекту, нужно подключить необходимые пространства имен в файле Program.cs. Добавляются 4 пространства имен:
using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using ClassLibrary1;
На данный момент текст файла Program.cs следующий
using System; using System.Collections.Generic; using System.Linq; using System.Text; // добавление пространств имен using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using ClassLibrary1; // библиотека общей сборки namespace ConsoleApplication1 { class Program { static void Main(string[] args) { } } }
⇑
3.3. Написание кода серверной части
3.3.1. Действия, которые нужно выполнить на сервере
На компьютере-сервере нужно выполнить три основных действия:
- Создать канал связи. Для этого хорошо подходит класс HttpChannel. Есть и другие классы. Создается объект канала типа HttpChannel;
- Зарегистрировать канал с помощью статического класса ChannelServices. В этом классе есть метод RegisterChannel(). Этот метод получает параметром канал (тип HttpChannel в нашем случае) и опцию задействования службы безопасности (bool);
- Зарегистрировать сервис как WKO-тип (Well Known Object, хорошо известный объект). Это осуществляется с помощью класса RemotingConfiguration. В этом классе есть статический метод RegisterWellKnownServiceType(), который осуществляет регистрацию сервиса.
⇑
3.3.2. Текст кода серверной части
Ниже приведен текст программного кода серверной части
using System; using System.Collections.Generic; using System.Linq; using System.Text; // добавление пространств имен using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using ClassLibrary1; // библиотека общей сборки namespace ConsoleApplication1 { class Program { static void Main(string[] args) { // 1. Создать канал между клиентом и сервером, создать объект типа HttpChannel HttpChannel ch = new HttpChannel(5000); // номер порта = 5000 - выбран наугад // 2. Зарегистрировать канал ch, в методе RegisterChannel() указывается уровень безопасности false ChannelServices.RegisterChannel(ch, false); // 3. Зарегистрировать сервис как WKO RemotingConfiguration.RegisterWellKnownServiceType( typeof(ClassLibrary1.Class1), "MathFunctions.soap", WellKnownObjectMode.Singleton); // 4. Вывести сообщения, что сервис в состоянии выполнения Console.WriteLine("MathFunctions service is ready..."); // 5. Остановить, оставить сервис в состоянии выполнения, пока не будет нажата клавиша Enter Console.ReadLine(); } } }
Объясним некоторые фрагменты кода серверной части.
Сначала создается канал связи, который представлен объектом ch типа HttpChannel. Конструктор класса HttpChannel() получает параметром номер порта, через который происходит связь между компьютерами. Номер порта может быть числом от 0 до 65535. Рекомендуется задавать номер порта выше значения 1024.
На втором этапе происходит регистрация канала ch с помощью класса ChannelServices. Это осуществляет метод класса RegisterChannel(). Первый параметр метода – это канал ch, который нужно зарегистрировать. Второй параметр – флажок типа bool. Значение флажка true требует запуск канала в пределах IIS (Internet Information Server) с целью обеспечения безопасности. Поскольку, наше приложение запускается за пределами IIS, то значение флажка нужно установить в false.
На третьем этапе нужно зарегистрировать сервис как WKO-тип (Well Known Object). Это реализуется с помощью класса RemotingConfiguration. В этом классе есть метод RegisterWellKnownServiceType(). Этот метод получает три параметра:
- тип данных, который есть типом нашего созданного класса Class1 в общей сборке ClassLibrary1. В нашем случае здесь нужно передать typeof(ClassLibrary1.Class1);
- строка, которая идентифицирует наш сервис на компьютере-сервере. В нашем случае выбрано «MathFunctions.soap». По желанию можно выбрать другое название;
- значением из перечисления WellKnownObjectMode.Singleton. Это значит, что серверный объект разделяется всеми клиентами сервера.
В конце вызывается метод Console.Readline(). Этот метод необходим, чтобы зафиксировать выполнение консольного приложения, поскольку серверная часть должна быть все время запущена. В это время клиенты смогут к ней обращаться. Завершение работы сервера – клавиша Enter.
⇑
3.4. Компиляция. Файлы серверной части
На этом шаге нужно откомпилировать текст командой
Build -> Build Solution
для получения исполняемого (*.exe) файла. В результате будет создан исполняемый файл с именем
D:ProgramsConsoleApplication1ConsoleApplication1binDebugConsoleApplication1.exe
На рисунке 9 отображено содержимое папки Debug с файлами проекта серверной части. Как видно из рисунка 9, автоматически подгружается файл общей сборки ClassLibrary1.dll.
Рис. 9. Содержимое папки Debug с исполняемым файлом ConsoleApplication1.exe серверной части и файлом сборки ClassLibrary1.dll
Оба файла ConsoleApplication1.exe и ClassLibrary1.dll нужно будет разместить (скопировать) на компьютере-сервере.
После этого проект, который соответствует серверной части, завершен.
⇑
4. Разработка приложения клиентской части
В сети с несколькими компьютерами, клиентов может быть сколько угодно, а сервер один. Сервер должен отвечать на запросы клиентов. В нашем случае достаточно разработать одно приложение, которое делает запросы к серверу. Затем исполняемый .exe файл (модуль) этого приложения и .dll файл общей сборки нужно скопировать (распространить) по всей сети на клиентские компьютеры.
4.1. Создание приложения клиентской части по шаблону Windows Forms
Проект по шаблону Windows Forms создается стандартным способом:
File->New Project...
В результате откроется окно New Project. В окне New Project нужно выбрать вкладку
Visual C# -> Windows Forms Application
как показано на рисунке 10.
Рис. 10. Проект типа Windows Forms Application, который будет размещаться на компьютере клиента
В проекте задаются:
- имя проекта WindowsFormsApplication1;
- папка проекта D:Programs.
После выбора OK будет создана новая форма проекта как показано на рисунке 11.
Рис. 11. Новая форма проекта
⇑
4.2. Проектирование формы
4.2.1. Размещение элементов управления Label, Button, TextBox на форме
Нужно разместить на форме следующие элементы управления (рисунок 12):
- четыре элемента управления типа Label. По умолчанию создаются объекты с именами label1, label2, label3, label4;
- один элемент управления типа Button. По умолчанию создается объект с именем button1.
Рис. 12. Размещение элементов управления на форме
⇑
4.2.2. Настройка свойств фомы и элементов управления
Подробное описание того, как осуществляется настройка элементов управления на форме можно найти в теме:
- Пример программирования события в C#. Разработка программы для определения площади поверхности шара
На этом шаге, с помощью окна Properties, нужно настроить следующие свойства формы и элементов управления:
- в форме Form1, свойство Text = «Решение квадратного уравнения»;
- в форме Form1, свойство StartPosition = CenterScreen;
- в Label1, свойство Text = «a = «;
- в Label2, свойство Text = «b = «;
- в Label3, свойство Text = «c = «;
- в Label4, свойство Text = «Результат: «.
После настройки элементов управления, окно формы будет иметь вид как показано на рисунке 13.
Рис. 13. Окно программы-клиента после настройки
После этого можно разрабатывать программный код клиента.
⇑
4.3. Текст модуля формы Form1.cs
Главной форме программы-клиента соответствует модуль (файл) с именем Form1.cs.
На данный момент листинг файла Form1.cs имеет следующий вид:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; namespace WindowsFormsApplication1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } } }
⇑
4.4. Алгоритм работы программы-клиента
На компьютере клиенте нужно выполнить следующие действия:
- создать канал соединения с сервером. В нашем случае используется класс HttpChannel. В отличие от сервера, клиенту номер порта не нужен. Поэтому, при создании класса будет вызываться конструктор HttpChannel() без номера порта. Для сервера номер порта обязательно нужно задавать, чтобы клиент знал где его найти. А клиенту номер порта не нужен, так как для создания канала система сама выберет первый подходящий порт;
- зарегистрировать канал в системе с помощью класса ChannelServices, в котором есть соответствующий метод RegisterChannel();
- создать экземпляр (объект) типа Class1. Этот класс есть реализованный в общей сборке (см. п.2). В этом классе есть метод вычисления квадратного уравнения CalcEquation(). Для создания экземпляра используется класс Activator. В классе Activator есть статический метод GetObject(), который создает прокси между клиентом и сервером для загруженного объекта WKO. Метод GetObject() возвращает объект типа Object, через который можно будет вызвать его отдаленные методы (в нашем случае, метод CalcEquation());
- вызвать метод CalcEquation() из созданного экземпляра. Получить результат решения квадратного уравнения и вывести его на форму. Это все будет осуществляться в обработчике события клика на кнопке «Вычислить».
⇑
4.5. Написание программного кода клиентской части
4.5.1. Добавление ссылок на сборки
Для того, чтобы использовать код общей сборки, на компьютере-клиенте нужно подключить:
- программный код общей сборки ClassLibrary1.dll;
- программный код сборки System.Runtime.Remoting.dll.
Это осуществляется командой «Add Reference…» так же, как было сделано для серверной части (см. п.п. 3.2.1, 3.2.2).
После подключения, вкладка References в окне Solution Explorer будет иметь вид, как показано на рисунке 14.
Рис. 14. Вкладка References в окне Solution Explorer. Подключенные сборки ClassLibrary1 и System.Runtime.Remoting
⇑
4.5.2. Подключение пространств имен
Как и в случае с серверной частью, в файле Form1.cs нужно подключить пространства имен общей сборки и System.Runtime.Remoting с помощью директивы using.
... // подключение пространств имен using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using ClassLibrary1; // общая сборка ...
⇑
4.5.3. Написание кода метода Form1_Load()
Создание канала между сервером и клиентом в клиентской части будет выполняться только один раз. После того, как канал создан, многократно может вызываться метод CalcEquation() из серверной части.
Учитывая вышесказанное, целесообразно запрограммировать событие Load формы Form1. Это событие вызывается в начале создания формы. В нашей программе этому событию соответствует обработчик Form1_Load(). В обработчике нужно разместить программный код создания канала, регистрации канала и создания отдаленного объекта.
Ниже приведен текст обработчика события Form1_Load():
private void Form1_Load(object sender, EventArgs e) { // Выполнение действий на стороне клиента // 1. Создать канал между клиентом и сервером HttpChannel ch = new HttpChannel(); // номер порта не нужен // 2. Зарегистрировать канал ChannelServices.RegisterChannel(ch, false); // 3. Создать объект удаленного доступа к серверу remote = (Class1)Activator.GetObject( typeof(Class1), "http://localhost:5000/MathFunctions.soap"); // localhost - временное имя нашего компьютера }
⇑
4.5.4. Программирование события клика на кнопке «Вычислить». Текст клиентской части
На этом шаге нужно запрограммировать обработку события клика на кнопке «Вычислить» (button1). Подробное описание того как программируется событие описывается в теме:
- Пример программирования события в C#. Разработка программы для определения площади поверхности шара.
Текст программы-клиента с реализованным обработчиком события клика на кнопке «Вычислить» приведен ниже
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; // подключение пространств имен using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Http; using ClassLibrary1; // общая сборка namespace WindowsFormsApplication1 { public partial class Form1 : Form { Class1 remote; // переменная, которая представляет удаленный доступ public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { // 4. Вызвать метод CalcEquation() из серверной части для вычисления квадратного уравнения double a, b, c; double x1, x2; bool f; a = Double.Parse(textBox1.Text); b = Double.Parse(textBox2.Text); c = Double.Parse(textBox3.Text); // вызов удаленного метода f = remote.CalcEquation(a, b, c, out x1, out x2); // вывод результата if (f) label4.Text = "Результат: x1 = " + x1.ToString() + "; x2 = " + x2.ToString(); else label4.Text = "Уравнение не имеет корней."; } private void Form1_Load(object sender, EventArgs e) { // Выполнение действий на стороне клиента // 1. Создать канал между клиентом и сервером HttpChannel ch = new HttpChannel(); // номер порта не нужен // 2. Зарегистрировать канал ChannelServices.RegisterChannel(ch, false); // 3. Создать объект удаленного доступа к серверу remote = (Class1)Activator.GetObject( typeof(Class1), "http://localhost:5000/MathFunctions.soap"); // localhost - временное имя нашего компьютера } } }
В вышеприведенном коде пока что задается общее имя нашего компьютера localhost. Если будет сеть, то это имя нужно будет заменить на компьютере-клиенте на сетевое имя компьютера сервера. Клиентский проект нужно будет снова перекомпилировать.
⇑
5. Тестирование взаимодействия между клиентом и сервером на одном компьютере без использования локальной сети
Тестирование взаимодействия между клиентом и сервером проводится на одном компьютере, который в системе носит имя localhost. После проведения тестирования это имя будет изменено на имя компьютера-сервера в сети.
Для проведения тестирования нужно:
- запустить серверное приложение (рисунок 15) из папки сервера (см. п. 3);
- запустить на выполнение клиентское приложение (рисунок 16) из папки клиента (см. п. 4);
- ввести в клиентское приложение данные и провести вычисления.
Рис. 15. Выполнение серверной части
Приблизительный результат выполнения клиентского приложения приведен на рисунке 16.
Рис. 16. Клиентская программа
Важно: если закрыть программу-сервер и попробовать провести вычисления на клиенте, то возникнет исключительная ситуация. Система не сможет найти приложение-сервер. Значит, для проведения тестирования, сервер должен все время выполняться.
Клиентов можно запускать сколько угодно и производить запросы к серверу.
⇑
6. Копирование серверной части на другой компьютер
На этом шаге нужно скопировать файлы ConsoleApplication1.exe и ClassLibrary1.dll на компьютер-сервер. Это осуществляется с помощью, например, обычного файлового менеджера. Файлы копируются в заведомо подготовленную папку.
⇑
7. Определение имени сервера в сети из компьютера-клиента
Если компьютеры объединены в локальную сеть, то каждый из них имеет свое уникальное имя. Способ определения имени компьютера в сети зависит от конкретной реализации операционной системы. В общем случае, чтобы определить это имя, нужно вызвать утилиту Control Panel (Панель управления). В этой утилите можно выбрать элемент System (Система). Откроется окно, в котором можно прочитать имя компьютера.
⇑
8. Корректирование клиентского программного кода. Замена имени localhost
Если известно имя компьютера-сервера в локальной сети, тогда нужно в клиентской части сделать изменения. В исходном коде файла Form1.cs в методе Form_Load() вместо строки
"http://localhost:5000/MathFunctions.soap"
нужно ввести
"http://MyComp:5000/MathFunctions.soap"
где MyComp – имя компьютера-сервера в локальной сети. Скорей всего у вас будет другое имя.
Итак, текст обработчика события загрузки формы в нашем случае следующий:
... private void Form1_Load(object sender, EventArgs e) { // Выполнение действий на стороне клиента // 1. Создать канал между клиентом и сервером HttpChannel ch = new HttpChannel(); // номер порта не нужен // 2. Зарегистрировать канал ChannelServices.RegisterChannel(ch, false); // 3. Создать объект удаленного доступа к серверу remote = (Class1)Activator.GetObject( typeof(Class1), "http://MyComp:5000/MathFunctions.soap"); // MyComp - имя сервера в сети } ...
После этого нужно перекомпилировать клиентский проект, чтобы сформировать новый исполняемый модуль. На рисунке 17 изображен файл WindowsFormsApplication1.exe клиентского модуля. В этой папке также есть общая сборка ClassLibrary1.dll.
Рис. 17. Папка с клиентским исполняемым модулем WindowsFormsApplication1
⇑
9. Тестирование взаимодействия между клиентом и сервером на разных компьютерах, объединенных между собой в локальную сеть
На этом этапе нужно протестовать работу между клиентом и сервером на разных компьютерах, которые объединенные между собой в локальную сеть.
Для этого, нужно:
- с помощью файлового менеджера скопировать в некоторую папку на компьютер-сервер файлы ConsoleApplication1.exe и ClassLibrary1.dll;
- запустить на сервере файл ConsoleApplication1.exe. Предварительно на сервере должна быть установлена библиотека .NET Framework 4.0;
- скопировать в нужную папку файлы WindowsFormsApplication1.exe (клиентское приложение) и ClassLibrary1.dll;
- запустить на клиентской части файл WindowsFormsApplication1.exe, ввести данные, проверить результат выполнения функции из сервера.
Важно: чтобы между клиентом и сервером было взаимодействие, компьютер-клиент должен иметь соответствующие права доступа к компьютеру-серверу. Например, на сервере может быть включен общий доступ к его ресурсам. Это все осуществляется соответствующими настройками на компьютере-сервере. В данной теме это не рассматривается.
⇑
Связанные темы
- Пример создания Web-приложения в MS Visual Studio — C#
⇑
Пример 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
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 }); } } }
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() — создает сокет для приема/передачи с готовым обратным адресом, создает двунаправленный поток (или два однонаправленных), затем читает запрос и передает ответ.
-
Запустите проект SimpleClient — все работает и результат такой
Обратите внимание, что если код работы нашей серверной программы не упаковать в отдельную нить 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. Конечно, в жизни так извращаться вряд ли придется, но и такой вариант где-нибудь может пригодиться.
Заставка появится сразу же, как будет обнаружен факт отсутствия или остановки сервера. А после обнаружения сервера заставка исчезнет. Этот механизм немедленно сработает благодаря используемому нами системному таймеру. Однако, сервера пока еще совсем нет и следует его изготовить.
Построение сервера БД как службу
- Командой 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[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|DataPictures.my2.mdb" providerName="System.Data.OleDb" /> </connectionStrings> </configuration>
-
В панели Solution Explorer контекстной командой Add/References для узла References добавьте к проекту ссылку на библиотеку System.Configuration.dll
Теперь создадим для сервиса инсталляционный файл.
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
Теперь нужно зарегистрировать созданный сервис (для нас — сервер) в операционной системе.
Вот одна из картинок, когда работа клиента (или всех клиентов) была временно прекращена остановкой сервера
Чтобы созданный сервис не загружал компьютер, его можно деинсталлировать той же утилитой InstallUtil.exe, только с опцией /u.
В последнем примере мы немного перегорячились и сразу стали делать сервер как службу Windows, чтобы не было видно пользовательского интерфейса (даже консольного окна). Это совершенно необязательно, сервер можно сделать и как обычное приложение с интерфейсом пользователя и запускать его по мере необходимости.
Главное, что нужно помнить и что послужило причиной создания нами сервера как службы, — на одном IP -адресе не может сидеть сразу несколько запущенных серверов. Иначе бы получилась неопределенность, с каким именно сервером должны общаться клиенты. А службы как раз и запускаются в одном экземпляре.
Но сечас мы, для разнообразия, создадим подобный сервер, но уже как консольное приложение. Клиенты останутся теми же самыми. Вот здесь-то как раз мы и убедимся, что при запуске второй копии консольного сервера операционная система выбросит исключение.
138 / 7 / 1 Регистрация: 31.03.2015 Сообщений: 395 |
|
1 |
|
11.06.2016, 06:35. Показов 7193. Ответов 24
Не очень знаком с клиент-серверной схемой работы — но мне надо чтобы по такому принципу работала форма winforms, через которую надо вводить данные и наполнять бд, с которой соответственно получать данные, и выводить их в этой (или другой форме). Вот меня интересует в чем конкретно эта схема работы заключается. Будет ли бд на сервере, а клиент — это приложение формы? Просмотрел много примеров по работе сокетов преимущественно встречал передачу неких строк (сообщений), запись в файл — а как быть здесь: надо ведь заполнять/изменять таблицы бд. Надо какой то общий метод который будет отправлять данные на сервер/бд? Вообще возможна ли такая схема при использование sql express server /management studio и visual c# express? То есть если я сделаю вариант без использование клиент-сервера насколько сложно будет переработать под эту схему?
0 |
10365 / 5096 / 1824 Регистрация: 11.01.2015 Сообщений: 6,226 Записей в блоге: 34 |
|
11.06.2016, 11:17 |
2 |
arts1, Не нужно вам никаких сокетов, используйте ADO.NET, оно умеет конектится к удаленному серверу.
0 |
138 / 7 / 1 Регистрация: 31.03.2015 Сообщений: 395 |
|
12.06.2016, 01:44 [ТС] |
3 |
Ну мне надо клиент- серверное приложение. Его что невозможно в этом случае реализовать? Например одна кнопка забирает данные с двух-трех текстовых полей, с групы радио-батонов или чек-боксов и заносит в таблицу. Другая делает это для другой таблицы. Еще две кнопки делают агрегацию/отчет тоже на основание отмеченых полей таблицы и введеных даных (даты). Еще остается вопрос возможно ли в экспрес версиях реализовать этот проэкт ибо читал что там возникает проблема сети, которая как бы локальная по умолчанию в экспресе или не существует вообще. Добавлено через 10 часов 31 минуту
0 |
138 / 7 / 1 Регистрация: 31.03.2015 Сообщений: 395 |
|
13.06.2016, 07:59 [ТС] |
4 |
У меня такой вопрос по существу работы форм: если у меня есть група значений переключателей и чекбоксов — как мне их конвертировать в значение (типы) чтобы передать их в бд? В первом случае нашел пример что использовать еnums, то есть идет некая конвертация к int, а с чекбоксами — вводить типа boolean или int?
0 |
5459 / 4232 / 1208 Регистрация: 12.10.2013 Сообщений: 12,223 Записей в блоге: 2 |
|
13.06.2016, 08:28 |
5 |
мне надо клиент- серверное приложение. Если вопрос стоит именно так — не просто удаленно взаимодействовать с БД на сервере, а через свою логику — то тогда используйте трехзвенную архитектуру. БД на сервере, блок серверного ПО (логика доступа к данным) и клиентское приложение.
0 |
138 / 7 / 1 Регистрация: 31.03.2015 Сообщений: 395 |
|
13.06.2016, 10:49 [ТС] |
6 |
Нет мне надо в винформс, ну и хотябы в связке форма-бд
0 |
5459 / 4232 / 1208 Регистрация: 12.10.2013 Сообщений: 12,223 Записей в блоге: 2 |
|
13.06.2016, 10:54 |
7 |
arts1, это уже зависит от вас.
0 |
10365 / 5096 / 1824 Регистрация: 11.01.2015 Сообщений: 6,226 Записей в блоге: 34 |
|
13.06.2016, 11:22 |
8 |
У меня такой вопрос по существу работы форм: если у меня есть група значений переключателей и чекбоксов — как мне их конвертировать в значение (типы) чтобы передать их в бд? В первом случае нашел пример что использовать еnums, то есть идет некая конвертация к int, а с чекбоксами — вводить типа boolean или int? Сначала спроектируйте базу данных. Составьте то, что называется инфологическая модель — таблицы, поля, связи. И после этого, вам уже станет понятно к каким типам приводить ваши чекбоксы.
Тем более что в экспресе кажется невозможно работать со среды вижуал студио с бд ескьюэл сервер напрямую … Создать базу данных вы можете в SQL Server Menagement Studio (которое входит в состав MS SQL Server).
0 |
138 / 7 / 1 Регистрация: 31.03.2015 Сообщений: 395 |
|
13.06.2016, 21:40 [ТС] |
9 |
Ну если разбивать на более мелкие части то там надо уже какую то двухуровневую иерархию — или вы имеете ввиду просто все разделить на максимально подробные категории? Таблицу я уже составил (вроде 2 достаточно) на бумаге, каких то общих значений кроме общий доход=< общий расход не надо. Хотя если надо еще обязательно планированые расходы — то это как бы лиш дополнительное поле. Менеджмент студио использую — но уже писал что выскакивает ошибка о необходимости бета версии. Здесь еще вопрос возник — пытаюсь определить initializecomponent() в класе Form1 но что при компиляции вызывает ошибку что он уже определен (-яется). Касательно конвертации может .ToInt32(checked) для флажка подойдет? Потом еще момент насколько важен здесь момент правильности (или ввода вообще) ввода даных, нужно или определение даты использовать встроеные класы они ведь на английском или вручную вводить. Добавлено через 6 часов 18 минут Добавлено через 1 час 7 минут
0 |
4 / 4 / 3 Регистрация: 29.01.2013 Сообщений: 12 |
|
13.06.2016, 23:08 |
10 |
Здесь еще вопрос возник — пытаюсь определить initializecomponent() в класе Form1 но что при компиляции вызывает ошибку что он уже определен (-яется). InitializeComponent() определен в конструкторе. Переопределите конструктор.
0 |
138 / 7 / 1 Регистрация: 31.03.2015 Сообщений: 395 |
|
14.06.2016, 17:02 [ТС] |
11 |
Как это переопределить конструкор? Это метод кажется переопределяется вне конструктора — а в самом конструкторе уже вызывается. И непонятна схема файлов form1.cs form.designer.cs — какое распределение ролей между ними. Может все можно было включить в один файл? И касательно: менеджмент студио — может оставить экспрес скьюэл сервер, а к нему доставить полный вижуал студио если возможно лиш установить его шарп компонент (и это если верно предположение что такая связка возможна при присутствие лиш одной экспрес версии из двух приложений)? Добавлено через 15 часов 9 минут
0 |
5459 / 4232 / 1208 Регистрация: 12.10.2013 Сообщений: 12,223 Записей в блоге: 2 |
|
14.06.2016, 17:29 |
12 |
Тихо сам с собою я веду беседу… arts1, ваши вопросы, как бы это помягче сказать, выдают полное наличие отсутствия знаний. Совсем.
0 |
Вежливость-главное оружие 233 / 234 / 86 Регистрация: 19.02.2013 Сообщений: 1,446 |
|
14.06.2016, 17:30 |
13 |
Вот пример Windows Forms приложения с использование Entity Framework 6.0 + Code First
0 |
138 / 7 / 1 Регистрация: 31.03.2015 Сообщений: 395 |
|
14.06.2016, 17:45 [ТС] |
14 |
Ну вы меня удивляете— у меня две экспрес версии вижуал студио С# и Slq server (management studio). Вместе они работать не могут. Для Шарп экспреса надо полную версию сервера которую я не могу найти. Здесь проблема не в знаниях — а в ограничение которое у меня выскакивает когда я хочу подсоединить бд.
0 |
Вежливость-главное оружие 233 / 234 / 86 Регистрация: 19.02.2013 Сообщений: 1,446 |
|
14.06.2016, 18:45 |
15 |
а в ограничение которое чаво? логи встудию
0 |
5459 / 4232 / 1208 Регистрация: 12.10.2013 Сообщений: 12,223 Записей в блоге: 2 |
|
14.06.2016, 18:54 |
16 |
Здесь проблема не в знаниях Сомневаюсь.
Windows Forms Programming in C#: Chris Sells — думаю этого достаточно, как и ado.net троэлсена. Прочитав хотя бы одну из них, таких вопросов бы просто не было.
непонятна схема файлов form1.cs form.designer.cs — какое распределение ролей между ними. Может все можно было включить в один файл? Так что учиться, учиться и еще раз учиться. Либо давайте ТЗ.
0 |
138 / 7 / 1 Регистрация: 31.03.2015 Сообщений: 395 |
|
14.06.2016, 19:02 [ТС] |
17 |
Ну что вы- я вам про одно а вы про другое. form1.cs form.designer.cs — да с формами не работал — но не думаю что у меня такая сложная форма будет — чтобы в процессе с этим не разобраться.
0 |
5459 / 4232 / 1208 Регистрация: 12.10.2013 Сообщений: 12,223 Записей в блоге: 2 |
|
14.06.2016, 19:06 |
18 |
вижуал студио экспрес 2005. arts1, 1. Есть 2015 Community, полностью бесплатная.
0 |
138 / 7 / 1 Регистрация: 31.03.2015 Сообщений: 395 |
|
15.06.2016, 00:17 [ТС] |
19 |
Так мне не экспрес сервер надо а не вижуад студио. Добавлено через 1 час 52 минуты Добавлено через 3 часа 2 минуты
0 |
138 / 7 / 1 Регистрация: 31.03.2015 Сообщений: 395 |
|
16.06.2016, 16:05 [ТС] |
20 |
У меня появилось несколько вопросов: как извлечь Date и Time из datetimepicker. datetimepicker.value.date не помогает. И если передавать данные из пикера в sql server — datetimepicker.value.toString(). Тогда ведь невозможно будет задать условие where если это будет просто текстовое значение?? Еще возник вопрос — если в моей таблице будет заполнена лиш одна колонка для одной записи не считая колонки с датой (например категория1 доходов или категория2 доходов и т.д. и в каждой строке дата) то другие 6-7 полей будут пустые. Будут ли эти поля NULL, и как тогда применить функцию сумирования ко всему полю таблицы — через WHERE FIELD IS NOT NULL? И надо выводить результат просмотра таблицы лиш в обьекте DATAGRIDVIEW — если запрос будет применятся к сумированию части полей таблицы в некий период времени?
0 |
IT_Exp Эксперт 87844 / 49110 / 22898 Регистрация: 17.06.2006 Сообщений: 92,604 |
16.06.2016, 16:05 |
20 |
Очень долго пытаюсь создать сервер-клиент приложение через сокеты и Tcp протокол. Запускаю на своем компе сервер и клиентов, отправка сообщений и ответ от сервера работают корректно, но если клиент запускает кто-то другой то вылетает исключение, что превышено время ожидания и сервер не дал ответа. Как быть?
Ниже прикладываю код сервера и клиента.
Сервер
string hostName = Dns.GetHostName();
IPHostEntry ipEntry = Dns.GetHostByName(hostName);
IPAddress[] ipAdresses = ipEntry.AddressList;
IPEndPoint ipEndPoint =
new IPEndPoint(IPAddress.Parse(ipAdresses[0].ToString()), 2017);
Socket sListener =
new Socket(IPAddress.Parse(ipAdresses[0].ToString()).AddressFamily, SocketType.Stream, ProtocolType.Tcp);
sListener.Bind(ipEndPoint);
sListener.Listen(10);
while (true)
{
updateLog("Waiting for connection on [" + ipEndPoint + "]");
Socket handler = sListener.Accept();
updateLog("New user join the channel. [" + handler.LocalEndPoint + "]");
string data = null;
byte[] bytes = new byte[1024];
int bytesRec = handler.Receive(bytes);
data += Encoding.UTF8.GetString(bytes, 0, bytesRec);
updateLog("Client[" + handler.LocalEndPoint + "]: " + data);
// Отправка ответа клиенту.
string reply = "Thanks for your query. We got it!";
byte[] replyMSG = Encoding.UTF8.GetBytes(reply);
handler.Send(replyMSG);
handler.Shutdown(SocketShutdown.Both);
handler.Close();
}
Клиент
byte[] bytes = new byte[1024];
IPAddress serverIp = IPAddress.Parse("Тут IP компьютера");
IPEndPoint ipEndPoint = new IPEndPoint(serverIp, port);
Socket sender = new Socket(serverIp.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
sender.Connect(ipEndPoint);
byte[] msg = Encoding.UTF8.GetBytes(msgTB.Text);
int bytesSend = sender.Send(msg);
int bytesRec = sender.Receive(bytes);
logTB.AppendText("Server: " + Encoding.UTF8.GetString(bytes, 0, bytesRec));
sender.Shutdown(SocketShutdown.Both);
sender.Close();