Зачем нужен Thrift и основы работы с ним на Scala
28 сентября 2015
Thrift — это такая штука для сериализации данных. Вы описываете схему данных в специальном формате. Из этого описания генерируются классы. Эти классы легко сериализуются и десериализуются. При этом схему можно изменять (например, добавлять-удалять поля в классах) так, что данные, сериализованные по старой схеме, будут успешно десериализованы по новой. Одну и ту же схему можно использовать в проектах на разных языках, и они будут успешно друг с другом взаимодействовать. Плюс к этому еще накручена возможность объявлять исключения и генерировать код для RPC. В этой заметке мы разберемся, как работать с Thrift на языке Scala.
Во-первых, нам понадобятся следующие зависимости:
"com.twitter" %% "scrooge-core" % "3.20.0",
Во-вторых, в project/plugins.sbt дописываем:
… а в build.sbt:
Таким образом мы подключили к проекту Scrooge-плагин и можем генерировать Scala-классы из thrift-файлов командой:
Более того, генерация будет происходить автоматически при сборке проекта, в том числе при выполнении команды sbt assembly
.
Далее создаем src/thrift/game.thrift следующего содержания:
namespace java me.eax.examples.thrift.game
// exception Ololo { ... }
// service Ololo { ... }
enum Weapon {
Sword = 1
Bow = 2
}
struct WarriorInfo {
1: optional Weapon weapon
2: required i64 arrowsNumber
}
enum Spell {
Fireball = 1
Thunderbolt = 2
}
struct MageInfo {
1: required set<Spell> spellbook
2: required i64 mana
}
union ClassSpecificInfo {
1: WarriorInfo warrior
2: MageInfo mage
}
struct Hero {
1: required string name
2: required i64 hp
3: required i64 xp
4: ClassSpecificInfo classSpecificInfo
}
Мне нужно было придумать пример, демонстрирующий использование required и optional полей, enum’ов, контейнеров типа set и map, а также, как при помощи Thrift получить алгебраические типы. В итоге придумались такие вот классы для RPG-игры. Мне кажется, тут все довольно очевидно, так что двигаемся дальше.
Примеры использования сгенерированных классов:
val mage = Hero(
name = "afiskon", hp = 25L, xp = 1024L,
ClassSpecificInfo.Mage(MageInfo(spellbook, mana = 100L))
)
val warrior = Hero(
name = "eax", hp = 50L, xp = 256L,
ClassSpecificInfo.Warrior(WarriorInfo(Some(Weapon.Sword), 0L))
)
Для тестирования сериализации и десериализации мной были написаны тесты с использованием ScalaCheck. Вот один из них:
val bytes = {
val out = new ByteArrayOutputStream()
data1.write(new TBinaryProtocol(new TIOStreamTransport(out)))
out.toByteArray
}
val data2 = {
val stream = new ByteArrayInputStream(bytes)
Hero.decode(new TBinaryProtocol(new TIOStreamTransport(stream)))
}
data1 shouldBe data2
}
Заметьте, что ScalaCheck откуда-то знает, как генерировать случайных героев. Это потому что для класса Hero мной специально был написан генератор. Генераторы в ScalaCheck пишутся очень просто. Выглядит это примерно так:
for {
name <- Arbitrary.arbitrary[String]
hp <- Gen.posNum[Long]
xp <- Gen.posNum[Long]
classSpecificInfo <- Arbitrary.arbitrary[ClassSpecificInfo]
} yield Hero(name, hp, xp, classSpecificInfo)
)
Генераторы для ClassSpecificInfo.Mage, ClassSpecificInfo.Warrior и, так сказать, «корневого» ClassSpecificInfo выглядят аналогично.
Обратите внимание, что выше приводится тест для TBinaryProtocol. Всего Thrift поддерживает четыре так называемых протокола сериализации:
- TBinaryProtocol — обычный бинарный протокол;
- TCompactProtocol — компактный бинарный протокол;
- TJSONProtocol — очень сложно читаемый JSON;
- TSimpleJSONProtocol — легко читаемый JSON, но без десериализации;
Использование этих протоколов выглядит аналогично. Например:
val out = new ByteArrayOutputStream()
hero.write(new TSimpleJSONProtocol(new TIOStreamTransport(out)))
new String(
ByteBuffer.wrap(out.toByteArray).array(),
StandardCharsets.UTF_8
)
}
Еще одна интересная фишка Thrift — возможность вручную закодировать список, map или set объектов:
val bytes = {
val out = new ByteArrayOutputStream()
val proto = new TBinaryProtocol(new TIOStreamTransport(out))
proto.writeListBegin(new TList(TType.STRUCT, data1.size))
data1.foreach(_.write(proto))
proto.writeListEnd()
out.toByteArray
}
val data2 = {
val stream = new ByteArrayInputStream(bytes)
val proto = new TBinaryProtocol(new TIOStreamTransport(stream))
val listInfo = proto.readListBegin()
val res = {
for(_ <- 1 to listInfo.size) yield Hero.decode(proto)
}.toList
proto.readListEnd()
res
}
data1 shouldBe data2
}
Вот, по большому счету, все, что я хотел рассказать сегодня про Thrift. Более подробную информацию вы найдете по следующим ссылкам:
- Хорошая статья «Thrift: The Missing Guide»;
- IRC-каналы и списки рассылок, посвященные Thrift;
- Полная версия исходников к этому посту;
А что вы используете для сериализации и десериализации?
Дополнение: Заметки Сериализация и десериализация в/из Protobuf на C++ и Сериализация в языке Go на примере библиотеки codec также могут быть вам интересны.
Метки: Scala, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.