Пиксельные войны: как создать свой Pixelflut-сервер на D

В этой статье, мы покажем вам, как можно в домашних условиях развязать настоящую пиксельную войну, используя компилятор D, домашнюю локальную сеть и креативную фантазию при сильном избытке досуга. Все, что нам потребуется — один свободный компьютер, который не жалко использовать как простой сервер, а также устройства, которые к нему подключаются и несколько человек, чтобы было веселее.

Если вам стало интересно, то добро пожаловать в эту занимательную статью.

Для начала, расскажем немного теории.

В двух словах, мы собираемся организовать простой Pixelflut-сервер своими силами. Простой, в данном случае, не означает, что он лишен какой-то части функционала или то, что он является учебным (т.е. тренировочным материалом). Отнюдь нет. Наш Pixelflut-сервер будет полнофункциональным, а простота в данном случае обозначает лишь то, что его можно просто создать менее чем за один вечер свободного времени.

Pixelflut — это два слова из немецкого языка; причем первое понятно носителю любого языка, а вот второе аналогично по смыслу английскому flood, которое буквально означает наводнение, поток однообразных сообщений приличного объема. Получается, что Pixelflut — это дословно «поток пикселей» или «наводнение пикселей», массированная пиксельная атака или же пиксельная война.

Конечно, это не совсем верно, и это может быть не то, что вы подумали.

Pixelflut — это простой текстовой сетевой протокол, который позволяет нескольким участникам одновременно рисовать пикселями на огромном холсте (т.е в идеале, на огромном экране). Также, нам попадалось и определение Pixelflut, которое описывает его, как «двумерную многопользовательскую игру по рисованию на холсте» (без шуток) — и именно, это описание максимально полно передает суть протокола: он разработан для того, что можно было порисовать с друзьями устроить настоящую пиксель-битву или же сделать настоящую демо-пати.

Протокол очень прост и предполагается, что сервер, который его реализует должен отвечать на следующие сообщения, которые подаются «как есть» (т.е. в текстовом виде):

  • SIZE — запрос размеров холста, т.е. доступного визуального пространства, на которое предполагается наносить пиксели. В ответ на такой запрос сервер отвечает сообщением вида SIZE <x> <y>, в котором в простом текстовом виде сообщает размеры холста.
  • PX <x> <y> — запрос на получение цвета пикселя, где <x> и <y> — абсолютные (в пределах холста) координаты интересующего пикселя. В ответ на такой запрос сервер отвечает сообщением вида PX <x> <y> <rrggbb>, где <rrggbb> — это шестнадцатеричная запись цвета в формате RGB. Такая же запись используется в HTML.
  • PX <x> <y> <rrggbb> — запрос на изменение цвета интересующего пикселя. Данное сообщение фактически совпадает с сообщением, которое сервер Pixelflut отправляет в ответ на запрос цвета пикселя и формат у него точно такой же. В ответ на данное сообщение сервер ничего не отправляет, но помещает пиксель нужного цвета по нужным координатам.
  • HELP — запрос списка поддерживаемых команд. В ответ на этот запрос сервер обязни предоставить текстовую справку со списком доступных команд.

У протокола нет какого-либо выделенного порта, а также нет никакого шифрования — все данные отправляются простым текстом, который закодирован как байты в формате ASCII. Также утверждается, что некоторые реализации могут поддерживать отправку нескольких команд в одном сообщении, просто раздлив их символом новой строки. Такое крайне короткое описание позволяет достаточно быстро реализовать свой сервер и при необходимости дополнить его своими командами (не рекомендуем так делать!).

Реализация на D

Наша реализация использует обычную систему команд Pixelflut, не предполагая отправки несколько команд в одном запросе. Это не делает реализацию не полнофункциональной, а значительно упрощает ее, делая совместимой с базовым описание протокола: если кому-то требуется несколько команд, то мы оставляем это как упражнение по созданию своей реализации Pixelflut. Также наша версия Pixelflut для большей простоты использует типовой сервер с использованием сокетов из std.socket, а в качестве графической части, реализующей холст, используется библиотека из реестра dub под названием turtle.

Полный код реализации представлен следующим скриптом для dub в режиме однофайловой сборки:

#!/usr/bin/env dub
/+ dub.sdl:
	dependency "turtle" version="~>0.0.11"
+/

import std.algorithm : remove;
import std.conv;
import std.socket;
import std.string;

import std.stdio;

import turtle;

enum ADDRESS = "10.0.0.159";
enum PORT    = 7777;
enum BACKLOG  = 10;
enum MAXIMAL_NUMBER_OF_CONNECTIONS = 60;

enum HELP_TEXT = 
`
HELP Returns a short introductional help text
SIZE Returns the size of the visible canvas in pixel as SIZE <w> <h>
PX <x> <y> Return the current color of a pixel as PX <x> <y> <rrggbb>
PX <x> <y> <rrggbb>: Draw a single pixel at position (x, y) with the specified hex color code
QUIT Shutdown Pixelflut server

`;

void main(string[] args)
{
    auto pf = new PixelflutCanvas;
    runGame(pf);
}

class PixelflutCanvas : TurtleGame
{
	private 
	{
		Socket 		listener;
		Socket[]    readableSockets;
		SocketSet 	sockets;
		
		ubyte[8192] _buffer;
		uint _width;
		uint _height;
	}
	
    override void load()
    {
		listener = new Socket(AddressFamily.INET, SocketType.STREAM);
		listener.bind(new InternetAddress(ADDRESS, PORT));
		listener.listen(BACKLOG);
		
		sockets = new SocketSet(MAXIMAL_NUMBER_OF_CONNECTIONS + 1);
        
        setBackgroundColor(
			color("#00000000") 
		);
    }

    override void update(double dt)
    {
        if (keyboard.isDown("escape")) 
        {
			exitGame;
			scope(exit) {
				listener.close;
			}
		}
    }

    override void draw()
    {
		ImageRef!RGBA fb = framebuffer();
		
		_width = fb.w;
		_height = fb.h;
		
        sockets.add(listener);

        foreach (socket; readableSockets)
        {
            sockets.add(socket);
		}

        Socket.select(sockets, null, null);

        for (size_t i = 0; i < readableSockets.length; i++)
        {
            if (sockets.isSet(readableSockets[i]))
            {
                ubyte[8912] inBuffer;
                
                auto realBufferSize = readableSockets[i].receive(inBuffer);

                
				if (realBufferSize != 0)
                {
					auto query = cast(string) inBuffer[0..realBufferSize];
					if (query != "")
					{
						auto r = query.strip.split(" ");
						
						switch (r[0])
						{
							case "SIZE":
								readableSockets[i].send(
										cast(ubyte[]) format(`SIZE %d %d`, _width, _height)
								);
								break;
							case "PX":
								auto x = parse!int(r[1]);
								auto y = parse!int(r[2]);
								
								if (r.length > 3)
								{
									auto rgb = strip(r[3]);
									fb[x, y] = color("#" ~ rgb);
								}
								else
								{
									readableSockets[i].send(
										cast(ubyte[]) format(`PX %d %d %s`,x, y, fb[x, y].toHex)
									);
								} 
								break;
							case "HELP":
								readableSockets[i].send(
										cast(ubyte[]) HELP_TEXT
								);
								break;
							case "QUIT":
								super.exitGame;
								break;
							default:
								break;
						}
					}
                }
				

                readableSockets[i].close;
                readableSockets = readableSockets.remove(i);
                i--;
            }
        }

        if (sockets.isSet(listener))
        {
            Socket currentSocket = null;
            
            scope (failure)
            {
                if (currentSocket)
                {
                    currentSocket.close;
				}
            }
            
            currentSocket = listener.accept;
            
            if (readableSockets.length < MAXIMAL_NUMBER_OF_CONNECTIONS)
            {
                readableSockets ~= currentSocket;
            }
            else
            {
                currentSocket.close;
            }
        }

        sockets.reset;
	}
}

Но, это только сервер, а где взять клиент для испытаний?

Вся прелесть Pixelflut в том, что для работы с ним необязательно иметь клиент. Для испытаний достаточно лишь стандартной утилиты наподобие netcat или telnet, которая умеет передавать нужные данные по сетевому соединению.

Вот примеры запросов к серверу (предварительно рекомендуем поменять адрес и порт в коде сервера) через netcat:

echo "SIZE" | nc 10.0.0.159 7777
echo "PX 12 13" | nc 10.0.0.159 7777
echo "PX 12 13 00ffee" | nc 10.0.0.159 7777

Отрисовка картинок по сети с помощью dlib

Однако, можно пойти еще дальше и используя netcat, а также небольшой скрипт на D с использованием нашей любимой библиотеки обработки изображений dlib, можно по сети рисовать целые изображения.

Пример скрипта для вывода изображений на холст сервера Pixelflut:

#!/usr/bin/env dub
/+ dub.sdl:
    dependency "dlib" version="~>0.23.0"
+/

import std.process;
import std.stdio;
import std.string;

import dlib.image;

// вывод цвета в HTML-формате
auto packColor(Color4f color)
{
    auto r = cast(uint) (255.0f * color.r);
    auto g = cast(uint) (255.0f * color.g);
    auto b = cast(uint) (255.0f * color.b);

  	// цвет из Color4f в hex-формат (без прозрачности)
    int createRGB(uint r, uint g, uint b)
    {
        return ((r & 0xff) << 16) + ((g & 0xff) << 8) + (b & 0xff);
    }

  	// цвет из Color4f в hex-формат (с прозрачностью)
    int createRGBA(int r, int g, int b, int a)
    {
        return ((r & 0xff) << 24) + ((g & 0xff) << 16) + ((b & 0xff) << 8) + (a & 0xff);
    }

    return createRGB(r, g, b);
}

void main()
{
    // загрузка нужной картинки
    auto img = loadImage(`/home/aquareji/Загрузки/lenna.jpg`);
    
    foreach (x; 0..img.width)
    {
        foreach (y; 0..img.height)
        {
            auto X = 250 + x;
            auto Y = 250 + y;

            auto cmd = `echo "PX %d %d %08x" | nc 10.0.0.159 7777`.format(X, Y, packColor(img[x, y]));
            executeShell(cmd);
        }
    }
}

Результат на сервере Pixelflut:

Отрисовка идет неторопливо…

Поверьте, результат очень радует :)

Используемые материалы

aquaratixc

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

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