Перейти к содержимому

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

  • автор:

Современная серверная разработка на языке Java: 2. Архитектура серверного приложения

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

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

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

Spring Boot

Spring – это стандартный framework для создания backend-сервисов на языке Java. Spring Boot — это расширение Spring, которое позволяет быстро подключать типовые функции приложения (web-сервер, подключение к базе данных, безопасность и т.д.) при помощи «стартеров».

Подключите библиотеки Spring Boot в проект, отредактировав файл build.gradle:

plugins < id 'java' // Плагины для Spring Boot проектов id "org.springframework.boot" version "2.6.7" id "io.spring.dependency-management" version "1.0.11.RELEASE" >group 'org.example' version '1.0-SNAPSHOT' repositories < mavenCentral() >dependencies < // Стартер для web-сервиса implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' >test

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

Затем необходимо задекларировать и стартовать Spring Boot приложение в файле Main.java:

package org.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; // Декларируем Spring Boot приложение @SpringBootApplication public class Main < public static void main(String[] args) < // Стартуем приложение SpringApplication.run(Main.class, args); >> 

При старте приложения мы увидим в журнале сообщений на консоли, как стартует web-сервер Tomcat:

Обратите внимание, приложение не завершается, как это было раньше. Оно работает как сервис — web-сервер ожидает запросов на порту 8080.

DTO – объекты для передачи данных

В большинстве запросов к сервисам передаются какие-то данные. Например, если мы хотим создать пользователя, то скорее всего нам надо передать в запросе на его создание хотя бы имя. Стандартом передачи данных в REST запросах являются Data Transfer Objects (DTO).

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

Добавьте новый java-package, где будут размещаться DTO-классы, назовите его web.dto:

Добавьте новый класс CreateUserDto в пакет web.dto:

package org.example.web.dto; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; /** * Запрос на создание пользователя */ /** Чтобы воспользоваться DTO-классом необходим механизм десериализации - превращения JSON-строки вида в экземпляр класса CreateUserDto. Класс Builder реализует шаблон Строитель, который принято использовать в классах моделей и DTO */ @JsonDeserialize(builder = CreateUserDto.Builder.class) public class CreateUserDto < /** Имя пользователя */ private final String name; public static Builder builder() < return new Builder(); >/** * Конструктор сделан закрытым, потому что объекты этого класса * надо порождать таким образом: * dto = CreateUserDto.builder().setName("John Doe").build() */ private CreateUserDto(Builder builder) < this.name = builder.name; >public String getName() < return name; >/** * Используется при выводе сообщений на экран */ @Override public String toString() < return "'; > /** * Подсказываем механизму десериализации, * что методы установки полей начинаются с set */ @JsonPOJOBuilder(withPrefix = "set") public static class Builder < private String name; public Builder setName(String name) < this.name = name; return this; >public CreateUserDto build() < return new CreateUserDto(this); >> > 

REST-контроллеры — обработчики запросов

Стандартом для написания web-сервисов является REST-архитектура. Она крайне проста – сервис получает http-запросы, обрабатывает их и отправляет ответы. Занимаются этим REST-контроллеры приложения.

На данном этапе у нас есть работающий web-сервер и описано, как будут передаваться данные для создания пользователя. Пришла пора написать первый REST-контроллер, который будет обрабатывать запросы на создание новых пользователей.

Создайте класс WebController в java-пакете web:

package org.example.web; import org.example.web.dto.CreateUserDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; /** * Обработчик web-запросов */ @RestController public class WebController < /** * Средство для вывода сообщений на экран */ private static final Logger LOGGER = LoggerFactory.getLogger(WebController.class); /** * Обработчик запросов на создание пользователя * @param createUserDto запрос на создание пользователя */ @PostMapping("/users") public void createUser(@RequestBody CreateUserDto createUserDto) < /** * Получили запрос на создание пользователя, * пока можем только залогировать этот факт */ LOGGER.info("Create user request received: <>", createUserDto); > > 

Создайте в проекте папку http-test, а в ней создайте файл test.http:

### Запрос на создание пользователя POST http://localhost:8080/users Content-type: application/json

Запустите приложение, а затем тестовый POST-запрос в файле test.http

В результате запуска запроса вы должны увидеть ответ Response code: 200 – это значит, что запрос выполнился успешно. В консоли приложения будет выведено сообщение: Create user request received: – это значит, что запрос «дошел» до приложения. Но на данном этапе мы пока не можем ничего сделать – нам негде хранить пользователей.

Валидация данных

Наше приложение уже умеет получать запросы на создание пользователей. Но, прежде чем приступить к обработке запроса, было бы неплохо проверить пришедшие данные на корректность. Предположим, мы хотим, чтобы в имени пользователя было от 5 до 25 символов и присутствовали только буквы латинского алфавита.

Добавьте стартер валидации в файл build.gradle:

plugins < id 'java' // Плагины для Spring Boot проектов id "org.springframework.boot" version "2.6.7" id "io.spring.dependency-management" version "1.0.11.RELEASE" >group 'org.example' version '1.0-SNAPSHOT' repositories < mavenCentral() >dependencies < // Стартер для web-сервиса implementation 'org.springframework.boot:spring-boot-starter-web' // Стартер для валидации implementation 'org.springframework.boot:spring-boot-starter-validation' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' >test

Добавьте правила валидации на поле name в классе CreateUserDto:

