Как создать клиент серверное приложение c
Перейти к содержимому

Как создать клиент серверное приложение c

  • автор:

Создание клиент-серверного приложения C#

Раньше я всегда создавал приложения на C# с помощью WinForms или WPF , в которых напрямую обращался к локальной базе данных через запросы с помощью SqlCommand или EntityFramework . Но как я понимаю это не лучшее решение, если, например, создавать приложение не только для себя, а для нескольких пользователей. Если я правильно понимаю, то клиент-серверное приложение — это клиент, с которым работает пользователь и который отправляет запросы к серверу, и собственно сам сервер, который принимает и обрабатывает запросы, а после отдаёт какой-то ответ. Клиент — это допустим WPF -приложение, в котором мы должны отправлять запросы к серверу, но я не совсем понимаю, что именно должно выполнять роль сервера, на чем его писать и как подключать к клиенту. Так как я никогда не работал с API и не создавал серверной части, то мне непонятно что именно гуглить для ответа на свой вопрос, всё, что я нашёл — это что сервер можно создать на ASP.NET WebAPI, но в данной статье только первая часть, посвященная созданию клиента. Итак, если обобщить всё выше написанное, то мой вопрос — как и на чем можно написать сервер для клиент-серверного приложения, где клиент — это WPF -приложение, и как потом этот сервер подключить к клиенту. P.S Понимаю, что вопрос достаточно тривиальный для C#-программистов, но так как сталкиваюсь впервые с этим, то мне подойдут ссылки на статьи, названия библиотек, ссылки на документацию и так далее, любая информация чтобы понять куда рыть для ответа на мой вопрос.

Отслеживать
задан 1 июл 2022 в 13:12
449 2 2 серебряных знака 14 14 бронзовых знаков
ASP.NET Core WebAPI — нормальное решение
1 июл 2022 в 13:31
плюсую комментарий @aepot, вот тут вы можете почитать подробнее о том как сделать такое приложение
1 июл 2022 в 13:31

Спасибо, буду смотреть в этом направлении, @iKuzmychov может быть есть какая-то статья по созданию клиент-серверного приложения именно wpf + asp,net?

1 июл 2022 в 13:34

@Pekor в WPF клиенте нет особо отличающегося, от того же WinForms. План довольно простой: 1. создаёте модели (классы) для таблиц базы данных и выносите их в отдельный проект «XXX.Models», чтобы ссылаться на него из других проектов 2. делаете проект «XXX.WebApi» — серверную часть приложения на ASP.NET, в которой создаёте контроллеры, которые взаимодействуют с БД и возвращают JSON в ответ 3. Создаёте проект «XXX.WebApiClient», в котором создаёте классы (обычно хватает одного), которые делают HTTP запросы к вашему API и парсят ответы в C# объекты 4. Используете API-клиет в WPF приложении

Как создать клиент серверное приложение c

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

Сервер на сокетах TCP в C# и .NET

Привязка к конечной точке. Метод Bind

Вначале серверный сокет с помощью метода Bind связывается с локальной точкой. В качестве параметра этот метод принимает локальную точку EndPoint, на которой сокет будет принимать подключения от клиентов:

public void Bind (EndPoint localEP);

Если не имеет значения, на каком именно локальном адресе сервер будет запущен, то можно в качестве адреса использовать значение IPAddress.Any . Тогда серверу будет назначен наиболее подходящий сетевой адрес (при наличии нескольких сетевых интерфейсов). Кроме того, если номер порта не имеет значения, то в качестве порта можно указать число 0. Тогда серверу будет предоставлен один из доступных портов. При использовании такого подхода точный адрес и порт затем можно будет получить через свойство LocalEndpoint .

using System.Net; using System.Net.Sockets; IPEndPoint ipPoint = new IPEndPoint(IPAddress.Any, 8888); using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Bind(ipPoint); // связываем с локальной точкой ipPoint // получаем конечную точку, с которой связан сокет Console.WriteLine(socket.LocalEndPoint); // 0.0.0.0:8888

