В первой части нашей трилогии мы рассказали вам о протоколе 9P/Styx и его устройстве, а также про нашу библиотеку styx2000. Сейчас же мы хотели бы вам показать пример работы с библиотекой на примере пустой папки, которую мы будем раздавать с помощь протоколу 9P/Styx.
Раздача пустой папки ? В чем смысл этого, казалось бы, бессмысленного действия ? Как ни странно, раздача пустой папки поможет показать минимальный набор операций, которые требуется реализовать для работы простого 9P/Styx-сервера, а также позволит пролить свет на процесс взаимодействия клиента и сервера.
Для начала создадим проект dub и добавим в зависимости библиотеку styx2000, в версии main, а не последней стабильной ветки:
dub init empty_folder
После этого, для работы всей сетевой части проекта добавим несколько модифицированную версию сервера, который использовался для создания Gopher-сервера. В данном случае модификация потребуется для того, чтобы сервер не сбрасывал подключение после запросов клиента, а держал бы его открытым. Для этой цели добавляем специальный флаг immediate, который позволит менять поведение сервера в части “удержания” клиента:
module empty_folder.server;
private {
import std.algorithm : remove;
import std.socket;
}
class GenericSimpleServer(uint BUFFER_SIZE, uint MAXIMAL_NUMBER_OF_CONNECTIONS)
{
protected {
ubyte[BUFFER_SIZE] _buffer;
string _address;
ushort _port;
bool _immediate;
Socket _listener;
Socket[] _readable;
SocketSet _sockets;
}
abstract ubyte[] handle(ubyte[] request);
final void run()
{
while (true)
{
serve;
scope(failure) {
_sockets = null;
_listener.close;
}
}
}
final void setup4(string address, ushort port, int backlog = 10, bool immediate = false)
{
_address = address;
_port = port;
_listener = new Socket(AddressFamily.INET, SocketType.STREAM);
_immediate = immediate;
with (_listener)
{
bind(new InternetAddress(_address, _port));
listen(backlog);
}
_sockets = new SocketSet(MAXIMAL_NUMBER_OF_CONNECTIONS + 1);
}
final void setup6(string address, ushort port, int backlog = 10, bool immediate = false)
{
_address = address;
_port = port;
_listener = new Socket(AddressFamily.INET6, SocketType.STREAM);
_immediate = immediate;
with (_listener)
{
bind(new Internet6Address(_address, _port));
listen(backlog);
}
_sockets = new SocketSet(MAXIMAL_NUMBER_OF_CONNECTIONS + 1);
}
private
{
final void serve()
{
_sockets.add(_listener);
foreach (socket; _readable)
{
_sockets.add(socket);
}
Socket.select(_sockets, null, null);
for (uint i = 0; i < _readable.length; i++)
{
if (_sockets.isSet(_readable[i]))
{
auto realBufferSize = _readable[i].receive(_buffer);
if (realBufferSize != 0)
{
auto data = _buffer[0..realBufferSize];
_readable[i].send(
handle(data)
);
}
if (_immediate)
{
_readable[i].close;
_readable = _readable.remove(i);
i--;
}
}
}
if (_sockets.isSet(_listener))
{
Socket currentSocket = null;
scope (failure)
{
if (currentSocket)
{
currentSocket.close;
}
}
currentSocket = _listener.accept;
if (_readable.length < MAXIMAL_NUMBER_OF_CONNECTIONS)
{
_readable ~= currentSocket;
}
else
{
currentSocket.close;
}
}
_sockets.reset;
}
}
}Экспериментальный 9P/Styx-сервер, который мы будем реализовать, будет производным от класса GenericSimpleServer и потому, единственное, что нужно сделать в нашем случае – это перегрузить метод handle у класса-наследника. В случае любого 9P/Styx-сервера можно создать перегрузку метода handle, который принимая запрос от клиента в виде потока байтов, обрабатывает его и возвращает ответ, также в виде потока байтов. Это выглядит так:
override ubyte[] handle(ubyte[] request)
{
import std.stdio : writeln;
auto r = decode(request);
auto q = process(r);
writeln(`-> `, r.toPlan9Message);
writeln(`<- `, q.toPlan9Message);
return encode(q);
}По сути, схема работы достаточно общая и разные реализации будут отличаться только функционалом, который заключен в метод process. Этот метод получает на вход аргумент типа StyxMessage (определен в модуле styx2000.extrautil.styxmessage и представляет собой массив из объектов протокола, которые описывают структуру некоторого сообщения) и выдает результат того же типа. Таким образом, в process осуществляется вся обработка поступающих сообщений 9P/Styx, а методы decode/encode (находятся в модуле styx2000.protomsg) служат для декодирования потока байтов в сообщения 9P/Styx и наоборот. Метод toPlan9Message позволяет отображать сообщения в текстовом виде, точно таком же, в котором выдают отладочную информацию утилиты из комплекта Plan9Port или из самой операционной системы Plan 9. Данный метод не является обязательным и служит для целей наблюдения за работой сервера и находится в модуле styx2000.extrautil.mischelpers.
Осталось разобраться в логике работы метода process, который в данном случае выглядит так:
StyxMessage process(StyxMessage q) {
StyxMessage r;
STYX_MESSAGE_TYPE type = q[1].toType.getType;
ushort tag = q[2].toTag.getTag;
switch (type)
{
case STYX_MESSAGE_TYPE.T_VERSION:
r = createRmsgVersion(tag);
break;
case STYX_MESSAGE_TYPE.T_ATTACH:
r = createRmsgAttach(tag, STYX_QID_TYPE.QTDIR, 0, hash8(_dir));
break;
case STYX_MESSAGE_TYPE.T_WALK:
string[] name = q[5].toNwname.getName;
r = (name != []) ? createRmsgError(tag, `File does not exists`) : createRmsgWalk(tag);
break;
case STYX_MESSAGE_TYPE.T_STAT:
r = makeStat(tag);
break;
case STYX_MESSAGE_TYPE.T_OPEN:
r = createRmsgOpen(tag, STYX_QID_TYPE.QTDIR, 0, hash8(_dir));
break;
case STYX_MESSAGE_TYPE.T_READ:
r = createRmsgRead(tag);
break;
case STYX_MESSAGE_TYPE.T_CLUNK:
r = createRmsgClunk(tag);
break;
default:
break;
}
return r;
}В начале самого метода, который принимает в качестве аргумента объект типа StyxMessage, создается также объект типа StyxMessage, в который будет помещаться сформированный ответ. Далее из переданного объекта (будем далее называть его запросом) извлекается тип сообщения протокола 9P/Styx: сначала выделяется 2 элемент массива (напоминаем, что в начале каждого сообщения протокола есть три обязательных поля – размер, тип сообщения и его тег), затем выделенный объект типа StyxObject (это общий тип для всех объектов протокола в библиотеке styx2000 и находиться он в модуле styx2000.protobj.styxobject) приводится с помощью функции-хэлпера toType (таких хэлперов много и каждый под свой тип объекта, а находятся все они в styx2000.extrautil.casts) к типу Type (находится в модуле styx2000.protobj.type) и далее из полученного результата, получается объект STYX_MESSAGE_TYPE (отвечает за тип сообщения протокола). Данный объект с типом сообщения будет использоваться для обработки всего сообщения, переданного в метод process, а также для идентификации возможных типов сообщений и формировании ответа на каждый конкретный тип сообщения.
Следующим шагом, в методе извлекается тег, точно таким же способом, как ранее извлекался тип сообщения. Далее тип и тег нам нужны будут для правильного формирования ответных сообщений от сервера к клиенту…
В блоке switch мы реализуем разбор минимально необходимого количества сообщений разных типов:
- T_VERSION – сообщение с описанием версии протокола, которое также служит способом согласования связи клиента и сервера. В данном случае мы используем одну из специальных функций-хэлперов из модуля styx2000.extrautil.styxmessage под названием createRmsgVersion, которая формирует корректное сообщение с упомянутым выше типом и генерирует объект типа StyxMessage. По умолчанию у данной функции заданы определенные аргументы и их три: тег, максимальный размер сообщения и версия протокола, но в данном случае, требуется задать только тег, поскольку именно тег идентифицирует конкретное сообщение, а остальные значения стандартные константы (размер – 8192, а версия протокола – константа STYX_VERSION, которая берется из модуля styx2000.protoconst и которая равна строковому значению “9P2000”). В следующих этапах разбора мы также будем применять функции из модуля styx2000.extrautil.styxmessage.
- T_ATTACH – сообщение, которое позволяет серверу понять с каким файловым деревом будет работать клиент, иными словами, это сообщение позволяет прикрепить клиента к нужной файловой иерархии. Данное сообщение формируется с помощью функции-хэлпера createRmsgAttach, которая принимает в качестве аргументов тег, тип идентификатора qid файлового дерева (файл, папка или что-то иное, описано в модуле styx2000.protoconst.qids), версию и путь (точно такие же аргументы нужны для формирования qid). Поскольку, мы раздаем пустую папку, то в функцию-хэлпер мы передаем тег сообщения, тип идентификатора – STYX_QID_TYPE.QTDIR (так как это папка, а не файл), версия – 0 (так как версия говорит о модификации папки или файла), а качестве пути используется 8-байтный хэш в виде единого числа от имени пустой папки (имя папки находится в переменной _dir, об этом мы расскажем чуть позже). Сама функция хэширования hash8 взята из модуля styx2000.extrautil.siphash.
- T_WALK – сообщение, которое позволяет перейти в нужную папку или осуществить продвижение в файловой иерархии (или найти некоторый файл/папку). Для разбора входного сообщения требуется извлечь путь, по которому осуществляется продвижение, а это 6 по счету аргумент во входном сообщении, и извлечение этих данных идет по такой же схеме, как и извлечение типа сообщения или тега. Далее после извлечения пути, проверяется равен ли путь пустому массиву (поскольку наша папка пуста и в ней нет элементов) и если это не так, то формируется сообщение с типом R_ERROR с помощью функции-хэлпера createRmsgError; если же путь и правда оказывается пустым, то формируется сообщение T_WALK с помощью createRmsgWalk. Эта функция принимает в качестве аргумента тег и список qid’ов для каждого элемента пути, но поскольку элементы пути пусты, то передается только тег.
- T_STAT – сообщение, которое позволяет получить информацию о заданном элементе файлового дерева и которое содержит все метаданные по некоторому пути: имя папки или файла, права доступа, размер, время последнего изменения и время последнего доступа, а также наименование группы владельца и т.д. Это сообщение самое сложное для формирования и сам объект протокола представляющий статистику (будем называть так всю информацию об объекте файловой иерархии) с типом Stat содержит множество разных полей, поэтому формированием данного объекта и размещением его в сообщении 9P/Styx занимается функция makeStat. В функции makeStat сначала формируется пустой объект типа StyxMessage, в который затем помещается сформированный объект типа Size (размер сообщения, пока что нулевой), затем помещается объект типа Type (с типом R_STAT) и соответствующим тегом. После того, как начальный заголовок сообщения сформирован, формируется идентификатор файлового дерева (qid), а затем создается объект типа Perm, который описывает права доступа и устанавливает их методом setPerm (из модулей styx2000.protobj.perm и styx2000.protoconst.permissions). Модуль styx2000.protoconst.permissions содержит константы, которые описывают права на чтение (READ), запись (WRITE) и исполнение (EXEC) для разных групп владельцев: владельца (OWNER), его группы (GROUP) и остальных (OTHERS). Данные константы описывают точно такую же систему прав доступа, которая принята в Unix-системах, и эти константы точно также складываются (с помощью побитового или |), что и применяется в формировании объекта Perm. Далее формируется объект Stat, в конструктор которого передаются тип и устройство (связано с особенностями архитектуры Plan 9, но экспериментальным способом мы нашли приемлемые значения: тип – 77, а устройство – 4), qid, права доступа, время доступа и время модификации (берутся из внутренней переменной класса _time), размер в байтах (для папок он по соглашению равен 0), наименование файла/папки (берется из внутренней переменной класса _dir), наименование пользователя-владельца (переменная _uid), наименование группы владельца (переменная _gid) и наименование группы остальных пользователей (пустая строка, поскольку для обработки это не имеет значения). Когда формирование объекта Stat закончится, он размещается в сообщении 9P/Styx с типом R_STAT.
- T_OPEN – сообщение, которое позволяет открыть некоторый путь для последующих операций. Данный вид сообщений формируется на основании тега из запроса, типа файлового идентификатора qid, версии и пути, аналогично тому, как это было в сообщении T_ATTACH, но с помощью иной функции-хэлпера createRmsgOpen, который имеет почти такую же сигнатуру как и createRmsgAttach.
- T_READ – сообщение, которое позволяет открыть и считать данные из некоторого файла, или получить содержимое некой папки. В нашем случае, данное сообщение нужно для получения сведений о содержимом папки, но поскольку содержимого нет, то функция-хэлпер createRmsgRead, которая формирует корректное сообщение, принимает лишь один аргумент – тег.
- T_CLUNK – сообщение, которое позволяет объявить некоторый fid (это файловый идентификатор на стороне клиента), что равносильно прекращению дальнейшей работы с файлом/папкой. Именно таким сообщением заканчивается вся работа в сессии (т.н транзакции), и именно это сообщение свидетельствует о конце обмена данными между сервером и клиентом, и единственное что требуется для этого сообщения – это тег. Функция createRmsgClunk, которая формирует сообщение принимает только один аргумент, и других аргументов у нее нет.
Код всего сервера с учетом вышеупомянутой механики выглядит следующим образом (сам код находится в файле emptydir.d):
module empty_folder.emtydir;
private {
import std.datetime.systime : Clock;
import styx2000.extrautil.casts : toNwname, toType, toTag;
import styx2000.extrautil.siphash : hash8;
import styx2000.extrautil.styxmessage;
import styx2000.extrautil.mischelpers : toPlan9Message;
import styx2000.protoconst.messages;
import styx2000.protoconst.qids;
import styx2000.protomsg : decode, encode;
import styx2000.protobj;
import empty_folder.server;
}
class EmptyDir : GenericSimpleServer!(8_192, 60)
{
private {
string _dir;
string _uid;
string _gid;
uint _time;
StyxMessage makeStat(ushort tag)
{
StyxMessage s;
s ~= new Size;
s ~= new Type(STYX_MESSAGE_TYPE.R_STAT);
s ~= new Tag(tag);
// server identifier for directory
Qid qid = new Qid(
STYX_QID_TYPE.QTDIR,
0,
hash8(_dir)
);
// chmod 755 for dir
Perm perm = new Perm;
perm.setPerm(
STYX_FILE_PERMISSION.DMDIR |
STYX_FILE_PERMISSION.OWNER_EXEC |
STYX_FILE_PERMISSION.OWNER_READ |
STYX_FILE_PERMISSION.OWNER_WRITE |
STYX_FILE_PERMISSION.GROUP_READ |
STYX_FILE_PERMISSION.GROUP_WRITE |
STYX_FILE_PERMISSION.GROUP_EXEC |
STYX_FILE_PERMISSION.OTHER_READ |
STYX_FILE_PERMISSION.OTHER_EXEC
);
Stat stat = new Stat(
// type and dev for kernel use (taken from some experiments with styxdecoder, see above)
77, 4,
qid,
// permissions
perm,
// access time
_time,
// modification time
_time,
// conventional length for all directories is 0
0,
// file name (this, directory name)
_dir,
// user name (owner of file)
_uid,
// user group name
_gid,
// others group name
""
);
s ~= stat;
return s;
}
StyxMessage process(StyxMessage q) {
StyxMessage r;
STYX_MESSAGE_TYPE type = q[1].toType.getType;
ushort tag = q[2].toTag.getTag;
switch (type)
{
case STYX_MESSAGE_TYPE.T_VERSION:
r = createRmsgVersion(tag);
break;
case STYX_MESSAGE_TYPE.T_ATTACH:
r = createRmsgAttach(tag, STYX_QID_TYPE.QTDIR, 0, hash8(_dir));
break;
case STYX_MESSAGE_TYPE.T_WALK:
string[] name = q[5].toNwname.getName;
r = (name != []) ? createRmsgError(tag, `File does not exists`) : createRmsgWalk(tag);
break;
case STYX_MESSAGE_TYPE.T_STAT:
r = makeStat(tag);
break;
case STYX_MESSAGE_TYPE.T_OPEN:
r = createRmsgOpen(tag, STYX_QID_TYPE.QTDIR, 0, hash8(_dir));
break;
case STYX_MESSAGE_TYPE.T_READ:
r = createRmsgRead(tag);
break;
case STYX_MESSAGE_TYPE.T_CLUNK:
r = createRmsgClunk(tag);
break;
default:
break;
}
return r;
}
}
this(string dir, string uid, string gid)
{
_uid = uid;
_gid = gid;
_dir = dir;
_time = cast(uint) Clock.currTime.toUnixTime;
}
override ubyte[] handle(ubyte[] request)
{
import std.stdio : writeln;
auto r = decode(request);
auto q = process(r);
writeln(`-> `, r.toPlan9Message);
writeln(`<- `, q.toPlan9Message);
return encode(q);
}
}Использовать данный сервер просто (файл app.d): достаточно сформировать объект класса EmptyDir, передать в него имя папки, имя пользователя-владельца и наименования группы владельца (т.е это те самые внутренние переменные _dir, _uid, _gid) и установить сетевой адрес/порт для сервера:
private {
import empty_folder.emtydir;
}
void main()
{
EmptyDir d = new EmptyDir(`test`, `lhs`, `lhs-user`);
d.setup4("127.0.0.1", 4444);
d.run;
}Испытать сервер можно следующим образом, используя утилиту 9p и удаленный компьютер, на котором будет запущен сервер, раздающий пустую папку:
9p -D -n -a 'tcp!127.0.0.1!4444' ls 9p -D -n -a 'tcp!127.0.0.1!4444' ls -lhd
Примечание автора. Команда 9p – это тривиальный клиент протокола 9p/Styx из комплекта утилит plan9port, который очень удобен для применения в скриптах. Опция -D программы говорит о том, что требуется отладочный вывод сообщений протокола, опция -n – говорит о том, что будет использоваться подключение без аутентификации, опция -a – используется для указания того, что будет использован сетевой адрес, а не локальный файл или сокет.
Все это выглядит следующим образом (компьютер с клиентом 9p):

С точки зрения сервера, точно такая же сессия, выглядит следующим образом:

На этом раздача пустой папки закончилась, и тот же самый подход можно применить и для раздачи файлов по протоколу 9P/Styx, или же для создания полноценных файловых серверов. Мы считаем, что приведенный нами небольшой обзор библиотеки styx2000 и ее модулей, поможет вам в дальнейшем в разработке ваших собственных проектов.
P.S: Все рассмотренные функции-хэлперы в статье имеют довольно простую базу в наименовании и имеют вот такую схему построения названий: create<тип сообщения – T или R>msg<наименование типа сообщения>. Данные функции покрывают все типы сообщений, кроме R_STAT и T_WSTAT, которые имеют сложную структуру и должны обрабатываться вручную (т.к в зависимости от ситуации, эти сообщения надо заполнять по разному). Кроме того, в библиотеке можно найти и иного рода функционал (см. модуль styx2000.extrautil), который может быть использован в ваших проектах.