1304 lines
116 KiB
TeX
1304 lines
116 KiB
TeX
%%% Ukázkový text a dokumentace stylu pro text závěrečné (bakalářské a
|
|
%%% diplomové) práce na KI PřF UP v Olomouci
|
|
%%% Copyright (C) 2012 Martin Rotter, <rotter.martinos@gmail.com>
|
|
%%% Copyright (C) 2014 Jan Outrata, <jan.outrata@upol.cz>
|
|
|
|
|
|
%% Pro získání PDF souboru dokumentu je třeba tento zdrojový text v
|
|
%% LaTeXu přeložit (dvakrát) programem pdfLaTeX.
|
|
|
|
%% V případě použití programu BibLaTeX pro tvorbu seznamu literatury
|
|
%% je poté ještě třeba spustit program Biber s parametrem jméno
|
|
%% souboru zdrojového textu bez přípony a následně opět (dvakrát)
|
|
%% přeložit zdrojový text programem pdfLaTeX.
|
|
|
|
%% Postup získání Postscriptového souboru je popsán v dokumentaci.
|
|
|
|
|
|
%% Třída dokumentu implementující styl pro závěrečnou práci. Vybrané
|
|
%% nepovinné parametry (ostatní v dokumentaci):
|
|
|
|
%% 'master' pro sazbu diplomové práce, jinak se sází bakalářská práce
|
|
|
|
%% 'program=kód' pro Váš studijní program/obor (specializaci), kódy
|
|
%% pro diplomovou práci 'infoi' pro Informatiku (Obecná informatika),
|
|
%% 'infui' pro Informatiku (Umělá inteligence), 'ainfpst' pro
|
|
%% Aplikovanou informatiku (Počítačové systémy a technologie), 'uinf'
|
|
%% pro Učitelství informatiky pro střední školy, 'binf' pro
|
|
%% Bioinformatiku, 'inf' pro Informatiku (bez specializací) a 'ainf'
|
|
%% pro Aplikovanou informatiku (bez specializací), jinak je výchozí
|
|
%% ainfvs pro Aplikovanou informatiku (Vývoj software), a pro
|
|
%% bakalářskou práci 'infoi' pro Informatiku (Obecná informatika),
|
|
%% 'itp' pro Informační technologie v prezenční formě, 'itk' pro
|
|
%% Informační technologie v kombinované formě, 'infv' pro Informatiku
|
|
%% pro vzdělávání, 'binf' pro Bioinfomatiku, 'inf' pro Informatiku
|
|
%% (bez specializací), 'ainfp' pro Aplikovanou informatiku (bez
|
|
%% specializací) v prezenční formě, 'ainfk' pro Aplikovanou
|
|
%% informatiku (bez specializací) v kombinované formě, jinak je
|
|
%% výchozí infpvs pro Informatiku (Programování a vývoj software)
|
|
|
|
%% 'printversion' pro sazbu verze pro tisk (nebarevné logo a odkazy,
|
|
%% odkazy s uvedením adresy za odkazem, ne odkazy do rejstříku),
|
|
%% jinak verze pro prohlížeč
|
|
|
|
%% 'biblatex' pro zapnutí podpory pro sazbu bibliografie pomocí
|
|
%% BibLaTeXu, jinak je výchozí sazba v prostředí thebibliography
|
|
|
|
%% 'language=jazyk' pro jazyk práce, jazyky english pro anglický,
|
|
%% slovak pro slovenský, jinak je výchozí czech pro český
|
|
|
|
%% 'font=sans' pro bezpatkový font (Iwona Light), jinak je výchozí
|
|
%% serif pro patkový (Latin Modern)
|
|
|
|
%% 'figures, tables, theorems a sourcecodes' pro sazbu seznamu
|
|
%% obrázků, tabulek, vět a zdrojových kódů, jinak při =false se
|
|
%% nesází (u theorems a sourcecodes výchozí)
|
|
|
|
\documentclass[
|
|
master,
|
|
% program=ainfvs,
|
|
% printversion,
|
|
biblatex,
|
|
% language=english,
|
|
% font=sans,
|
|
figures=false,
|
|
% tables=false,
|
|
% theorems,
|
|
% sourcecodes,
|
|
glossaries,
|
|
index
|
|
]{kidiplom}
|
|
|
|
%\title{Hra}
|
|
%\title[english]{A style for thesis}
|
|
|
|
|
|
|
|
%% Informace pro úvodní strany. V jazyku práce (pokud není v komentáři
|
|
%% uvedeno česky) a anglicky. Uveďte všechny, u kterých není v
|
|
%% komentáři uvedeno, že jsou volitelné. Při neuvedení se použijí
|
|
%% výchozí texty. Text pro jiný než nastavený jazyk práce (nepovinným
|
|
%% parametrem language makra \documentclass, výchozí český) se zadává
|
|
%% použitím makra s uvedením jazyka jako nepovinného parametru.
|
|
|
|
%% Název práce, česky a anglicky. Měl by se vysázet na jeden řádek.
|
|
\title{Metody optimalizace přenosu dat v distribuovaném systému}
|
|
\title[english]{Methods of data transfer optimizations in distributed systems}
|
|
|
|
%% Volitelný podnázev práce, česky a anglicky. Měl by se vysázet na
|
|
%% jeden řádek. Výchozí je prázdný.
|
|
%% \subtitle{Ukázkový text a dokumentace stylu v \LaTeX{}u}
|
|
%% \subtitle[english]{Sample text and documentation of the \LaTeX{} style}
|
|
|
|
%% Jméno autora práce. Makro nemá nepovinný parametr pro uvedení
|
|
%% jazyka.
|
|
\author{Martin Šlachta}
|
|
|
|
%% Jméno vedoucího práce (včetně titulů). Makro nemá nepovinný
|
|
%% parametr pro uvedení jazyka.
|
|
\supervisor{Mgr. Tomáš Urbanec, Ph.D.}
|
|
|
|
%% Volitelný rok odevzdání práce. Výchozí je aktuální (kalendářní)
|
|
%% rok. Makro nemá nepovinný parametr pro uvedení jazyka.
|
|
%\yearofsubmit{\the\year}
|
|
|
|
%% Anotace práce, včetně anglické (obvykle překlad z jazyka
|
|
%% práce). Jeden odstavec!
|
|
\annotation{Ukázkový text závěrečné práce na Katedře informatiky
|
|
Přírodovědecké fakulty Univerzity Palackého v Olomouci, který je
|
|
zároveň dokumentací stylu pro text práce v \LaTeX{}u. Zdrojový text
|
|
v \LaTeX{}u je doporučeno použít jako šablonu pro text skutečné
|
|
závěrečné práce studenta.}
|
|
|
|
\annotation[english]{Sample text of thesis at the \kitextdepten,
|
|
\kitextfacultyen, \kitextuniven{} and, at the same time,
|
|
documentation of the \LaTeX{} style for the text. The source text in
|
|
\LaTeX{} is recommended to be used as a template for real student's
|
|
thesis text.}
|
|
|
|
%% Klíčová slova práce, včetně anglických. Oddělená (obvykle) středníkem.
|
|
\keywords{styl textu; závěrečná práce; dokumentace; ukázkový text}
|
|
\keywords[english]{text style; thesis; documentation; sample text}
|
|
|
|
%% Volitelná specifikace příloh textu práce, i anglicky. Výchozí je
|
|
%% 'elektronická data v systému katedry informatiky / electronic data
|
|
%% in system of department of computer science'.
|
|
%\supplements{nejlepší software všech dob}
|
|
%\supplements[english]{the best software of all times}
|
|
|
|
%% Volitelné poděkování. Stručné! Výchozí je prázdné. Makro nemá
|
|
%% nepovinný parametr pro uvedení jazyka.
|
|
\thanks{Děkuji, děkuji, děkuji.}
|
|
|
|
%% Cesta k souboru s bibliografií pro její sazbu pomocí BibLaTeXu
|
|
%% (zvolenou nepovinným parametrem biblatex makra
|
|
%% \documentclass). Použijte pouze při této sazbě, ne při (výchozí)
|
|
%% sazbě v prostředí thebibliography.
|
|
\bibliography{bibliografie.bib}
|
|
|
|
%% Další dodatečné styly (balíky) potřebné pro sazbu vlastního textu
|
|
%% práce.
|
|
\usepackage{lipsum}
|
|
\usepackage{longtable}
|
|
\usepackage{svg}
|
|
|
|
\begin{document}
|
|
%% Sazba úvodních stran -- titulní, s bibliografickými údaji, s
|
|
%% anotací a klíčovými slovy, s poděkováním a prohlášením, s obsahem a
|
|
%% se seznamy obrázků, tabulek, vět a zdrojových kódů (pokud jejich
|
|
%% sazba není vypnutá).
|
|
\maketitle
|
|
|
|
%% Vlastní text závěrečné práce. Pro povinné závěry, před přílohami,
|
|
%% použijte prostředí kiconclusions. Povinná je i příloha s obsahem
|
|
%% elektronických dat.
|
|
|
|
%% -------------------------------------------------------------------
|
|
|
|
\newcommand{\BibLaTeX}{\textsc{Bib}\LaTeX}
|
|
|
|
|
|
\section{Úvod}
|
|
|
|
|
|
|
|
Hry pro více hráčů jsou stále populárnější. Například na internetovém tržišti her Steam 9 z 10 nejhranějších her podporuje hru více hráčů 6 z nich dokonce ani nepodporuje hru jednoho hráče. Dokonce vznikly sporty v počítačových hrách, tzv. \uv{e-sporty}, ve kterých se utkávají profesionální týmy proti sobě a kompetetivních hrách pro více hráčů. Trochu jiný typ her pro více hráčů, ale o nic míň výdělečný, jsou masivně online hry pro více hráčů. Kompetetivní hry jsou rozdělené do zápasů pro pár hráčů a jsou optimalizované pro minimální odezvu mezi akcemi hráče a reakcí systému. Naopak MMO hry jsou masivní a zvládnou masivní online světy pro tisíce hráčů. Optimalizují pro obrovský datový tok z tisíce klientů na server, který musí rozdělit svou zátěž na vícero dalších serverů.
|
|
|
|
V práci se zaměřujeme na metody optimalizace datového přenosu v síťových systémech ve hrách více hráčů. To znamená nejen velikost dat, ale i odezvu mezi vstupem hráče a reakcí stavu hry. Tyto optimalizace jsou obecné pro síťové systémy, distribuované nevyjímaje. Hru stavíme na vlastním herním enginu tak, abychom různé algoritmy měli pod kontrolou. Například jsme vytvořili vlastní protokol pro obousměrné posílání spolehlivých i nespolehlivých zpráv zvaný QUICr. Popíšeme, jakým způsobem jsme optimalizace měřili a jaké metriky jsou k tomu potřeba.
|
|
|
|
% Vytvořili jsme hru vlastní herní engine a v něm hru pro více hráčů. Implementovali jsme nástroje pro různé optimalizace přenosu dat. Analyzovalili jsme výkon pomocí různých nástrojů a výsledky demonstrujeme. Nesoustředíme se jen na síťové protokoly, ale i na koncové body, které potřebují posíláním informací koordinovat a kompresovat. Změřili jsme různé datové struktury a naše výsledky představíme.
|
|
% V naší práci se zaměříme právě na síťové a distribuované systémy, které slouží pro hry. Zajímali nás optimalizace, které pro různé herní světy můžeme použít. Vytvořili jsme jednoduchou hru, ve které se hráči mohou pohybovat. Hra pro komunikaci mezi klienty a servery využívá náš vlastní protokol QUICr, který umožňuje posílat zprávy spolehlivě i nespolehlivě pro snížení odezvy a eliminaci blokování. Představíme formu serializace, která kombinuje možnost vlastního rychlého zápisu, ale pro zprávy využívá fallback ProtoBuf.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
\newpage
|
|
|
|
% Distribované systémy
|
|
% - 1. Základní popis síťových systémů: centralizované a distribuované
|
|
% - 2. Motivace: Expanze a integrace
|
|
% - 3. Vlastnosti: Škálovatelnost, Robustnost, Odezva
|
|
% - 4. Zmínit World Wide Web nebo DNS
|
|
%
|
|
% Komunikace
|
|
% - 1. Jak probíhá komunikace v distribuovaných systémech bez sdílené paměti
|
|
% - 2. Dva přístupy: MOM a RPC
|
|
% - 3. Middleware, např. HTTP nebo RPC
|
|
%
|
|
% Počítačová síť
|
|
% - 1. Popsat jak vlastně jsou počítače propojeny
|
|
% - 2. Co je to síť a jak funguje
|
|
% - 3. TCP/IP model - vrstvy. Každý protokol poskytuje službu
|
|
% - 4. Popsání protokolů v aplikační vrstvě: HTTP/1 2 a 3
|
|
% - 5. Popsání protokolů v transportní vrstvě, včetně QUIC
|
|
% - 6. Popsání IP, především jak funguje politika fronty na switchi a fragmentace
|
|
%
|
|
% Sockety
|
|
% - 1. Představit rozhraní transportní vrstvy přes Berkeley Sockety
|
|
% - 2. Ukázat další rozhraní jako ZMQ nebo Boost ASIO
|
|
% - 3. Představit asynchroní posílání zpráv
|
|
|
|
\section{Distribuované systémy}
|
|
|
|
V této kapitole představíme distribuované systémy. Popíšeme, na čem stojí a jaké jsou cíle, pokud se takový systém rozhodneme implementovat. Začneme od síťových systémů, na kterých jsou distribuované systémy stavěné. Ukážeme softwarové a systémové architektury v distribovaných systémech. Poté podrobněji rozebereme komunikaci v síti.
|
|
|
|
|
|
Počítačový systém se skládá ze služeb, každá implementovaná jako kolekce procesů, které dohromady plní společný úkol. Moderní systémy jsou ale čím dál větší a úkoly, které musí plnit, jsou složitější. To vedlo ke vzniku síťových systémům, ve kterých jsou procesy rozmístěné přes více počítačů a komunikují spolu posíláním zpráv. Výhod je hned několik. Část systému může být umístěna počítači blíž zákazníkovi a snížit tak odezvu. Když jeden proces selže, může být jiný, který plní stejnou službu a může systém udržet v provozu. Konkrétní skupinou jsou distribuované systémy, které mají mnoho procesů rozmístěných přes více počítačů, které aktivně spolupracují a jeví se jako jeden celek.
|
|
|
|
|
|
% Počítačový systém se skládá ze služeb, každá je implementována jako kolekce procesů a dohromady plní společný úkol. Systém, ve kterém jsou procesy služeb na různých počítačích propojených v síti, se nazývá \uv{síťový systém}. Zmíníme dva důvody, proč procesy takto rozmístit. Prvním může být záměr \uv{integrace} více existujících systémů. Například se v průběhu života software zjistí, že neposkytuje všechny potřebné služby. Mohou se změnit požadavky nebo se rozšíří uživatelská základna o skupinu, se kterou se původně nepočítalo. Pokud tuto službu poskytuje jiný systém na jiném počítači, můžeme oba systémy spojit. Druhým důvodem může být \uv{expanze}, kdy jeden počítač nesplňuje požadavky na výkon.
|
|
|
|
% Síťové systémy můžeme dále rozdělit na \uv{decentralizované} a \uv{distribuované}. Decentralizované jsou většinou ty, které se rozrostly přes více počítačů kvůli integraci. Distribuovaný systém se snaží být dostatečně rozprostřen přes více počítačů tak, aby dokázal plnit svůj úkol. Například aby umožňil snadno přidávat do systému služby, měl dostatečně nízkou odezvu nebo aby bylo možné ho škálovat pro velké množství požadavků.
|
|
|
|
Příkladem distribuovaného systému je World Wide Web, zkráceně WWW. Jedná se o informační systém, který umožňuje prohlížet, ukládat a odkazovat dokumenty umístěné na internetu. Dokumenty mohou být například webové stránky, obrázky nebo videa a jsou uloženy na webových serverech. Odkazy na ně jsou ve formátu URL. Distribuovanost systému umožňuje snadné rozšíření, protože každý může snadno přidat svůj server se svými dokumenty, které se tak stanou dostupné v systému. To zároveň rozloží zátěž přes více počítačů a systém tak zvládá miliardy požadavků denně. Zároveň se celý systém jeví jako jeden celek, který z URL adresy vyhledá server a dokument vrátí.
|
|
|
|
|
|
\subsection{Architektury}
|
|
|
|
V praktické části jsme navrhli síťový systém. V návrhu jsme využívali známé vzory, které představíme. Návrh systému rozdělíme na dvě části: softwarovou a systémovou architekturu. V softwarové architektuře řešíme komponenty a konektory mezi nimi. Mezi typy softwarových architektur patří například vrstvená, orientovaná na služby nebo publish-subscribe. Druhá část architektury, která řeší role jednotlivých služeb, se nazývá \uv{systémová architektura}. Mezi příklady patří peer-to-peer nebo klient-server.
|
|
|
|
Pro ilustraci rozdílu představíme známý příklad třívrstvé softwarové architektury: databázová, výpočetní a frontendová vrstva. Systémová architektura definuje, že databáze jako služba pro výpočetní vrstvu má roli serveru. Naopak služba pro replikaci ve skupině databázových serverů má roli peer-to-peer. Typy si rozebereme podrobněji v této kapitole.
|
|
|
|
\subsubsection{Softwarová architektura}
|
|
|
|
V první části se zaměříme na softwarovou architekturu. Budeme řešit komponenty a konektory mezi nimi. Představíme tři známé vzory: vrstvená architektura, architektura orientovaná na služby a publish-subscribe architektura.
|
|
|
|
Pokud komponenty organizujeme do vrstev, kde komponenta ve vrstvě $N$ může volat rozhraní vrstvy $N-1$, říkáme tomu \uv{vrstvená architektura}. Příkladem je vrstva operačního systému, nad kterým je vrstva uživatelského prostoru. Občas je možné, aby nižší vrstva volala vyšší, ale mělo by se dít přes rozhraní definované nižší vrstvou, které vyšší vrstva pouze implementuje. Příkladem je operační systém, který oznamuje událost aplikaci. Aplikace proto registruje funkci, která se v případě události zavolá. Rozhraní funkce ale určuje vrstva pod ní: operační systém.
|
|
|
|
Nevýhodou vrstvené architektury je silná provázanost mezi vrstvami. Namísto toho lze software organizovat do nezávislých entit, kde každá zapouzdřuje službu. Takovým entitám se říká: služba, objekt nebo mikroslužba. Na komponenty se můžeme dívat jako na objekty a konektory mezi nimi jsou volání metod. Instance objektu mohou být rozmístěny na více počítačích. To, že jiný objekt je na jiném počítači, by mělo být skryté. Když chce klient použít jiný objekt, lokálně si vytvoří jeho instanci, která ale po zavolání metody volání zabalí do zprávy a odešle objektu, který metodu implementuje. Tomuto modelu se říká \uv{remote procedure call}, nebo zkráceně RPC. Klient obdrží zprávu s výsledkem, kterou rozbalí a pokračuje v běhu. Lokální instanci se někdy říká \uv{proxy} nebo \uv{klient-stub}.
|
|
|
|
|
|
Co občas může vadit je, že jsou služby \uv{referenčně vázané}. To znamená, že služby musejí znát adresu nebo jméno jiné, pokud ji chtějí používat. Od tohoto omezení můžeme upustit například tím, že budou \uv{publikovat} události do různých \uv{témat} a dělat na tyto témata dotazy. Pokud například služba publikuje událost do tématu $A$, jiná služba, která udělá dotaz na téma $A$, dostane právě tuto událost. Odesílatel tak neví, kdo zprávu zpracuje, kolikrát a kdy. Implementace je přes \uv{brokera}, který slouží jako jednotný bod pro všechny procesy, do kterého publikují události a broker je třídí vhodným jiným procesům. Tento princip lze využít u systémů pro hry více hráčů pro komunikační kanály. Klient hráče chce například napsat do lokálního kanálu pro město, ve kterém se v herním světě nachází. Komponenta pro chatovou službu tuto zprávu zařadí do správného kanálu a rozešle klientům, kteří tento kanál také odebírají.
|
|
|
|
|
|
|
|
|
|
\subsubsection{Systémové architektury} \label{sec:system_architecture}
|
|
|
|
Pro systémové architektury představíme a využijeme dva vzory: asymetrickou klient-server a symetrickou peer-to-peer.
|
|
|
|
Asymetrickou architekturou, kde je komponenta buď server, nebo klient, se nazývá \uv{klient-server}. Pouze klienti mohou serverům posílat dotazy a dostávat od nich odpovědi. Vztah je tedy asymetrický. Příkladem jsou webové servery a webové prohlížeče, které fungují jako klienti. Tento model se hodí například pro autoritativní server, který určuje stav hry a pouze jej replikuje klientům. Usnadňuje synchronizaci jednotlivých klientů a zvyšuje efektivitu, protože se účastníci nemusí shodovat, ale pouze přijmout fakt ze serveru.
|
|
|
|
Symetrická architektura, kdy obě strany jsou si rovny, se nazývá \uv{peer-to-peer}. Znamená to, že obě strany mohou posílat požadavky a zprávy na druhou stranu. Využívá se například při replikaci, kdy máme několik replik, které jsou si rovny. Pouze řešíme, která vlastní jaký zdroj tak, aby nedošlo ke konfliktu dvou replik při psaní konkrétního záznamu. Libovolná replika má ale možnost odeslat svůj stav jiné, protože jsou si rovny. Tento model je vhodný například pro horizontální škálování stavových serverů, které si rozdělují část stejné služby. Například vícero serverů může zpracovávat různé části herního světa a předávají si mezi sebou entity, které mají na starost.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
\subsection{Komunikace}
|
|
|
|
V síťovém systému není sdílená paměť. Místo toho se pro komunikaci mezi procesy používá posíláním primitivních zpráv pomocí systémových volání. To se snažíme v distribuovaných systémech skrýt za vhodnějším rozhraním. V této podkapitole si představíme různé abstrakce a v podkapitole \ref{sec:NetworkCommunication} ukážeme podrobněji, jak se taková komunikace realizuje.
|
|
|
|
Pro schování komunikace a platformy, na které proces běží, se využívá \uv{middleware}. Jedná se o komponentu, která leží mezi aplikací a operačním systémem a poskytuje komunikační služby. Není závislá na žádné konkrétní aplikaci. Příkladem je distribuovaná služba DNS, která podle doménového jména vyhledá síťovou adresu, jako například adrese \texttt{www.seznam.cz} přiřadí síťovou IP adresu \texttt{77.75.77.222}.
|
|
|
|
Pro různé účely a situace máme jiné modely komunikace. Například email je typický příklad \uv{persistentní} komunikace. Po odeslání si persistentní middleware zprávu uloží do té doby, dokud ji příjemce nepřijme. To znamená, že proces příjemce nemusí běžět v době, kdy odesílatel odesílá.
|
|
|
|
Na druhou stranu máme \uv{transientní} komunikaci, kdy zpráva je uložena jen po dobu, kdy běží proces odesílatele a příjemce. To znamená, že pokud příjemce není dostupný, zprávu nikdy nepřijme. Protokoly v transportní vrstvě jsou transientní.
|
|
|
|
Dále může být komunikace \uv{synchronní} nebo \uv{asynchronní}. Asynchronní znamená, že odesílatel pokračuje v běhu okamžitě po odeslání zprávy. Ta se dočasně uloží v middleware do doby, než se odešle. Na druhou stranu při synchronní komunikaci volající proces zastaví, dokud neobdrží odpověď z cílového procesu. Obecně tedy definujeme tři body, kde může nastat synchronizace (proces odesílatele pokračuje v práci). Zaprvé hned poté, co middleware převzal zprávu a zodpovědnost za její doručení. Zadruhé v moment, kdy byla zpráva doručena druhému procesu. Zatřetí až v moment, kdy druhá strana zpracovala požadavek a dostali jsme odpověď.
|
|
|
|
|
|
\subsubsection{Modely pro komunikaci}
|
|
|
|
Pro každý účel musíme vhodně zvolit konfiguraci perzistence a synchronizace. Představíme dva modely komunikace, které posílání primitivních zpráv skrývají a jsou implementovány jako middleware: Message Oriented Middleware (MOM) a Remote Procedure Call (RPC).
|
|
|
|
Prvním přístupem je Remote Procedure Call, který procesu umožňuje zavolat lokální proceduru s implementací na jiném počítači. Když proces A zavolá proceduru na počítači B, A se pozastaví a začne se vykonávat na B. Jakmile se dokončí, pošle se výsledek zpět na A, které poté pokračuje. Tento model je synchronní a transientní. Cílem je, aby volání vypadalo jako by implementace byla lokální a skrýt tak komunikaci mezi počítači.
|
|
|
|
Jedná se o intuitivní řešení, ale přináší pár problémů. Dva počítače mají různý adresní prostor, proto je třeba vyřešit, jak bude procedura využívat ukazatele do svého adresního prostoru, nebo jestli tuto vlastnost zakáže.
|
|
|
|
Druhým přístupem je posílání zpráv. V tomto přístupu posílají procesy zprávy na logické cíle pomocí systémového jména, nikoliv fyzické adresy. Tento přístup méně schovává fakt, že procesy jsou rozmístěny na více počítačích a abstrakce z fyzických adres na jména pomáhá. Využívá se v architektuře publish-subscribe nebo občas v architektuře orientované na služby.
|
|
|
|
Opět je potřeba, aby se dva koncové body shodli na významu bitů jednotlivých zpráv. Většinou jsou v programu zprávy reprezentovány objektem, který je pro síť převeden na n-tici bitů, procesem zvaným \uv{serializace}. V praktické části představíme serializaci podrobněji.
|
|
|
|
% - Proces A chce poslat zprávu procesu B
|
|
% - Ve svém adresním prostoru zprávu sestaví
|
|
% - Systémovým voláním zprávu odešle
|
|
% - Procesy se musejí dohodnout na významu bitů
|
|
|
|
% Druhým přístupem je posílání zpráv. V tomto přístupu aplikace posílají zprávy na čistě logický cíl. Např. identifikovaný typem zprávy. Aplikace pak může projevit zájem o různé typy zpráv a middleware se postará o to, že tyto zprávy aplikace dostane. Přístupu se říká \uv{publish-subscribe}.
|
|
|
|
|
|
|
|
|
|
\subsection{Počítačová Síť} \label{sec:NetworkCommunication}
|
|
|
|
Nedílnou součástí distribuovaného systému je síť, přes kterou mohou procesy posílat primitivní zprávy. V této podkapitole se zaměříme na komunikaci mezi dvěma procesy v síti, a to i těch, které nejsou součástí jednoho síťového systému. Počítačová síť je skupina propojených počítačů, které si mezi sebou přenášejí data. Například lokální sítě (LAN) propojují až tisíce počítačů, které jsou geograficky blízko, například v rámci jedné budovy. Rozsáhlé sítě (WAN) propojují miliony různých zařízení po celém světě. Příkladem je síť Internet.
|
|
|
|
Počítačovou síť si lze představit jako graf, ve kterém počítače představují vrcholy a fyzická média mezi nimi jsou hrany. Vrcholy dále dělíme na \uv{koncové body} a \uv{propojovací prvky}. Koncové body jsou počítače, mobilní telefony a jiná zařízení, na kterých běží procesy. Dva procesy na dvou různých počítačích si mezi sebou mohou posílat zprávy. Propojovací prvky jsou přepínače, rozbočovače a opakovače. Ty naopak slouží pouze k směrování zpráv po síti. Využití je propojení skupiny počítačů do sítě přes jedno spojení jako vidíme na obrázku TODO. Propojovací body jsou E a D. Není potřeba připojovat každý počítač s každým.
|
|
|
|
Dva počítače, přímo propojené fyzickým médiem, komunikují posíláním n-tic bytů zvané \uv{rámce}. Variantou jsou \uv{přepínané} sítě, které nemusejí mít mezi všemi dvojicemi počítačů přímé spojení, ale využívají \uv{přepínače}. Proces, kdy doručujeme zprávu na cílový počítač přes vícero přepínačů se nazývá \uv{směrování}. Každý vrchol v takové síti musí mít přiřazenou unikátní \uv{síťovou adresu} a posílali se formátované rámce zvané \uv{pakety}. Ty se skládají z hlavičky, ve které najdeme informace ke směrovaní, a tělu obsahujícím samotnou přenášenou zprávu.
|
|
|
|
\subsection{Komunikace v síti}
|
|
|
|
V této části představíme jak je realizována komunikace v síti. Aby si dva počítače navzájem rozuměly, musejí se shodnout na významu jednotlivých bitů rámců, které si mezi sebou budou posílat. K takové definici slouží \uv{komunikační protokol}: soubor pravidel pro výměnu informací mezi počítači. Definuje syntaxi, sémantiku a synchronizaci zpráv.
|
|
|
|
Každý protokol poskytuje komunikační služby. Tyto služby rozdělujeme na dvě skupiny: ty co před komunikací navážou spojení a ty co ne. V prvním případě musejí obě strany přijmout a navázat spojení a potencionálně se domluvit na jeho dalších parametrech. Jakmile jejich komunikace skončí, spojení se ukončí. Příkladem takové služby je telefoní linka. V druhém případě může odeslat zprávu kdykoliv a bez předchozího upozornění druhé strany. Příkladem takové komunikace je posílání emailu.
|
|
|
|
\subsubsection{Rodina protokolů TCP/IP}
|
|
|
|
Rodina protokolů pro komunikaci v síti Internet je TCP/IP. Jedná se o více protokolů organizovaných do vrstev, kde každá může používat jen tu pod ní. Vrstvenou architekturu vidíme na obrázku \ref{fig:layer_architecture}. Vrstva $N$ používá rozhraní vrstvy $N-1$. Každá vrstva poskytuje komunikační služby. Počítač A i B tak vidí stejnou službu. Představme si, že rozhraní má dvě funkce: pro čtení a pro zápis. Aplikace do vrstvy $N$ zapíše zprávu, kterou chce, aby si proces využívající stejnou službu, ale na druhém počítači, mohl přečíst. Je potřeba, aby vrstva zapsala zprávu ve formátu, kterému bude rozumnět strana A i strana B. Tento formát je právě protokol, jak vidíme na obrázku \ref{fig:layer_architecture}.
|
|
|
|
\begin{figure}
|
|
\begin{center}
|
|
\includegraphics[width=1\textwidth]{graphics/layer_architecture.png}
|
|
\end{center}
|
|
\caption{Vrstvená architektura TODO předělat}
|
|
\label{fig:layer_architecture}
|
|
\end{figure}
|
|
|
|
|
|
Název se skládá ze dvou důležitých protokolů: IP (Internet Protocol) a TCP (Transmission Control Protocol). IP slouží pro směrování paketů a TCP pro řízení přenosu. IP umožňuje komunikaci libovolných dvou uzlů počítačů v propojených sítích. TCP zajišťuje spolehlivý obousměrný přenos dat mezi procesy na dvou počítačích (ne nutně různých).
|
|
|
|
Protokol IP má za úkol doručit paket od odesílatele na příjemce pouze podle IP adresy, která je v hlavičce paketu. IP nenavazuje spojení a nezaručuje doručení. Důvodem je princip \uv{end-to-end}, který říká, že body mezi odesílatelem a příjemcem, jako routery a přepínače, by měli být co nejjednodušší. Spolehlivost musejí zaručit až dva koncové body, například číslováním paketů a sledováním stavu doručení. V případě, že paket chybí, ho musí odesílatel odeslat znova.
|
|
|
|
Router udržuje frontu příchozích paketů, která má fixní velikost. Jakmile objem příchozích paketů překročí kapacitu fronty, router má různé politiky, jak se v takové situaci zachovat. Nejběžnější je politika \uv{tail-drop}, kdy paket zahodí. Proto musejí koncové body hlídat a měřit zahlcení sítě. Měli by hlídat, jak moc paketů se ztrácí a případně snížit frekvenci odesílání. V TCP/IP se o to stará protokol TCP, který si má frontu paketů, které musí odeslat a udržuje si ve frontě okno paketů, které jsou odeslané ale nepotvrzené neboli \uv{in-flight}. Pokud není potvrzeno až příliš paketů, okno zmenší a tím sníží počet paketů, které mohou být najednou v síti.
|
|
|
|
Konkrétně architektura TCP/IP je rozdělena do čtyř vrstev: aplikační, transportní, síťová a síťové rozhraní. Každá vrstva obsahuje množinu protokolů a pro každou situaci lze sestavit ideální čtveřice. Vrstvy podrobněji představíme.
|
|
|
|
\begin{description}
|
|
\item[{Aplikační}] \hfill \\
|
|
Nejvyšší je \uv{aplikační} vrstva, ve které jsou aplikace. Příkladem protokolů jsou FTP, SMTP, HTTP nebo gRPC.
|
|
|
|
\item[{Transportní}] \hfill \\
|
|
Druhá vrstva je \uv{transportní}. Tato vrstva, stejně jako aplikační, je rozbalena až na koncových bodech, jak vidíme na obrázku \ref{fig:tcpip}. Proto se jim někdy říká end-to-end protokoly. Není totiž důležitá pro směrování paketů. Stará se o to, jaké přišli, v jakém pořadí a jestli nejsou poškozené. Příkladem protokolů jsou TCP, UDP nebo QUIC.
|
|
|
|
\item[{Síťová}] \hfill \\
|
|
Pro adresaci je síťová vrstva. Ta směruje a předává jednotlivé datagramy. Příkladem protokolů jsou IP, ARP, RARP nebo IPSEC. Je nutné, aby stejně jako vrstva síťového rozhraní, byla implementována ve všech vrcholech na cestě mezi dvěma počítači, které chtějí komunikovat přes internet.
|
|
|
|
\item[{Síťové rozhraní}] \hfill \\
|
|
Nejnižší vrstva, která se stará o přenos přes fyzické médium. Pro různé média bude vyžadovat různé protokoly. Příkladem jsou Ethernet, Token ring, apod.
|
|
\end{description}
|
|
|
|
Na obrázku \ref{fig:tcpip} vidíme, jak mezi sebou jednotlivé vrstvy komunikují. Aplikace na počítači $A$ zapíše do transportní vrstvy, kterou počítač $D$ ze stejné vrsvy přečte. Vrstva zná pouze vrstvu pod sebou. Plnou čarou vidíme datový tok zprávy, jak putuje přes jednotlivé vrstvy. Šrafovaná čára reprezentuje abstrahovaný datový tok tak, jak ho vidí vrstva. Počítače B a C jsou propojovací body. Všimněme si, že se nepoužívají transportní nebo aplikační vrstvy, pouze si rozbalí IP paket a zjistí, kam mají pakety posílat dál. Vrstva síťového rozhraní pracuje s různými médii, jak také vidíme na obrázku.
|
|
|
|
|
|
\begin{figure}
|
|
\begin{center}
|
|
\includegraphics[width=1\textwidth]{graphics/TcpIp}
|
|
\end{center}
|
|
\caption{Komunikace vrstev TCP/IP}
|
|
\label{fig:tcpip}
|
|
\end{figure}
|
|
|
|
|
|
\subsection{Protokoly v aplikační vrstvě}
|
|
|
|
Nejvyšší v rodině protokolů TCP/IP je aplikační vrstva s protokoly jako HTTP, gRPC nebo SMTP. Aplikačním protokolům, které nejsou specifické pro konkrétní aplikaci, se říká middleware. Mezi takové řadíme právě HTTP, gRPC nebo DNS, které jsou pro obecné posílání zpráv.
|
|
|
|
\subsubsection{HTTP}
|
|
|
|
% - Velmi častý
|
|
% - Původně pro přenos HTML hypertextových dokumentů, ale i obrázků, videí apod. - například REST API
|
|
% - Klient-server protokol - pouze jedna strana posílá požadavky
|
|
% - Čitelný člověkem
|
|
% - Rozšiřitelný - pomocí hlaviček
|
|
% - Bezestavová, ale ne bez relace. Pomocí cookies lze ukládat kontext.
|
|
% - HTTP/1 - jeden požadavek = jedno TCP spojení
|
|
% - Pomalé - pokaždé je nutný handshake
|
|
% - Head of line (HOL) blocking
|
|
% - HTTP/2 - multiplexová komunikace - jedno spojení lze využít znova
|
|
% - Odpovědi musí chodit ve stejném pořadí, jako přišli požadavky
|
|
% - Zde může docházet k blokování
|
|
|
|
Velmi používaný protokol v systému World Wide Web je HTTP, neboli Hyper Text Transfer Protokol. Především slouží pro přenos hypertextových dokumentů, jako například HTML. Původně byl navržen pro komunikaci mezi prohlížečem, neboli klientským agentem, a webovým serverem. Dnes se využívá i pro API dotazy. Protokol je klient-server, to znamená, že jedna strana je klient a druhá server. Pouze klient může na server posílat dotazy a server posílá zpátky odpověď. Server nemůže poslat dotaz na klienta. I když už jsou způsoby, jak může server posílat alespoň události.
|
|
|
|
Protokol je textový a snadno čitelný člověkem. Navíc je rozšiřitelný pomocí hlaviček. Protokol je bezestavový. To znamená, že server mezi dvěma dotazy nemá žádnou vazbu. Podporuje ale relace, kdy v dotazu můžeme serveru sdělit identifikátor kontextu, který chceme použít. Server si musí tyto kontexty ukládat. Je request-reply. V první verzi, HTTP/1, bylo třeba otevřít nové TCP připojení pro všechny. Ve verzi HTTP/2 je podpora pro multiplexní dotazování. To znamená, že jedno navázané spojení lze využít na více dotazů. To může ušetřit čas, protože se tak vyhneme navazování spojení. Funguje to tak, že přes jedno spojení lze odeslat více dotazů a odpovědi musejí chodit ve stejném pořadí. Už zde je vidět možný problém, který se nazývá \uv{head-of-line} blokování. Znamená to, že pokud první dotaz trvá dlouho a přitom je nepodstatný, a druhý dotaz je krátký a podstatný, může první dotaz blokovat ten druhý. Klienti by se měli snažit nejprve odeslat jednoduché dotazy a ty zdlouhavější až potom.
|
|
|
|
|
|
|
|
\subsubsection{HTTP/3}
|
|
|
|
Zajímavou novinkou je HTTP/3, která je založena na protokolu QUIC, který je založen na UDP namísto TCP. Podporuje více \uv{multiplexových} proudů už na transportní vrstvě. To znamená, že agent klienta může v jednom HTTP/3 stahovat najednou vícero dokumentů nezávisle. Pro jednu stránku je třeba stáhnout CSS, JS i HTML dokumenty. V HTTP/2 se všechny stahují jedním proudem střídavě, což způsobuje tzv. head-of-line blokování. Na obrázku \ref{fig:http_blocking} vidíme porovnání TCP v HTTP/2 a QUIC v HTTP/3. V prvním případě stahujeme dva dokumenty: $A$ z částí $A1$ a $A2$ a $B$ z částí $B1$ a $B2$. Když server odesílá v pořadí $A1$, $B1$, $A2$ a $B2$, a $B1$ se nedoručí, tak i když už se stihli doručit $A2$ a $B2$, tak je nemůžeme začít zpracovávat, protože protokol čeká na B1. Na druhou stranu protokol QUIC může mít pro každý dokument vlastní frontu a tím blokování kompletně eliminovat.
|
|
|
|
\begin{figure}
|
|
\begin{minipage}{0.48\textwidth}
|
|
\centering
|
|
\includegraphics[width=\textwidth]{graphics/http2.pdf}
|
|
\par\smallskip(a) HTTP/2
|
|
\end{minipage}
|
|
\hfill
|
|
\begin{minipage}{0.48\textwidth}
|
|
\centering
|
|
\includegraphics[width=\textwidth]{graphics/http3.pdf}
|
|
\par\smallskip(b) HTTP/3
|
|
\end{minipage}
|
|
\caption{Vizualizace HTTP/2 blokování}
|
|
\label{fig:http_blocking}
|
|
\end{figure}
|
|
|
|
% \subsubsection{gRPC}
|
|
|
|
% Představíme konkrétní implementaci RPC a to gRPC. Jedná se o multiplatformní middleware od Google. Konkrétně gRPC využívá pro transport protokol HTTP/2 a ProtoBuf pro kódování dat. Využívá binární formát. ProtoBuf umožňuje definovat rozhraní pomocí zpráv a \texttt{rpc} koncových bodů.
|
|
|
|
|
|
|
|
\subsection{Protokoly v transportní vrstvě}
|
|
|
|
Protokoly transportní vrstvy poskytují end-to-end komunikační služby pro aplikace. Příkladem takových služeb může být komunikace s navázáním spojení, spolehlivost doručení, zamezení zahlcení sítě nebo multiplexing.
|
|
|
|
Nejčastějším protokolem je TCP, který naváže spojení a zaručuje spolehlivé doručení dat jako proud bytů. Alternativou je UDP, které nezaručuje doručení, ale je vhodnější pro streamovací služby. Poslední příklad, který zmíníme, je protokol QUIC od Google, který poskytuje spolehlivou komunikaci přes navázané spojení a s podporou multiplexování. Byl důležitou inspirací pro náš vlastní protokol z praktické části.
|
|
|
|
\subsubsection{TCP}
|
|
|
|
Nejběžnější protokol je TCP. Při použití mezi sebou dva koncové body vytvoří spojení s obousměrným proudem bytů. Protokol garantuje spolehlivé doručení dat v pořadí, ve kterém byly odeslány. Zároveň řeší zahlcení sítě udržováním okna paketů, které jsou v oběhu a redukuje jeho velikost v případě, že je síť přehlcená. To pozná tak, že si pakety čísluje a příjemce musí za každý odeslat potvrzení přijetí. Pokud odesílatel toto potvrzení nedostane, odešle paket znovu. Pokud nedostává potvrzení pro až moc velkou část okna, může toto okno zmenšit.
|
|
|
|
Pro navázání spojení a následnou komunikaci musejí oba procesy vytvořit koncový bod, kterému se říká \uv{soket}. Jedná se o objekt, který se chová jako soubor. Odesílatel do něj zapisuje data, která chce odeslat a příjemce je z něj může přečíst. Na jednom počítači může být otevřeno více soketů a jsou identifikované \uv{portem}. Ten slouží pro směrování paketů správnému soketu, ze kterého příjemce čte. Pro navázání musí jedna strana spojení iniciovat a druhá ho musí přijmout.
|
|
|
|
Popíšeme si, jak TCP zajišťuje spolehlivost pomocí odesílání potvrzení o přijetí. Pokaždé, co příjemce dostane paket, odešle druhému koncovému bodu zprávu označenou jako \uv{ACK} s číslem paketu. V případě, že odesílatel potvrzení o přijetí nedostane, odešle paket znova. Příjemce si u sebe postupně skládá seřazený proud bytů. Jakmile je na socketu seřazená posloupnost bytů, umožní ji ze soketu přečíst.
|
|
|
|
V protokolu na transportní vrstvě dochází k head-of-line blokování. Pokud jedna strana odešle proud bytů po částech $A$, $B$ a $C$, a příjemci přijde nejprve $C$ a $B$, nebude moct příjemce ze soketu nic přečíst, dokud nepřijde i část $A$. Většinou je toto vhodná služba, ale jsou situace, například ve hrách, kdy se to stává problémem. Například posíláním zpráv s pozicí hráče. Pokud strana pošle pozici pro čas $t_1$, $t_2$ a pak $t_3$, tak by ztracená zpráva $t_1$ blokovala příjemce, který by ji v zápětí přepsal zprávou pro čas $t_3$. Lepší by bylo, aby seřazení definovala až aplikace, která by začala zpracovávat $t_3$ hned a docílila tak nižší odezvu mezi serverem a klientem.
|
|
|
|
Nevýhoda pro nás byla, že přenáší proud dat. Jinými slovy, v protokolu není proud rozdělen na jednotlivé bloky se zprávou. Tuto logiku si musíme implementovat sami. Proto jsme v praktické části vytvořili vlastní protokol, který umí v proudu zprávy správně oddělit.
|
|
|
|
\subsubsection{UDP}
|
|
|
|
Méně spolehlivá alternativa je UDP, neboli User Datagram Protocol. Ten komunikuje posíláním \uv{datagramů}, na rozdíl od proudu bytů, jako je tomu v případě TCP. Znamená to, že to co vrátí jedno systémové volání pro čtení ze soketu je právě jedno volání pro zápis, které udělal odesílatel. Protokol nenavazuje spojení a nezaručuje doručení datagramů.
|
|
|
|
Je vhodný jako základ pro vlastní transportní protokol. Například protokol QUIC, který podrobně popíšeme níže, ho využívá pro vlastní implementaci spolehlivosti a multiplexing. My jsme na něm založili vlastní protokol, inspirovaný QUIC, který umožňuje aplikaci odeslat zprávu i nespolehlivě, například pokud se jedná o pozici hráče.
|
|
|
|
\subsubsection{QUIC}
|
|
|
|
QUIC je spolehlivý protokol transportní vrstvy, který poskytuje multiplexní komunikaci založenou na TLS 1.3, od společnosti Google. Jeho cílem je být efektivnější varianta TCP, která využívá protokol UDP. Znamená to například, že lze snížit množství blokování, ke kterému dochází z důvodu ztracených paketů. Mezi dvěma koncovými body vytváří spolehlivé spojení. Na rozdíl od TCP umožňuje otevřít více proudů v jednom spojení a modelovat tak částečné uspořádání. Když se čeká na paket v TCP, tak se pozastaví celý tok dat, ale v případě QUIC se zastaví jen konkrétní proud.
|
|
|
|
Mezi dvěma koncovými body se posílají pakety. Ty obsahují rámce, které se rozdělují na kontrolní a proudové. Kontrolní slouží pro ovládání koncových bodů: otevírání a zavírání proudů, přesun ve stavovém stroji koncového bodu nebo udržování spojení naživu. Proudové obsahují aplikační data. Stejně jako TCP, i QUIC přenáší proud bytů a oddělení jednotlivých zpráv je nutné implementovat zvlášť.
|
|
|
|
\begin{description}
|
|
\item[{Pakety}] \hfill \\
|
|
Dvě strany komunikující přes QUIC protokol mezi sebou posílají sekvenční pakety různých typů. Příkladem jsou: \texttt{Initial}, \texttt{0-RTT}, \texttt{Handshake} a \texttt{1-RTT}. Liší se v síle šifrování, které používají, každý typ má svůj číselný prostor a liší se i hlavička. Každý paket má své unikátní pořadové číslo, které se v rozumném čase nesmí použít znova, a to ani při opakovaném poslání stejného paketu z důvodu ztráty. Různé typy paketů mají ale čítač nezávislý. Pakety typu \texttt{Initial} a \texttt{Handshake} mají dlouhou hlavičku a \texttt{0-RTT} a \texttt{1-RTT} mají krátkou hlavičku.
|
|
|
|
\item[{Spolehlivost}] \hfill \\
|
|
Protokol QUIC zajišťuje spolehlivost jednotlivých rámců. Příjemce musí odeslat zpět odesílateli rámec typu \texttt{ACK} se zakódovaným číslem paketu, ve kterém rámce získal. Tento rámec může obsahovat i více paketů zároveň a protokol toho využívá tak, že neposílá \texttt{ACK} tak často.
|
|
|
|
\item[{Šifrování}] \hfill \\
|
|
QUIC už podporuje šifrování přes TLS 1.3 přímo v transportní vrstvě. To znamená dvě výhody: protokol autentizuje klienta, který nám posílá zprávy a tajný klíč lze domluvit už při navazování spojení. TCP naváže spojení pomocí handshake a následně pokračuje další handshake pro TLS. QUIC je v tomto ohledu schopný navázat spojení rychleji.
|
|
|
|
\item[{Unikátní identifikátory koncových bodů}] \hfill \\
|
|
Protokol UDP směřuje datagramy do portů a identifikovat klienty umožňuje pomocí zdrojové IP adresy. Protokol QUIC přidává další vrstvu směrování pomocí \uv{unikátních identifikátorů koncových bodů}. Před začátkem komunikace se oba koncové body domluví na svých identifikátorech a odesílatel do hlavičky paketu zapíše identifikátor cílového spojení. Toto číslo je náhodně vygenerované a s délkou až 62 bitů. Díky šifrování nemůže potenciální útočník toto číslo podvrhnout a vydávat se za někoho jiného, protože kvůli neznalosti klíče by nebyl schopný správně zašifrovat zprávu.
|
|
Výhoda je, že klient může změnit IP adresu, např. z důvodu přepnutí z Wi-Fi na mobilní data, není třeba spojení znova navazovat. V případě TCP se často stává, že spojení musí vypršet, a pak znova navázat.
|
|
|
|
|
|
\end{description}
|
|
|
|
Popíšeme, jak v QUIC funguje handshake, který vidíme na obrázku \ref{fig:quic_handshake}. Nejprve začíná strana, která se chce připojit, posláním paketu typu \texttt{Initial} s rámcem CRYPTO s ClientHello. Server odpoví několika rámci: \texttt{Initial} s \texttt{CRYPTO} rámcem se ServerHello společně s ACK rámcem a paketem typu \texttt{Handshake} ve kterém je \texttt{CRYPTO} rámec s potřebnými informacemi pro TLS. Aplikační data jsou v paketu typu \texttt{1-RTT} a \texttt{0-RTT}. Jakmile klient dostane tyto informace, mohou si strany posílat šifrovaná data. Toho QUIC využívá a první odpověď ze serveru už může obsahovat paket \texttt{1-RTT} se \texttt{STREAM} rámci s šifrovanými daty, jak vidíme na obrázku \ref{fig:quic_handshake}. Dále si všimněme, že každý typ paketu má vlastní prostor čísel. QUIC používá UDP a proto posílá datagramy. Do jednoho datagramu spojuje více paketů. Tři pakety ze serveru, které přijdou jako odpověď, se tak mohou odeslat jako jeden datagram.
|
|
|
|
Při handshake se dva koncové body mimo jiné domlouvají na svých unikátních číslech. Při Client-Hello se server podívá na Source Connection ID a zapamatuje si ho jako ID připojení druhé strany. Následně vygeneruje své vlastní ID a přiřadí mu instanci relace. Pak se podívá na Destination Connection ID a nastaví si, aby pakety, které mají toto číslo jako Destination Connection ID, se připisovaly nově vytvořené relaci. Toto číslo jako takové nebude používat a v odpovědi jako Source Connection ID bude jeho vlastní vygenerované.
|
|
|
|
\begin{figure}
|
|
\begin{center}
|
|
\includegraphics[width=0.5\textwidth]{graphics/quic_handshake.pdf}
|
|
\end{center}
|
|
\caption{QUIC handshake}
|
|
\label{fig:quic_handshake}
|
|
\end{figure}
|
|
|
|
Všimněme si speciálního typu paketu \texttt{0-RTT}, který slouží pro poslání aplikačních dat ještě před samotným navázáním spojení, ale pouze se základním šifrováním. Pro tento paket se využívá první Destination Connection ID, které ale vygeneroval iniciátor komunikace. Jedno spojení může mít přiřazeno více ID. Spojení, které si založí druhá strana, si přiřadí jak ID vygenerované touto stranou, tak ID, které zvolil iniciátor. Když přijde později paket \texttt{0-RTT}, bude správně nasměrován na instanci spojení.
|
|
|
|
Způsobem, jakým QUIC redukuje nebo dokonce eliminuje head-of-line blocking, je multiplexing. Přes jedno spojení je možné otevřít více paralelních proudů, které se navzájem neovlivňují.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
\subsection{Sokety}
|
|
|
|
Populární rozhraní pro použití protokolů v transportní vrstvě jsou tzv. \uv{Berkeley sokety}. Soket je komunikační koncový bod, do kterého aplikace zapisuje data, která chce poslat přes síť druhému procesu, a ze kterého zároveň může číst příchozí data. Jedná se o abstrakci nad konkrétním otevřeným portem. Nelze vytvořit dva sokety se stejným portem.
|
|
|
|
|
|
|
|
\subsection{Asynchroní vstupně výstupní operace}
|
|
|
|
Čtení a zápis na socket je běžně blokující operace. To znamená, že při systémovém volání pro čtení 10 bytů, bude volání blokovat, dokud na socket nepřijde 10 bytů. V případě serveru pro online hru je to nevhodné, protože pokud nám nic nepřišlo, můžeme mezitím vykonávat jinou logiku hry. Socket lze ale nastavit tak, aby vracel hned a počet bytů neznamenal konkrétní počet, ale pouze maximum. Je ale poté třeba zprávu opět sestavit z postupně přečtených částí v aplikaci.
|
|
|
|
Podobnému přístupu se říká \uv{asynchronní sockety}. Jinými slovy to znamená oddělení volání a samotného vykonání. V tomto případě sice zavoláme operaci čtení, ale toto volání se vloží do fronty, a jiné samostatné vlákno ho vykoná asynchroně později. Protějšek z reálného světa je hození dopisu do schránky a pokračování v jiné práci. Doručení se stane v nejbližších dnech a to někým jiným, protějšek jiného vlákna. Pro lepší představu si pár existujících implementací představíme.
|
|
|
|
\subsubsection{ZeroMQ}
|
|
|
|
Při použití knihovny ZeroMQ vytváříme objekt kontextu pomocí \inlcpp{ctx=zmq\_context(num\_threads)}. Ten obsahuje konkrétní počet vláken specifikovaný parametrem \inlcpp{num\_thread}. Když následně zavoláme \inlcpp{zmq\_send(ctx, x)}, protějšek klasického \inlcpp{send(x)}, tak nedojde k blokování. Místo toho se zpráva $x$ umístí do fronty, ze které pak vlákna z \inlcpp{zmq\_context} tzv. \uv{kradou} a odesílají v pozadí.
|
|
|
|
To samé platí o \inlcpp{zmq\_receive}, které si v pozadí skládá celou zprávu. TCP totiž nemá žádnou strukturu zprávy, a proto když zavoláme operaci pro čtení ze soketu, můžeme dostat jen část. ZeroMQ pracuje v proudu s celými zprávami, které za nás skládá.
|
|
|
|
\subsubsection{Work stealing}
|
|
|
|
Běžnou strategií pro využití dynamického počtu vláken je \uv{work stealing}. Jiné strategie jsou třeba \uv{work sharing}. Implementace obsahuje synchronizovanou frontu, do které vkládáme popisy úkolů, které se mají vykonat. Vlákno, které se snaží z této fronty krást, je \uv{worker} neboli pracovník. Pokud je fronta správně synchronizovaná, může z ní krást libovolný počet vláken. Jsou různé možnosti, co může být popis úkolu. Například anonymní funkce nebo ID. Zajímavou vlastností tohoto přístupu je, že součást úkolu může být vložení
|
|
|
|
\subsubsection{Async/Await}
|
|
|
|
Populární implementací, kterou vidíme například v Rust nebo C\#, je \uv{async/await}. V programu označíme funkci jako asynchronní pomocí klíčového slova \inlcpp{async}. Zavoláním takové funkce speciálním \inlcpp{await} voláním způsobí, že se celý aktuální kontext vloží jako úkol do fronty, ze které pracovníci kradou. Tento přístup vyžaduje, aby měly prvky ve frontě definované závislosti mezi sebou, protože úkol s aktuálním kontextem bude závislý na dokončení právě funkce, kterou jsou zavolali pomocí \inlcpp{await}. Toto volání tak do fronty vložíme taky. Pro definice závislostí se používají ukazatele na čítače. Pokud úkol $A$ závisí na úkolu $B$, nastaví si $A$ čítač $C_A$ na hodnotu $1$ a $B$ dostane na čítač $C_A$ ukazatel. Po dokončení tento čítač dekrementuje. Pracovník může krást z fronty jen ty úkoly, které mají hodnotu čítače roven nule.
|
|
|
|
Tento přístup je často používán v kontext asynchronních vstupně výstupních operací. Vlákno, které čeká na blokující operaci může v mezičase pracovat na jiném úkolu v aplikaci namísto čekání. V základu metoda neslouží k paralelismu, k tomu je potřeba další operace zvaná \uv{počkej na všechny}, která vloží do fronty vícero úkolů a aktuální kontext, který závisí právě na všech těchto úkolech. Ty tak mají šanci běžet paralelně.
|
|
|
|
|
|
Příklad z reálného světa je poslat dopis s otázkou. Opět stačí hodit dopis do schránky. Až si ho někdo přečte, může nám poslat odpověď. Když dostaneme odpověď, vzpomeneme si, kde jsme skončili, když jsem práci přerušili a odeslali dopis s otázkou, a když už máme odpověď, tak pokračujeme.
|
|
|
|
\subsubsection{Boost Asio}
|
|
|
|
Velmi podobná ZeroMQ je Boost ASIO. Ta ale nechává vytváření vláken na nás. Pouze vytvoříme objekt kontextu \inlcpp{asio\_context}, který má frontu úkolů a metodu \inlcpp{run}. Když z vlákna zavoláme tuto metodu, stane se vlákno pracovníkem, který krade práci z fronty, která je v objektu kontextu.
|
|
|
|
Tento přístup je intuitivnější, protože se jedná přesně o to, jak pracovníci fungují. Jedná se o funkci, která se opakovaně snaží odebrat z fronty úkolů. Pokud fronta není prázdná a pracovník se dostane z ní úkol, začne na něm pracovat. Jakmile práci dokončí, vrací se ke kroku odebírání z fronty.
|
|
|
|
Důvod, proč nevytvořit thread pro každou odeslanou zprávu je, že vyžaduje systémové volání, které může způsobit zpomalení. Proto se často používají tzv. green thready, fibery, user thready, ... což jsou vlákna kompletně v uživatelském prostoru. Není tak třeba žádné systémové volání.
|
|
|
|
% \subsubsection{Stack pointer}
|
|
|
|
% Problém může být definovat práci pro thready. Jak vypadá takový objekt `Job`? Intuitivně by nás napadlo, že by obsahoval ukazatel na funkci. Nebo v případě, že máme jen velmi omezený počet různých implementací, může obsahovat jen typ, a worker už si dohledá, co má dělat.
|
|
|
|
% Druhým způsobem, který vidíme při použití `await/async` v jazycích jako Rust nebo C\#, je prostě zavolat funkci, která reprezentuje práci, pomocí `await`. Funkce await si uloží aktuální ukazatel na stack, vytvoří nový stack speciálně pro job, a objekt `Job` tak vypadá tak, že obsahuje tento nový stack pointer, ukazatel na funkci, a všechny potřebné registry.
|
|
|
|
% Všimněme si, že můžeme job vytvářet i z jiného jobu, a tím v podstatě větvit. Zároveň můžeme kód paralerizovat tak, že najednou budeme awaitovat více jobů. Představme si, že máme fyzický systém s metodou `step`. Ta zavolá `awaitAll(jobs)`, kde `jobs` je list jobů. Automaticky se všechny dostanou do fronty, a různé workery je mohou začít vykonávat paralerně.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
\newpage
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
% Popsat, co je hra. Má stav a akce, které stav mění.
|
|
% - V případě her více hráčů různé počítače získávají vstupy a jiné
|
|
% - Server posílá snapshoty nebo události
|
|
% - Klienti posílají příkazy
|
|
|
|
\section{Hra}
|
|
|
|
Pro zkoušku různých technik jsme vytvořili hru pro více hráčů. V ní budou hráči používat program klienta, kterým se připojí na společný server. Po připojení hráči uvidí herní svět, ve kterém se mohou pohybovat. Systém hry automaticky synchronizuje stavy hráčů mezi klienty. Server bude celou hru řídit a bude mít autoritu. Program klienta tento stav vykreslí svému uživateli.
|
|
|
|
Nejprve představíme, jak vypadá softwarová architektura hry jednoho hráče. Popíšeme jak vypadá herní engine a z jakých komponent se program hry skládá.
|
|
|
|
V další části navážeme a rozšíříme hru pro více hráčů. Představíme architekturu. První implementace využívá klient-server. Vytvořili jsme dva programy: server a klient. Podrobně popíšeme komponenty, ze kterých se skládá server a klient. Ve hře se jednotlivý hráči uvidí, budou se moct pohybovat. Budeme porovnávat plynulost pohybu a rychlost odezvy, které jsou pro hratelnost důležité.
|
|
|
|
Mezi hlavní prvky patří náš vlastní komunikační protokol transportní vrstvy: \uv{QUICr}. Je to obousměrný protokol pro posílání zpráv, který upravuje QUIC pro využití ve hrách. Náš protokol lépe zachycuje závislosti mezi zprávami a umožňuje posílat zprávy nespolehlivě. Díky tomu se úplně zbavuje ahead-of-line blokování.
|
|
|
|
Zjistili jsme, že důležitým vylepšením je vlastní serializace primitivních zpráv. Pokud například obsahuje pouze seznam pozic, může být vlastní serializace rozdíl mezi hratelnou a nehratelnou odezvou. Popíšeme, jak jsme rychlost měřili a jaké metody jsme pro vlastní serializaci použili.
|
|
|
|
Nakonec představíme i architekturu peer-to-peer, kdy umožníme, aby v systému bylo více spolupracujících serverů, které si mezi sebou budou rozdělovat práci. Zároveň porovnáme i případ, kdy není žádný autoritativní server a hráči se mezi sebou synchronizují samovolně.
|
|
|
|
|
|
|
|
\subsection{Softwarová Architektura}
|
|
|
|
% Vstupy od hráče -> derivace
|
|
% Derivace + Stav -> integrace
|
|
% Chceme říct, že iterujeme.
|
|
%
|
|
% Hra má definované akce, které hráči mohou dělat a jak
|
|
% Vstup od hráče -> akce
|
|
% Co je herní stav, jak probíhá kolo?
|
|
|
|
Program hry jsme rozdělili na \uv{stav} a \uv{řídící logiku}. Stav obsahuje množinu entit, které jsou ve hře a jejich vlasnosti a atributy. Entity jsou objekty v herním světě, jako postava hráče, truhly s poklady nebo nepřátele. Entita nepřítele může mít vlastnost "množství životů" a truhla vlastnost "poklad", která definuje, co je v truhle obsaženo. Stavu hry se někdy říká pouze \uv{svět}.
|
|
|
|
Druhá část je řídící logika, která po iteracích mění stav hry. Tato změna může být pouze na základě stavu světa, například posune objekt, který má nenulový vektor zrychlení. Mimo to může řídící logika reagovat na údalosti. Například stisk tlačítka W je událost, který vyvolá změnu vektoru zrychlení hráče, který tlačítko stiskl. V důsledku toho se postava hráče pohne.
|
|
|
|
Každá iterace řídící logiky trvá přibližně stejně dlouho, většinou 1/60 sekundy nebo 1/24 sekundy. Vyšší frekvence znamená nižší odezvu a lepší plynulost. Nižší frekvence znamená menší výpočetní náročnost. Pro pomalejší hry bez rychlých akčních pasáží se využívá frekvence 24Hz a u kompetentních her i 120Hz.
|
|
|
|
https://www.riotgames.com/en/news/peeking-valorants-netcode
|
|
|
|
\subsubsection{Entity-Component-System}
|
|
|
|
V programu využíváme architekturu entity-component-system, která je pro hry běžná. Entity už jsme popsali výše. Ke konkrétním entitám vážeme instance komponent, které entitě dají stav. Většinou je komponenta přiřazena právě jedné entitě. Řídící logika hry se skládá ze \uv{systémů}, je řídící logika, která prochází komponenty a aktualizuje je. Systémů máme více a každý se stará o nějakou část.
|
|
|
|
Herní stav se skládá z \uv{entit}, které jsou v herním světě a jejich \uv{vlastností}. Každou iteraci řídící logika všechny entity aktualizuje. Některá logika může být pro dvě entity společná, nebo velmi malá. Obecně ale předpokládáme, že každá entita přidá na výpočetní náročnosti a moderní hry mohou mít miliony entit. Zároveň každá entita může být jiná.
|
|
|
|
Architektura her je tomu přizpůsobená. Entity tvoříme skládáním různých komponent a řídící logika mění jejich stav pomocí \uv{systému}. Systém se skládá z iterátoru přes n-tici typů komponent a aplikační funkce na prvky této n-tice. Výhody jsou téměř žádná virtualizovaná volání, která by se ve velkém počtu negativně projevila na výkonu a lepší \uv{datová lokalita}. To znamená, že procesor je schopný lépe předpovědět, kterou část paměti si má připravit dopředu, protože na ni bude provádět výpočet. Horší lokalita znamená, že bude často načítat zbytečné části, které nepotřebuje a výpočet bude delší.
|
|
|
|
https://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/
|
|
|
|
Této architektuře se říká entity-component-system, zkráceně ECS. Existují různé knihovny, které poskytují registry a nástroje pro tvorbu systémů, jako například Entt, kterou využíváme my.
|
|
|
|
|
|
|
|
\subsection{Engine}
|
|
|
|
Pro hru jsme vyvinuly vlastní herní engine tak, abychom mohli celý systém do hloubky upravovat. Implementace je v jazyce C++, protože je v tomto jazyce napsáno spousty nástrojů a knihoven právě pro vývoj her. Díky tomu se vývoj značně akceleroval. Zároveň je to vhodný jazyk pro práci na nižší úrovni, a to je důležité některé metody optimalizace. Engine je implementovaný jako modulární monolit. Jednotlivé moduly představíme.
|
|
|
|
\subsubsection{Fyzický engine}
|
|
|
|
Hra je 3D a chtěli jsme simulovat otevřený svět, ve kterém platí zákony fyziky. Pro simulaci jsme využili existující fyzický engine \uv{Jolt} a zabalili ho do modulu. Jolt je populární projekt, který je využíván například v Horizon: Forbidden West nebo Death Stranding 2.
|
|
|
|
Pro použití jsme vytvořili instanci fyzického světa, do kterého naskládáme objekty a jejich vlastnosti, jako hmotnost a tvar. Zavoláním metody pro aktualizaci engine posune stav entit (zrychlení a pozice). Vše jsme obalili do \inlcpp{JoltPhysicsWorld}.
|
|
|
|
Jolt umí využít více vláken procesoru a podporuje \uv{rollback}, který umožňuje vracet stav v historii simulace dozadu. Je to důležitá věc pro hry více hráčů v případě, že se stav dvou vrcholů v systému neshoduje i když by měl.
|
|
|
|
\subsubsection{Vykreslování}
|
|
|
|
K vykreslování jsme použili náš vlastní engine. Pro vykreslení 3D objektu je potřeba popsat jeho povrch jako množinu polygonů. Této množině se říká \uv{mesh}. Vykreslení jednoho snímku lze v enginu popsat jako graf operací. Běžně bude obsahovat operaci, která seznam mesh vykreslí do obrázku. Tento celý proces lze shrnout jednou operací, protože o celý algoritmus a logiku se stará grafická karta. Vývojář jen popisuje mesh a například jakou má mít barvu apod.
|
|
|
|
Mezi operacemi v grafu definujeme závislosti. Díky tomu můžeme definovat operaci, která vykreslí 3D mesh a druhou operaci, která je na ni závislá, a která přes obrázek vykreslí uživatelské rozhraní.
|
|
|
|
V našem případě máme právě operaci pro vykreslení statických objektů, jako povrchu, po kterém se hráči pohybují apod. Druhá operace vykreslí samotné hráče a jiné dynamické objekty. Poslední operaci vykreslí uživatelské rozhraní obsahující různé nástroje pro různé ladění programu.
|
|
|
|
\subsubsection{Entt}
|
|
|
|
Pro ECS využíváme knihovnu \uv{Entt}, která umožňuje vytvářet registry entit a jejich komponent a aplikovat na ně funkce. Entita je v tomto případě pouze unikátně identifikační číslo a komponenta je libovolný typ. V registru pak můžeme vytvářet \uv{pohledy} na n-tici typů komponent, které jsou seznam právě všech entit v registru, které všechny tyto komponenty mají. Ten můžeme procházet a libovolně měnit atributy komponent.
|
|
|
|
\begin{kicode}{cpp}{}{Příklad použití knihovny Entt}
|
|
entt::registry registry;
|
|
|
|
entt::entity entity = registry.create();
|
|
|
|
registry.emplace<Transform>(entity, Transform::from_position(position));
|
|
|
|
registry.view<Transform, CharacterBody>()
|
|
.each([&](Transform& ts, CharacterBody& rb) {
|
|
auto character = rb.m_character;
|
|
|
|
auto transform = character->GetWorldTransform();
|
|
memcpy(&ts.transform, &transform, sizeof(glm::mat4));
|
|
});
|
|
\end{kicode}
|
|
|
|
Představíme komponenty, které jsme pro hru definovali a proč:
|
|
|
|
\begin{description}
|
|
|
|
\item[{Transform}] \hfill \\
|
|
Obsahuje transformační matici. Entity, které chceme zobrazit ve světě, tuto komponentu potřebují. Příkladem entity, která ji mít nebude, je zpráva v chatu.
|
|
|
|
\item[{Mesh}] \hfill \\
|
|
Obsahuje mesh, který má hra využít pro vykreslení entity. Mesh je n-tici bodů, které dohromady dávají 3D povrch objektu. Komponenta neobsahuje body, ale pouze odkaz na buffer, ve kterém je najdeme a kde. Systém, který entity vykresluje si vytvoří pohled na Mesh a Transform, protože je potřeba vědět kam entitu vykreslit.
|
|
|
|
\item[{Rigidbody}] \hfill \\
|
|
Slouží pro fyzikální informace o entitě, jako hmotnost a zrychlení, které na entitu budeme aplikovat. Systém pro tuto komponentu bude ve fyzickém enginu.
|
|
|
|
\end{description}
|
|
|
|
|
|
|
|
|
|
\newcommand{\inlcpp}[1]{\kiinlinecode{cpp}{!}{#1}}
|
|
|
|
|
|
|
|
|
|
\subsubsection{Stav hry}
|
|
|
|
% Detaily k implementaci, ale stále dost abstrahované.
|
|
% Zmínit svět, ve kterém držíme stav hry. Zmínit derivaci (proces sbírání vstupů, prozatím jen od hráče). Zmínit integraci jako aplikaci derivace na stav.
|
|
|
|
Stav hry reprezentujeme kompletně ve třídě \inlcpp{World}. Ta obsahuje registr z knihovny Entt. Řídící logika je ve třídě \inlcpp{WorldController}. Obsahuje smyčku, která v každé iteraci posbírá vstupy od hráče, převede je na akce a aplikuje je společně s další definovanout logikou. Například v naší hře má vstup: stisknutí klávesy W, definovaný příkaz: \uv{posuň dopředu}, která změní vektor zrychlení hráče. Dalšími prvky, které mění změnu stavu je například fyzický engine, který upravuje zrychlení podle nárazů a nebo vlivů sil, například gravitace.
|
|
|
|
% Budou existovat dvě verze: pro hru jednoho hráče a pro hru více hráčů. Verze pro jednoho hráče bude vstupy číst přímo z periferií od hráče. Druhá verze bude tyto vstupy transformovat na příkazy, a posílat je na server. Aktualizovat stav světa bude až v moment, co od serveru dostane snapshot nebo událost.
|
|
|
|
\subsection{Hra více hráčů}
|
|
|
|
Nejprve her více hráčů je často klient-server model, protože poskytuje nižší celkovou odezvu. Stav hry řídí autoritativní server, který ho replikuje klientům a ti ho zobrazí hráči. Autorita serveru zjednodušuje konzistenci a detekci podvádění. Klienti na server neposílají jak vypadá jejich stav, ale pouze akce, které chtějí provést. Příkladem může být akce pohybu dopředu, kterou vyvolal hráč. Server rozesílá klientům snapshoty pro průběžně měnící se stav (např. pozice entit) a události pro jednorázové akce (např. událost o konci hry).
|
|
|
|
Druhá varianta je model peer-to-peer. Model je složitější z hlediska synchronizace. Účastníci si mezi sebou posílají akce a každý si udržuje svůj stav, který nikomu nereplikuje. Je potřeba, aby každý měl kompletní historii akcí všech účastníků systému ve správném pořadí. Tomuto se říká \uv{event-sourcing}. Nevýhoda je, že každý hráč
|
|
|
|
Navíc musí výsledek každé aplikace akce být všude stejný. Komplikací může být fyzický engine, který postupně integruje pozice entit podle derivace, která je reprezentována vektorem zrychlení. Každá integrace musí definovat, o jak velký časový krok se jedná (tzv. delta-time) a všichni účastníci se na něm musejí shodnout. Různé délky by rychle způsobily desynchronizaci. Zároveň všechny pseudonáhodné generátory musejí mít stejný seed a musíme si dát pozor, protože na různých platformách může aritmetrika s plovoucí desetinou čárkou dopadnout trochu jinak. I malé rozdíly by se mohli rychle projevit.
|
|
|
|
\subsection{Klient-Server architektura}
|
|
|
|
Pro systém jsem zvolili model klient-server, který je pro hry vhodnější, protože usnadňuje synchronizaci. V té jsou dva programy: server a klienta. Řídící logika probíhá na serveru, a ten svůj stav synchronizuje na klienty. Každý hráč, který chce hrát, musí mít svůj proces pro klienta a připojit se na server. Aplikace klienta na server posílá akce od hráče a zobrazuje aktuální stav hráči, který ze serveru přišel.
|
|
|
|
Každá akce, která přijde na server, musí být autorizována. Například ověřit, že se hráč nestaží interagovat s objektem, od kterého je moc daleko. Pokud vše proběhne v pořádku, bude přidána do fronty pro aktuální iteraci, kterou pak celou aplikuje na stav podle definované logiky. V případě, že autorizace selže, může server, s určitou tolerancí, klienta odpojit. Svůj stav průběžně replikuje klientům. Frekvence závisí na typu hry a pohybuje se od 20Hz pro pomalejší nekompetentní hry, až po 120Hz pro e-sport hry, jako například hra Valorant TODO ODKAZ.
|
|
|
|
\begin{figure}
|
|
\begin{center}
|
|
\includegraphics[width=1\textwidth]{graphics/client-server-architecture.pdf}
|
|
\end{center}
|
|
\caption{Softwarová architektura hry}
|
|
\label{fig:client_server_architecture}
|
|
\end{figure}
|
|
|
|
\subsection{Server}
|
|
|
|
Představíme, jak vypadá architektura pro server a komponenty, ze kterých se skládá. Stejně jako hra jednoho hráče si ukládá stav hry, tedy instanci \inlcpp{World}. Navíc pro hru více hráčů jsou komponenty \uv{replikátor} a \uv{manažer zájmu}. Replikátor posílá snapshoty a replikuje tak stav na serveru. Může posílat i jednorázové události.
|
|
|
|
Replikátor pracuje pouze s entitami a komponentami. Proto můžeme snapshot zjednoduššit na seznam entit s komponentami. Dále bude obsahovat seznam entit, které jsou nové a které naopak už mají zmizet. Stará se jen o to, jak tento stav synchronizovat pomocí posílání zpráv.
|
|
|
|
Různé entity na serveru jsou pro některé hráče jinak důležité. Například ty, které jsou daleko, nemusíme synchronizovat, protože po vykreslení by nebyly vidět. Komponenta, která se o to stará je \uv{manažer zájmu}.
|
|
|
|
Obě tyto komponenty, společně se stavem a řídící logikou, jsou ve třídě \texttt{WorldServerController}. Architektura tak využívá komponenty z hry pro jednoho hráče a skrývá distribuovanost pomocí vrstvené architektury.
|
|
|
|
\subsubsection{Registr klientů}
|
|
|
|
Komponenta, která spravuje spojení s hráči, se nazývá \uv{registr spojení}. Slouží jako sifon pro všechny zprávy, které klienti odesílají. Zároveň z ní lze zjistit kdo se právě připojil a kdo odpojil. K tomu slouží metody: \inlcpp{\\popDisconnectedPlayers} a \inlcpp{\\popConnectedPlayers}. Udržuje seznam nových připojení od posledního zavolání \inlcpp{\\popConnectedPlayers}.
|
|
|
|
Třída si u každého klienta hlídá počet po sobě jdoucích selhání. Jakmile počet překročí určitou hranici, např. 5 chyb, relaci s klientem ukončí. Díky tomu, že zbytek aplikace přímo nepoužívá n-tici klientů, nemusí odpojení řešit. Pro získání seznamu existuje metoda \inlcpp{\\getClients}.
|
|
|
|
\subsubsection{Server Replikátoru}
|
|
|
|
Komponenta, která se snaží synchronizovat stav klientů s lokálním stavem na serveru, se nazývá \uv{replikátor}. Pro každého klienta si drží seznam entit, které musí danému klientovi synchronizovat, aby svůj úkol splnil. V případě klient-server architektury odesílá server svůj stav klientům. Základní implementace by posílala každý snímek kompletní stav entity se všemi komponentami. Lepší varianta je posílat pouze změny. Pokud se například pozice nezmění, není třeba posílat stejnou pozici jako minule. Na druhé straně u klienta máme klienta replikátoru. Ten se stará o dekódování zpráv a správné aktualizaci stavu.
|
|
|
|
\subsubsection{Správa zájmů}
|
|
|
|
Komponenta, která řeší důležitost entit pro jednotlivé hráče, je \uv{manažer zájmu}. Důležitost definujeme pro každou dvojici klienta a entity. Manažer zájmu podle definované logiky, například na základě vzdálenosti, určí důležitost entity pro klienta. V základní verzi jsme měli pouze dvě hodnoty: důležitá, takže je nutné ji synchronizovat, a nedůležitá, která není. Replikátor se dotazuje manažera zájmu v moment, kdy klientovi chce stav synchronizovat.
|
|
|
|
Dotazu na všechny body, které jsou dostatečně blízko od konkrétního bodu, se říká `range-query`. Datové struktury které tuto operaci akcelerují jsou například fixní mřížka, dynamická mřížka nebo quad-tree.
|
|
|
|
|
|
|
|
|
|
|
|
\subsection{Klient}
|
|
|
|
Program klienta se skládá z třídy \texttt{World}, která reprezentuje stav a \texttt{ClientWorldController}, která obsahuje jednodušší řídící logiku. Řídící logika posbírá každou iteraci vstupy, převede je na akce a ty odešle na server.
|
|
|
|
\subsubsection{Klient Replikátoru}
|
|
|
|
Abychom oddělili to, jak replikátor funguje, vytvořili jsme pro něj na straně klienta ovladač. To nám později umožnilo měnit protokol mezi klientem a server podle potřeby. Komponenta pouze přijme snapshot od serveru, provede autentizaci a pak podle něj změní stav na klientovi.
|
|
|
|
\subsubsection{Interpolace}
|
|
|
|
Když jsme začali systém měřit, zjistili jsme, že vysoká frekvence replikace až příliš zatěžuje síť. Graf s průměry pro počet hráčů vidíme na grafu. Snížili jsme proto frekvenci na 20Hz. To snížilo zátěž na třetinu. Problém ale najednou byl neplynulost hry. Klient sice vykresloval s frekvencí 60Hz, ale méně časté aktualizace způsobili neplynulý pohyb. Řešení je na klientovi interpolovat spojité proměnné, jako pozice nebo rotace. Do klienta replikátoru jsme přidali komponentu \uv{interpolátor}. Ta si ukládá historii konkrétních hodnot a jejich časovou známkou do bufferu. Hodnota, kterou nastaví, je pak interpolovaná z bufferu fixní čas zpět.
|
|
|
|
\subsubsection{Rollback}
|
|
|
|
Interpolovat zlepšilo plynulost, ale nepříjemně zvýšilo odezvu mezi vstupem od hráče a vyvolanou změnou stavu. Možným řešením je simulovat řídící logiku i na klientovi, ale pouze pro konkrétní entitu, kterou hráč ovládá. Odezva bude v podstatě nulová, ale navíc bude nižší než před interpolací.
|
|
|
|
Vytvořili jsme buffer, do kterého ukládáme konkrétní moment stavu, číslo iterace stavu a seznam akcí, která se v danou iteraci mají aplikovat. Pokud dostaneme snapshot pro snímek $x$ a z bufferu přečteme, jaký byl v iteraci stav. Pokud se neshodují, celý původní stav načteme a opravíme podle snapshotu. Následně znova aplikujeme všechny akce a přepíšeme tak náš buffer, až se dostaneme do aktuální iterace. Poté normálně pokračujeme s opraveným stavem.
|
|
|
|
|
|
|
|
|
|
\subsection{Posílání zpráv}
|
|
|
|
% Popis našeho middleware pro posílání zpráv.
|
|
% Sedí na dvojicí TCP a později QUICr.
|
|
% Měl by poskytovat možnost, jak posílat zprávy nespolehlivě.
|
|
% Zpráva přijde vždy buď celá, nebo vůbec.
|
|
|
|
Popíšeme náš middleware pro posílání zpráv a jeho protokol založený na TCP. Poskytuje službu oboustranného peer-to-peer posílání zpráv na různé koncové body, které jsou identifikované čtyř bytovým číslem. Strana může pro libovolný identifikátor definovat handler, který se po získání takové zprávy spustí. Protokol zajišťuje spolehlivost na úrovni zpráv. To znamená, že zpráva dorazí buď celá, nebo vůbec. Později v kapitole \ref{sec:quicr} představíme náš protokol v transportní vrstvě, který umožňuje posílat zprávy spolehlivě i nespolehlivě.
|
|
|
|
\subsubsection{Kódování zpráv}
|
|
|
|
Middleware využívá TCP, které na rozhraní při čtení a zápisu pracuje s proudem bytů, který není logicky rozdělený. Jediné pevné body jsou začátek a konec proudu. UDP na druhou stranu zaručuje, že celý buffer dat odeslaný přes \texttt{write} operaci bude přečtený vždy právě jedním voláním \texttt{read} operace. Náš protokol definuje oddělení zpráv tak, aby šli v proudu najít.
|
|
|
|
První čtyři byty každé zprávy jsou tzv. MAGIC číslo. Konstanta, která nemá jiný význam než záchytný bod při čtení. Dekodér v počátečním stavu hledá v proudu tuto hodnotu. Pokud ji do určitého počtu bytů nenajde, ukončí spojení z důvodu narušení protokolu. Když konstantu najde, přečte další čtyři byty, které reprezentují délku těla a další čtyři byty reprezentující ID koncového bodu.
|
|
|
|
Vlastnost, kterou jsme v průběhu testování potřebovali, byla možnost request-reply modelu. Zároveň jsme ale chtěli mít možnost posílat zprávy stylem fire-and-forget. Nemohli jsme tedy použít způsob, jaký používá HTTP/2. Místo toho lze při odeslání zprávy definovat, že čeká na odpověď a její identifikační číslo. Druhá strana naopak může poslat odpověď, když zprávu definuje jako odpověď pomocí příznaku a identifikačního čísla zprávy, na kterou odpovídá.
|
|
|
|
\subsubsection{Implementace}
|
|
|
|
Vytvořili jsme třídu \inlcpp{MessagingSession}, která má na rozhraní metody \inlcpp{set\_handler(endpoint\_id)}, \inlcpp{send} a \inlcpp{request}. První metoda nastaví pro koncový bod \inlcpp{endpoint\_id} handler lambdu. Druhá pošle zprávu stylem fire-and-forget. To znamená, že se nestaráme o odpověď a synchronizace je v moment, kdy si zprávu převezme middleware. Poslední metoda umožňuje poslat požadavek a nastavit handler pro odpověď.
|
|
|
|
\subsubsection{Detekce a náprava chyb}
|
|
|
|
Občas mohou nastat v proudu chyby, kdy zprávu nelze dekódovat, nebo nastane chyba při deserializaci. Proto jsme do kódování implementovali nápravu chyb. Už jsme definovali maximální délku pro hledání magické konstanty. Dále si dekodér pro spojení počítá chyby. Ten se po každé úspěšně dekódované zprávě resetuje na 0. Pokud počet překročí definovanou toleranci 3 chyb, spojení bude ukončeno. Příkladem takové chyby je selhání deserializace nebo že TCP spojení bylo přerušeno.
|
|
|
|
|
|
|
|
|
|
\subsection{Serializace}
|
|
|
|
Zpráva je v programu reprezentovaná objektem. Proces, který ji převede na n-tici bytů se říká \uv{serializace}. Existují různé knihovny, které umí objekty serializovat. Některé umí serializovat přímo třídy definované v konkrétním programovacím jazyce. Jazyk C++ ale nemá runtime reflexi jako C\# nebo silná makra jako Rust, a tento způsob není možný. Druhá varianta je definovat tyto třídy ve speciálním jazyce pro definici schémat. Před sestavením programu z této definice vygenerujeme třídy v jazyce, který potřebujeme. Do tříd se vygenerují i metody pro serializaci. Tento přístup umožňuje například ProtoBuf, Cap'n'Proto a další. Jeho výhoda je možnost vygenerovat třídy v různých programovacích jazycích. Pokud máme architekturu orientovanou na služby, kde každá je implementována v jiném jazyce, nemusíme definice lokalizovat.
|
|
|
|
Hned na začátku jsme se rozhodli nepoužít JSON nebo XML, protože jsou to textové formáty a jejich forma je často větší než binárních formátů. Například číslo 1 000 000 se v JSON zakóduje do 7 bytů, i když binární reprezentace stejného čísla se vleze do 4 bytů. Rozdíl je téměř dvojnásobný. Každý atribut objektu se v JSONu reprezentuje řetězcem, namísto identifikačním číslem o 2 bytech, které by bylo menší. Výhody formátu jsou především liberalní zpracování, kdy se schémata dvou koncových bodů mohou i trochu lišit, ale pokud má zpráva všechny potřebné atributy, tak se strany domluví. Pokud v binárním formátu je atribut navíc, nemusí se druhé straně podařit zprávu deserializovat. Proto se někdy JSON používá jako záložní formát, kdy dvě strany provedou handshake a domluví se na formátu. Pokud zjistí, že jedna strana používá starší definici schématu, mohou použít záložní JSON.
|
|
|
|
Rozhodli jsme se začít velmi rozšířeným ProtoBuf formátem. Začneme definicí zpráv mezi serverem a klientem. Vytvořili jsme WorldServerMessages.proto soubor, do kterého budeme zprávy definovat. Trochu formát \inlcpp{.proto} představíme. V kódu \ref{code:entity_spawn_message} vidíme definici zprávy, která říká, že si příjemce má do svého stavu přidat novou entitu. Obsahuje tři atributy: ID pro navázání budoucích zpráv o entitě, příznak, jestli se jedná o hráče a jméno.
|
|
|
|
\begin{kicode}{cpp}{}{cpp}
|
|
message EntitySpawnMessage {
|
|
uint32 entity_id = 1;
|
|
bool is_player = 2;
|
|
string name = 3;
|
|
}
|
|
\label{code:entity_spawn_message}
|
|
\end{kicode}
|
|
|
|
Jazyk vypadá podobně jako C. Nejprve použijeme klíčové slovo \inlcpp{message} a název typu. Do těla píšeme atributy oddělené středníkem. Všimněme si, že pro každý atribut musíme definovat jeho unikátní číslo. To slouží při identifikaci a pomáhá při úpravách definice, abychom měli kontrolu nad tím, pod jakým klíčem se hodnota serializuje.
|
|
|
|
FlatBuffers je opět protokol od Googlu, který se vyvýjel pro použití v herních enginech pro hry více hráčů. Jeho výhoda je, že není třeba zprávu "deserializovat". Instanci třídy čte atributy přímo z n-tice bytů. V případě 60 zpráv za vteřinu zprávy od stovek hráčů, je rozdíl mezi deserializací a FlatBuffers znát.
|
|
|
|
Posledním příkladem je Cap'n'Proto, čteno Captain Proto. Je to volně dostupný protokol, který nemá deserializaci, a je tak velmi rychlý. Jeho tvůrce navíc pracoval právě na ProtoBuf.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
\subsection{Metriky}
|
|
|
|
Představíme metody měřění a metriky v systému, které jsme využili pro hledání potenciálních míst pro optimalizaci. Pro sbírání metrik je třeba mít úložistě, kam budeme metriky ukládat a rozhraní, přes které je budeme vizualizovat. Úložiště může být například CSV soubor nebo relační databáze. Existují optimalizované databáze pro vkládání dat s časovou známkou, kterým se říká \uv{timescale}. Jsou ideální právě pro metriky nebo logy, protože narozdíl od klasických databází jsou optimalizované pro rychlé vkládání. Rozhodli jsme se použít relační databázi \uv{PostgreSQL} s rozšířením \uv{TimescaleDB}. Pro vizualizaci jsme použili službu \uv{Grafana}, která umí z TimescaleDB číst. Všechny tyto služby popíšeme v následujících částech.
|
|
|
|
\subsubsection{PostgreSQL}
|
|
|
|
Populární relační databáze je PostgreSQL. Rozhodli jsme se právě pro tu, protože je to otevřený a svobodný software zdarma. Lze si ho snadno nasadit lokálně, ať už daemoném nebo jako Docker kontejner. Skrze rozšíření jako TimescaleDB lze přidat podporu a optimalizace pro různé použití. V našem případě se chceme ukládat informace o hráčích a stav světa pro perzistenci a metriky. Informace o hráčích a stav světa zvládne bez problémů v základní verzi. Pro metriky použijeme zmíněné rozšíření.
|
|
|
|
\subsubsection{TimescaleDB}
|
|
|
|
Pro optimalizaci PostgreSQL pro časová data jsme zvolili TimescaleDB. Díky tomu, že se jedná o rozšíření, nemusíme spravovat jinou službu, než PostgreSQL. TimescaleDB umožňuje vytvářet tzv. hypertables, které jsou optimalizované pro rychlé vkládání. Vytvořili jsme komponentu \inlcpp{NetworkMetricsReporter}, která má na rozhraní různé metody pro získávání metrik. Např. \inlcpp{report\_outbound} a \inlcpp{report\_inbound}. Tyto údaje posílá do komponenty \inlcpp{TimescaleDbWriter}. To je konkrétní implementace pro TimescaleDB, které má na rozhraní metodu \inlcpp{report(metric\_name, value)}. V rámci optimalizace si \inlcpp{NetworkMetricsReporter} metriky ukládá do bufferu, který až pomocí metody \inlcpp{flush} odešle do databáze. To se neděje nutně po každé metrice, ale v našem případě si reportér ukládá hodnoty do bufferu a po intervalech je vkládá do databáze.
|
|
|
|
\subsubsection{Grafana}
|
|
|
|
Grafana je služba, která skrze webovou stránku poskytuje rozhraní pro vizualizaci metrik. Lze nastavit svůj dashboard a v něm různé grafy. Pro naše účely jsme vytvořili grafy pro odchozí a příchozí množství bytů. Služba slouží spíše pro sledování stavu a neumožňuje pokročilejší analýzu.
|
|
|
|
\begin{figure}
|
|
\begin{center}
|
|
\includegraphics[width=1\textwidth]{graphics/grafana.png}
|
|
\end{center}
|
|
\caption{Příklad Grafana dashboardu}
|
|
\label{fig:grafana}
|
|
\end{figure}
|
|
|
|
\subsubsection{Orchestrace}
|
|
|
|
Popíšeme, jak jsme všechny služby zprovoznili a nastavili. Podrobněji se na orchestraci podíváme později.
|
|
|
|
|
|
|
|
\subsubsection{ImGui}
|
|
|
|
Pro uživatelské rozhraní jsme použili knihovnu ImGui a ImPlot. Tyto knihovny využívají pro vykreslování grafickou kartu a snadno se dali integrovat do našeho vykreslovacího enginu. Knihovna funguje procedurálně, nikoliv objektově. Každý snímek, který vykreslujeme, musíme celé rozhraní definovat znova. To uděláme pomocí volání funkcí. Nejprve zavoláme funkci \inlcpp{Begin("Název okna")}, která zobrazí okno, do kterého můžeme vložit další prvky, jako texty, textové vstupy, nebo tlačítka. Tento přístup je pro herní enginy a podobné kreativní aplikace ideální, protože umožňuje velmi snadno a rychle dělat dynamické uživatelské rozhraní.
|
|
|
|
Knihovnu jsme využili pro vykreslení metrik přímo uživateli klienta.
|
|
|
|
\subsubsection{Tracy}
|
|
|
|
Pro měření a pokročilejší analýzu výkonu programu klienta nebo serveru používám profiler Tracy. Je zdarma, open-source, napsaný v C++ a pro vykreslování UI využívá ImGui. Poskytuje přesnost na nanosekundy, což je užitečné u serverů, na kterém může být miliony různých entity. Navíc umožňuje připojit se k programu, který měří, vzdáleně. Případně sbírat metriky do souboru, ten si ze serveru stáhnout a analyzovat později.
|
|
|
|
V kódu umožňuje definovat jednotlivé iterace řídící logiky zvané snímky. Následně umožňuje definovat tzv. \uv{zóny}, které následně měří. Zóna může být průběh funkce nebo její konkrétní část. Na obrázku \ref{fig:frame_in_tracy} vidíme, jak taková analýza vypadá. Vidíme právě jeden snímek. V něm máme označené části replikátoru a manažera zájmu. Také vidíme dole zóny.
|
|
|
|
\begin{figure}
|
|
\begin{center}
|
|
\includegraphics[width=1\textwidth]{graphics/300playerstrace.png}
|
|
\end{center}
|
|
\caption{Jeden snímek v Tracy}
|
|
\label{fig:frame_in_tracy}
|
|
\end{figure}
|
|
|
|
\subsubsection{Konkrétní metriky}
|
|
|
|
Vyjmenujeme metriky, které budeme měřit. Je dobré zmínit, že místo, ze kterého budeme metriky sbírat, je server.
|
|
|
|
\begin{description}
|
|
|
|
\item[{Inbound/Outbound bandwidth}] \hfill \\
|
|
Nejpřirozenější metrika je příchozí a odchozí množství dat v bytech. Už zde pro mě bylo nutné si uvědomit důležitost oddělit vrstvu, která kóduje a dekóduje zprávy, od zbytku. Přesně na tomto místě totiž budu metriky dělat. Metriku budu sčítat.
|
|
|
|
\item[{Odezva}] \hfill \\
|
|
Budu měřit odezvu mezi odesláním vstupu od klienta pro snímek x a odpovědí od serveru pro snímek x. Tuto metriku budu průměrovat.
|
|
|
|
\item[{Počet snímků za vteřinu}] \hfill \\
|
|
Ze začátku se mi stávalo, že uzké hrdlo nebyla síť, ale síťová vrstva v aplikaci, která třeba dlouho skládala zprávy. Proto si udržuju i přehled o počtu snímků, které server stíhá. Podle toho se lze orientovat při výběru hardware pro server.
|
|
\end{description}
|
|
|
|
\subsubsection{Sbírání v reálném čase}
|
|
|
|
Přímo v hráčově klientské aplikaci jsme chtěli mít možnost vidět metriky v reálném čase. Do síťové vrstvy jsme přidali komponentu pro reportování událostí a jejich metrik zvanou \inlcpp{NetworkMetricReporter}, která obsahuje metody jako \inlcpp{report\_inbound} a \inlcpp{report\_outbound}. První metoda hlásí příchozí byty a druhá odchozí.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
\subsection{Protokol QUICr}
|
|
|
|
Naše hra používala ke komunikaci mezi vrcholy protokol TCP. Zmínili jsme ale, že ten pro hry nemusí být vhodný. Vytvořil jsme vlastní protokol inspirovaný QUIC, který využívá UDP, ale upustili jsme podmínky pro spolehlivost, konkrétně jistotu doručení a uspořádání zpráv. Nakonec jsme TPC a QUICr porovnali, abychom se o optimalizaci přesvědčili.
|
|
|
|
Protokol je obousměrný proud paketů se spolehlivými a nespolehlivými rámci využívající UDP. Jeden poslaný UDP datagram může mít více paketů. Rámce můžeme rozdělit na dva typy: datové a ovládací. Ovládací slouží pro handshake, ukončení spojení nebo nastavení jiných parametrů spojení a budou vždy spolehlivě doručené. Datové obsahují aplikační data a mohou definovat svou spolehlivost. Ty, které budou obsahovat pozice hráčů, tak budeme moct odesílat nespolehlivě, zatímco změny stavu, jako výměna mesh, posíláme spolehlivě. Architektura se skládá ze dvou komponent: \uv{koncový bod} a \uv{spojení}.
|
|
|
|
Koncový bod se stará o soket pro UDP: čtení a zápis na něj, a udržuje si registr navázaných spojení. Při přečtení datagramu, který splňuje formát, ho správně nasměruje podle registru. Nechtěli jsme, aby si implementace QUICr spravovala vlastní asynchronní kontext. Namísto toho jsme definovali metodu pro ťik, která posbírá zprávy od jednotlivých navázaných spojení a pošle je. Následně přečte všechna data ze soketu a roztřídí je do navázaných spojení.
|
|
|
|
Objekt navázaného spojení je stavový stroj a fronta příchozích a fronta odchozích zpráv, podobně jako TCP spojení. Stavy se mění v reakci na příchozí kontrolní rámce. Jeden objekt navázaného spojení může mít přiřazených více ID. Tento objekt si datagram přečte a přeloží na pakety a rámce. Ty postupně prochází. Stavový stroj bude důležitý hlavně v procesu handshake, který definujeme níže.
|
|
|
|
\subsubsection{Rámec}
|
|
|
|
Hlavním prvkem protokolu jsou právě rámce. Jak už bylo nastíněno, dělíme je na kontrolní a datové. Pro využití nespolehlivosti jsme u každého datového rámce umožňili definovat, jestli je spolehlivý nebo nespolehlivý. O spolehlivost obecně se stará komponenta \inlcpp{ReliabilityUnit}, kterou podpobněji představíme v kapitole \ref{sec:quicr_reliability}.
|
|
|
|
|
|
% U UDP je další problém, a to ten, že když implementuju připojení pro UDP, tak všechny datagramy budou chodit na socket, na kterém poslouchám. Když budu ze socketu číst, dostanu libovolný datagram, který zrovna přišel, společně s adresou, ze které přišel. Tuto adresu přečtu z IP hlavičky, která není zašifrovaná, a tudíž ji kdokoliv může zfalšovat. Tomu se říká tzv. IP spoofing. Tento útok je poměrně slabý, protože v případě odpovědi budu stále odesílat data zpět na správnou adresu. Útok může sloužit například pro poslání požadavku na odpojení, nebo posílat jiné vstupy. Jedná se tedy spíše o neškodný ale otravný charakter útoku.
|
|
|
|
|
|
|
|
|
|
|
|
\subsubsection{Handshake}
|
|
|
|
Objekt navázaného spojení obsahuje stavový stroj a postupně mezi stavy přechází. Počáteční stav je \inlcpp{Closed}. Jedna strana musí začít a odeslat paket s \inlcpp{Hello} rámcem. Po obdržení se stavový stroj druhé strany přesune do \inlcpp{ReceivedHello} a spolehlivě odešle \inlcpp{Hello}. Když strana dostane rámec \inlcpp{Hello} tak ví, že v hlavičce paketu je cílové i lokální ID. Proto už touto výměnou se strany shodli na ID navázaného spojení, které budou používat. Poté už si vymění rámec \inlcpp{HandshakeDone} a spojení je navázané a připravené k posílání.Celý stavový diagram vidíme na obrázku \ref{fig:quicr_handshake}.
|
|
|
|
% Nové připojení projde všechny rámce v paketu. Měl by narazit na rámec Hello, který když najde, a je ve stavu `Initial`, tak z hlavičky zjistí unikátní ID druhé strany a uloží si ho. Reaguje na rámec tak, že druhé straně odešle přes koncový bod paket, který obsahuje: rámec ACK o tom, že paket s číslem přijal, a rámec Hello. ACK si pro začátek představme zakódovaný jako: první 4 byty reprezentují kladné celé číslo pro počet rámců, řekněme $n$, které chceme označit jako přijaté. Následně přečteme $4n$ bytů. Každé 4 byty představují číslo paketu, který označíme jako ACKed. Jednotce, která udržuje stav o tom, jaké pakety jsou potřeba oznámit jako ACK a které je třeba poslat znovu, budu říkat `RealiabilityUnit` a podrobněji ji rozeberu později.
|
|
|
|
% Přeskočme zpět na klienta, který dostává paket typu Initial, ve kterém je rámec Hello a ACK. Pro rámec Hello se chová stejně jako druhá strana, a pokud je ve stavu `SendHello`, nastaví si Destination ID. Všechna čísla paketů z ACK rámce předá do jednotky pro spolehlivost. Jelikož víme, že tam je číslo paketu, který obsahuje právě prvotní Hello, tak si jednotka tento rámec odebere z fronty pro opakované odeslání.
|
|
|
|
\begin{description}
|
|
|
|
\item[{Closed}] \hfill \\
|
|
Počátečním stavem je `Closed`. Zároveň se jedná o stav, do kterého se spojení dostane, pokud dlouho s druhou stranou nekomunikuje.
|
|
|
|
\item[{SentHello}] \hfill \\
|
|
Po odeslání \texttt{Hello} rámce se spojení dostane do stavu \texttt{SentHello}. V tomto stavu čeká na \texttt{Hello} rámec od druhé strany.
|
|
|
|
\item[{ReceivedHello}] \hfill \\
|
|
Jakmile strana dostane rámec \texttt{Hello}, přejde do stavu \texttt{ReceivedHello}. V ten moment čeká na potvrzení, že spojení bylo navázáno, tedy rámec \texttt{HandshakeDone}.
|
|
|
|
\item[{Established}] \hfill \\
|
|
Po odeslání HandshakeDone zprávy se klient dostane do stavy Established. Jakmile server přijme zprávu HandshakeDone, taky se dostává do stavu Established.
|
|
|
|
\end{description}
|
|
|
|
|
|
|
|
\begin{figure}
|
|
\begin{center}
|
|
\includegraphics[width=1\textwidth]{graphics/quicr_handshake.pdf}
|
|
\end{center}
|
|
\caption{Stavový stroj QUICr handshake}
|
|
\label{fig:quicr_handshake}
|
|
\end{figure}
|
|
|
|
\subsubsection{Spolehlivost} \label{sec:quicr_reliability}
|
|
|
|
O spolehlivost doručení se protokol stará posíláním \inlcpp{Ack} rámců s číslem paketu, který získal. Navíc ale rozlišujeme spolehlivost na základě jednotlivých rámců. Pokud paket neobsahuje žádný spolehlivý rámec, nemusí pro něj příjemce posílat \inlcpp{Ack} rámec. Protokol v tento moment neřeší spolehlivost pořadí paketů. V našem případě posíláme například akce z klienta na server a u každé definujeme číslo snímku. Server si udržuje nejvyšší číslo snímku, které dostal, a všechny zprávy z nižších snímků zahazuje. Stejně to dělá klient se snapshoty. Proto neřešíme pořadí na transportní vrstvě.
|
|
|
|
Spolehlivost řeší komponenta \inlcpp{ReliabilityUnit}, která si udržuje frontu rámců, které je potřeba odeslat, čas, kdy je potřeba rámec odeslat a jejich spolehlivost. Čas slouží pouze pro prioritu a málokdy se stane, že se rámec odešle právě v tento čas. Objekt \inlcpp{QuicrEndpoint} se při ťiku dotazuje \inlcpp{QuicrConnection} na další datagram, který chce odeslat. Ten pomocí \inlcpp{ReliabilityUnit} začne datagram skládat z paketů. Když přijde rámec na řadu a je spolehlivý, hned se umístí na konec fronty a nastaví se mu čas odeslání. Většinou aktuální čas s přičteným konstantním intervalem. Pokud je nespolehlivý, do fronty se nevrátí. Krátce představíme rozhraní \inlcpp{ReliabilityUnit} objektu.
|
|
|
|
\begin{description}
|
|
|
|
\item[\texttt{push\_reliable\_frame\_to\_send(deadline, frame)}] \hfill \\
|
|
Uloží si zakódovaný rámec a nastaví si u něj čas odeslání. Jakmile bude po deadline, jednotka se bude snažit dostat rámec do dalšího datagramu. Rámec se ukládá v zakódované podobě tak, aby byla předpovídatelná jeho velikost a snadno se odhadovalo, jestli se do dalšího datagramu vleze.
|
|
|
|
\item[\texttt{peek/pop\_reliable\_frames\_to\_send(packet\_number)}] \hfill \\
|
|
Vrátí rámce, které je potřeba v tomto okamžiku odeslat. Číslo paketu \texttt{packet\_number} si jednotka přiřadí jako verifikátor doručení rámce, který metoda vrátí. To znamená, že když toto číslo přijde v \texttt{Ack} rámci, bude jednotka vědět, který rámec může odebrat. Zároveň může mít vícero vazeb čísla paketu na jeden rámec. Libovolné číslo paketu smaže provázaný rámec. Důvod je, aby mohlo být v oběhu více paketů se stejným rámcem.
|
|
|
|
\item[\texttt{push\_ack(packet\_number)}] \hfill \\
|
|
Vloží do seznamu čísel paketů. Celý tento seznam se odešle v \texttt{Ack} rámci v dalším datagramu.
|
|
|
|
\item[\texttt{peek/pop\_acks\_to\_send()}] \hfill \\
|
|
Vrátí seznam čísel paketů, které musí druhé straně oznámit jako doručené.
|
|
|
|
\end{description}
|
|
|
|
\subsubsection{Enkodér}
|
|
|
|
Pro kódování jsme vytvořili komponentu \inlcpp{QuicrEncoder}, která přes rozhraní umožňuje kódovat pakety a rámce do libovolné alokované paměti. Specifická vlastnost, kterou jsme potřebovali, je kódovat, dokud je v bufferu místo. Pokud zbývá $r$ bytů místa a replikátor by chtěl zakódovat rámec, který má $> r$ bytů, musí metoda vrátit informaci o neúspěchu, ale nevyhazovat vyjímku, ani neposunovat kurzor. Důvodem je, že UDP je nad protokolem IP, který může začít fragmentovat datagramy. Proto se většina protokolů snaží držet velikost datagramů kolem 1200 bytů. Podobný přístup jsme adaptovali my.
|
|
|
|
Dále musel umožňovat zakódovat hodnotu zpětně, jako například délku, kterou kvůli omezení bufferu víme až později. Potom, co přes rozhraní zapíšeme vše, co je potřeba, zavoláme metodu \inlcpp{finish}, která hodnoty pro délku přenastaví na správnou hodnotu.
|
|
|
|
|
|
\subsubsection{Testování}
|
|
|
|
Při implementaci jsme zjistili, že je dobré začít testy, které definují jednotlivé vlastnosti protokolu, jako například: "Spojení začne komunikaci rámcem Hello", "Po rámci Hello odpoví spojení rámcem Hello a ACK", "Na pakety obsahující spolehlivé rámce odpoví spojení ACK rámcem" apod. Tomuto přístupu se říká test driven development. Původně jsme začali implementací, ale bylo těžké domyslet, jak by se měl protokol implementovat.
|
|
% Pro vývoj protokolu jsem si nejprve zkoušel zapsat specifikaci, následně udělat vývoj a nakonec otestovat. Tento přístup ale způsoboval časté chyby v návrhu protokolu. Daleko více se mi osvědčil tzv. Test Driven Development, kdy začneme tvorbou testů, které slouží v podstatě jako specifikace chování. Příkladem jsou testy: "Client begins with Hello datagram", "Connection reacts to Hello with ACK \& Hello" nebo "Both connections have correct IDs after Initial exchange". Tímto způsobem jsem definoval jak chci, aby se protokol a komponenty v něm chovali.
|
|
|
|
% Popsal jsem problémy, které jsem potřeboval vyřešit. Představím protokol, který je pro mě řeší. Vytvořil jsem třídu `QuicrConnection`, která bude reprezentovat stavový stroj připojení a zároveň umožní číst a zapisovat. Rozhodl jsem se, že zapisovat bude zprávy, nikoliv pouze proud bytů. Proto jsem použil tzv. message frame. Pokud něco popnu z QuicrConnection, je to vždy celá zpráva, která přišla. Stejný princip používá i ZeroMQ. Vytvořil jsem dvě třídy: QuicrConnection a QuicrConnectionListener. QuicrConnection obsahuje stavový stroj připojení a rozhraní pro příjímání a odesílání zpráv. QuicrConnectionListener obsahuje rozhraní pro poslouchání nově příchozích spojení. Nasluchač bude spravovat UDP socket. Když přijde nový datagram z adresy, kterou ještě nezná, a přišla zpráva Hello, tak si do seznamu přidá připojení ve stavu HelloReceived. Při datagramu z adresy, kterou zná, pošle tento datagram přímo do spojení na spracování. Každé spojení si bude udržovat svoji frontu příchozích zpráv.
|
|
|
|
\subsubsection{Měření}
|
|
|
|
Protokol měl za cíl snížit odezvu na nespolehlivé síti a neblokovat zprávy. Vytvořili jsme test, který simuluje program hry. V testu jsou dvě vlákna, jedno pro klienta a druhé pro server. Oba mají svou vlastní smyčku, ve kterém iterují. Klient odesílá hodnotu derivace typu double. Server si udržuje stav hodnoty, nazvěme ji \uv{pozice}. Při obdržení derivace ji přičte k aktuálnímu stavu. Každý snímek server odesílá na klienta stav pozice, kterou si klient aplikuje.
|
|
|
|
Pro nasimulování nespolehlivosti sítě jsme využili Linuxový nástroj \uv{tc}. Ten obsahuje \uv{network emulator}, který obsahuje různá nastavení spolehlivosti sítě. Například kolik procent paketů má ztratit, jaká má být odezva nebo jak moc náhodné bude pořadí paketů. Budeme simulovat odezvu 200 milisekund s rozptylem 20ms, ztrátu 3\% paketů a 1\% paketů bude duplikovaných. Celý příkaz pro nastavení můžeme vidět v příkladu . Vidíme, že používáme pouze rozhraní \inlcpp{lo}, tedy loopback, které se týká posílání mezi programy na lokální adrese.
|
|
|
|
\begin{kicode}{cpp}{}{Příklad volání TC}
|
|
# tc qdisc add dev lo root netem \
|
|
delay 200ms 20ms 25% \
|
|
loss 3% \
|
|
duplicate 1% \
|
|
reorder 25% 50%
|
|
\end{kicode}
|
|
|
|
|
|
V prvním testu jsme pouze měřili odezvu mezi odesláním vstupu od klienta pro snímek $n$ a získáním jeho integrovaného stavu ze serveru. Na obrázku \ref{fig:latencycomparison} vidíme, že TCP obsahuje skoky a QUICr je stabilnější.
|
|
|
|
\begin{figure}
|
|
\begin{center}
|
|
\includegraphics[width=0.8\textwidth]{graphics/latencycomparison.pdf}
|
|
\end{center}
|
|
\caption{Porovnání odezvy TCP a QUICr}
|
|
\label{fig:latencycomparison}
|
|
\end{figure}
|
|
|
|
\begin{figure}
|
|
\begin{center}
|
|
\includegraphics[width=0.8\textwidth]{graphics/integrationcomparison.pdf}
|
|
\end{center}
|
|
\label{fig:integrationcomparison}
|
|
\caption{Porovnání integrace TCP a QUICr}
|
|
\end{figure}
|
|
|
|
Následně jsme zkusili, jak se nespolehlivost sítě a skoky odezvy z prvního měření projeví v integraci proměnné. Pro derivaci jsme použili sinusoidu, abychom ji mohli zreplikovat snadno v TCP i QUICr. Na obrázku \ref{fig:integrationcomparison} vidíme, že TCP při ztrátě paketů způsobuje skoky, které nemusí být pro hráče příjemné. Naopak náš protokol má integraci plynulejší.
|
|
|
|
Nakonec jsme integrovali protokol přímo do replikátoru. Vytvořili jsme simulovaného klienta, který na server posílá náhodné vstupy a způsobuje na serveru svůj pohyb. Těchto simulovaných klientů můžeme spouštět libovolný počet. Každý naváže se serverem spojení a komunikuje s ním klasicky přes replikátor a ovladač. Můžeme tak snadno měřit, jak se zatížení projeví v síti, a zároveň můžeme využít nástroje jako tc pro různé podmínky. Na obrázku \ref{fig:playerbenchmark} vidíme 301 hráčů, kde 300 je simulovaných individuálních připojení a jeden je náš hráč. Server počítal každý snímek dlouho, asi 250ms, ale zátěž zvládl. Dalším krokem bylo zvýšit počet snímku za sekundu pro stovky hráčů.
|
|
|
|
\begin{figure}
|
|
\centering
|
|
\includegraphics[width=0.5\textwidth]{graphics/playerbenchmark.png}
|
|
\label{fig:playerbenchmark}
|
|
\caption{301 připojených hráčů}
|
|
\end{figure}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
\subsection{Optimalizace replikátoru}
|
|
|
|
Test s 301 hráči odhalil nedostatečný výkon serveru. Pomocí Tracy jsme začali program analyzovat. Na obrázku \ref{fig:tracyreplicator} vidíme, že pouze aktualizace replikátoru trvá každý snímek kolem 100ms. Pro představu, pokud bychom chtěli, aby server integroval 20 krát za sekundu, museli bychom všechno, nejen aktualizaci replikátoru, stihnout do 50 milisekund. Proto je aktuální stav nepřijatelně dlouhá doba. Úzké hrdlo najednou nebyla síť, ale příprava smysluplných dat, kterými síť zatížíme. Soustředili jsme další optimalizace právě na replikátor.
|
|
|
|
\begin{figure}
|
|
\centering
|
|
\includegraphics[width=1\textwidth]{graphics/300playerstrace.png}
|
|
\label{fig:tracyreplicator}
|
|
\caption{Analýza replikátoru}
|
|
\end{figure}
|
|
|
|
Z analýzy vyšlo najevo, že problém je při skládání zpráv. Tedy kódování pozic entit, o které se klient zajímá, do objektu. Je to právě tento objekt, který se serializuje přes ProtoBuf a odesílá přes soket klientům.
|
|
|
|
Replikátor pro každého klienta zjistil jeho zájem a pro každou entitu v něm vyhledával jeho komponenty. Vyhledávání je v případě Entt implementováno hashovací tabulkou a časová složitost je v $O(1)$. Začali jsme měřit i jiná řešení. Primárně nás zajímali metody, které jsou vhodné pro ECS architekturu. První řešení, které jsme zkusili, bylo vytvořit buffer instancí zpráv pro každého klienta najednou. Přes komponentu, kterou jsme chtěli replikovat, např. Transform, jsme vytvořili Entt pohled a iterovali. Pro každý prvek jsem komponentu zapsali do zprávy pro každého klienta, kterého zajímala. Výslednou analýzu vidíme na obrázku \ref{fig:ecs_optimization_tracy_01}. Tato optimalizace, která jen změnila pořadí, v jakém nahlížíme na data, snížila dobu, kterou trvá aktualizace replikátoru, na polovinu.
|
|
|
|
\begin{figure}
|
|
\centering
|
|
\includegraphics[width=1\textwidth]{graphics/ecs_optimization_tracy_01.png}
|
|
\label{fig:ecs_optimization_tracy_01}
|
|
\caption{Analýza optimalizace replikátoru pro ECS}
|
|
\end{figure}
|
|
|
|
\begin{figure}
|
|
\centering
|
|
\includegraphics[width=1\textwidth]{graphics/ecs_optimization_tracy_02.png}
|
|
\label{fig:ecs_optimization_tracy_02}
|
|
\caption{Analýza optimalizace replikátoru pro ECS}
|
|
\end{figure}
|
|
|
|
Replikátor jsme začali analyzovat do větších detailů. Na obrázku \ref{fig:ecs_optimization_tracy_02} vidíme, které jeho části zabrali jakou dobu. Zjistili jsme, že problém byl samotný ProtoBuf. Formát je podobně jako JSON dělaný pro složité struktury objektů. V našem případě je ale zpráva pro snapshot světa primitivní. Rozhodli jsme udělat porovnání s FlatBuffers. V testu jsme simulovali 100 snímků, tak, abychom si mohli dopředu alokovat všechnu potřebnou paměť, kterou můžeme mezi snímky využívat. Dopředu si vytvoříme vektor pozic, které v testu budeme serializovat. Stejně jako v replikátoru používáme architekturu orientovanou na ECS a procházíme všechny pozice, ty pak umisťujeme do alokovaných bloků paměti pro každého klienta a simulujeme tak skládání rámců, které můžeme odesílat. Všimli jsme si, že v případě primitivní zprávy, jako je snapshot stavu světa, by nám stačila klasická funkce ze standartní C knihovny zvaná \inlcpp{memcpy} a do testu jsme ji zařadili. Výsledky vidíme v tabulce \ref{table:serialization_comparison}.
|
|
|
|
\begin{figure}
|
|
\begin{center}
|
|
\begin{tabular}{ | m{5em} | m{3cm}| m{3cm} | m{3cm} | }
|
|
\hline
|
|
& ProtoBuf & FlatBuffers & Memcpy \\
|
|
\hline
|
|
Čas (s) & 9.455691762 & 10.345692894 & 0.754350866 \\
|
|
\hline
|
|
\end{tabular}
|
|
\label{table:serialization_comparison}
|
|
\caption{Porovnání serializátorů}
|
|
\end{center}
|
|
\end{figure}
|
|
|
|
Zjistili jsme, že problémem byl ProtoBuf a FlatBuffers by nám nepomohl. Nepřekvapilo nás, že memcpy byl nejrychlejší. Rozhodli jsme se, že pro serializaci a deserializaci snapshotů světa budeme používat vlastní serializaci.
|
|
|
|
Pro kódování jsme využili už existující \inlcpp{ByteBuffer}, který umožňuje snadné kódování po bytech do vektoru. Třída \inlcpp{WorldStateWriter} obsahuje konkrétní kódování pro tyto zprávy a přes konstruktor je nutné předat instanci \inlcpp{ByteBuffer}.
|
|
|
|
Zpráva obsahuje hlavičku, ve které je počet aktualizovaných entit a počet nových entit. Počet odebraných entit můžeme vynechat, ten jsme schopni zjistit z délky zprávy a zbylých počtů. Pro čtení slouží \inlcpp{WorldStateReader}.
|
|
|
|
|
|
|
|
\subsection{Optimalizace manažera zájmů}
|
|
|
|
Všimli jsme si, že replikátor trval každou iteraci kolem 60 milisekund. Nejprve jsme se rozhodli přidat datové struktury. Druhým cílem bylo zlepšit API, kterým se replikátor dotazuje, jestli je entita pro klienta zajímavá.
|
|
|
|
Jedna s datových struktur, která má nízkou složitost pro dotaz na seznam entit ve vzdálenosti maximálně, je klasická mřížka. Charakteristika hráčů ve hrách je, že se hodně pohybují. Mřížka nepotřebuje žádný přepočet, ale pouze vložit do předem alokovaného bufferu, nebo z něj naopak odebrat.
|
|
|
|
Testovali jsme 4 implementace, z toho 3 různé datové struktury a naivní přístup. Simulovali jsme pohyb 100 000 různých entit po dobu 100 snímků. Všechny možnosti představíme. Naivní přístup využívá pouze Pythagorovu větu, aby získal vzdálenost dvou bodů. Jediná optimalizace je vynechat odmocninu a umocnit místo toho vzdálenost. Druhá a třetí implementace využívá mřížku a entity rozděluje do svých polí. První je fixní mřížka, která je vhodná pro světy, které nemění svou velikost, jako např. World of Warcraft. Naopak hashovací mřížka přiřazuje entity do polí pomocí hashe jejich pozice. Jsou tak ideální pro dynamické a potenciálně nekonečné světy, jako např. Minecraft. Poslední je quad tree. Jedná se o obdobu binárního stromu rozšířenou o rozměr. V tabulce \ref{table:range_query_comparison} vidíme výsledky testu. Celkový čas je milisekundách. Ve třetím a čvrtém sloupci je změřená část vkládání a dotazování. Díky tomu vidíme, že Quad tree má rychlejší dotazování, ale delší vkládání. Nejrychlejší je fixní mřížka. Má ale vysokou paměťovou náročnost a při použití je tak nutné zvážit, jestli není hashovací mřížka vhodnější. Quad tree implementace se ukázala jako nevhodná. S přibývající hloubkou je navíc pomalejší. Ideální se ukázali varianty s fixní a hashovací mřížkou.
|
|
|
|
\begin{figure}
|
|
\begin{center}
|
|
\begin{tabular}{ | m{5em} | m{3cm} | m{3cm} | m{3cm} | }
|
|
\hline
|
|
Název testu & Celkový čas & Čas vkládání & Čas dotazování \\
|
|
\hline
|
|
Naivní & 38770 & 0 & 38650 (99.96\%) \\
|
|
\hline
|
|
Fixní mřížka & 467 & 301 (64.56\%) & 17 (3.74\%) \\
|
|
\hline
|
|
Hashovací mřížka & 1070 & 736.63 (68.76\%) & 42 (3.93\%) \\
|
|
\hline
|
|
Quad tree & 1830 & 1560 (85.45\%) & 30.29 (1.66\%) \\
|
|
\hline
|
|
\end{tabular}
|
|
\label{table:range_query_comparison}
|
|
\caption{Porovnání algoritmů}
|
|
\end{center}
|
|
\end{figure}
|
|
|
|
|
|
|
|
% \subsection{Optimalizace QUICr}
|
|
|
|
|
|
|
|
% \subsubsection{Šifrování}
|
|
|
|
% Jednotlivé datagramy a zprávy je třeba šifrovat a pro to je třeba, aby si dvě strany vyměnili tajný klíč. Představím metodu, kterou pro výměnu budu využívat.
|
|
|
|
% \subsubsection{SPR-6}
|
|
|
|
% Metodu, kterou jsem viděl například u síťového protokolu pro hru World Of Warcraft je SRP-6. Jedná se o protokol, který nevyžaduje, aby server znal heslo klienta, ale měl u sebe pouze validátor, a klient mu díky němu dokáže, že heslo zná. Pro tento přístup je třeba udělat handshake.
|
|
|
|
|
|
\subsubsection{Congestion control}
|
|
|
|
\subsubsection{Bandwidth control}
|
|
|
|
Entity, které jsou od hráče daleko, není třeba aktualizovat tak často jako ty, které jsou k němu blízko. Již jsme nastínili, že můžeme můžeme spočítat důležitost jednotlivých entit pro klienta a omezit tak počet entit, které musí replikátor synchronizovat. Například můžeme vzdálenější entity synchronizovat méně často. Důležité je, aby se i ty dostali někdy na řadu.
|
|
|
|
\subsection{Sharding}
|
|
|
|
Proces, při kterém hráče rozdělíme do skupin, které se navzájem nevidí, i když jsou na stejném serveru blízko sebe, se říká sharding. Podobný princip funguje i v databázích.
|
|
|
|
\subsection{Horizontální škálování}
|
|
|
|
Další z metod podobná shardování, je \uv{horizontální škálování}, kdy do systému přidáváme servery, které si tak rovnoměrně rozdělí práci. V případě naší hry jsme rozdělili herní svět na zóny a o každou se stará jiný server. Takový server je \uv{zone server} a dohromady tvoří \uv{zone cluster}. Do systému jsme dále přidali \uv{cluster koordinátora}, který říká, který zone server má jakou zónu. Model mezi koordinátorem a zone serverem je klient-server. Na druhou stranu zone servery mezi sebou komunikují přímo a využívají model peer-to-peer.
|
|
|
|
Představíme, jak spolu zone servery komunikují. Využijeme k tomu už existující komponenty: replikátor a manažera zájmu. Nejprve jsme stav hry, manažera zájmu a fyzický svět do třídy \inlcpp{ZoneManager}. Tuto třídu obalíme \inlcpp{ZoneServer} třídou. Ta bude obsahovat replikátor a \inlcpp{ZoneCoordinatorClient}. Manažer zón si bude udržovat registr sousedních zón a bude se k ním chovat jako klasickým klientům. Rozdíl bude ten, že klient má jako zájem definovaný pouze bod a manažer zájmu klientovi přiřadí do zájmu entity, které jsou od bodu v dostatečné vzdálenosti. Sousedi budou mít jako zájem definovanou celou jejich plochu. Manažer zájmu má nyní na rozhraní dvě metody: \inlcpp{register\_client\_interest(interest\_id, entity)} a \inlcpp{register\_zone\_interest(interest\_id, area)}. Každý zájem je definovaný unikátním ID. O to, jaké entity pak konkrétní zájem zajímají získáme voláním \inlcpp{get_interest(interest_id)}.
|
|
|
|
Replikátor poté replikuje sousedům ty entity, které jsou od plochy souseda v dostatečné blízkosti. Pokud klient překročí do jiné zóny, měl by manažer zóny předat klienta druhé zóně. Toto modelujeme přes \inlcpp{ZoneProxy}, která reprezentuje souseda pro manažera konkrétní zóny. Implementace obsahuje \inlcpp{ZoneClusterLink}, která obsahuje QUICr endpoint, na který ostatní zóny mohou posílat cluster data.
|
|
|
|
Systémovou architekturu vidíme na obrázku \ref{fig:zone_cluster_architecture}. Vidíme, že \inlcpp{ZoneClusterLink} slouží pro komunikaci mezi servery v clusteru a zároveň pro replikaci entit, které jsou blízko hranic se sousední zónou. Když se nový server spustí, nejprve se registruje koordinátorovi. Ten mu přiřadí ID, plochu a seznam jeho sousedů včetně \inlcpp{ZoneClusterLink} adres. S každým sousedem přes QUICr naváže spojení a představí se pomocí zprávy \inlcpp{ZoneHello}. Tím si ho druhý server přidá do registru a může na něj začít přenášet klienty.
|
|
|
|
\begin{figure}
|
|
\centering
|
|
\includegraphics[width=1\textwidth]{graphics/zone_server_architecture.pdf}
|
|
\label{fig:zone_cluster_architecture}
|
|
\caption{Architektura systému s zone clusterem}
|
|
\end{figure}
|
|
|
|
|
|
\subsection{Komprese}
|
|
|
|
Poslední optimalizací, kterou jsem pro přenos informací použili, byla komprese. Nejprve jsme nahradili byte buffer, který pracuje pouze s byty, bit bufferem, který nám umožnil pracovat s jednotlivými bity.
|
|
|
|
\subsubsection{Bit Buffer}
|
|
|
|
Snadným zmenšením paketů je zaměřit se na bity namísto bytů. Původně jsem nově příchozí datagramy obalil pomocí ByteBuffer, který umožnil dekódovat postupně příchozí byty. Často ale potřebujeme mít možnost zakódovat informaci pomocí jednotlivých bitů.
|
|
|
|
|
|
|
|
\subsection{Závěry}
|
|
Závěr práce by se měl poskytnout jak v původním jazyce práce, tak v jazyce anglickém. Pro sazbu závěru jsou k dispozici příslušná makra. Berte na vědomí, že v anglickém závěru se aktivuje plně anglická sazba se všemi konvencemi. Tedy je třeba používat anglické uvozovky a další správné typografické prvky.
|
|
|
|
\begin{kicode}{TeX}{}{Sazba závěrů}
|
|
% Tiskne český závěr práce.
|
|
\begin{kiconclusions}
|
|
Závěr práce v \uv{českém} jazyce.
|
|
\end{kiconclusions}
|
|
|
|
% Tiskne anglický závěr práce.
|
|
\begin{kiconclusions}[english]
|
|
Thesis conclusions written in \uv{English}.
|
|
\end{kiconclusions}
|
|
\end{kicode}
|
|
|
|
|
|
%% Závěry práce. V jazyce práce a anglicky. Text pro jiný než
|
|
%% nastavený jazyk práce (nepovinným parametrem language makra
|
|
%% \documentclass, výchozí český) se zadává použitím makra s uvedením
|
|
%% jazyka jako nepovinného parametru.
|
|
\begin{kiconclusions}
|
|
Závěr práce v \uv{českém} jazyce.
|
|
\end{kiconclusions}
|
|
|
|
\begin{kiconclusions}[english]
|
|
Thesis conclusions in \uv{English}.
|
|
\end{kiconclusions}
|
|
|
|
%% Přílohy obsahu textu práce, za makrem \appendix.
|
|
\appendix
|
|
|
|
\section{První příloha}
|
|
Text první přílohy
|
|
|
|
\section{Druhá příloha}
|
|
Text druhé přílohy
|
|
|
|
%% Obsah elektronických dat. Poslední příloha. Upravte podle vlastní
|
|
%% práce!
|
|
\section{Obsah elektronických dat} \label{sec:ObsahData}
|
|
|
|
Na samotném konci textu práce je uveden stručný popis obsahu
|
|
elektronických dat odevzdaných v systému katedry informatiky spolu s
|
|
textem. Tato data jsou nedílnou součástí práce a tvoří (datovou)
|
|
přílohu textu práce. Povinné položky struktury dat jsou:
|
|
|
|
\begin{description}
|
|
|
|
\item[\texttt{text/}] \hfill \\
|
|
Adresář s textem práce ve formátu PDF, vytvořený s~použitím
|
|
závazného stylu KI PřF UP v~Olomouci pro závěrečné práce, včetně
|
|
všech (textových) příloh, a~všechny soubory potřebné pro
|
|
bezproblémové vytvoření PDF dokumentu textu (případně v~ZIP
|
|
archivu), tj.~zdrojový text textu a příloh, vložené obrázky, apod.
|
|
|
|
\item[\texttt{README.*}] \hfill \\
|
|
Textový soubor (s příponou např. \texttt{.txt}) s informacemi o
|
|
opakovatelném způsobu použití ostatních dat práce -- typicky plně
|
|
reprodukovatelný co nejúplnější funkční postup zprovoznění software
|
|
vytvořeného v~rámci práce, tzn. jeho případné instalace/nasazení a
|
|
spuštění, včetně uvedení všech požadavků pro bezproblémový provoz;
|
|
za zprovoznění software se nepovažuje zpřístupnění (např. po
|
|
Internetu) již někde zprovozněného software.
|
|
|
|
\item[\texttt{*}] \hfill \\
|
|
Adresáře a soubory s veškerými ostatními autorskými daty práce
|
|
(případně v~ZIP archivu) -- typicky spustitelné a další soubory
|
|
software vytvořeného v rámci práce potřebné pro bezproblémový provoz
|
|
software, případně jeho instalační program, a kompletní zdrojové
|
|
texty software a další data nutná pro plně reprodukovatelné korektní
|
|
vytvoření spustitelných souborů.
|
|
|
|
\end{description}
|
|
|
|
Dále mohou data obsahovat například:
|
|
|
|
\begin{itemize}
|
|
|
|
%\item[\texttt{data/}] \hfill \\
|
|
\item
|
|
ukázková a~testovací data použitá v~práci nebo pro potřeby posouzení
|
|
práce v rámci její obhajoby,
|
|
|
|
%\item[\texttt{literature/}] \hfill \\
|
|
\item
|
|
položky bibliografie v elektronické podobě, příp.~jiná relevantní literatura
|
|
a dokumentace vztahující se k~práci,
|
|
|
|
%\item[\texttt{install/}] \hfill \\
|
|
\item
|
|
cizí data (software) potřebná pro bezproblémové použití autorských
|
|
dat práce (software), která nejsou standardní součástí
|
|
předpokládaného (softwarového) vybavení uživatele.
|
|
|
|
\end{itemize}
|
|
|
|
U~veškerých cizích obsažených materiálů jejich
|
|
zahrnutí dovolují podmínky pro jejich veřejné šíření nebo přiložený souhlas
|
|
držitele práv k užití. Pro všechny použité (a~citované) materiály,
|
|
u~kterých toto není splněno a~nejsou tak obsaženy, je uveden
|
|
jejich zdroj, např.~webová adresa, v~bibliografii nebo textu práce
|
|
nebo souboru \texttt{README.*}.
|
|
|
|
%% -------------------------------------------------------------------
|
|
|
|
%% Sazba volitelného seznamu zkratek, za přílohami.
|
|
\printglossary
|
|
|
|
%% Sazba povinné bibliografie, za přílohami (případně i za seznamem
|
|
%% zkratek). Při použití BibLaTeXu použijte makro
|
|
%% \printbibliography. jinak prostředí thebibliography. Ne obojí!
|
|
|
|
%% Sazba i v textu necitovaných zdrojů, při použití
|
|
%% BibLaTeXu. Volitelné.
|
|
\nocite{*}
|
|
%% Vlastní sazba bibliografie při použití BibLaTeXu.
|
|
\printbibliography
|
|
|
|
%% Bibliografie, včetně sazby, při NEpoužití BibLaTeXu.
|
|
% \begin{thebibliography}{9}
|
|
%\bibitem{kniha2} \uppercase{Hawke}, Paul. NanoHttpd: Light-weight HTTP server designed for embedding in other applications. GitHub [online]. 2014-05-12. [cit. 2014-12-06]. Dostupné z: \url{https://github.com/NanoHttpd/nanohttpd}
|
|
%
|
|
%\bibitem{jeske13} \uppercase{Jeske}, David; \uppercase{Novák}, Josef. Simple HTTP Server in \csharp: Threaded synchronous HTTP Server abstract class, to respond to HTTP requests. CodeProject: For those who code [online]. 2014-05-24. [cit. 2014-12-06]. Dostupné z: \url{http://www.codeproject.com/Articles/137979/Simple-HTTP-Server-in-C}
|
|
%
|
|
%\bibitem{uzis2012} \uppercase{ÚSTAV ZDRAVOTNICKÝCH INFORMACÍ A STATISTIKY ČR}. Lékaři, zubní lékaři a farmaceuti 2012 [online]. Praha 2, Palackého náměstí 4: Ústav zdravotnických informací a statistiky ČR, 2012 [cit. 2014-12-06]. ISBN 978-80-7472-089-5. Dostupné z: \url{http://www.uzis.crypublikace/lekari-zubni-lekari-farmaceuti-2012}
|
|
% \end{thebibliography}
|
|
|
|
%% Sazba volitelného rejstříku, za bibliografií.
|
|
\printindex
|
|
|
|
\end{document}
|
|
|
|
%%% Local Variables:
|
|
%%% mode: latex
|
|
%%% TeX-master: t
|
|
%%% End:
|