В этом примере сокет будет прослушивать подключения по 8888 порту на любых локальных адресах. То есть клиент должен будет подключаться к локальному адресу, например, к 127.0.0.1, и порту 8888.

Прослушивание подключений. Метод Listen

Для запуска прослушивания подключений на выбранной локальной конечной точке применяется метод Listen :

public void Listen (); public void Listen (int backlog);

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

stem.Net; using System.Net.Sockets; IPEndPoint ipPoint = new IPEndPoint(IPAddress.Any, 8888); using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Bind(ipPoint); // связываем с локальной точкой ipPoint socket.Listen(1000); // запускаем прослушивание // количество входящих подключений, которые можно поместить в очередь, равно 1000

Подключение клиента

После начала прослушивания сокет готов принимать подключения. Для приема подключений применяются методы Accept()/AcceptAsync() . Эти методы имеют ряд перегруженных версий. Отмечу саму простую из них:

public Task AcceptAsync ();

Все версии методов Accept()/AcceptAsync() в качестве результа возвращают объект Socket, который инкапсулирует входящее подключение, то есть по сути представляет подключенного клиента.

using System.Net; using System.Net.Sockets; IPEndPoint ipPoint = new IPEndPoint(IPAddress.Any, 8888); using Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); socket.Bind(ipPoint); socket.Listen(); Console.WriteLine("Сервер запущен. Ожидание подключений. "); // получаем входящее подключение using Socket client = await socket.AcceptAsync(); // получаем адрес клиента Console.WriteLine($"Адрес подключенного клиента: ");

Через свойства Socket можно получить информацию о подключении клиента, в частности, свойство RemoteEndPoint позволяет получить адрес подключенного клиента.

Для такого просто сервера для теста определим клиент. Возьмем новый проект консольного приложения на C# и определим в нем следующий код:

using System.Net.Sockets; using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); try < await socket.ConnectAsync("127.0.0.1", 8888); Console.WriteLine($"Подключение к установлено"); > catch (SocketException) < Console.WriteLine($"Не удалось установить подключение с "); >

Здесь в метод ConnectAsync передаем данные конечной точки сервера и при успешном подключении выводим сообщение.

Запустим сервер, а затем запустим клиент. В итоге после подключения клиента к серверу в консоли сервера мы увидим что-то наподобие:

Сервер запущен. Ожидание подключений. Адрес подключенного клиента: 127.0.0.1:52767

В данном случае мы видим, что в моем случае для подключения к серверу сокет-клиент использует адрес 127.0.0.1:52767. А в консоли клиента отобразится сообщение об успешном подключении:

Подключение к 127.0.0.1:8888 установлено

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

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

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

Сервер TCP

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

Схема .NET-сервера

Вот полный код программы 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[0]; 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("Ожидаем соединение через порт ", ipEndPoint); // Программа приостанавливается, ожидая входящее соединение Socket handler = sListener.Accept(); string data = null; // Мы дождались клиента, пытающегося с нами соединиться byte[] bytes = new byte[1024]; 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[0]; 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("Ожидаем соединение через порт ", 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

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

Клиентское приложение, использующее сокеты

Вот полный код для SocketClient.cs и его объяснение:

