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

Как написать сервер на java

  • автор:

Клиент-сервер шаг — за — шагом, от однопоточного до многопоточного (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(); >> >

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

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

Узнайте, как создать и использовать прокси-сервер на Java для обеспечения безопасности и управления доступом в нашей пошаговой статье!

Алексей Кодов
Автор статьи
10 июля 2023 в 17:48

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

Шаг 1: Создание прокси-сервера

Для создания прокси-сервера на Java, воспользуйтесь следующими шагами:

  1. Создайте новый Java-проект и добавьте в него класс MyProxyServer .
  2. В этом классе создайте два объекта Socket : один для соединения с клиентом, другой для соединения с целевым сервером.
Socket clientSocket = null; Socket targetServerSocket = null;
  1. Создайте конструктор для класса MyProxyServer , который будет принимать порт прокси-сервера и адрес целевого сервера.

public MyProxyServer(int proxyPort, String targetServerAddress)

  1. В методе run() создайте ServerSocket , который будет слушать соединения от клиентов.
ServerSocket serverSocket = new ServerSocket(proxyPort);
  1. В бесконечном цикле принимайте соединения от клиентов и создавайте потоки для обработки их запросов.

while (true)

Шаг 2: Обработка запросов клиентов

  1. Создайте класс ClientRequestHandler , который будет обрабатывать запросы клиентов.
  2. В этом классе создайте конструктор, который будет принимать Socket клиента и адрес целевого сервера.

public ClientRequestHandler(Socket clientSocket, String targetServerAddress)

  1. В методе run() класса ClientRequestHandler считывайте запрос клиента, обрабатывайте его и отправляйте на целевой сервер. Затем получите ответ от целевого сервера и перенаправьте его обратно клиенту.
InputStream clientInputStream = clientSocket.getInputStream(); OutputStream clientOutputStream = clientSocket.getOutputStream(); // Чтение запроса клиента и обработка данных // . // Отправка запроса на целевой сервер // . // Получение ответа от целевого сервера и отправка его клиенту // .
  1. Закройте соединения с клиентом и целевым сервером после обработки запроса.
clientSocket.close(); targetServerSocket.close();

Java-разработчик: новая работа через 11 месяцев
Получится, даже если у вас нет опыта в IT

Шаг 3: Запуск прокси-сервера

Чтобы запустить ваш прокси-сервер, просто создайте и запустите объект MyProxyServer с необходимыми параметрами.

public static void main(String[] args)

Теперь ваш прокси-сервер готов к использованию! ��

Заключение

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

Не забудьте изучить дополнительные материалы и практиковаться для улучшения своих навыков в Java-разработке. Удачи вам в изучении Java! ��

Простейший сервер и клиент

Этот пример покажет простейшее использование серверного и клиентского сокета. Все, что делает сервер, это ожидает соединения, затем использует сокет, полученный при соединении, для создания InputStream’а и OutputStream’а. Они конвертируются в Reader и Writer, которые оборачиваются в BufferedReader и PrintWriter. После этого все, что будет прочитано из BufferedReader’а будет переправлено в PrintWriter, пока не будет получена строка «END», означающая, что пришло время закрыть соединение.

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

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

//: c15:JabberServer.java
// Очень простой сервер, который просто отсылает
// назад все, что посылает клиент.
//
import java.io.*;

public class JabberServer // Выбираем порт вне пределов 1-1024:
public static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException ServerSocket s = new ServerSocket ( PORT ) ;
System.out.println ( «Started: » + s ) ;
try // Блокирует до тех пор, пока не возникнет соединение:
Socket socket = s.accept () ;
try System.out.println ( «Connection accepted: » + socket ) ;
BufferedReader in = new BufferedReader ( new InputStreamReader (
socket.getInputStream ())) ;
// Вывод автоматически выталкивается из буфера PrintWriter’ом
PrintWriter out = new PrintWriter ( new BufferedWriter (
new OutputStreamWriter ( socket.getOutputStream ())) , true ) ;
while ( true ) String str = in.readLine () ;
if ( str.equals ( «END» ))
break ;
System.out.println ( «Echoing: » + str ) ;
out.println ( str ) ;
>
// Всегда закрываем два сокета.
>
finally System.out.println ( «closing. » ) ;
socket.close () ;
>
>
finally s.close () ;
>
>
> // /:~

Вы можете видеть, что для ServerSocket’а необходим только номер порта, а не IP адрес (так как он запускается на локальной машине!). Когда вы вызываете accept( ), метод блокирует выполнение до тех пор, пока клиент не попробует подсоединится к серверу. То есть, сервер ожидает соединения, но другой процесс может выполнятся (смотрите Главу 14). Когда соединение установлено, метод accept( ) возвращает объект Socket, представляющий это соединение.

Здесь тщательно обработана отвественность за очистку сокета. Если конструктор ServerSocket завершится неудачей, программа просто звершится (обратите внимание, что мы должны предположить, что конструктор ServerSocket не оставляет никаких открытых сокетов, если он зваершается неудачей). По этой причине main( ) выбрасывает IOException, так что в блоке try нет необходимости. Если конструктор ServerSocket завершится успешно, то все вызовы методов должны быть помещены в блок try-finally, чтобы убедиться, что блок не будет покинут ни при каких условиях и ServerSocket будет правильно закрыт.

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

И ServerSocket и Socket, производимый методом accept( ), печатаются в System.out. Это означает, что автоматически вызывается их метод toString( ). Вот что он выдаст:

ServerSocket [ addr= 0.0.0.0 ,PORT= 0 ,localport= 8080 ]
Socket [ addr= 127.0.0.1 ,PORT= 1077 ,localport= 8080 ]

Короче говоря, вы увидите как это соответствует тому, что делает клиент.

Следующая часть программы выглядит, как открытие файла для чтения и записи за исключением того, что InputStream и OutputStream создаются из объекта Socket. И объект InputStream’а и OutputStream’а конвертируются в объекты Reader’а и Writer’а с помощью «классов-конвертеров» InputStreamReader и OutputStreamreader, соответственно. Вы можете также использовать классы из Java 1.0 InputStream и OutoutStream напрямую, но, с точки зрения вывода, есть явное преимущество в использовании этого подхода. Оно проявляется в PrintWriter’е, который имеет перегруженный конструктор, принимающий в качестве второго аргумента флаг типа boolean, указывающий, нужно ли автоматическое выталкивание буфера вывода в конце каждого выражения println( ) (но не print( )). Каждый раз, когда вы записываете в вывод, буфер вывода должен выталкиваться, чтобы информация проходила по сети. Выталкивание важно для этого конкретного примера, поскольку клиент и сервер ожидают строку от другой стороны, прежде, чем приступят к ее обработке. Если выталкивание буфера не произойдет, информация не будет помещена в сеть до тех пор, пока буфер не заполнится, что может привести к многочисленным проблемам в этом примере.

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

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

В бесконечном цикле while происходит чтение строк из входного BufferedReader’а и запись информации в System.out и в выходной PrintWriter. Обратите внимание, что вход и выход могут быть любыми потоками, так случилось, что они связаны с сетью.

Когда клиент посылает строку, содержащую «END», программа прекращает цикл и закрывает сокет.

//: c15:JabberClient.java
// Очень простой клиент, который просто посылает
// строки на сервер и читает строки,
// посылаемые сервером.
//
import java.net.*;

public class JabberClient public static void main ( String [] args ) throws IOException // Передаем null в getByName(), получая
// специальный IP адрес «локальной заглушки»
// для тестирования на машине без сети:
InetAddress addr = InetAddress.getByName ( null ) ;
// Альтернативно, вы можете использовать
// адрес или имя:
// InetAddress addr =
// InetAddress.getByName(«127.0.0.1»);
// InetAddress addr =
// InetAddress.getByName(«localhost»);
System.out.println ( «addr = » + addr ) ;
Socket socket = new Socket ( addr, JabberServer.PORT ) ;
// Помещаем все в блок try-finally, чтобы
// быть уверенным, что сокет закроется:
try System.out.println ( «socket = » + socket ) ;
BufferedReader in = new BufferedReader ( new InputStreamReader ( socket
.getInputStream ())) ;
// Вывод автоматически Output быталкивается PrintWriter’ом.
PrintWriter out = new PrintWriter ( new BufferedWriter (
new OutputStreamWriter ( socket.getOutputStream ())) , true ) ;
for ( int i = 0 ; i < 10 ; i++ ) out.println ( "howdy " + i ) ;
String str = in.readLine () ;
System.out.println ( str ) ;
>
out.println ( «END» ) ;
>
finally System.out.println ( «closing. » ) ;
socket.close () ;
>
>
> // /:~

В main( ) вы можете видеть все три способа получение InetAddress IP адреса локальной заглушки: с помощью null, localhost или путем явного указания зарезервированного адреса 127.0.0.1, если вы хотите соединится с машиной по сети, вы замените это IP адресом машины. Когда печатается InetAddress (с помощью автоматического вызова метода toString( )), то получается результат:

При передачи в getByName( ) значения null, он по умолчанию ищет localhos и затем производит специальныйы адрес 127.0.0.1.

Обратите внимание, что Socket создается при указании и InetAddress’а, и номера порта. Чтобы понять, что это значит, когда будете печатать один из объектов Socket помните, что Интернет соединение уникально определяется четырьмя параметрами: клиентским хостом, клиентским номером порта, серверным хостом и серверным номером порта. Когда запускается сервер, он получает назначаемый порт (8080) на localhost (127.0.0.1). Когда запускается клиент, он располагается на следующем доступном порту на своей машине, 1077 — в данном случае, который так же оказался на той же самой машине (127.0.0.1), что и сервер. Теперь, чтобы передать данные между клиентом и сервером, каждая сторона знает, куда посылать их. Поэтому, в процессе соединения с «известным» сервером клиент посылает «обратный адрес», чтобы сервер знал, куда посылать данные. Вот что вы видите среди выводимого стороной сервера:

Socket [ addr= 127.0.0.1 ,port= 1077 ,localport= 8080 ]

Это означает, что сервер просто принимает соединение с адреса 127.0.0.1 и порта 1077 во время прослушивания локального порта (8080). На клиентской стороне:

Socket [ addr=localhost/ 127.0.0.1 ,PORT= 8080 ,localport= 1077 ]

Это значит, что клиент установил соединение с адресом 127.0.0.1 по порту 8080, используя локальный порт 1077.

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

Как только объект Socket будет создан, процесс перейдет к BufferedReader и PrintWriter, как мы это уже видели в сервере (опять таки, в обоих случаях вы начинаете с Socket’а). В данном случае, клиент инициирует обмен путем посылки строки «howdy», за которой следует число. Обратите внимание, что буфер должен опять выталкиваться (что происходит автоматически из-за второго аргумента в конструкторе PrintWriter’а). Если буфер не будет выталкиваться, процесс обмена повиснет, поскольку начальное «howdy» никогда не будет послана (буфер недостаточно заполнен, чтобы отсылка произошла автоматически). Каждая строка, посылаемая назад сервером, записывается в System.out, чтобы проверить, что все работает корректно. Для завершения обмена посылается ранее оговоренный «END». Если клиент просто разорвет соединение, то сервер выбросит исключение.

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

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

Обслуживание множества клиентов

JabberServer работает, но он может обработать только одного клиента одновременно. В обычных серверах вы захотите, чтобы была возможность иметь дело со многими клиентами одновременно. Ответом является многопоточность, и в языках, которые не поддерживают многопоточность напрямую, это означает что вы встретите все возможные трудности. В Главе 14 вы видели, что многопоточность в Java проста насколько это возможно, учитывая это, можно сказать, что многопоточность весьма сложная тема. Поскольку нити (потоки) в Java достаточно прамолинейны, то создание сервера, который обрабатывает несколько клиентов, относительно простое заняте.

Основная схема состоит в создании единственного ServerSocket’а на сервере и вызове метода accept( ) для ожидания новых соединений. Когда accept( ) возвращается, вы получаете результирующий сокет и используете его для создания новой нити (потока), работа которой будет состоять в ослуживании определенного клиента. Затем вы вызовите метод accept( ) снова, чтобы подождать нового клиента.

В следующем коде сервера вы можете видеть, что он очень похож на пример JabberServer.java, за исключением того, что все операции по обслуживанию определенного клиента былы помещены внутрь отдельного thread-класса:

//: c15:MultiJabberServer.java
// Сервер, который использует многопоточность
// для обработки любого числа клиентов.
//
import java.io.*;

class ServeOneJabber extends Thread private Socket socket;
private BufferedReader in;
private PrintWriter out;

public ServeOneJabber ( Socket s ) throws IOException socket = s;
in = new BufferedReader ( new InputStreamReader ( socket.getInputStream ())) ;
// Включаем автоматическое выталкивание:
out = new PrintWriter ( new BufferedWriter ( new OutputStreamWriter ( socket
.getOutputStream ())) , true ) ;
// Если любой из вышеприведенных вызовов приведет к
// возникновению исключения, то вызывающий отвечает за
// закрытие сокета. В противном случае, нить
// закроет его.
start () ; // вызываем run()
>

public void run () try while ( true ) String str = in.readLine () ;
if ( str.equals ( «END» ))
break ;
System.out.println ( «Echoing: » + str ) ;
out.println ( str ) ;
>
System.out.println ( «closing. » ) ;
>
catch ( IOException e ) System.err.println ( «IO Exception» ) ;
>
finally try socket.close () ;
>
catch ( IOException e ) System.err.println ( «Socket not closed» ) ;
>
>
>
>

public class MultiJabberServer static final int PORT = 8080 ;

public static void main ( String [] args ) throws IOException ServerSocket s = new ServerSocket ( PORT ) ;
System.out.println ( «Server Started» ) ;
try while ( true ) // Блокируется до возникновения нового соединения:
Socket socket = s.accept () ;
try new ServeOneJabber ( socket ) ;
>
catch ( IOException e ) // Если завершится неудачей, закрывается сокет,
// в противном случае, нить закроет его:
socket.close () ;
>
>
>
finally s.close () ;
>
>
> // /:~

Нить ServeOneJabber принимает объект Socket’а, который производится методом accept( ) в main( ) при каждом новом соединении с клиентом. Затем, как и прежде, с помощью Socket, создается BufferedReader и PrintWriter с возможностью автоматического выталкивания буфера. И наконец, вызывается специальный метод нити start( ). Здесь выполняются те же действия, что и в предыдущем примере: читается что-то из сокета и затем отсылается обратно до тех пор, пока не будет прочитан специальный сигнал «END».

Ответственность за очистку сокета должна быть, опять таки, внимательно спланирована. В этом случае, сокет создается вне ServeOneJabber, так что ответственность может быть совместная. Если конструктор ServeOneJabber завершится неудачей, он просто выбросит исключение тому, кто его вызвал, и кто должен очистить нить. Но если конструктор завершился успешно, то объект ServeOneJabber принимает ответственность за очистку нити на себя, в своем методе run( ).

Обратите внимание на упрощенность MultiJabberServer. Как и прежде создается ServerSocket и вызывается метод accept( ), чтобы позволить новое соединение. Но в это время возвращаемое значение метода accept( ) (сокет) передается в конструктор для ServeOneJabber, который создает новую нить для обработки этого соединения. Когда соединение завершиется, нить просто умирает.

Если создание ServerSocket’а проваливается, то из метода main( ), как и прежде, выбрасывается исключение. Но если создание завершается успешно, внешний блок try-finally гарантирует очистку. Внутренний try-catch гарантирует только от сбоев в конструкторе ServeOneJabber. Если конструктор завершится успешно, то нить ServeOneJabber закроет соответствующий сокет.

Для проверки этого сервера, который реально обрабатывает несколько клиентов, приведенная ниже программа создает несколько клиентов (используя нити), которые соединяются с одним и тем же сервером. Максимальное допустимое число нитей определяется переменной final int MAX_THREADS.

//: c15:MultiJabberClient.java
// Клиент, который проверяет MultiJabberServer,
// запуская несколько клиентов.
//
import java.net.*;

class JabberClientThread extends Thread private Socket socket;
private BufferedReader in;
private PrintWriter out;
private static int counter = 0 ;
private int id = counter++;
private static int threadcount = 0 ;

public static int threadCount () return threadcount;
>

public JabberClientThread ( InetAddress addr ) System.out.println ( «Making client » + id ) ;
threadcount++;
try socket = new Socket ( addr, MultiJabberServer.PORT ) ;
>
catch ( IOException e ) System.err.println ( «Socket failed» ) ;
// Если создание сокета провалилось,
// ничего ненужно чистить.
>
try in = new BufferedReader ( new InputStreamReader ( socket
.getInputStream ())) ;
// Включаем автоматическое выталкивание:
out = new PrintWriter ( new BufferedWriter ( new OutputStreamWriter (
socket.getOutputStream ())) , true ) ;
start () ;
>
catch ( IOException e ) // Сокет должен быть закрыт при любой
// ошибке, кроме ошибки конструктора сокета:
try socket.close () ;
>
catch ( IOException e2 ) System.err.println ( «Socket not closed» ) ;
>
>
// В противном случае сокет будет закрыт
// в методе run() нити.
>

public void run () try for ( int i = 0 ; i < 25 ; i++ ) out.println ( "Client " + id + ": " + i ) ;
String str = in.readLine () ;
System.out.println ( str ) ;
>
out.println ( «END» ) ;
>
catch ( IOException e ) System.err.println ( «IO Exception» ) ;
>
finally // Всегда закрывает:
try socket.close () ;
>
catch ( IOException e ) System.err.println ( «Socket not closed» ) ;
>
threadcount—; // Завершаем эту нить
>
>
>

public class MultiJabberClient static final int MAX_THREADS = 40 ;

public static void main ( String [] args ) throws IOException,
InterruptedException InetAddress addr = InetAddress.getByName ( null ) ;
while ( true ) if ( JabberClientThread.threadCount () < MAX_THREADS )
new JabberClientThread ( addr ) ;
Thread.currentThread () .sleep ( 100 ) ;
>
>
> // /:~

Конструктор JabberClientThread принимает InetAddress и использует его для открытия сокета. Вероятно, вы заметили шаблон: сокет всегда используется для создания определенного рода объектов Reader’а и Writer’а (или InputStream и/или OutputStream), которые являются тем единственным путем, которым может быть использован сокет. (Вы можете, конечно, написать класс или два для автоматизации этого процесса вместо набора этого текста, если вас это беспокоит.) Далее, start( ) выполняет инициализацию нити и запуск run( ). Здесь сообщение посылается на сервер, а информация с сервера отображается на экране. Однако, нить имеет ограниченноен время жизни и, в конечном счете, завершается. Обратите внимание, что сокет очищается, если конструктор завершился неудачей после создания сокета, но перед тем, как конструктор завершится. В противном случае, ответственность за вызов close( ) для сокета ложиться на метод run( ).

Threadcount хранит информацию о том, сколько в настоящее время существует объектов JabberClientThread. Эта переменная инкрементируется, как часть конструктора и декрементируется при выходе из метода run( ) (что означает, что нить умерла). В методе MultiJabberClient.main( ) вы можете видеть, что количество нитей проверяется, и если их много, то нить более не создается. Затем метод засыпает. Таким образом, некоторые нити, в конечном счете, умрут, и другие будут созданы. Вы можете поэкспериментировать с MAX_THREADS, чтобы увидеть, когда ваша конкретная система почувствует затруднения со множеством соединений.

Дейтаграммы

Пример, который вы недавно видели, использует Transmission Control Protocol (TCP, также известный, как сокет, основанный на потоках), который предназначен для наибольшей надежности и гарантии, что данные будут доставлены. Он позволяет передавать повторно потерянные данные, он обеспечивает множественные пути через различные маршрутизаторы в случае, если один из них отвалится, а байты будут доставлены в том порядке, в котором они посланы. Весь этот контроль и надежность добавляют накладные расходы: TCP сильно перегружен.

Существует второй потокол, называемый User Datagram Protocol (UDP), который не гарантирует, что пакет будет доставлен и не гарантирует, что пакеты достигнут точки назначения в том же порядке, в котором они были отправлены. Он называется «ненадежным протоколом» (TCP является «надежным протоколом»), что звучит плохо, но так как он намного быстрее, он может быть полезнее. Существуют приложения, такие как аудио сигнал, в которых не критично, если несколько пакетов потеряются здесь или там, а скорость жизненно необходима. Или например сервер времени, для которого реально не имеет значения, если одно из сообщений будет потеряно. Также, некоторые приложения могут быть способны отправлять UDP сообщения к серверу и затем считать, если нет ответа в разумный период времени, что сообщения были потеряны.

Обычно вы будете выполнять ваше прямое сетевое программирование с помощью TCP, и только иногда вы будете использовать UDP. Есть более общее толкование UDP, включая пример, в первой редакции этой книги (доступра на CR-ROM’е, сопровождающем это книгу или может быть свободно загружено с www.BruceEckel.com).

Использование URL’ов из апплета

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

getAppletContext () .showDocument ( u ) ;

в которой u является объектом типа URL. Вот простой пример, который перенаправляет вас на другую страницу. Хотя вы просто перенаправляете на HTML страницу, вы можете также перенаправить на вывод, который дает CGI программа.

public class ShowHTML extends JApplet JButton send = new JButton ( «Go» ) ;
JLabel l = new JLabel () ;

public void init () Container cp = getContentPane () ;
cp.setLayout ( new FlowLayout ()) ;
send.addActionListener ( new Al ()) ;
cp.add ( send ) ;
cp.add ( l ) ;
>

class Al implements ActionListener public void actionPerformed ( ActionEvent ae ) try // Это может быть CGI программа вместо
// HTML страницы.
URL u = new URL ( getDocumentBase () , «FetcherFrame.html» ) ;
// Отображается вывод URL с помощью
// Web броузера, как обычная страница:
getAppletContext () .showDocument ( u ) ;
>
catch ( Exception e ) l.setText ( e.toString ()) ;
>
>
>

public static void main ( String [] args ) Console.run ( new ShowHTML () , 100 , 50 ) ;
>
> // /:~

Красота класса URL состоит в том, что он отлично защищает вас. Вы можете соединится с Web серверами без знания многого из того, что происходит за занавесом.

Клиент-сервер на Java

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

Всего в приложении TCP / IP 4 варианта.

APPLICATION NUMBER FUNCTIONALITY
1st application Client to server communication (one-way)
2nd application Server to client communication (one-way)
3rd application Server sends file contents to client (two-way, non-continuous)
4th application Chat program (two-way, continuous)

1-е Приложение клиент-сервер

Приложение состоит из двух программ. Клиентская программа, работающая на стороне клиента, и серверная программа, работающая на стороне сервера. Клиентская программа WishesClient.java отправляет серверу наилучшие пожелания, а серверная программа WishesServer.java получает сообщение и печатает на своем терминале (мониторе).

Клиентская программа – WishesClient.java

import java.net.Socket; import java.io.OutputStream; import java.io.DataOutputStream; public class WishesClient < public static void main(String args[]) throws Exception < Socket sock = new Socket("127.0.0.1", 5000); String message1 = "Accept Best Wishes, Serverji"; OutputStream ostream = sock.getOutputStream(); DataOutputStream dos = new DataOutputStream(ostream); dos.writeBytes(message1); dos.close(); ostream.close(); sock.close(); >> Socket sock = new Socket ("127.0.0.1", 5000);

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

OutputStream ostream = sock.getOutputStream ();

Метод getOutputStream() класса Socket возвращает объект OutputStream, здесь объект является ostream. Это отправная точка всего общения (программы). Здесь сокет связан с потоками. Потоки способствуют передаче данных.

DataOutputStream dos = new DataOutputStream (ostream); dos.writeBytes (message1);

OutputStream является абстрактным классом; он не может быть использован напрямую. В приведенном выше коде он связан с конкретным классом DataOutputStream. Метод writeBytes() объекта DataOutputStream принимает строковое сообщение и передает его в Socket. Теперь клиентский сокет отправляется на другой сокет на сервере. Когда работа закончится, закройте потоки и сокет. Он освобождает дескрипторы (ссылки), связанные с системными ресурсами.

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

  • Socket(“127.0.0.1”, 5000) выдает UnknownHostException
  • getOutputStream() генерирует IOException
  • writeBytes (message1) выдает IOException
  • Все методы close() выдают IOException
  • Серверная программа – WishesServer.java
import java.net.ServerSocket; import java.net.Socket; import java.io.InputStream; import java.io.DataInputStream; public class WishesServer < public static void main(String args[]) throws Exception < ServerSocket sersock = new ServerSocket(5000); System.out.println("server is ready"); // message to know the server is running Socket sock = sersock.accept(); InputStream istream = sock.getInputStream(); DataInputStream dstream = new DataInputStream(istream); String message2 = dstream.readLine(); System.out.println(message2); dstream .close(); istream.close(); sock.close(); sersock.close(); >>

ServerSocket sersock = новый ServerSocket (5000);

У сервера есть два задания: одно, как и ожидалось, должно связываться, а другое связывает соединение с номером порта 5000. Для связи он использует Socket, а для привязки – ServerSocket.

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

Socket sock = sersock.accept ();

accept() – это метод класса ServerSocket, используемый сервером для привязки соединения по номеру порта 5000, запрошенного клиентом.

InputStream istream = sock.getInputStream();

Метод getInputStream() объекта Socket возвращает объект InputStream, и это отправная точка серверной программы. Сервер использует входной поток при получении сообщения.

DataInputStream dstream = new DataInputStream (istream);

Поскольку InputStream является абстрактным классом, его нельзя использовать напрямую. Он связан с конкретным классом DataInputStream.

String message2 = dstream.readLine();

Метод readLine() объекта DataInputStream читает строку сообщения из сокета и возвращает ее. Это сообщение печатается на консоли.

Примечание. При компиляции этой программы вы получаете предупреждение из-за метода readLine() объекта DataInutStream; но программа выполняется. Чтобы избежать этого предупреждения, в следующей программе используется BufferedReader.

Выполнение клиентских и серверных программ

В одной системе, чтобы действовать как клиент и сервер, откройте два шDOS и обработайте одно как клиент, а другой – как сервер. Из одного приглашения DOS сначала запустите серверную программу, а из другого приглашения DOS запустите клиентскую программу. Вы получаете вывод при запросе сервера DOS.

Это приложение и следующее – только односторонняя связь, отправляющая или получающая. Но второй набор (после следующего) приложений является двусторонним, когда клиент и сервер могут отправлять и получать (оба). Для тестирования на выделенных серверах, можно обратиться сюда https://www.mixtelecom.ru/arenda-serverov.html

Для лучшего понимания вопрос-ответ из пакета java.lang.

Сколько существует типов внутренних классов?
Ответ: 4 типа.

Что такое файлы JAR?
Ответ: JAR-файл – это заархивированный файл, сжатый JVM.

Как преобразовать строку в форму типа данных?
Ответ: Преобразование строки в тип данных – байтовое, короткое, целое, длинное, плавающее, двойное, символьное и логическое.

Как преобразовать объект в строку?
Ответ: Объект в строку – toString()

Как сравнить два объекта?
Ответ: Сравнение объектов – hashCode() & equals()

Средняя оценка 3.1 / 5. Количество голосов: 16

Спасибо, помогите другим — напишите комментарий, добавьте информации к статье.

Или поделись статьей

Видим, что вы не нашли ответ на свой вопрос.

Помогите улучшить статью.

Напишите комментарий, что можно добавить к статье, какой информации не хватает.

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

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