package org.example.web.dto; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotNull; import javax.validation.constraints.Pattern; /* Запрос на создание пользователя */ /** Чтобы воспользоваться DTO-классом необходим механизм десериализации - превращения JSON-строки вида в экземпляр класса CreateUserDto. Класс Builder реализует шаблон Строитель, который принято использовать в классах моделей и DTO */ @JsonDeserialize(builder = CreateUserDto.Builder.class) public class CreateUserDto < /** * Имя пользователя * Ключ "name" - обязательный * Длина - от 5 до 25 символов * Может содержать только символы латинского алфавита */ @NotNull(message = "Key 'name' is mandatory") @Length(min = 5, max = 25, message = "Name length must be from 5 to 25") @Pattern(regexp = "^[a-zA-Z]+$", message = "Name must contain only letters a-z and A-Z") private final String name; public static Builder builder() < return new Builder(); >/** * Конструктор сделан закрытым, потому что объекты этого класса * надо порождать таким образом: * dto = CreateUserDto.builder().setName("John Doe").build() */ private CreateUserDto(Builder builder) < this.name = builder.name; >public String getName() < return name; >/** * Используется при выводе сообщений на экран */ @Override public String toString() < return "'; > /** * Подсказываем механизму десериализации, * что методы установки полей начинаются с set */ @JsonPOJOBuilder(withPrefix = "set") public static class Builder < private String name; public Builder setName(String name) < this.name = name; return this; >public CreateUserDto build() < return new CreateUserDto(this); >> > 

Добавьте проверку входного параметра метода createUser в классе WebController:

package org.example.web; import org.example.web.dto.CreateUserDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; /** * Обработчик web-запросов */ @RestController public class WebController < /** * Средство для вывода сообщений на экран */ private static final Logger LOGGER = LoggerFactory.getLogger(WebController.class); /** * Обработчик запросов на создание пользователя * @param createUserDto запрос на создание пользователя */ @PostMapping("/users") public void createUser(@Valid @RequestBody CreateUserDto createUserDto) < /* Получили запрос на создание пользователя, пока можем только залогировать этот факт */ LOGGER.info("Create user request received: <>", createUserDto); > > 

Запустите приложение и попробуйте послать тестовые запросы в файле http.test с разными вариантами значения поля name. Убедитесь, что при допустимых значениях код ответа равен 200 (успех), иначе – 400 (некорректный запрос).

Модель данных

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

Создайте java-пакет model, а в нем java-класс UserInfo, для операций над сущностью пользователя:

package org.example.model; /* Информация о пользователе */ public class UserInfo < /** * Имя пользователя */ private final String name; public static Builder builder() < return new Builder(); >/** * Конструктор сделан закрытым, потому что объекты этого класса * надо порождать таким образом: * dto = User.builder().setName("John Doe").build() */ private UserInfo(Builder builder) < this.name = builder.name; >public String getName() < return name; >/** * Используется при выводе сообщений на экран */ @Override public String toString() < return "'; > public static class Builder < private String name; public Builder setName(String name) < this.name = name; return this; >public UserInfo build() < return new UserInfo(this); >> > 

Внимательный читатель может заметить, что класс UserInfo очень похож на класс CreateUserDto. Неудивительно – если честно, я создал его копированием, удалив аннотации и поправив комментарии. Зачем в приложении два почти одинаковых класса?

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

Транспортные классы отделены в транспортный слой от остального приложения – это признак хорошей архитектуры. В данном случае транспортный слой находится в пакете web. Зачем это нужно?

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

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

Если транспортные классы вашего приложения «проникли» куда-то за пределы транспортной логики, то придется переписывать все приложение. Придется иметь несколько версий приложения: «старую» и «новую», и дорабатывать их параллельно. А это уже не просто дублирование кода – это грозит дублированием всего процесса ведения доработок: постановка, разработка, тестирование.

Но если вы «выдержали» архитектуру, то в вашем приложении просто появится новый транспортный сервис, а бизнес-логику можно будет переиспользовать. Как это делается, будет продемонстрировано позже.

Liquibase — создание базы данных и подключение к ней

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

В промышленных приложениях используют СУБД PostgreSQL, Oracle или MS SQL Server, но мы воспользуемся H2, которая отлично подходит для учебных целей и может создать эфемерную базу данных в оперативной памяти при каждом запуске приложения.

Подключите стартер JDBC, библиотеку Liqubase и библиотеку H2 в файле build.gradle:

. dependencies < . // Стартер jdbc implementation 'org.springframework.boot:spring-boot-starter-jdbc' // Библиотеки Liquibase implementation 'org.liquibase:liquibase-core:4.9.1' // Библиотеки H2 implementation 'com.h2database:h2:2.1.212' testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1' >. 

Создайте файл application.yml в каталоге resources, указав параметры подключения к базе данных:

db: driverClassName: org.h2.Driver url: jdbc:h2:mem:user_db;DB_CLOSE_DELAY=-1;INIT=CREATE SCHEMA IF NOT EXISTS user_db username: admin password: admin maxPoolSize: 10 

В каталоге resources создайте каталог db, а в нем файл changelog.xml, по которому liquibase создаст для нас таблицу user_info:

    create table user_info ( name varchar(25) primary key );   

Создайте java-пакет configuration, а в нем класс DataBaseConfiguration, который обеспечит для приложения возможность отправлять запросы к базе данных:

