Молодогвардейцев 454015 Россия, Челябинская область, город Челябинск 89085842764
MindHalls logo

Пример простого клиент-серверного приложения на Java

«Клиент-сервер» это очень распространенная и логичная архитектура приложений. Мне кажется, что в наши дни редко можно встретить 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

Заключение

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