«Клиент-сервер» это очень распространенная и логичная архитектура приложений. Мне кажется, что в наши дни редко можно встретить 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
Заключение
Главная цель, которую я преследовал при написании этой статьи заключается в том, чтобы дать возможность мне или кому-либо еще за считанные минуты «собрать» готовое клиент-серверное приложение. Кажется, я с этим справился, если будут дополнения или замечания, пишите в комментариях или на почту. А на сегодня у меня все, спасибо за внимание!