// SocketClient.cs using System; using System.Text; using System.Net; using System.Net.Sockets; namespace SocketClient < class Program < static void Main(string[] args) < try < SendMessageFromSocket(11000); >catch (Exception ex) < Console.WriteLine(ex.ToString()); >finally < Console.ReadLine(); >> static void SendMessageFromSocket(int port) < // Буфер для входящих данных byte[] bytes = new byte[1024]; // Соединяемся с удаленным устройством // Устанавливаем удаленную точку для сокета IPHostEntry ipHost = Dns.GetHostEntry("localhost"); IPAddress ipAddr = ipHost.AddressList[0]; IPEndPoint ipEndPoint = new IPEndPoint(ipAddr, port); Socket sender = new Socket(ipAddr.AddressFamily, SocketType.Stream, ProtocolType.Tcp); // Соединяем сокет с удаленной точкой sender.Connect(ipEndPoint); Console.Write("Введите сообщение: "); string message = Console.ReadLine(); Console.WriteLine("Сокет соединяется с ", sender.RemoteEndPoint.ToString()); byte[] msg = Encoding.UTF8.GetBytes(message); // Отправляем данные через сокет int bytesSent = sender.Send(msg); // Получаем ответ от сервера int bytesRec = sender.Receive(bytes); Console.WriteLine("\nОтвет от сервера: \n\n", Encoding.UTF8.GetString(bytes, 0, bytesRec)); // Используем рекурсию для неоднократного вызова SendMessageFromSocket() if (message.IndexOf("") == -1) SendMessageFromSocket(port); // Освобождаем сокет sender.Shutdown(SocketShutdown.Both); sender.Close(); > > > 

Единственный новый метод - метод Connect(), используется для соединения с удаленным сервером. На рисунке ниже показаны клиент и сервер в действии:

Клиент-сервер шаг — за — шагом, от однопоточного до многопоточного (Client-Server step by step)

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

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

— Потоки: для того чтобы не перепутать что именно подразумевается под потоком я буду использовать существующий в профессиональной литературе синоним — нить, чтобы не путать Stream и Thread, всё-таки более профессионально выражаться — нить, говоря про Thread.

— Сокеты(Sockets): данное понятие тоже не однозначно, поскольку в какой-то момент сервер выполняет — клиентские действия, а клиент — серверные. Поэтому я разделил понятие серверного сокета — (ServerSocket) и сокета (Socket) через который практически осуществляется общение, его будем называть сокет общения, чтобы было понятно о чём речь.

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

Спасибо за подсказку про Thread.sleep();!
Конечно в реальном коде Thread.sleep(); устанавливать не нужно — это моветон! В данной публикации я его использую только для того чтобы выполнение программы было нагляднее, что бы успевать разобраться в происходящем.
Так что тестируйте, изучайте и в своём коде никогда не используйте Thread.sleep();!

1) Однопоточный элементарный сервер.
2) Клиент.
3) Многопоточный сервер – сам по себе этот сервер не участвует в общении напрямую, а лишь является фабрикой однонитевых делегатов(делегированных для ведения диалога с клиентами серверов) для общения с вновь подключившимися клиентами, которые закрываются после окончания общения с клиентом.
4) Имитация множественного обращения клиентов к серверу.

Итак, начнём с изучения структуры однопоточного сервер, который может принять только одного клиента для диалога. Код приводимый ниже необходимо запускать в своей IDE в этом идея всей статьи. Предлагаю все детали уяснить из подробно задокументированного кода ниже:

  • 1) Однопоточный элементарный сервер.