package org.example.configuration; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import liquibase.integration.spring.SpringLiquibase; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.liquibase.LiquibaseProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.support.TransactionTemplate; import javax.sql.DataSource; /** * Конфигурация компонентов для работы с БД */ @Configuration @EnableTransactionManagement public class DatabaseConfiguration < @Value("$") private String driver = "org.postgresql.Driver"; @Value("$") private int poolLimit = 10; private final String dbUrl; private final String userName; private final String userPassword; @Autowired public DatabaseConfiguration(@Value("$") String userName, @Value("$") String userPassword, @Value("$") String dbUrl) < this.userName = userName; this.userPassword = userPassword; this.dbUrl = dbUrl; >@Bean(destroyMethod = "close") public HikariDataSource hikariDataSource() < HikariConfig config = new HikariConfig(); config.setDriverClassName(driver); config.setJdbcUrl(dbUrl); config.setUsername(userName); config.setPassword(userPassword); config.setMaximumPoolSize(poolLimit); return new HikariDataSource(config); >@Bean public TransactionAwareDataSourceProxy transactionAwareDataSource() < return new TransactionAwareDataSourceProxy(hikariDataSource()); >@Bean public DataSourceTransactionManager dataSourceTransactionManager() < return new DataSourceTransactionManager(transactionAwareDataSource()); >@Bean public TransactionTemplate transactionTemplate() < return new TransactionTemplate(dataSourceTransactionManager()); >@Bean public JdbcTemplate jdbcTemplate() < return new JdbcTemplate(hikariDataSource()); >@Bean public NamedParameterJdbcTemplate namedParameterJdbcTemplate() < return new NamedParameterJdbcTemplate(jdbcTemplate()); >@Bean @ConfigurationProperties(prefix = "spring.datasource.liquibase") public LiquibaseProperties mainLiquibaseProperties() < LiquibaseProperties liquibaseProperties=new LiquibaseProperties(); liquibaseProperties.setChangeLog("classpath:/db/changelog.xml"); return liquibaseProperties; >@Bean public SpringLiquibase springLiquibase() < LiquibaseProperties liquibaseProperties = mainLiquibaseProperties(); return createSpringLiquibase(hikariDataSource(), liquibaseProperties); >private SpringLiquibase createSpringLiquibase(DataSource source, LiquibaseProperties liquibaseProperties) < return new SpringLiquibase() < < setDataSource(source); setDropFirst(liquibaseProperties.isDropFirst()); setContexts(liquibaseProperties.getContexts()); setChangeLog(liquibaseProperties.getChangeLog()); setDefaultSchema(liquibaseProperties.getDefaultSchema()); setChangeLogParameters(liquibaseProperties.getParameters()); setShouldRun(liquibaseProperties.isEnabled()); setRollbackFile(liquibaseProperties.getRollbackFile()); setLabels(liquibaseProperties.getLabels()); >>; > > 

Если все сделано правильно, то в журнале сообщений при старте приложения будет запись:

ChangeSet db/changelog.xml::user::dev ran successfully

Это означает, что в памяти была создана база данных user_db, а в ней таблица user_info, в которой мы будем сохранять данные о пользователях.

DAO — отправка запросов к базе данных

Благодаря Liquibase наше приложение обеспечено базой данных с необходимой нам таблицей user_info. Пришла пора научиться записывать туда данные. Делается это при помощи Data Access Object (DAO) – специализированного класса, который принято выносить в отдельный DAO-слой приложения.

Создайте java-пакет dao и в нем класс UserInfoDao, который будет отвечать за отправку запросов к таблице user_info:

package org.example.dao; import org.example.dao.mapper.UserInfoRowMapper; import org.example.model.UserInfo; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; /** * Запросы к таблице user_info */ public class UserInfoDao < /** * Объект для отправки SQL-запросов к БД */ private final NamedParameterJdbcTemplate jdbcTemplate; public UserInfoDao(NamedParameterJdbcTemplate jdbcTemplate) < this.jdbcTemplate = jdbcTemplate; >/** * Создает запись о пользователе в БД * @param userInfo информация о пользователе */ public void createUser(UserInfo userInfo) < jdbcTemplate.update( "INSERT INTO user_info (name) VALUES (:name) ", new MapSqlParameterSource("name", userInfo.getName()) ); >/** * Возращает информацию о пользователе по имени * @param userName имя пользователя * @return информация о пользователе */ public UserInfo getUserByName(String userName) < return jdbcTemplate.queryForObject("SELECT * FROM user_info WHERE name = :name", new MapSqlParameterSource("name", userName), new UserInfoRowMapper() ); >/** * Удаляет пользователя из БД * @param userName имя пользователя */ public void deleteUser(String userName) < jdbcTemplate.update( "DELETE FROM user_info WHERE name = :name", new MapSqlParameterSource("name", userName) ); >> 

DAO-классу UserInfoDao необходим вспомогательный класс, отвечающий за преобразование записи из таблицы БД в java-класс UserInfo. В пакете dao создайте пакет mapper и в нем класс UserInfoRowMapper:

package org.example.dao.mapper; import org.example.model.UserInfo; import org.springframework.jdbc.core.RowMapper; import java.sql.ResultSet; import java.sql.SQLException; /** * Трансляция записи из таблицы user_info в java-класс UserInfo * * Используется в */ public class UserInfoRowMapper implements RowMapper  < /** * Возвращает информацию о пользователе * @param rs запись в таблице user_info * @param rowNum номер записи * @return информация о пользователе * @throws SQLException если в таблице нет колонки */ @Override public UserInfo mapRow(ResultSet rs, int rowNum) throws SQLException < return UserInfo.builder() .setName(rs.getString("name")) .build(); >> 

Мы описали DAO-класс, теперь надо добавить конфигурацию, по которой Spring Boot создаст при старте приложения “bean” – экземпляр этого класса. В java-пакете configuration создайте класс DaoConfiguration:

package org.example.configuration; import org.example.dao.UserInfoDao; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; /** * Создание "бинов" DAO-классов */ @Configuration public class DaoConfiguration < @Bean UserInfoDao userInfoDao(NamedParameterJdbcTemplate jdbcTemplate) < return new UserInfoDao(jdbcTemplate); >> 

Добавьте в класс WebController использование класса UserInfoDao для работы с БД:

package org.example.web; import org.example.dao.UserInfoDao; import org.example.model.UserInfo; import org.example.web.dto.CreateUserDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; /** * Обработчик web-запросов */ @RestController public class WebController < /** * Средство для вывода сообщений на экран */ private static final Logger LOGGER = LoggerFactory.getLogger(WebController.class); /** * Объект для операциями с БД * TODO: Позже надо перейти на использование сервисного слоя */ private final UserInfoDao userInfoDao; /** * Инъекция одних объектов в другие происходит через конструктор * и обеспечивается библиотеками Spring */ public WebController(UserInfoDao userInfoDao) < this.userInfoDao = userInfoDao; >/** * Обработчик запросов на создание пользователя * @param createUserDto запрос на создание пользователя */ @PostMapping("/users") public void createUser(@Valid @RequestBody CreateUserDto createUserDto) < LOGGER.info("Create user request received: <>", createUserDto); /** * Сохраняем пользователя, преобразуя DTO в модель */ userInfoDao.createUser( UserInfo.builder().setName(createUserDto.getName()).build() ); > /** * Обработчик запросов на получение информации о пользователе * @param userName имя пользователя * @return информация о пользователе */ @GetMapping("/users/") public UserInfo getUserInfo(@PathVariable String userName) < return userInfoDao.getUserByName(userName); >/** * Обработчик запросов на удаление пользователя * @param userName имя пользователя */ @DeleteMapping("/users/") public void deleteUser(@PathVariable String userName) < userInfoDao.deleteUser(userName); >> 

Дополните тестовый файл test.http новыми запросами:

### Запрос на создание пользователя POST http://localhost:8080/users Content-type: application/json < "name": "JohnDoe" >### Запрос информации о пользователе GET http://localhost:8080/users/JohnDoe ### Запрос на удаление пользователя DELETE http://localhost:8080/users/JohnDoe 

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

Сервисный слой и бизнес-логика

Приложение работает, но имеет пока скрытую архитектурную проблему – обращение к DAO-классу происходит напрямую из транспортного слоя.

Предположим, мы хотим избежать появления пользователей с именами типа «administrator», «root» или «system». Или, прежде чем посылать запросы на создание и удаление пользователя, неплохо было бы проверить его наличие в БД.

Писать эту логику в транспортном слое нельзя – при появлении нового транспорта, этот фрагмент кода придется дублировать.

Добавлять эту проверку в UserInfoDao тоже не стоит потому что:

  1. Нарушается принцип единой ответственности, класс начинает терять свою специализацию «работа с таблицей user_info»
  2. DAO-классы – это тоже, в известном смысле, «деталь» приложения, которую, возможно, придется заменить или дополнить при переходе на новую СУБД. Это будет сложнее сделать, если код утяжелен какой-то дополнительной логикой, кроме отсылки SQL-запроса.

Для написания подобной «бизнес-логики» правильно будет создать отдельный «сервисный» слой – это смысловое ядро приложения, вокруг которого крутятся сравнительно легко заменяемые «детали»: транспорт, база данных, клиенты других сервисов т.д.

Добавьте java-пакет service и создайте в нем класс UserInfoService, который будет отвечать за бизнес-операции над сущностью пользователя:

package org.example.service; import org.example.dao.UserInfoDao; import org.example.model.UserInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.EmptyResultDataAccessException; import java.util.Set; /** * Бизнес-логика работы с пользователями */ public class UserInfoService < private static final Logger LOGGER = LoggerFactory.getLogger(UserInfoService.class); /** * Объект для работы с таблице user_info */ private final UserInfoDao userInfoDao; /** * Иньекция испольуземых объектов через конструктор * @param userInfoDao объект для работы с таблице user_info */ public UserInfoService(UserInfoDao userInfoDao) < this.userInfoDao = userInfoDao; >/** * Создание пользователя * @param userInfo информация о пользователе */ public void createUser(UserInfo userInfo) < checkNameSuspicious(userInfo.getName()); if (!isUserExists(userInfo.getName())) < userInfoDao.createUser(userInfo); LOGGER.info("User created by user info: <>", userInfo); > else < // TODO Заменить на своё исключение RuntimeException exception = new RuntimeException("User already exists with name " + userInfo.getName()); LOGGER.error("Error creating user by user info <>", userInfo, exception); throw exception; > > /** * Возвращает информацию о пользователе по его имени * @param userName имя пользователя * @return информация о пользователе */ public UserInfo getUserInfoByName(String userName) < try < return userInfoDao.getUserByName(userName); >catch (EmptyResultDataAccessException e) < LOGGER.error("Error getting info by name <>", userName, e); // TODO Заменить на своё исключение throw new RuntimeException("User not found by name " + userName); > > /** * Удаление пользователя * @param userName имя пользователя */ public void deleteUser(String userName) < if (isUserExists(userName)) < userInfoDao.deleteUser(userName); LOGGER.info("User with name <>deleted", userName); > > /** * Проверка на сущестование пользователя с именем * @param userName имя пользователя * @return true - если пользователь сущестует, иначе - false */ private boolean isUserExists(String userName) < try < userInfoDao.getUserByName(userName); return true; >catch (EmptyResultDataAccessException e) < return false; >> /** * Проверка на то, что имя пользователя не содержится в стоп-листе * @param userName имя пользователя */ private void checkNameSuspicious(String userName) < if (Set.of("administrator", "root", "system").contains(userName)) < // TODO: Заменить на свое исключение RuntimeException exception = new RuntimeException(userName + " is unacceptable"); LOGGER.error("Check name failed", exception); throw exception; >> > 

Замените использование dao-объекта на использование сервисного класса в WebController:

package org.example.web; import org.example.model.UserInfo; import org.example.service.UserInfoService; import org.example.web.dto.CreateUserDto; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; /** * Обработчик web-запросов */ @RestController public class WebController < /** * Средство для вывода сообщений на экран */ private static final Logger LOGGER = LoggerFactory.getLogger(WebController.class); /** * Объект для работы с информацией о пользователе */ private final UserInfoService userInfoService; /** * Иньекция одних объектов в другие происходит через конструктор * и обеспечивается библиотеками Spring */ public WebController(UserInfoService userInfoService) < this.userInfoService = userInfoService; >/** * Обработчик запросов на создание пользователя * @param createUserDto запрос на создание пользователя */ @PostMapping("/users") public void createUser(@Valid @RequestBody CreateUserDto createUserDto) < LOGGER.info("Create user request received: <>", createUserDto); /** * Сохраняем пользователя, преобразуя DTO в модель */ userInfoService.createUser( UserInfo.builder().setName(createUserDto.getName()).build() ); > /** * Обработчик запросов на получение информации о пользователе * @param userName имя пользователя * @return информация о пользователе */ @GetMapping("/users/") public UserInfo getUserInfo(@PathVariable String userName) < LOGGER.info("Get user info request received userName=<>", userName); return userInfoService.getUserInfoByName(userName); > /** * Обработчик запросов на удаление пользователя * @param userName имя пользователя */ @DeleteMapping("/users/") public void deleteUser(@PathVariable String userName) < LOGGER.info("Delete user info request received userName=<>", userName); userInfoService.deleteUser(userName); > > 

