«Клиент-сервер» это очень распространенная и логичная архитектура приложений. Мне кажется, что в наши дни редко можно встретить standalone-клиентское приложение. Поэтому я принял решение рассмотреть пример построения клиент-серверного приложения на Java без привязки к конкретной задаче. Сначала вкратце пробежимся по классовой структуре приложения, потом посмотрим на отдельную реализацию каждого класса. В самом конце статьи я дам ссылку на скачивание архива с готовой структурой приложения. Итак, начнем.
Основные компоненты приложения
Основными компонентами, естественно, являются непосредственно клиент и сервер. Однако, кроме них необходим еще пакет вспомогательных классов, которые, в простейшем случае, будут отвечать за обмен сообщениями между клиентом и сервером. В минимальной комплектации нужны такие классы: MessageReader/MessageWriter(считывает/записывает сообщение в поток на сокете), MessageFactory(содержит идентификаторы всех возможных сообщений), набор сообщений-запросов(Request) и набор сообщений-ответов(Response). Все они будут размещены в пакете «core», который должны иметь у себя и клиент и сервер.
Рассмотрим классовую структуру всего проекта, а потом перейдем к реализации.
Классовая структура клиент-серверного приложения
- client (пакет файлов клиента) Client.java (логика клиента) ClientLauncher.java (запуск клиента) - core (вспомогательные классы) - communication (обмен сообщениями) MessageFactory.java (здесь хранятся все сообщения) MessageReader.java (чтение сообщений из потока) MessageWriter.java (запись сообщений в поток) - requests HandshakeRequest.java (запрос на обмен рукопожатиями) - responses HandshakeResponse.java (ответ на обмен рукопожатиями) IMessage.java (интерфейс для сообщения) Request.java (абстрактный класс "запрос") Response.java (абстрактный класс "ответ") - server (файлы сервера) ClientSession.java (сессия отдельного клиента - обработка запросов этого клиента) Context.java (контекст - общая информация сервера для всех клиентов) Server.java (логика сервера) ServerLauncher.java (запуск сервера) SessionsManager.java (хранит все текущие сессии)
Исходный код клиента на Java
Разобраться с клиентом гораздо проще, он по сути своей не делает ничего супер сложного, просто создает сокет и подключается к сервер-сокету с помощью связки host:port. Лаунчер создает объект класса Client и запускает его работу. Исходный код привожу без импортов, ибо любая IDE вам их подключит(те, кто пишет на Java точно знают, что без IDE очень сложно). Кроме того, в конце статьи вы сможете скачать архив с этим проектом.
ClientLauncher.java
public class ClientLauncher { public static void main(String[] args) { try { InetAddress host = InetAddress.getByName(args[0]); int port = Integer.parseInt(args[1]); //System.out.println(id); Client client = new Client(host, port); //Запускаем логику клиента client.start(); } catch (UnknownHostException e) { e.printStackTrace(); } } }
Client.java
public class Client { private final InetAddress host; private final int port; public Client(InetAddress host, int port) { this.host = host; this.port = port; } //Создает сокет, ридер райтер и запускает логику public void start() { //Создаем клиентский сокет try (Socket socket = new Socket(this.host, this.port)) { //Создаем ридер и райтер для обмена сообщениями MessageReader reader = new MessageReader(socket.getInputStream()); MessageWriter writer = new MessageWriter(socket.getOutputStream()); //Шлем серверу первое сообщение "рукопожатие" writer.writeRequest(new HandshakeRequest()); //Получаем ответ UniqueMessage msg = reader.readMessage(); //Проверяем, что это ответ на рукопожатие if(!(msg.message instanceof HandshakeResponse)) { return; } //Запуск логики приложения this.logicStart(); //socket.close(); } catch (IOException e) { e.printStackTrace(); } } public void logicStart() { //Логика приложения //..... } }
Под словами «логика приложения» я подразумеваю протокол обмена сообщениями с сервером, передачу каких-либо данных для достижения конечной цели.
Исходный код сервера на Java
Задача сервера поднять свой серверный сокет на нужном адресе и ждать новых подключений. Для каждого подключения, которое принято называть клиентской сессией, создается отдельный поток обработки логики работы с клиентом.
Напомню, что в классе ClientSession описан основной алгоритм работы с клиентом, обмен сообщениями, данными и прочее. В классе Context содержится общая информация для всех клиентов сервера, например, пути для сохранения логов.
ServerLauncher.java
public class ServerLauncher { public static void main(String[] args) { Server server = new Server(); server.run(); } }
Server.java
public class Server implements Runnable { private final int port; private Context context; public Server() { this.port = 5000; this.context = new Context(); } @Override public void run() { try { ServerSocket ss = new ServerSocket(this.port); //Цикл ожидания подключений while(!this.context.stopFlag) { System.out.println("Waiting connection on port:" + this.port); //Момент ухода в ожидание подключения Socket clientSocket = ss.accept(); System.out.println("New client connected to server"); //Создается клиентская сессия ClientSession clientSession = new ClientSession(clientSocket, this.context); this.context.getSessionsManger().addSession(clientSession); //Запуск логики работы с клиентом clientSession.start(); } ss.close(); } catch (IOException e) { e.printStackTrace(); } } }
Context.java
//Данные, общие для всех клиентских сессий public class Context { private SessionsManager sessinonsManager; public boolean stopFlag; //Другие важные поля, которые должны знать все клиенты //... public Context() { this.stopFlag = false; this.sessinonsManager = new SessionsManager(); } public SessionsManager getSessionsManger() { return this.sessinonsManager; } }
ClientSession.java
//Основная логика клиента public class ClientSession extends Thread { private final Socket socket; private final MessageReader reader; private final MessageWriter writer; private final Context context; public ClientSession(final Socket socket, final Context context) throws IOException { this.socket = socket; this.reader = new MessageReader(socket.getInputStream()); this.writer = new MessageWriter(socket.getOutputStream()); this.context = context; } public void run() { UniqueMessage msg; try { msg = reader.readMessage(); //Рукопожатие if(msg.message instanceof HandshakeRequest) { if(((HandshakeRequest)msg.message).match()) { writer.writeResponse(new HandshakeResponse(), msg.uniqueId); } } //Обменялись рукопожатиями, начинаем работу this.doWork(); //выход this.socket.close(); } catch (IOException e) { e.printStackTrace(); } } private void doWork() {} }
SessionsManager.java
public class SessionsManager { private final Set<ClientSession> sessions = new HashSet<ClientSession>(); public SessionsManager() {} public synchronized void addSession(ClientSession session) { sessions.add(session); } public synchronized void removeSession(ClientSession session) { sessions.remove(session); } }
Вспомогательные классы из пакета «core»
Помещу все вспомогательные классы под один кат, название классов в точности соответствует названиям из списка «классовая структура» выше, по нему вы можете определить пакет каждого класса.
public class MessageFactory { //Идентификаторы запросов public static final int REQUEST_HANDSHAKE = 1; //Идентификаторы ответов private static final int RESPONSE_BASE = 0x40000000; public static final int RESPONSE_HANDSHAKE = RESPONSE_BASE + 1; //Ассоциативный массив: класс сообщения => идентификатор сообщения private static final Map<Class<? extends IMessage>, Integer> idMap = new HashMap<Class<? extends IMessage>, Integer>(); static { idMap.put(HandshakeRequest.class, REQUEST_HANDSHAKE); idMap.put(HandshakeResponse.class, RESPONSE_HANDSHAKE); } private MessageFactory() { } //Создает сообщение по идентификатору public static IMessage createMessage(int messageId) throws IOException { if (messageId > RESPONSE_BASE) { switch (messageId) { case RESPONSE_HANDSHAKE: return new HandshakeResponse(); default: throw new IOException("Unknown message type " + messageId); } } else { switch (messageId) { case REQUEST_HANDSHAKE: return new HandshakeRequest(); default: throw new IOException("Unknown message type " + messageId); } } } public static int getMessageId(final IMessage message) { Integer id = idMap.get(message.getClass()); return id.intValue(); } } public class MessageReader { //Длина заголовка сообщения public static final int HEADER_LENGTH = 12; private final DataInputStream dis; public MessageReader(InputStream is) { this.dis = new DataInputStream(is); } public UniqueMessage readMessage() throws IOException { //Читаем длину пакета из начала int packageLength = dis.readInt(); if (packageLength < HEADER_LENGTH) { throw new IOException("Wrong package length"); } //Считываем сообщение byte[] buf = new byte[packageLength - 4]; dis.readFully(buf); DataInputStream messageIS = new DataInputStream(new ByteArrayInputStream(buf)); int uniqueId = messageIS.readInt(); int message_id = messageIS.readInt(); IMessage message = MessageFactory.createMessage(message_id); message.readExternal(messageIS); System.out.println("Message " + message.getClass().getName() + " received."); return new UniqueMessage(message, uniqueId); } public static class UniqueMessage { public final IMessage message; public final int uniqueId; private UniqueMessage(IMessage message, int uniqueId) { this.message = message; this.uniqueId = uniqueId; } } } public class MessageWriter { private static final int INITIAL_BUFFER_SIZE = 128; private final DataOutputStream out; private Integer requestIdCounter = 0; public MessageWriter(OutputStream os) { this.out = new DataOutputStream(os); } private int getNewRequestId() { synchronized (requestIdCounter) { return ++requestIdCounter; } } private void writeMessage(final IMessage message, final int uniqueId) throws IOException { int messageId = MessageFactory.getMessageId(message); ByteArrayOutputStream baos = new ByteArrayOutputStream( INITIAL_BUFFER_SIZE); message.writeExternal(new DataOutputStream(baos)); int messageLength = baos.size() + MessageReader.HEADER_LENGTH; synchronized (out) { out.writeInt(messageLength); out.writeInt(uniqueId); out.writeInt(messageId); baos.writeTo(out); out.flush(); } System.out.println("Message " + message.getClass().getName() + " sent."); } public int writeRequest(final Request request) throws IOException { int uniqueId = getNewRequestId(); writeMessage(request, uniqueId); return uniqueId; } public void writeResponse(final Response response, int requestId) throws IOException { writeMessage(response, requestId); } } public class HandshakeRequest extends Request { public static final String HANDSHAKE_STRING = "handshake request"; private String handshake; @Override public void readExternal(DataInputStream dis) throws IOException { handshake = dis.readUTF(); } @Override public void writeExternal(DataOutputStream dos) throws IOException { dos.writeUTF(HANDSHAKE_STRING); } public boolean match() { return HANDSHAKE_STRING.equals(handshake); } } public class HandshakeResponse extends Response { public static final String HANDSHAKE_RESPONSE_STRING = "handshake response"; private String handshake; @Override public void readExternal(DataInputStream dis) throws IOException { handshake = dis.readUTF(); } @Override public void writeExternal(DataOutputStream dos) throws IOException { dos.writeUTF(HANDSHAKE_RESPONSE_STRING); } public boolean match() { return HANDSHAKE_RESPONSE_STRING.equals(handshake); } } public interface IMessage { public void writeExternal(DataOutputStream dos) throws IOException; public void readExternal(DataInputStream dis) throws IOException; } public abstract class Request implements IMessage { } public abstract class Response implements IMessage { }
Пара слов о сообщениях, классы Request и Response являются абстрактными и играют роль классификаторов сообщения. Благодаря этому очень удобно разграничивать «запросы» от «ответов». В этом примере я привел только одно сообщение — Handshake, которое отвечает за первое «рукопожатие» клиента и сервера. Все последующие сообщения должны быть прописаны в классе MessageFactory по примеру этих двух.
Скачать архив с шаблоном клиент-серверного приложения на Java
Заключение
Главная цель, которую я преследовал при написании этой статьи заключается в том, чтобы дать возможность мне или кому-либо еще за считанные минуты «собрать» готовое клиент-серверное приложение. Кажется, я с этим справился, если будут дополнения или замечания, пишите в комментариях или на почту. А на сегодня у меня все, спасибо за внимание!