import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class TestAsServer < /** * * @param args * @throws InterruptedException */ public static void main(String[] args) throws InterruptedException < // стартуем сервер на порту 3345 try (ServerSocket server= new ServerSocket(3345))< // становимся в ожидание подключения к сокету под именем - "client" на серверной стороне Socket client = server.accept(); // после хэндшейкинга сервер ассоциирует подключающегося клиента с этим сокетом-соединением System.out.print("Connection accepted."); // инициируем каналы для общения в сокете, для сервера // канал записи в сокет DataOutputStream out = new DataOutputStream(client.getOutputStream()); System.out.println("DataOutputStream created"); // канал чтения из сокета DataInputStream in = new DataInputStream(client.getInputStream()); System.out.println("DataInputStream created"); // начинаем диалог с подключенным клиентом в цикле, пока сокет не закрыт while(!client.isClosed())< System.out.println("Server reading from channel"); // сервер ждёт в канале чтения (inputstream) получения данных клиента String entry = in.readUTF(); // после получения данных считывает их System.out.println("READ from client message - "+entry); // и выводит в консоль System.out.println("Server try writing to channel"); // инициализация проверки условия продолжения работы с клиентом по этому сокету по кодовому слову - quit if(entry.equalsIgnoreCase("quit"))< System.out.println("Client initialize connections suicide . "); out.writeUTF("Server reply - "+entry + " - OK"); out.flush(); Thread.sleep(3000); break; >// если условие окончания работы не верно - продолжаем работу - отправляем эхо-ответ обратно клиенту out.writeUTF("Server reply - "+entry + " - OK"); System.out.println("Server Wrote message to client."); // освобождаем буфер сетевых сообщений (по умолчанию сообщение не сразу отправляется в сеть, а сначала накапливается в специальном буфере сообщений, размер которого определяется конкретными настройками в системе, а метод - flush() отправляет сообщение не дожидаясь наполнения буфера согласно настройкам системы out.flush(); > // если условие выхода - верно выключаем соединения System.out.println("Client disconnected"); System.out.println("Closing connections & channels."); // закрываем сначала каналы сокета ! in.close(); out.close(); // потом закрываем сам сокет общения на стороне сервера! client.close(); // потом закрываем сокет сервера который создаёт сокеты общения // хотя при многопоточном применении его закрывать не нужно // для возможности поставить этот серверный сокет обратно в ожидание нового подключения System.out.println("Closing connections & channels - DONE."); > catch (IOException e) < e.printStackTrace(); >> >

Сервер запущен и находится в блокирующем ожидании server.accept(); обращения к нему с запросом на подключение. Теперь можно подключаться клиенту, напишем код клиента и запустим его. Клиент работает когда пользователь вводит что-либо в его консоли (внимание! в данном случае сервер и клиент запускаются на одном компьютере с локальным адресом — localhost, поэтому при вводе строк, которые должен отправлять клиент не забудьте убедиться, что вы переключились в рабочую консоль клиента!).
После ввода строки в консоль клиента и нажатия enter строка проверяется не ввёл ли клиент кодовое слово для окончания общения дальше отправляется серверу, где он читает её и то же проверяет на наличие кодового слова выхода. Оба и клиент и сервер получив кодовое слово закрывают ресурсы после предварительных приготовлений и завершают свою работу.
Посмотрим как это выглядит в коде:

import java.io.BufferedReader; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; import java.net.UnknownHostException; public class TestASClient < /** * * @param args * @throws InterruptedException */ public static void main(String[] args) throws InterruptedException < // запускаем подключение сокета по известным координатам и нициализируем приём сообщений с консоли клиента try(Socket socket = new Socket("localhost", 3345); BufferedReader br =new BufferedReader(new InputStreamReader(System.in)); DataOutputStream oos = new DataOutputStream(socket.getOutputStream()); DataInputStream ois = new DataInputStream(socket.getInputStream()); ) < System.out.println("Client connected to socket."); System.out.println(); System.out.println("Client writing channel = oos & reading channel = ois initialized."); // проверяем живой ли канал и работаем если живой while(!socket.isOutputShutdown())< // ждём консоли клиента на предмет появления в ней данных if(br.ready())< // данные появились - работаем System.out.println("Client start writing in channel. "); Thread.sleep(1000); String clientCommand = br.readLine(); // пишем данные с консоли в канал сокета для сервера oos.writeUTF(clientCommand); oos.flush(); System.out.println("Clien sent message " + clientCommand + " to server."); Thread.sleep(1000); // ждём чтобы сервер успел прочесть сообщение из сокета и ответить // проверяем условие выхода из соединения if(clientCommand.equalsIgnoreCase("quit"))< // если условие выхода достигнуто разъединяемся System.out.println("Client kill connections"); Thread.sleep(2000); // смотрим что нам ответил сервер на последок перед закрытием ресурсов if(ois.read() >-1) < System.out.println("reading. "); String in = ois.readUTF(); System.out.println(in); >// после предварительных приготовлений выходим из цикла записи чтения break; > // если условие разъединения не достигнуто продолжаем работу System.out.println("Client sent message & start waiting for data from server. "); Thread.sleep(2000); // проверяем, что нам ответит сервер на сообщение(за предоставленное ему время в паузе он должен был успеть ответить) if(ois.read() > -1) < // если успел забираем ответ из канала сервера в сокете и сохраняем её в ois переменную, печатаем на свою клиентскую консоль System.out.println("reading. "); String in = ois.readUTF(); System.out.println(in); >> > // на выходе из цикла общения закрываем свои ресурсы System.out.println("Closing connections & channels on clentSide - DONE."); > catch (UnknownHostException e) < // TODO Auto-generated catch block e.printStackTrace(); >catch (IOException e) < // TODO Auto-generated catch block e.printStackTrace(); >> >