Запустите последовательно три тестовых запроса. Если все сделано правильно, будут получены ответы с кодом 200. Обратите внимание на записи в консоли приложения:

Create user request received:
User created by user info:
Get user info request received userName=JohnDoe
Delete user info request received userName=JohnDoe
User with name JohnDoe deleted

Они могут быт очень полезными при диагностике неполадок. Общие рекомендации по логгированию такие:

  1. Информационное сообщение сразу при получении запроса с выводом содержимого запроса.
  2. Информационное сообщение об успешности операции перед выходом из метода в сервисном классе.
  3. Сообщение об ошибке сразу после catch. Не забудьте показать само исключение.
  4. Сообщение об ошибке перед throw, если не было catch
  5. Отладочные сообщения в сложных алгоритмах

Как написать клиент-сервер на java, используя внешний ip?

Я начинающий в программировании, и решил сделать простое клиент-серверное приложение. Хочется разобраться, как можно передавать данные между сервером и клиентом, которые имеют разные внешние ip (проще говоря — через интернет). Вот код моего сервера:

import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.net.InetAddress; import java.net.ServerSocket; import java.net.Socket; public class Server < private static ServerSocket server; private static Socket connection; private static ObjectOutputStream output; private static ObjectInputStream input; private static Boolean isAlreadyConnected = false; public static void main(String[] args) < try < server = new ServerSocket(6666, 100); while(true) < connection = server.accept(); if(!isAlreadyConnected) < System.out.println("Connected"); isAlreadyConnected = true; >output = new ObjectOutputStream(connection.getOutputStream()); output.flush(); input = new ObjectInputStream(connection.getInputStream()); String message = (String) input.readObject(); System.out.println("User sent you: " + message); output.writeObject("Your message: " + message); output.flush(); > > catch (IOException e) < System.out.println("Probably something wrong with connection. Application will be switched off."); System.exit(-1); >catch (ClassNotFoundException e) < e.printStackTrace(); >finally < close(); >> // Don't know if it's necessary: private static void close() < try < output.close(); input.close(); connection.close(); >catch (IOException e) < e.printStackTrace(); >> > 

Вот мой клиент:

import java.io.*; import java.net.InetAddress; import java.net.Socket; import java.util.Scanner; public class Dispatcher implements Runnable < private static Socket connection; private static ObjectOutputStream output; private static ObjectInputStream input; private static Scanner scanner; public static void main(String[] args) < new Thread(new Dispatcher()).start(); >@Override public void run() < scanner = new Scanner(System.in); try < while(true) < // Next line means connection to this computer connection = new Socket(InetAddress.getByName("127.0.0.1"), 6666); // TODO: Connection by external IP // connection = new Socket(InetAddress.getByName("91.243.199.142"), 6666); output = new ObjectOutputStream(connection.getOutputStream()); output.flush(); input = new ObjectInputStream(connection.getInputStream()); sendData(scanner.nextLine()); System.out.println(input.readObject()); >> catch (IOException e) < e.printStackTrace(); >catch (ClassNotFoundException e) < e.printStackTrace(); >> private static void sendData(Object object) < try < output.flush(); output.writeObject(object); output.flush(); >catch (IOException e) < e.printStackTrace(); >> > 

Можете сказать, как переделать этот код, чтоб я мог подключиться на компьютер у которого другой внешний ip? Или подскажите, как это можно сделать другим способом, желательно на Java.

Классы Socket и ServerSocket, или «Алло, сервер? Ты меня слышишь?»

Введение: «На столе был комп, за ним был кодер…» Классы Socket и ServerSocket, или «Алло, сервер? Ты меня слышишь?» - 1Как-то один мой однокурсник выкладывал очередной результат своего изучения Java, в виде скриншота новой программы. Этой программой был многопользовательский чат. Я тогда только начинал свой собственный путь в освоении программирования на данном языке, но точно отметил для себя – «хочу!». Шло время и закончив работу с очередным проектом в рамках углубления своих знаний программирования, я вспомнил про тот случай и решил – пора. Как-то я уже начинал чисто из любопытства копать эту тему, но в моём основном учебнике по Java (это было полное руководство Шилдта) было предоставлено пакету java.net всего лишь 20 страниц. Это и понятно – книга и так очень большая. Там были приведены таблицы методов и конструкторов основных классов, но и всё. Следующий шаг – разумеется всемогущий гугл: мириады всевозможных статей, где представлено одно и тоже — два-три слова про сокеты, и готовый пример. Классический подход (как минимум в моем стиле учебы) – это сначала понять, что мне нужно из инструментов для работы, что они из себя представляют, зачем они нужны и только потом если решение задачи неочевидно ковырять готовые листинги, развинчивая из на гайки и болтики. Но я разобрался что к чему и в итоге написал многопользовательский чат. Внешне получилось как-то так: Классы Socket и ServerSocket, или «Алло, сервер? Ты меня слышишь?» - 2Здесь я постараюсь дать вам понимание основ клиент-серверных приложений на основе сокетов Java на примере проектирования чата. На курсе джавараш вы будете делать чат. Он будет кардинально другого уровня, красивый, большой, многофункциональный. Но всегда в первую очередь нужно заложить фундамент, поэтому тут нам с вами нужно разобраться что же лежит в основе подобного раздела. (Если вы нашли какие-то недочеты или ошибки, напишите в ЛС или в комментарии под статьёй). Начнём. Голова Один: «Дом, который…» Для объяснения как же происходит сетевое соединение между сервером и одним клиентом, возьмем, ставший уже классическим, пример с многоквартирным домом. Допустим, клиенту нужно каким-то образом установить связь с определённым сервером. Что нужно знать ищущему об объекте поиска? Да, адрес. Сервер, это не магическая сущность на облаке, и поэтому он должен находиться на определённой машине. По аналогии с домом, где должна произойти встреча двух согласованных сторон. И что бы найти друг друга в многоквартирном доме одного адреса здания недостаточно, необходимо указать номер квартиры, в которой произойдет встреча. Так и на одной вычислительной машине может быть сразу несколько серверов, и клиенту, чтобы связаться с конкретным нужно указать ещё и номер порта по которому произойдет соединение. Итак, адрес и номер порта. Адрес подразумевает под собой идентификатор машины в пространстве сети Internet. Он может быть доменным именем, например, «javarush.ru», или обычным IP. Порт — уникальный номер, с которым связан определённый сокет (этот термин будет рассмотрен далее), проще говоря, его занимает определённая служба для того что бы по нему могли связаться с ней. Так что для того что бы произошла встреча как минимум двух объектов на территории одного (сервера) — хозяин местности (сервер) должен занять конкретную квартиру (порт) на ней (машине), а второй должен найти место встречи зная адрес дома (домен или ip), и номер квартиры (порт). Голова Два: Знакомьтесь, Socket Среди понятий и терминов, связанных с работой в сети, если одно очень важное – Сокет. Оно обозначает точку, через которую происходит соединение. Проще говоря, сокет соединяет в сети две программы. Класс Socket реализует идею сокета. Через его каналы ввода/вывода будут общаться клиент с сервером: Классы Socket и ServerSocket, или «Алло, сервер? Ты меня слышишь?» - 3Объявляется этот класс на стороне клиента, а сервер воссоздаёт его, получая сигнал на подключение. Так происходит общение в сети. Для начала вот возможные конструкторы класса Socket :

 Socket(String имя_хоста, int порт) throws UnknownHostException, IOException Socket(InetAddress IP-адрес, int порт) throws UnknownHostException 

«имя_хоста» — подразумевает под собой определённый узел сети, ip-адрес. Если класс сокета не смог преобразовать его в реальный, существующий, адрес, то сгенерируется исключение UnknownHostException . Порт — есть порт. Если в качестве номера порта будет указан 0, то система сама выделит свободный порт. Также при потере соединения может произойти исключение IOException . Следует отметить тип адреса во втором конструкторе — InetAddress . Он приходит на помощь, например, когда нужно указать в качестве адреса доменное имя. Так же когда под доменом подразумевается несколько ip-адресов, то с помощью InetAddress можно получить их массив. Тем не менее с ip он работает тоже. Так же можно получить имя хоста, массив байт составляющих ip адрес и т.д. Мы немного затронем его далее, но за полными сведениями придется пройти к официальной документации. При инициализации объекта типа Socket , клиент, которому тот принадлежит, объявляет в сети, что хочет соединиться с сервером про определённому адресу и номеру порта. Ниже представлены самые часто используемые методы класса Socket : InetAddress getInetAddress() – возвращает объект содержащий данные о сокете. В случае если сокет не подключен – null int getPort() – возвращает порт по которому происходит соединение с сервером int getLocalPort() – возвращает порт к которому привязан сокет. Дело в том, что «общаться» клиент и сервер могут по одному порту, а порты, к которым они привязаны – могут быть совершенно другие boolean isConnected() – возвращает true, если соединение установлено void connect(SocketAddress адрес) – указывает новое соединение boolean isClosed() – возвращает true, если сокет закрыт boolean isBound() — возвращает true, если сокет действительно привязан к адресу Класс Socket реализует интерфейс AutoCloseable , поэтому его можно использовать в конструкции try-with-resources . Тем не менее закрыть сокет также можно классическим образом, с помощью close(). Голова Три: а это ServerSocket Допустим мы объявили, в виде класса Socket , на стороне клиента запрос на соединение. Как сервер разгадает наше желание? Для это сервер имеет такой класс как ServerSocket , и метод accept() в нём. Его конструкторы представлены ниже:

Python-университет

 ServerSocket() throws IOException ServerSocket(int порт) throws IOException ServerSocket(int порт, int максимум_подключений) throws IOException ServerSocket(int порт, int максимум_подключений, InetAddress локальный_адрес) throws IOException 

При объявлении ServerSocket не нужно указывать адрес соединения, потому что общение происходит на машине сервера. Только при многоканальном хосте нужно указать к какому ip привязан сокет сервера. Голова Три.Один: Сервер, который говорит нет Так как предоставлять программе больше ресурсов чем ей необходимо — и затратное и не разумное дело, поэтому в конструкторе ServerSocket вам предлагают объявить максимум соединений, принимаемых сервером при работе. Если оно не указано, то умолчанию это число будет считаться равным 50. Да, по идее можно предположить, что ServerSocket это такой же сокет, только для сервера. Но он играет совершенно иную роль нежели класс Socket . Он нужен только на этапе создания соединения. Создав объект типа ServerSocket необходимо выяснить, что с сервером кто-то хочет соединиться. Тут подключается метод accept(). Искомый ждёт пока кто-либо не захочет подсоединиться к нему, и когда это происходит возвращает объект типа Socket , то есть воссозданный клиентский сокет. И вот когда сокет клиента создан на стороне сервера, можно начинать двухстороннее общение. Создать объект типа Socket на стороне клиента и воссоздать его с помощью ServerSocket на стороне сервера – вот необходимый минимум для соединения. Голова Четыре: Письмо «деду морозу» Вопрос: Как конкретно общаются клиент и сервер? Ответ: Через потоки ввода вывода. Что мы уже имеем? Сокет с адресом сервера и номером порта у клиента, и тоже самое, благодаря accept(), на стороне сервера. Так что разумно предположить, что общаться они будут как раз через сокет. Для этого есть два метода которые дают доступ к потокам InputStream и OutputStream объекта типа Socket . Вот они:

 InputStream getInputStream() OutputStream getOutputStream() 

Так как читать и писать голые байты не так эффективно — потоки можно обернуть в классы адаптеры, буферизированные, или нет. Например:

 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream())); BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); 

Что бы общение было двунаправленным такие операции необходимо проделать на обеих сторонах. Теперь вы можете отослать что-то с помощью in, и принять с помощью out, и наоборот. Собственно, это практически единственная функция класса Socket . И да, не забывайте про метод flush() для BufferedWriter – он выталкивает содержимое буфера. Если этого не сделать, информация не будет передана, а, следовательно, не будет получена. Так же принимающий поток ждет указатель конца строки – «\n», иначе сообщение не будет принято, так как фактически сообщение не окончено, и не является целым. Если вам это кажется неудобным, не расстраивайтесь, всегда можно воспользоваться классом PrintWriter , которым нужно обернуть out, указать вторым аргументом true и тогда выталкивание из буфера будет происходить автоматически:

 PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true); 

Так же при этом указывать конец строки нет необходимости, за вас это делает данный класс. Но является ли ввод/вывод строк пределом возможностей сокета? Нет, хотите оправлять объекты через потоки сокета? Ради бога. Сериализуйте их, и вперед:

 ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream()); ObjectInputStream in = new ObjectInputStream(socket.getInputStream()); 
  1. Наш будущий чат, как утилита, такими способностями не обладает. Он может лишь установить соединение и принять/отправить сообщение. То есть он не обладает реальными возможностями сервера.
  2. Наш сервер, содержащий лишь данные сокета и потоков ввода/вывода, не может работать как реальный WEB- или FTP-сервер, то имея лишь это мы не сможем соединиться по сети Internet.
  1. Написать в качестве аргумента адреса «localhost», означающий локальную заглушку. Так же для этого подходит «127.0.0.1» — это всего лишь цифровая форма заглушки.
  2. С помощью InetAddress:
    1. InetAddress.getByName(null) — null указывает на локальный хост
    2. InetAddress.getByName(«localhost»)
    3. InetAddress.getByName(«127.0.0.1»)
     import java.io.*; import java.net.ServerSocket; import java.net.Socket; public class Server < private static Socket clientSocket; //сокет для общения private static ServerSocket server; // серверсокет private static BufferedReader in; // поток чтения из сокета private static BufferedWriter out; // поток записи в сокет public static void main(String[] args) < try < try < server = new ServerSocket(4004); // серверсокет прослушивает порт 4004 System.out.println("Сервер запущен!"); // хорошо бы серверу // объявить о своем запуске clientSocket = server.accept(); // accept() будет ждать пока //кто-нибудь не захочет подключиться try < // установив связь и воссоздав сокет для общения с клиентом можно перейти // к созданию потоков ввода/вывода. // теперь мы можем принимать сообщения in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); // и отправлять out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream())); String word = in.readLine(); // ждём пока клиент что-нибудь нам напишет System.out.println(word); // не долго думая отвечает клиенту out.write("Привет, это Сервер! Подтверждаю, вы написали : " + word + "\n"); out.flush(); // выталкиваем все из буфера >finally < // в любом случае сокет будет закрыт clientSocket.close(); // потоки тоже хорошо бы закрыть in.close(); out.close(); >> finally < System.out.println("Сервер закрыт!"); server.close(); >> catch (IOException e) < System.err.println(e); >> 

    «Client.java»

     import java.io.*; import java.net.Socket; public class Client < private static Socket clientSocket; //сокет для общения private static BufferedReader reader; // нам нужен ридер читающий с консоли, иначе как // мы узнаем что хочет сказать клиент? private static BufferedReader in; // поток чтения из сокета private static BufferedWriter out; // поток записи в сокет public static void main(String[] args) < try < try < // адрес - локальный хост, порт - 4004, такой же как у сервера clientSocket = new Socket("localhost", 4004); // этой строкой мы запрашиваем // у сервера доступ на соединение reader = new BufferedReader(new InputStreamReader(System.in)); // читать соообщения с сервера in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); // писать туда же out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream())); System.out.println("Вы что-то хотели сказать? Введите это здесь:"); // если соединение произошло и потоки успешно созданы - мы можем // работать дальше и предложить клиенту что то ввести // если нет - вылетит исключение String word = reader.readLine(); // ждём пока клиент что-нибудь // не напишет в консоль out.write(word + "\n"); // отправляем сообщение на сервер out.flush(); String serverWord = in.readLine(); // ждём, что скажет сервер System.out.println(serverWord); // получив - выводим на экран >finally < // в любом случае необходимо закрыть сокет и потоки System.out.println("Клиент был закрыт. "); clientSocket.close(); in.close(); out.close(); >> catch (IOException e) < System.err.println(e); >> > 
    1. Номер порта.
    2. Список, в который он записывает новое соединение.
    3. И ServerSocket , в единственном (!) экземпляре.
     public class Server < public static final int PORT = 8080; public static LinkedListserverList = new LinkedList<>(); // список всех нитей public static void main(String[] args) throws IOException < ServerSocket server = new ServerSocket(PORT); try < while (true) < // Блокируется до возникновения нового соединения: Socket socket = server.accept(); try < serverList.add(new ServerSomthing(socket)); // добавить новое соединенние в список >catch (IOException e) < // Если завершится неудачей, закрывается сокет, // в противном случае, нить закроет его при завершении работы: socket.close(); >> > finally < server.close(); >> > 

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

     class ServerSomthing extends Thread < private Socket socket; // сокет, через который сервер общается с клиентом, // кроме него - клиент и сервер никак не связаны private BufferedReader in; // поток чтения из сокета private BufferedWriter out; // поток записи в сокет public ServerSomthing(Socket socket) throws IOException < this.socket = socket; // если потоку ввода/вывода приведут к генерированию исключения, оно пробросится дальше in = new BufferedReader(new InputStreamReader(socket.getInputStream())); out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); start(); // вызываем run() >@Override public void run() < String word; try < while (true) < word = in.readLine(); if(word.equals("stop")) < break; >for (ServerSomthing vr : Server.serverList) < vr.send(word); // отослать принятое сообщение с // привязанного клиента всем остальным включая его >> > catch (IOException e) < >> private void send(String msg) < try < out.write(msg + "\n"); out.flush(); >catch (IOException ignored) <> > > 

    Итак, в конструкторе серверной нити должен быть инициализирован сокет, через который нить будет общаться с конкретным клиентом. Также потоки ввода/вывода, и ко всему прочему нужно запустить нить прямо из конструктора. Хорошо, но что будет происходить при чтении сообщения от клиента для серверной нити? Отсылать обратно только своему клиенту? Не очень-то эффективно. Мы делаем многопользовательский чат, поэтому нам нужно что бы каждый подключенный клиент получил то что написал кто-то один. Нужно воспользоваться списком всех серверных нитей, привязанных к своим клиентам, и отослать каждое присланное конкретной нити сообщение, что бы та отослала его своему клиенту:

    Java-университет

     for (ServerSomthing vr : Server.serverList) < vr.send(word); // отослать принятое сообщение // с привязанного клиента всем остальным, включая его >
     private void send(String msg) < try < out.write(msg + "\n"); out.flush(); >catch (IOException ignored) <> > 

    Теперь все клиенты узнают то, что сказал один из них! Если вы не хотите, чтобы сообщение приходило тому, кто его отправил (он и так знает, что он написал!) просто при переборе нитей укажите что бы при обработке объекта this цикл переходил к следующему элементу, не выполняя над ним никаких действий. Или же, если хотите, отправьте сообщение клиенту, в котором написано, что сообщение успешно принято и разослано. С сервером теперь все понятно. Перейдём к клиенту, а точнее к клиентам! Там все так же, по аналогии с клиентом из прошлой главы, только создавая экземпляр нужно как было показано в данной главе с сервером, создать все необходимое в конструкторе. Но что если при создании клиента он ещё не успел ничего ввести, а ему уже что-то отправили? (Например, историю переписки тех, кто уже подключился к чату до него). Так что циклы, в которых буду обрабатываться присланные сообщения должны быть отделены от тех в которых читаются сообщения с консоли и отправляются на сервер для пересылки остальным. На помощь снова приходят нити. Нет смысла создавать клиента как нить. Удобнее сделать нить с циклом в методе run читающую сообщения, а также по аналогии — пишущую:

     // нить чтения сообщений с сервера private class ReadMsg extends Thread < @Override public void run() < String str; try < while (true) < str = in.readLine(); // ждем сообщения с сервера if (str.equals("stop")) < break; // выходим из цикла если пришло "stop" >> > catch (IOException e) < >> > 
     // нить отправляющая сообщения приходящие с консоли на сервер public class WriteMsg extends Thread < @Override public void run() < while (true) < String userWord; try < userWord = inputUser.readLine(); // сообщения с консоли if (userWord.equals("stop")) < out.write("stop" + "\n"); break; // выходим из цикла если пришло "stop" >else < out.write(userWord + "\n"); // отправляем на сервер >out.flush(); // чистим > catch (IOException e) < >> > > 

    В конструкторе клиента необходимо просто запустить эти нити. А как правильно закрыть ресурсы клиента если тот захочет выйти? Нужно ли закрывать ресурсы серверной нити? Для этого необходимо будет скорее всего создать отдельный метод, вызывающийся при выходе из цикла обработки сообщений. Там нужно будет закрыть сокет и потоки ввода/вывода. Тот же сигнал окончания сессии для конкретного клиента должен быть отправлен его серверной нити, которая должна сделать тоже со своим сокетом и удалить себя из списка нитей в основном классе сервера. Голова Восемь: Нет предела совершенству Можно бесконечно долго выдумывать новые фичи для совершенствования своего проекта. Но что точно должно быть передано ново подключившемуся клиенту? Я думаю, что последние десять событий, произошедших до его прихода. Для это необходимо создать класс, в котором в объявленный список будет заноситься последнее действие с любой серверной нитью, и, если список уже полон (то есть 10 уже есть), удалить первое и занести последним пришедшее. Для того что бы содержимое этого списка получил новый подключившийся, нужно при создании серверной нити, в потоке вывода, отослать их клиенту. Как это сделать? Например, так:

     public void printStory(BufferedWriter writer) < // . >
    1. Thinking in Java Enterprise, by Bruce Eckel et. Al. 2003
    2. Java 8, Полное руководство, Герберт Шилдт, 9 издание, 2017 (Глава 22)
    3. Программирование сокетов на Java статья про сокеты
    4. Socket в официальной документации
    5. ServerSocket в официальной документации
    6. исходники на GitHub

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

    Есть цель — написать сервер на Java. Должен держать 2-3 тясячи клиентов, работать с БД. Опыт разработки серверов — одна лабораторная по сетям в универе. Посоветуйте пожалуйста в сторону каких технологий смотреть и что изучать для написания такого проекта. Заранее благодарю.

    • Вопрос задан более трёх лет назад
    • 26924 просмотра

    Комментировать

    Решения вопроса 1

    А вот этот курс на интуите.

    Ответ написан более трёх лет назад

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

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