Время на прочтение
11 мин
Количество просмотров 239K
Цель публикации показать начинающим Java программистам все этапы создания многопоточного сервера. Для полного понимания данной темы основная информация содержится в комментариях моего кода и в выводимых в консоли сообщениях для лучшего понимания что именно происходит и в какой именно последовательности.
В начале будет рассмотрено создание элементарного клиент-сервера, для усвоения базовых знаний, на основе которых будет строиться многопоточная архитектура.
Понятия.
— Потоки: для того чтобы не перепутать что именно подразумевается под потоком я буду использовать существующий в профессиональной литературе синоним — нить, чтобы не путать Stream и Thread, всё-таки более профессионально выражаться — нить, говоря про Thread.
— Сокеты(Sockets): данное понятие тоже не однозначно, поскольку в какой-то момент сервер выполняет — клиентские действия, а клиент — серверные. Поэтому я разделил понятие серверного сокета — (ServerSocket) и сокета (Socket) через который практически осуществляется общение, его будем называть сокет общения, чтобы было понятно о чём речь.
Кроме того сокетов общения создаётся по одному на каждом из обменивающихся данными приложении, поэтому сокет приложения которое имеет у себя объект - ServerSocket и первоначально открывает порт в ожидании подключения будем называть сокет общения на стороне сервера, а сокет который создаёт подключающееся к порту по известному адресу второе приложение будем называть сокетом общения на стороне клиента.
Спасибо за подсказку про Thread.sleep();!
Конечно в реальном коде Thread.sleep(); устанавливать не нужно — это моветон! В данной публикации я его использую только для того чтобы выполнение программы было нагляднее, что бы успевать разобраться в происходящем.
Так что тестируйте, изучайте и в своём коде никогда не используйте Thread.sleep();!
Оглавление:
1) Однопоточный элементарный сервер.
2) Клиент.
3) Многопоточный сервер – сам по себе этот сервер не участвует в общении напрямую, а лишь является фабрикой однонитевых делегатов(делегированных для ведения диалога с клиентами серверов) для общения с вновь подключившимися клиентами, которые закрываются после окончания общения с клиентом.
4) Имитация множественного обращения клиентов к серверу.
По многочисленным замечаниям выкладываю ссылку на исходники на GitHub:
(https://github.com/merceneryinbox/Clietn-Server_Step-by-step.git)
Итак, начнём с изучения структуры однопоточного сервер, который может принять только одного клиента для диалога. Код приводимый ниже необходимо запускать в своей 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();
}
}
}
- 2) Клиент.
Сервер запущен и находится в блокирующем ожидании 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();
}
}
}
- 3) Многопоточный сервер
А что если к серверу хочет подключиться ещё один клиент!? Ведь описанный выше сервер либо находится в ожидании подключения одного клиента, либо общается с ним до завершения соединения, что делать остальным клиентам? Для такого случая нужно создать фабрику которая будет создавать описанных выше серверов при подключении к сокету новых клиентов и не дожидаясь пока делегированный подсервер закончит диалог с клиентом откроет 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();
}
}
}
Запускайте, вносите изменения в код, только так на самом деле можно понять работу этой структуры.
Спасибо за внимание.
Архитектура Web-приложений
Архитектура современных приложений состоит из отдельных модулей, как показано на рисунке выше. Эти модули часто называют Frontend и Backend. Frontend – это модуль, который отвечает за юзер-интерфейс и логику, которые предоставляется приложением при использовании. Так, например когда мы заходим в соцсети через браузер, мы взаимодействуем именно с FrontEnd-модулем приложения. То, как отображаются наши посты в виде сторисов или карточек, сообщения и другие активности реализуются именно в FrontEnd-модуле. А все данные, которые мы видим, хранятся и обрабатываются в Backend или серверной части приложения. Эти модули обмениваются между собой посредством разных архитектурных стилей: REST, GRPC и форматов сообщений – JSON и XML.
В этой статье мы напишем примитивную серверную часть социальной сети с использованием Spring Boot, запустим свой сервер, рассмотрим разные типы HTTP запросов и их применение.
Необходимое требование к читателю: умение писать на Java и базовые знания Spring Framework. Данная статья познакомит вас со Spring Boot и даст базовые понятия данного фреймворка.
Инициализация проекта
Чтобы создать Spring Boot проект, перейдем на страницу https://start.spring.io/ и выберем необходимые зависимости: в нашем случае Spring Web. Чтобы запустить проект, необходима минимальная версия Java 17. Скачиваем проект и открываем в любом IDE (в моем случае – Intellij Idea)
Spring Web
– зависимость, которая предоставляет контейнер сервлетов Apache Tomcat (является дефолтным веб-сервером). Проще говоря, сервлеты – это классы, которые обрабатывают все входящие запросы.
Открываем проект и запускаем.
Мы видим, что проект запустился и готов обрабатывать запросы на порту 8080 – Tomcat started on port(s): 8080 (http)
.
Теперь создадим свой первый класс – GreetingController
. Controller-классы ответственны за обработку входящих запросов и возвращают ответ.
Чтобы сделать наш класс Controller
, достаточно прописать аннотацию @RestController
. @RequestMapping
указывает, по какому пути будет находиться определённый ресурс или выполняться логика.
package io.proglib;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/greet")
public class GreetingController {
@GetMapping
public String greet() {
return "Hello";
}
}
Перезапускаем проект, и сервер готов уже обрабатывать наши запросы.
Открываем браузер по адресу http://localhost:8080/greet
и получаем следующий вывод.
Если отправить запрос по адресу http://localhost:8080/
, мы получим ошибку, т. к. по этому пути не определены логика обработки запроса и ресурсы.
Request Params
При отправке запросов мы часто используем переменные в запросе, чтобы передавать дополнительную информацию или же делать запросы гибкими. Параметр в запросе передаётся в конце адреса (=url
) сервера и указывается после вопросительного знака (=?
).
Например, http://localhost:8080/greet?name=Alice
. Параметр запроса является = name
cо значением = Alice
.
Чтобы обрабатывать переменную запроса, используется аннотация @RequestParam
. Параметры запроса могут быть опциональными или же обязательными. @RequestParam("name")
означает следующее: взять ту переменную из запроса, название которого равно name
.
@RestController
@RequestMapping("/greet")
public class GreetingController {
@GetMapping
public String greet(@RequestParam("name") String name) {
return "Hello, " + name;
}
}
Вдобавок, запрос может содержать несколько параметров.
Например, http://localhost:8080/greet/full?name=John&surname=Smith
. Параметры выделяются знаком &
. В этом запросе два параметра: name=John
и surname=Smith
.
Чтобы обработать каждый параметр запроса, нужно пометить каждую переменную @RequestParam
.
package io.proglib;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/greet")
public class GreetingController {
@GetMapping
public String greet(@RequestParam("name") String name) {
return "Hello, " + name;
}
@GetMapping("/full")
public String fullGreeting(@RequestParam("name") String name,
@RequestParam("surname") String surname) {
return "Nice to meet you, " + name + " " + surname;
}
}
Path Variable
PathVariable
по применению похож на @Request Param
. @PathVariable
также является параметром запроса, но используются внутри адреса запроса. Например,
RequestParam
– http://localhost:8080/greet/full?name=John&surname=Smith
PathVariable
– http://localhost:8080/greet/John
. В этом случае John
является PathVariable.
В запросе можно указывать несколько PathVariable, как и в случае RequestParam
package io.proglib;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/greet")
public class GreetingController {
@GetMapping
public String greet(@RequestParam("name") String name) {
return "Hello, " + name;
}
@GetMapping("/full")
public String fullGreeting(@RequestParam("name") String name, @RequestParam("surname") String surname) {
return "Nice to meet you, " + name + " " + surname;
}
@GetMapping("/{name}")
public String greetWithPathVariable(@PathVariable("name") String name) {
return "Hello, " + name;
}
}
Чтобы протестировать, открываем браузер и переходим по адресам: http://localhost:8080/greet/John/Smith
и http://localhost:8080/greet/John
Запрос с двумя параметризованными PathVariable.
HTTP-методы
Когда мы говорим о запросах, мы также подразумеваем HTTP-метод, который используется при отправке этого запроса. Каждый запрос представляет собой некий HTTP-метод. Например, когда мы переходим в браузере по адресу http://localhost:8080/greet/John/Smith
, наш браузер отправляет GET-запрос на сервер.
Большая часть информационных систем обмениваются данными посредством HTTP-методов. Основными HTTP-методами являются – POST
, GET
, PUT
, DELETE
. Эти четыре запроса также называют CRUD-запросами.
- POST-метод – используется при создании новых ресурсов или данных. Например, когда мы загружаем новые посты в соцсетях, чаще всего используется POST-запросы. POST-запрос может иметь тело запроса.
- GET-метод – используется при получении данных. Например, при открытии любого веб-приложения, отправляется именно GET-запрос для получения данных и отображения их на странице. GET-запрос не имеет тела запроса.
- PUT-метод – используется для обновления данных, а также может иметь тело запроса, как и POST.
- DELETE-метод – используется для удаления данных.
Реализация основных методов
Давайте создадим сущности и реализуем методы, чтобы наш сервер принимал все четыре запроса. Для этого создадим сущности User
и Post
, и будем проводить операции над ними.
Для простоты User
имеет только два поля: username
и список постов posts
, а сущность Post
имеет поле description
и imageUrl
.
Сущность User
:
package io.proglib;
import java.util.ArrayList;
import java.util.List;
public class User {
private String username;
private List<Post> posts;
public User() {
posts = new ArrayList<>();
}
public User(String username, List<Post> posts) {
this.username = username;
this.posts = posts == null ? new ArrayList<>() : posts;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public List<Post> getPosts() {
return posts;
}
public void setPosts(List<Post> posts) {
this.posts = posts;
}
}
Сущность Post
:
package io.proglib;
public record Post(
String description,
String imageUrl
) {
}
Создаем новый класс контроллер – UserActivityController
, который будет обрабатывать наши запросы – POST, GET, PUT, DELETE.
Наш контроллер: UserActivityController.
Будем использовать список – List<User> users
в качестве локальной базы данных, где будем хранить все наши данные.
package io.proglib;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserActivityController {
private final static List<User> users = new ArrayList<>();
}
POST-запрос: добавление нового пользователя
Чтобы указать, что метод принимает POST-запросы используем аннотацию – @PostMapping
. Так как запрос имеет тело запроса, где мы передаем пользователя, нужно пометить переменную user
аннотацией @RequestBody
.
@PostMapping("")
public User addUser(@RequestBody User user) {
users.add(user);
return user;
}
GET-запрос: получение пользователей
@GetMapping("")
public List<User> getUsers() {
return users;
}
@GetMapping("/{username}")
public User getUserByUsername(@PathVariable("username") String username) {
return users.stream().filter(user -> user.getUsername().equals(username))
.findFirst().get();
}
@Getmapping("")
указывает, что методы обрабатывают GET-запросы. Значение, которое передаётся внутри аннотации, является частью url или адреса. Например, запрос http://localhost:8080/users/baggio
обработается методом getUserByUsername()
, а запрос http://localhost:8080/users/
обработается методом http://localhost:8080/users
.
PUT-запрос: обновление данных
@PutMapping("/{username}")
public Post update(@PathVariable("username") String username, @RequestBody Post post) {
users.stream().filter(user ->
user.getUsername().equals(username))
.findAny()
.ifPresent(user -> user.getPosts().add(post));
return post;
}
@PutMapping("/{username}")
указывает, что метод принимает PUT-запросы. В нашем примере в запросе мы передаем параметр запроса, а также тело запроса – post
. Метод принимает – username
, ищет юзера из списка с таким username
и добавляем новый пост к его списку постов.
Delete-запрос: удаление данных
@DeleteMapping("/{username}")
public String deleteUser(@PathVariable("username") String username) {
users.stream().filter(user ->
user.getUsername().equals(username))
.findAny()
.ifPresent(users::remove);
return "User with username: " + username + " has been deleted";
}
@DeleteMapping("/{username}")
– указывает, что метод принимает DELETE-запросы.
Данный метод получает параметр username
из запроса и удаляет из списка пользователя с таким username.
Запуск приложения и тестирование
Чтобы убедиться, что все работает, мы можем отправить каждый вид запроса и протестировать. Для этого нам необходим API-client, который может посылать запросы. В примерах я использую POSTMAN.
Post-запрос: создание нового пользователя.
Тело запроса отправляется в виде JSON.
GET-запрос: получение пользователей
PUT-запрос: обновление списка постов пользователя
DELETE-запрос: удаление пользователя по username
***
В этой статье мы рассмотрели архитектуру современных web-приложений, а также написали свою серверную часть приложения, получив поверхностные знания по Spring Boot
, HTTP запросы
и параметры запросов.
Ссылка на репозиторий
Исходный код можно найти по ссылке.
Материалы по теме
- ☕ Сертификаты и тренинги для Java-разработчика
- ☕🛣️ Дорожная карта Java-разработчика в 2023 году: путь с нуля до первой работы
Программирование на Java – перспективное направление у современных разработчиков. Согласно данным Google данный язык кодинга пользуется огромным спросом, несмотря на то, что был создан изначально весьма давно. А именно – в 1995 году. С тех пор Джава перетерпел много модификаций. Нынче это – один из самых перспективных языков программирования. Весьма прост в освоении, что значительно упрощает задачи, поставленные перед новичками.
Особенности Java
Google и другие поисковые системы помогут понять, что это за язык программирования. Он имеет ряд ключевых особенностей. А именно:
- относительно простой синтаксис;
- отличное комьюнити;
- множество документации (в том числе на русском языке);
- наличие ООП;
- собственный движок.
New programs создавать при помощи данного варианта способен даже начинающий программист.
Основное предназначение Java – это работа с Сетью. Идеально подходит для веб-программирования. Но и «обычные» утилиты посредством соответствующего семейства пишутся без существенных затруднений.
Сервер типа http – определение
При работе с сетью (new или old – не так важно) компьютеры подключаются к так называемым веб-серверам. Без них невозможна работа в интернете.
HTTP-сервер – это веб server. Имеет непосредственное отношение как к «железу» компьютера, так и к программному обеспечению:
- В качестве аппаратного устройства это – new машина, которая отвечает за хранение ресурсов того или иного сайта. Включает в себя доставку на устройство юзера через интернет-обозреватели и иные утилиты. Чаще всего подключается к интернету. Доступ предоставляется через доменные имена.
- Как ПО, согласно Google, веб-сервер представляет собой некое «приложение», совмещающее в себе функционал для контроля доступа web-пользователей к размещенным на сервере документов. HTTP Sever – часть программного обеспечения, понимающая URLs и HTTP-протоколы (они нужны для просмотра и отображения страничек в Сети).
Google указывает на то, что, когда браузер нуждается в каком-то файле, помещенном на веб-сервере, происходит запрос посредством http. Когда запрос доходит до нужного «железа», соответствующий сервер (программное обеспечение) осуществляет обратную передачу через упомянутый ранее «канал».
Виды серверов
Если разработчик на Джаве решил создать new servers, важно понимать – для опубликования сайта требуется подобрать один из нескольких видов оных. Упомянутый элемент бывает:
- Статическим (static void main). Носит название стека. Включает в себя компьютер с сервером HTTP. Последний будет посылать new файлы в интернет-обозреватель без каких-либо корректировок.
- Динамическим. Включает в себя статическую «модель» и дополнительное ПО. Чаще всего – базы данные или серверные приложения. Последние будут вносить изменения в документы перед тем, как отправить их в обозреватель Сети.
За счет new servers можно отображать страницы в браузерах. Итоговый результат удобен и понятен пользователям без навыков в сфере программирования. Для своей работы, согласно Google, рассматриваемый элемент может задействовать шаблоны информации из БД Пример – Википедия. Это – не полноценный сайт, а HTML-шаблон. За счет соответствующего приема удается значительно ускорить сопровождение web-софта.
Все наготове – встроенные возможности Java
При помощи Google и иных поисковиков можно достаточно быстро разобраться в упомянутом элементе, задействованном при программировании. Но не каждый пользователь готов заниматься созданием полноценного сервисного контента.
Для Джава поставленная задача не выступает в качестве тривиальной. Язык программирования содержит встроенные возможности, посредством которых new http создается без существенных затруднений. Всего 100 строчек кода – и перед разработчиком окажется весьма неплохой вариант, поддерживающий обработку запросов и иные HTTP-команд.
HTTPServer
Серверные возможности в Джаве предоставляются через SDK. Они имеют следующие особенности:
- название – HttpServer;
- пакетный класс – com.sun.net;
- запись: httpServer server = httpServer.create (new InetSocketAddress(“localhost”, 8001), 0));.
Приведенный пример – это создание экземпляра в пределах локального узла. Номер порта – 8001. Аргумент 0 здесь выступает для организации так называемой обратной регистрации.
Как выполняется запрос – очередность
Если рассматриваемый элемент работает исправно, он будет отправлять и получать разнообразные команды-запросы. В Google по этому поводу очень много информации. Новички в ней рискуют запутаться.
Чтобы понимать принципы работы серверных возможностей Джавы, стоит уяснить, какой очередности подчиняется обработка посланного запроса:
- Клиент формирует запрос. Происходит его отправка.
- Ставится очередь операционной системы.
- Происходит передача на сервер для дальнейшей обработки.
- Одновременные запросы ставятся в очередь. Их количество определяется ОС автоматически.
Важно: предложенный пример не требует отправлять в очередь никаких «команд», так как имеет значение аргумента, равное нулю.
О коде
Вот пример, который поможет лучше разобрать в изучаемой сфере даже без Google:
Здесь происходит создание контекста test. Он выступает корнем контекста утилиты. Второй параметр – экземпляр так называемого обработчика. Он будет работать с HTTP-командами.
Теперь допускается применение потокового пула. В приведенном примере их 10 штук:
New Thread PoolExecutor ThreadPoolExecutor = (ThreadPoolExecutor)Executors.newFixedThreadPool(10);
Далее new server требует запуска. Операция осуществляется путем задействования кода: server.start();.
Handler
А вот интерфейс согласно данным Google, использующий метод handle():
Внимание: в приведенном примере вышедший за рамки изображения код имеет вид private void (может быть и public static void) handleResponse(HttpExchange httpExchange, String requestParamValue) throws IOException {
OutputStream outputStream = httpExchange.getResponseBody();
StringBuilder htmlBuilder = new StringBuilder();
Кодификация обрабатывает запрос, затем отправляет ответ непосредственно клиенту. Обработка осуществляется через класс HttpExchange.
Запрос GET
Об обработке запроса Get необходимо знать следующее:
- написавшие его будут использовать метод HandleGETRequest;
- далее происходит вызов getRequestURL(), который принадлежит классу HttpExchange.
Несмотря на то, что это – минимум, обрабатывающий единичный запрос, он поможет справиться с самыми разными задачами.
Работа с ответом
После того, как произошел поиск ответа, его нужно направить клиенту. Делается это через handleResponse(). Пользователь получит выходной поток через обращение к методу gerResponseBody(). Чуть позже удастся записать информацию из HTML в выходные потоки.
Response header – это крайне важный момент. Если он будет упущен, в обозревателе Сети юзер увидит ошибку ERR_Empty_Response. В случае, когда все хорошо, браузер покажет тот или иной ответ.
Близкое знакомство – собственный чат через WebSoket и SpringBoot на Java
Сегодня трудно представить юзера, который не умел бы работать в интернете. Здесь не только отдыхают, но и отправляют сообщения, трудятся, создают что-то полезное. Коммуникации помогает поддерживать так называемый чат. И это – элементарный серверный пример.
В Google можно отыскать немало new идей относительно того, как создать собственный chat. При определенной сноровке соответствующий ресурс будет действительно уникальным: со смайликами, эмодзи, анимацией и другими элементами.
Для написания подобного контента в Java можно использовать:
- WebSoket;
- SpringBoot.
При помощи соответствующих элементов даже новичок сможет без труда разобраться с поставленной задачей. Ему не придется долго изучать Google, а также «непонятные» элементы кода (типа ioexception e, public void run, override public и так далее – на первых порах в них можно запутаться).
Определение WebSoket
WebSoket – это протокол, при помощи которого осуществляется установка двусторонней связи клиент-сервер. Переключение, как говорит Google, происходит после специального http-запроса. Его формирует и отправляет клиент: Upgrade: websocket.
При поддержке вебсокетов будет получен ответ «Yes». Далее произойдет общение через new протоколы WebSocket. С HTTP оный не имеет ничего общего.
Как создать приложение
Для того, чтобы сделать new application, можно прибегнуть к работе с сайтом-инициализатором. В этом случае алгоритм действий будет следующим:
- Зайти на страницу.
- Выбрать в списке Spring Boot 2.
- Указать имя группы и артефакта проекта.
- Активировать зависимость «вебсокета».
- Провести генерацию new project.
Далее предстоит разархивировать проект, а также сделать import java в редактор. Именем утилиты будет – Maven. Дополнительно необходимо создать пакеты config, controller и model.
Настройка
Теперь предстоит работать с public class, а также с такими элементами как static final int и private static. Ведь самое простое позади – далее требуется провести настройку «вебсокета».
Начинается процесс с конечной точки и брокера сообщений. Проводится операция в config. Класс конфигурации будет иметь следующий вид:
Здесь:
- аннотация @configuration – устанавливается обязательно в классе конфигурации Spring;
- аннотация @EnableWebSocketMessageBroker – активирует new WebsokcketServer;
- метод registerStompEndpoints() – отвечает за регистрацию конечной точки, которую клиенты задействуют для подключения к серверу;
- configMessageBroker() – настройка брокера для отправки сообщений между клиентами.
В рассматриваемом примере задействован встроенный брокер. Это самый простой вариант.
Модели сообщений
Следующие исходники – это настройка моделей сообщений. Создается пакет model, в котором после размещается класс ChatMessage:
Соответствующий фрагмент кода еще не позволит отправлять текст друг другу. Для реализации поставленной задачи предстоит выполнить иные действия.
Контроллер сообщений
Google поможет разобраться в том, как создавать собственные чаты. Чтобы в них можно было отправлять сообщения и получать оные, стоит:
- сделать пакет controller;
- разместить внутри класс chatController;
- внутри ЧатКонтроллер присутствуют методы, которые отвечают за доставку сообщений от одного пользователя и трансляцию всем остальным.
Для настройки контроллера используется следующая кодификация:
События и Front-End
Немаловажно настроить события подключение/отключение. Это необходимо для передачи сообщений на всеобщее обозрение:
Статистика – тоже немаловажный нюанс. If юзер хочет получить полноценный чат, ему предстоит выйти за пределы Джавы.
Чтобы справиться с поставленной задачей требуется:
- сделать папку static;
- расположить ее по пути scr/main/resources.
Выглядеть это будет так:
HTML и скрипты
В Google также говорится о том, что для работы полноценного чата требуется создать HTML-файл и JavaScript.
В первом случае используется new запись:
Скрипт Джавы требуется для соединения с итоговой точкой, а также отправки/получения сообщений. Он будет иметь имя main.js:
Стилизация
Исходники CSS можно создавать самостоятельно или подключать уже готовые варианты. Данная «опция» отвечает за внешний вид программы.
Здесь можно найти исходники соответствующего кода (пункт 7).
Запуск
Все, что теперь остается – это проверить Spring через Boot-файл с функцией main в корне иерархии имеющихся папок.
После запуска веб-сервера требуется перейти по адресу и пользоваться созданным контентом.
Для того, чтобы лучше разбираться в соответствующей сфере и термины int port, close и иные составляющие Джавы не были чем-то непонятным, стоит закончить специализированные курсы. Они без Google помогут освоить Java, а также всего его тонкости.
How To Make A Java-WebServer
Tutorial made at 10. June 2021
» Table of contents
- First Words
- What you need?
- Project Structure
- First class, main method and other things
- The mystical part of creating a server
- How to handle the request
- How to write data to the user
- Other things
- The code
- Contact
» First Words
Hey,
If you want to use this code in your project, I would like to mention that this is no Professional Code and there are many better ways to implement HTTP for Java, like Spring.
This code is for self learning and not for using, because I don’t think that this is much extendable or good usable code for big projects or companies.
This code is for new users of the Java-Language that want to learn how the implementation of HTTP in Java works, to maybe use it for creating better and bigger frameworks.
~ Marius
» What you need?
The nice thing at Java is that you need: nothing!
You only need a working JDK.
Info: I test this on JDK 14.0.2.
The tutorial might not work on newer or older versions of Java, but it should work
And it would be good if you have an IDE like IntelliJ IDEA, Eclipse, Apache NetBeans or anything else that adds help for code editing and smart suggestions to your life (I would say)
Later I use a JSON library to validate raw json strings. You can find the library here
» Structure
I think the best way to structure the project is that:
- Project Dir
- www
- index.html
- 404.html
- […]
- WebServer.java
If you have another structure you want to use: Do! (but I’m using this one…)
» The first class, main method and other things
So as you seen in the Structure, I like to put my web server code in a class named WebServer.java
, soothers can easily see, what the class does.
Then you have to create the main
method. Your class should look like this:
package some.nice.name; public class WebServer { public static void main(String[] args) { } }
Now we specify some basic fields above the main()
method.
private static final String REGEX_URL_SPLIT = "/"; private static final int PORT = 8080; private static final boolean verbose = true;
The
REGEX_URL_SPLIT
we use later for splitting the request path, so we get all text of the request path as one array
The
PORT
is, as it says, the port we want our server to listen on. If you don’t want to specify the port everytime you connect to the server, you should set it to80
.Warning: If you are running any other service like
nginx
orApache HTTP
, this could come to conflicts because they want also to use this port. Then please choose another port for any of these services or for this server
The
verbose
is used to specify if any extra logging should be made.
As next step you can implement java.lang.Runnable
in your WebServer
class and implement the run()
method
Now you create a local final field with type of java.net.Socket
with the name socket
. Then you create a constructor where you require this Socket as parameter and give it back to the local final field.
Your code should look like this now:
package some.nice.name; public class WebServer implements Runnable { private static final String REGEX_URL_SPLIT = "/"; private static final int PORT = 8080; private static final boolean verbose = true; private final Socket socket; public WebServer(Socket socket) { this.socket = socket; } public static void main(String[] args) { } @Override public void run() {} }
» The mystical part of creating a server
So go back to the main()
method. Now create a try-catch block with a IOException
-catch-block
In the try-block, you now create an instance of a java.net.ServerSocket
:
ServerSocket serverSocket = new ServerSocket(WebServer.PORT);
and you can optionally add a logging that the server is online now:
System.out.println("Server started.nListening for connections on port : " + PORT + " ...n");
Then we need an infinite-loop.
My ways to create infinite loops are these:
There are also more ways, but these I use
Now create an instance of the WebServer
class with your ServerSocket as parameter:
WebServer server = new WebServer(serverSocket.accept());
Then add an optional logging and start the web server:
if (verbose) { System.out.println("Connection opened. (" + new Date() + ")"); } new Thread(server).start();
The main()
method should now look like this:
try { ServerSocket serverSocket = new ServerSocket(WebServer.PORT); System.out.println("Server started.nListening for connections on port : " + PORT + " ...n"); while (true) { WebServer server = new WebServer(serverSocket.accept()); if (verbose) { System.out.println("Connection opened. (" + new Date() + ")"); } new Thread(server).start(); } } catch (IOException e) { System.err.println("Server Connection error : " + e.getMessage()); }
» How to handle the request?
Through the fact, that we implemented the Runnable
-Interface in our class, we can use the run()
method to interact with the user.
In the run
method you can now create a try-catch-finally-block that is looking like this:
try (BufferedReader requestReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter headerWriter = new PrintWriter(socket.getOutputStream()); BufferedOutputStream contentWriter = new BufferedOutputStream(socket.getOutputStream())) { } catch (IOException exception) { System.err.println("Server error : " + exception); } finally { if (verbose) { System.out.println("Connection closed.n"); } }
You maybe wonder about the syntax I use here. It is called try-with-resources Statement
.
I don’t want to explain it here but if you want to know more about that, read the documentation that is linked here
An official documentation page of this you can find here
But I want to explain the arguments in the try
-block to you:
requestReader
==> Every time you call the server, a request will be created where developers read things like your IP, request headers, auth states, and the path you query fromheaderWriter
==> As every request, also every response has headers. There are things like the status of you request, the type of the response (html/json/etc) and other cool thingscontentWriter
==> With the content writer you can write content to the user that will be displayed in their Web Browser, Terminal, or wherever they call this server
Now we can check the request and read the HTTP Method and the requested path from there:
StringTokenizer parse = new StringTokenizer(requestReader.readLine()); String method = parse.nextToken().toUpperCase(); String requested = parse.nextToken().toLowerCase(); if (!method.equals("GET")) { if (verbose) { System.out.println("501 Not implemented : " + method + " method."); } sendJson(headerWriter, contentWriter, 501, "{"error":"Method not implemented. Please use GET instead"}"); } else { String[] urlSplit = requested.split(WebServer.REGEX_URL_SPLIT); }
And you might have wondered about the sendJson
method. We will create this soon in the next section
But what are we doing in the code?
Basically we read the request and parse it into a StringTokenizer
. Then we read the method
and the requested path (requested
) from there.
In the if
we check if the user calls the server with HTTP GET
. If that isn’t the case, we send the user a deny-message in json. Otherwise, we split the requested path at REGEX_URL_SPLIT
(/
) and put it into an array.
After the array, we can write now the code where we send the data to the user. But wait for that until the next section!
After this code the class should look like this:
public class WebServer implements Runnable { static final String REGEX_URL_SPLIT = "/"; static final int PORT = 8080; static final boolean verbose = true; private final Socket socket; public WebServer(Socket socket) { this.socket = socket; } public static void main(String[] args) { try { ServerSocket serverSocket = new ServerSocket(WebServer.PORT); System.out.println("Server started.nListening for connections on port : " + PORT + " ...n"); while (true) { WebServer server = new WebServer(serverSocket.accept()); if (verbose) { System.out.println("Connection opened. (" + new Date() + ")"); } new Thread(server).start(); } } catch (IOException e) { System.err.println("Server Connection error : " + e.getMessage()); } } @Override public void run() { try (BufferedReader requestReader = new BufferedReader(new InputStreamReader(socket.getInputStream())); PrintWriter headerWriter = new PrintWriter(socket.getOutputStream()); BufferedOutputStream contentWriter = new BufferedOutputStream(socket.getOutputStream())) { StringTokenizer parse = new StringTokenizer(requestReader.readLine()); String method = parse.nextToken().toUpperCase(); String requested = parse.nextToken().toLowerCase(); if (!method.equals("GET")) { if (verbose) { System.out.println("501 Not implemented : " + method + " method."); } sendJson(headerWriter, contentWriter, 501, "{"error":"Method not implemented. Please use GET instead"}"); } else { String[] urlSplit = requested.split(WebServer.REGEX_URL_SPLIT); } } catch (IOException exception) { System.err.println("Server error : " + exception); } finally { if (verbose) { System.out.println("Connection closed.n"); } } } }
» How to write data to the user?
To write data to the user, we have our headerWriter
and our contentWriter
.
HTTP headers let the client, and the server pass additional information with an HTTP request or response. A complete documentation for you can find here
Good to know
The data we send over the
contentWriter
are bytes, not plain strings.You can get the bytes of a string by using:
String randomString = "test"; byte[] randomStringBytes = randomString.getBytes();or you can specify also a Charset (like UTF-8):
byte[] randomStringBytes = randomString.getBytes(StandardCharsets.UTF_8);
So, what we need in our response?
- Headers
- Format of the response (like
HTTP/1.1 200 OK
) - The name of the server (
Server: yourserver
) - The date of the response (
Date:
and add behind this an+ new Date()
) - The content type of our response (
Content-Type: text/html
orContent-Type: application/json
) - The content length of the response data we want to send (
Content-Length: 1000
)
- Format of the response (like
- Data
So lets build a method for handle that all!
I will a method called write
for that. As method parameters we need these:
PrintWriter headerWriter BufferedOutputStream contentWriter, int statusCode, String contentType, byte[] response, int responseLength
The statusCode
is the status the request got at your server.
So as example: If we call the index.html
page on a web server, you should EVERYTIME get a 200 status, because every website should have an index page. BUT if the page doesn’t have this requested page, there will be sent a 404 status code back, that means that the page does not exist.
If you know that you will only send HTML or JSON you can remove this contentType
and later add the response type where the contentType
should be used.
In the response
we have the data that will be sent to the user. No string, raw bytes.
static void write(PrintWriter headerWriter, BufferedOutputStream contentWriter, int statusCode, String contentType, byte[] response, int responseLength) throws IOException { HttpStatusCode httpStatusCode = HttpStatusCode.getByResult(statusCode); headerWriter.println(String.format("HTTP/1.1 %d %s", statusCode, httpStatusCode == null ? "Unknown" : httpStatusCode.name())); headerWriter.println("Server: HTTP Server : 1.0"); headerWriter.println("Date: " + new Date()); headerWriter.println("Content-type: " + contentType); headerWriter.println("Content-length: " + responseLength); headerWriter.println(); headerWriter.flush(); contentWriter.write(response, 0, responseLength); contentWriter.flush(); }
And as you might see here, I had implemented a HttpStatusCode
enum. I will come to this soon.
So what we are doing is that:
- First send the headers, we defined here
- Then we write the data and the length of the data through the BufferedOutputStream
And that was the magic behind Java WebServers.
The HttpStatusCode enum
So to implement this, you basically only need to copy the HttpStatusCode.java
file of the src/
directory in this repository. Then put this in your code and that was it.
In there are — kinda — all Http Status Codes, with their name and status code.
If you know the status code but not the name, you can easily check for it:
String status = "unknown"; int statusCode = 200; HttpStatusCode httpStatusCode = HttpStatusCode.getByResult(statusCode); if (httpStatusCode != null) { status = httpStatusCode.name(); }
Helper methods are nice! Don’t say anything else…
The write()
method is good but what is, if we don’t want to specify text/html
as content type, everytime we call the method. We could make a method that do this automatically for us.
static void sendHtml(PrintWriter headerWriter, BufferedOutputStream contentWriter, int statusCode, String content) throws IOException { write(headerWriter, contentWriter, statusCode, "text/html", content.getBytes(StandardCharsets.UTF_8), content.length()); }
The method is very similar to the first one, but has nice changes. We don’t have to specify a content type, or the content length anymore. Also, we have now a string as data. The java compiler does now the work with transforming the string into bytes and the calculating of the length for you. It’s easy! And you can do it for everything you want
» Other things
JSON validation and sending
As I mentioned earlier I will use the org.json.json
library for this project. Feel free to use some library like Jackson
or json-simple
. If you want to download the library I use, look here
Simple Introduction to
org.json.json
JSONObject json = new JSONObject("{"test": "nice", "message": ["test"]}"); String test = json.getString("test"); JSONArray messages = json.getJSONArray("message"); List<String> messageList = messages.toList().stream().filter(String.class::isInstance).map(String.class::cast).collect(Collectors.toList()); json.put("name", "Marius"); String rawJSON = json.toString(4);Java Streams… WTF?
if you aren’t familiar with Java Streams, that code might look very obvious to you.
Basically, because themessages.toList()
method returns objects, I check if the given object is a String and filter it out. Then I cast all other remaining string objects to strings
The nice thing is, you’ll get an exception if you create an instance of a JSON Object, and the parsed json isn’t valid.
So we can easily see, if there is valid json.
public boolean isValidJson(String raw) [ try { new JSONObject(raw); return true; } catch(JSONException e) { return false; } ]
With this knowledge, we can create a method that sends json to the user:
static void sendJson(PrintWriter headerWriter, BufferedOutputStream contentWriter, int statusCode, String json) throws IOException { try { new JSONObject(json); // the code will cancel here if the json is not valid write(headerWriter, contentWriter, statusCode, "application/json", json.getBytes(StandardCharsets.UTF_8), json.length()); } catch(JSONException e) { throw new IOException(e.getMessage()); } }
» The code
You can find the code of the whole project in the src/
directory of the repository. Feel free to copy it
» Contact
You can write me on Discord. My tag is Marius#0686
If you want to create a translated version of this, please link my repository as originally one
Программирование, HTML, Сетевые технологии, JAVA, Серверное администрирование
Рекомендация: подборка платных и бесплатных курсов Python — https://katalog-kursov.ru/
Лучший способ понять устройство и принцип работы чего-либо – сделать это что-то самому.
Заинтересовавшись однажды сетевыми технологиями и, среди прочего, серверами, я пришёл к мысли, что было бы неплохо написать одн такой сервер самому, используя Java.
Однако большая часть Java-литературы, что попадалась мне на глаза, если и объясняла нечто по теме, то лишь в самых общих чертах, не идя далее обмена текстовыми сообщениями между клиент-серверными приложениями. В интернете подобная информация встречалась не намного чаще, в основном в виде крупиц знаний на обучающийх сайтах или отрывочных сведений от пользователей разнообразных форумов.
Таким образом, собрав эти знания воедино и написав таки удобоваримое серверное прилоение, спобоное обрабатывать браузерные запросы, я решил сделать выжимку из подобных знаний и поэтапно описать процесс создания простейшего web-сервера на Java.
Надеюсь, в этой статье найдутся полезные знания для начинающих Java-программистов и других людей, изучающих связанные технологии.
Итак, поскольку программа предполагает простейшие функции сервера, она будет состоять из одного класса без графического интерфейса. Этот класс (Server) наследует поток и имеет одно поле – сокет:
class Server extends Thread {
Socket s;
}
В главном методе создаём новый ServerSocket и задаём для него порт (в данном случае использован порт 1025) и в бесконечном цикле ожидаем соединения с клиентом. При наличии соединения мы создаем новый поток, передавая ему соответствующий сокет. В случае неудачи выводим сообщение об ошибке:
try {
ServerSocket server = new ServerSocket(1025);
while(true) {
new Server(server.accept());
}
}
catch(Exception e) {
System.out.println("Error: " + e);
}
Для того, чтобы сделать возможным создание нового потока подобным образом мы, естественно, должны описать для него соответствующий конструктор. В конструкторе мы маркируем поток как демон и здесь же запускаем:
public Server(Socket socket) {
this.socket = socket;
setDaemon(true);
start();
}
Далее описываем функционал потока в методе run(): в первую очередь, создаем из сокета поток исходящих и входящих данных:
public void run() {
try {
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream();
Для считывания входящих данных мы будем применять буфер, представляющий из себя байт-массив определёной размерности. Создание подобного буфера не является обязательным, т.к. возможно принимать входящие данные и другими способами, не лимитируя количество принимаемой информации – но для простейшего web-сервера, описанного здесь, вполне достаточно 64 кбайт для получения необходимых данных от клиента.
Помимо байт-массива, мы также создаем int переменную, которая будет хранить в себе количество реально принятых буфером байт. Это необходимо для того, чтобы в последствии особыми образом создать строку клиентского запроса из полученных данных:
byte [] buffer = new byte[64*1024];
int bytes = input.read(buffer);
String request = new String(buf, 0, r);
В строке request будет содержаться http-запрос от клиента. Среди прочей информации, содержащейся в данном запросе, нас в данный момент интересует адрес запрашиваемого файла и его расширение.
Не вдаваясь в подробности структуры http-запроса скажу лишь, что нужная нам информация будет находиться в первой строчке данного запроса приблизительно в таком виде:
GET /index.html HTTP/1.1
В данном примере запрашивается страница на сервере по адресу
/index.html
Страница с таким адресом выдается на большинстве серверов по умолчанию. Наша задача – с помощью собственноручно написанного метода getPath() вычленить этот адрес из запроса. Существует множество вариантов подобного метода и здесь их приводить нет смысла. Ключевой момент здесь состоит в том, что получив путь до нужного файла и записав его в строковую переменную path, мы можем попробовать создать на основе этих данных файл и, в случае успеха, вернуть этот файл, а в случае неудачи – вернуть специфическое сообщение об ошибке:
String path = getPath(request);
File file = new File(path);
Проверяем, является ли данный файл дирекорией. Если такой файл существует и является директорией, то мы возвращаем упомянутый выше файл по умолчанию – index.html:
boolean exists = !file.exists();
if(!exists)
if(file.isDirectory())
if(path.lastIndexOf(""+File.separator) == path.length()-1) {
path = path + "index.html";
}
else {
path = path + File.separator + "index.html";
file = new File(path);
exists = !file.exists();
}
Если файла по указанному адресу не существует, то мы создаем http-ответ в строке response с указанием того, что файл не найден, добавляя в нужном порядке следующие заголовки:
if(exists){
String response = "HTTP/1.1 404 Not Foundn";
response +="Date: " + new Date() + "n";
response +="Content-Type: text/plainn";
response +="Connection: closen";
response +="Server: Servern";
response +="Pragma: no-cachenn";
response += "File " + path + " Not Found!";
После формирования строки response мы отправляем их клиенту и закрываем соединение:
output.write(response.getBytes());
socket.close();
return;
}
Если же файл существует, то перед формированием ответа необходимо выяснить его расширение и, следовательно, MIME-тип. Для начала мы выясним индекс точки, стоящей перед расширением файла и сохраним его в int-переменную.
int ex = path.lastIndexOf(".");
Затем вычленим расширение файла, стоящее после неё. Список возможным MIME-типов можно расширить, но в данном случае буде использовать всего по одной из форм 3-х форматов: html, jpeg и gif. По умолчанию будем использовать MIME-тип для текста:
String mimeType = “text/plain”;
if(ex > 0) {
String format = path.substring(r);
if(format.equalsIgnoreCase(".html"))
mimeType = "text/html";
else if(format.equalsIgnoreCase(".jpeg"))
mimeType = "image/jpeg";
else if(format.equalsIgnoreCase(".gif"))
mimeType = "image/gif";
Формируем ответ клиенту:
String response = "HTTP/1.1 200 OKn";
response += "Last-Modified: " + new Date(file.lastModified())) + "n";
response += "Content-Length: " + file.length() + "n";
response += "Content-Type: " + mimeType + "n";
response +="Connection: closen";
В конце заголовков обязательно должно быть две пустые строки, иначе ответ не будет корректны образом обработан клиентом.
response += "Server: Servernn";
output.write(response.getBytes());
Для отправки самого файла можно использовать следующую конструкцию:
FileInputStream fis = new FileInputStream(path);
int write = 1;
while(write > 0) {
write = fis.read(buffer);
if(write > 0) output.write(buffer, 0, write);
}
fis.close();
socket.close();
}
Наконец, завершаем блок try-catch, указанный в начале.
catch(Exception e) {
e.printStackTrace();
} }
Поскольку, как уже было сказано, данная реализация web-сервера является одной из простейших, она может быть легко доработана путём добавления графического интерфейса пользователя, количества поддерживаемых расширений, ограничителя подключений и т.п. Одним словом – простор для творчества остаётся огромным. Дерзайте.