Погружаемся в воды Стикса. Часть II: раздаем пустую папку.

В первой части нашей трилогии мы рассказали вам о протоколе 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), который может быть использован в ваших проектах.

aquaratixc

Программист-самоучка и программист-любитель

Добавить комментарий