А что если к серверу хочет подключиться ещё один клиент!? Ведь описанный выше сервер либо находится в ожидании подключения одного клиента, либо общается с ним до завершения соединения, что делать остальным клиентам? Для такого случая нужно создать фабрику которая будет создавать описанных выше серверов при подключении к сокету новых клиентов и не дожидаясь пока делегированный подсервер закончит диалог с клиентом откроет accept() в ожидании следующего клиента. Но чтобы на серверной машине хватило ресурсов для общения со множеством клиентов нужно ограничить количество возможных подключений. Фабрика будет выдавать немного модифицированный вариант предыдущего сервера(модификация будет касаться того что класс сервера для фабрики будет имплементировать интерфейс — Runnable для возможности его использования в пуле нитей — ExecutorServices). Давайте создадим такую серверную фабрику и ознакомимся с подробным описанием её работы в коде:

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author mercenery * */ public class MultiThreadServer < static ExecutorService executeIt = Executors.newFixedThreadPool(2); /** * @param args */ public static void main(String[] args) < // стартуем сервер на порту 3345 и инициализируем переменную для обработки консольных команд с самого сервера try (ServerSocket server = new ServerSocket(3345); BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) < System.out.println("Server socket created, command console reader for listen to server commands"); // стартуем цикл при условии что серверный сокет не закрыт while (!server.isClosed()) < // проверяем поступившие комманды из консоли сервера если такие // были if (br.ready()) < System.out.println("Main Server found any messages in channel, let's look at them."); // если команда - quit то инициализируем закрытие сервера и // выход из цикла раздачии нитей монопоточных серверов String serverCommand = br.readLine(); if (serverCommand.equalsIgnoreCase("quit")) < System.out.println("Main Server initiate exiting. "); server.close(); break; >> // если комманд от сервера нет то становимся в ожидание // подключения к сокету общения под именем - "clientDialog" на // серверной стороне Socket client = server.accept(); // после получения запроса на подключение сервер создаёт сокет // для общения с клиентом и отправляет его в отдельную нить // в Runnable(при необходимости можно создать Callable) // монопоточную нить = сервер - MonoThreadClientHandler и тот // продолжает общение от лица сервера executeIt.execute(new MonoThreadClientHandler(client)); System.out.print("Connection accepted."); > // закрытие пула нитей после завершения работы всех нитей executeIt.shutdown(); > catch (IOException e) < e.printStackTrace(); >> >
  • Модифицированный Runnable сервер для запуска из предыдущего кода:
import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; public class MonoThreadClientHandler implements Runnable < private static Socket clientDialog; public MonoThreadClientHandler(Socket client) < MonoThreadClientHandler.clientDialog = client; >@Override public void run() < try < // инициируем каналы общения в сокете, для сервера // канал записи в сокет следует инициализировать сначала канал чтения для избежания блокировки выполнения программы на ожидании заголовка в сокете DataOutputStream out = new DataOutputStream(clientDialog.getOutputStream()); // канал чтения из сокета DataInputStream in = new DataInputStream(clientDialog.getInputStream()); System.out.println("DataInputStream created"); System.out.println("DataOutputStream created"); /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // основная рабочая часть // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // начинаем диалог с подключенным клиентом в цикле, пока сокет не // закрыт клиентом while (!clientDialog.isClosed()) < System.out.println("Server reading from channel"); // серверная нить ждёт в канале чтения (inputstream) получения // данных клиента после получения данных считывает их String entry = in.readUTF(); // и выводит в консоль System.out.println("READ from clientDialog message - " + entry); // инициализация проверки условия продолжения работы с клиентом // по этому сокету по кодовому слову - quit в любом регистре if (entry.equalsIgnoreCase("quit")) < // если кодовое слово получено то инициализируется закрытие // серверной нити System.out.println("Client initialize connections suicide . "); out.writeUTF("Server reply - " + entry + " - OK"); Thread.sleep(3000); break; >// если условие окончания работы не верно - продолжаем работу - // отправляем эхо обратно клиенту System.out.println("Server try writing to channel"); out.writeUTF("Server reply - " + entry + " - OK"); System.out.println("Server Wrote message to clientDialog."); // освобождаем буфер сетевых сообщений out.flush(); // возвращаемся в началло для считывания нового сообщения > /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // основная рабочая часть // ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // если условие выхода - верно выключаем соединения System.out.println("Client disconnected"); System.out.println("Closing connections & channels."); // закрываем сначала каналы сокета ! in.close(); out.close(); // потом закрываем сокет общения с клиентом в нити моносервера clientDialog.close(); System.out.println("Closing connections & channels - DONE."); > catch (IOException e) < e.printStackTrace(); >catch (InterruptedException e) < // TODO Auto-generated catch block e.printStackTrace(); >> >

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

  • 4) Имитация множественного обращения клиентов к серверу.
import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Main < // private static ServerSocket server; public static void main(String[] args) throws IOException, InterruptedException < // запустим пул нитей в которых колличество возможных нитей ограничено - // 10-ю. ExecutorService exec = Executors.newFixedThreadPool(10); int j = 0; // стартуем цикл в котором с паузой в 10 милисекунд стартуем Runnable // клиентов, // которые пишут какое-то количество сообщений while (j < 10) < j++; exec.execute(new TestRunnableClientTester()); Thread.sleep(10); >// закрываем фабрику exec.shutdown(); > >

Как видно из предыдущего кода фабрика запускает — TestRunnableClientTester() клиентов, напишем для них код и после этого запустим саму фабрику, чтобы ей было кого исполнять в своём пуле:

import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.Socket; public class TestRunnableClientTester implements Runnable < static Socket socket; public TestRunnableClientTester() < try < // создаём сокет общения на стороне клиента в конструкторе объекта socket = new Socket("localhost", 3345); System.out.println("Client connected to socket"); Thread.sleep(2000); >catch (Exception e) < e.printStackTrace(); >> @Override public void run() < try ( // создаём объект для записи строк в созданный скокет, для // чтения строк из сокета // в try-with-resources стиле DataOutputStream oos = new DataOutputStream(socket.getOutputStream()); DataInputStream ois = new DataInputStream(socket.getInputStream())) < System.out.println("Client oos & ois initialized"); int i = 0; // создаём рабочий цикл while (i < 5) < // пишем сообщение автогенерируемое циклом клиента в канал // сокета для сервера oos.writeUTF("clientCommand " + i); // проталкиваем сообщение из буфера сетевых сообщений в канал oos.flush(); // ждём чтобы сервер успел прочесть сообщение из сокета и // ответить Thread.sleep(10); System.out.println("Client wrote & start waiting for data from server. "); // забираем ответ из канала сервера в сокете // клиента и сохраняем её в ois переменную, печатаем на // консоль System.out.println("reading. "); String in = ois.readUTF(); System.out.println(in); i++; Thread.sleep(5000); >> catch (IOException e) < // TODO Auto-generated catch block e.printStackTrace(); >catch (InterruptedException e) < // TODO Auto-generated catch block e.printStackTrace(); >> >

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *