воскресенье, 22 мая 2011 г.

Redis: пишем Java-клиент

В одном из прежних постов я описывал свои впечатления от знакомства с Redis. Тогда я использовал библиотеку Jedis. А недавно мне попалось описание протокола Redis-сервера и я решил поупражняться в его освоении. Чтобы было нагляднее я "завернул" результат в простое swing-приложение, которое можно скачать на моём сайте. Итак...

Коротко о протоколе
Для соединения с сервером используем tcp-протокол, порт по умолчанию - 6379. Команды и наборы данных в ответах завершаются последовательностью "\r\n" (CRLF). Первый символ ответа указывает на тип возвращаемого значения. Тут ":" - число, "-" - сообщение об ошибке, "+" - однострочный ответ. Интереснее ответы, начинающиеся с "$". Это, так называемые "составные" ответы. Они содержат две строки, первая из которых - число байт, которые нужно прочитать во второй. Такие ответы будем получать наиболее часто, их возвращает команда GET для обычных ключей. Если же ключ необычный, например хэш, то ответ будет "многосоставной". В нём первый символ - "*", затем число результатов в ответе, затем результаты, каждый по две строке в виде составных ответов, начинающихся с "$".

Архитектура
В последней (на сегодня) версии Redis я насчитал более 120 команд. Из них в обычной практике понадобится не более десятка, но хорошая библиотека должна легко расширяться.
Схема работы будет такой: каждая команда будет представлена одним классом, в нём - специфическая логика по формированию запроса и интерпретации ответа. Сам метод, который пишет/читает данные будет реализован в их общем предке. Все классы будут реализовывать один и тот же абстрактный метод. Приложение о классах команд ничего знать не должно, для него у нас будет Factory, которое будет принимать запрос, подгружать в рантайме нужный класс, создавать его инстанс, инициализировать его (например отдавать ему уже открытый сокет) и дёргать его стандартный метод. В коде это выглядит так:

  1. public class RedisFactory {
  2.   
  3.   private Socket s;
  4.   public static final String COMMAND_AUTH = "redisui.connector.command.Auth";
  5.   public static final String COMMAND_KEYS = "redisui.connector.command.Keys";
  6.   public static final String COMMAND_GET = "redisui.connector.command.Get";
  7.   public static final String COMMAND_TYPE = "redisui.connector.command.Type";
  8.   public static final String COMMAND_HGETALL = "redisui.connector.command.Hgetall";
  9.   public static final String COMMAND_DEL = "redisui.connector.command.Del";
  10.   
  11.   public void connect(String host, String port, String pass) throws Exception {
  12.     String[] params = {host, port, pass};
  13.     exec(COMMAND_AUTH, params);
  14.   }
  15.   
  16.   public Response exec(String cmd, String[] params) throws Exception {
  17.     Class<? extends CommandBased> command = Class.forName(cmd).asSubclass(CommandBased.class);
  18.     CommandBased ci = command.newInstance();
  19.     ci.setS(this.s);
  20.     Response r = ci.exec(params);
  21.     this.s = ci.getS();
  22.     return r;
  23.   } 
  24. }
Так, во имя стандартизации мы параметры в запрос передаём как массив строк, а ответ получаем в виде такого себе объекта-оборотня Response:

  1. public class Response {
  2.  
  3.   private String type;
  4.   private byte[] out;
  5.   public static final String TYPE_LINE = "+";
  6.   public static final String TYPE_ERROR = "-";
  7.   public static final String TYPE_INT = ":";
  8.   public static final String TYPE_STRING = "$";
  9.   public static final String TYPE_ARRAY = "*";
  10.  
  11.   public Response(String type, byte[] out) {
  12.     this.type = type;
  13.     this.out = out;
  14.   }
  15.  
  16.   public String getLineOut() {
  17.     ...
  18.   }
  19.   
  20.   public int getIntOut() {
  21.     ...
  22.   }
  23.   
  24.   public String getStringOut() {
  25.     ...
  26.   }
  27.  
  28.   public String[] getArrayOut() {
  29.     ...
  30.   }
  31.  
  32.   public String getType() {
  33.     return type;
  34.   }
  35. }
На стороне приложения мы либо знаем, какого типа будет ответ и в таком случае вызываем нужный getter, либо обрабатываем все варианты, получая тип конкретного ответа из getType().
И, наконец, пример класса команды:

  1. public class Get extends CommandBased {
  2.  
  3.   @Override
  4.   public Response exec(String[] params) throws Exception {
  5.     String command = "get "+params[0]+"\r\n";
  6.     Response res = interrupt(command);
  7.     if (res.getType().equals(Response.TYPE_ERROR)) {
  8.       throw new Exception(res.getLineOut());
  9.     }
  10.     return res;
  11.   }
  12. }
Как видим, добавлять новые команды в такую библиотеку - одно удовольствие.

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

2 комментария:

  1. Ваш сайт http://multipi.net/ не работает(

    ОтветитьУдалить
  2. Больше двух лет прошло. Тот проект потерял актуальность. Но специально для желающих разобраться в вопросе подробнее, я выгрузил RedisUI на GitHub. Клонировать и доработать при желании проект вы сможете отсюда: https://github.com/smmarat/RedisUI

    ОтветитьУдалить