ProjektPR/sprawozdanie/sprawozdanie.tex
2018-04-17 21:45:19 +02:00

356 lines
17 KiB
TeX

\documentclass[]{article}
\usepackage[T1]{fontenc}
\usepackage{polski}
\usepackage[utf8]{inputenc}
\usepackage[polish]{babel}
\usepackage[margin=1.25in]{geometry}
\usepackage{titling}
\usepackage{pdfpages}
\usepackage{float}
\usepackage{amsmath}
\usepackage{amstext}
\usepackage{pgfplots}
\usepackage{tikz}
\usepackage{enumerate}
\usepackage{lmodern}
\usepackage{amsfonts}
\usepackage{mathtools}
\usepackage{algorithm}
\usepackage{algpseudocode}
\usepackage{wrapfig}
\usepackage{braket}
\usepackage{subcaption}
\usepackage{multirow}
\usepackage{etoolbox}
\usepackage{color}
\usepackage{pgfkeys}
\usepackage{siunitx}
\pgfplotsset{compat=1.15}
\usepgfplotslibrary{groupplots}
\usepgfplotslibrary{external}
\usetikzlibrary{decorations.pathmorphing, arrows.meta, positioning}
\usetikzlibrary{shapes.geometric, arrows, intersections}
\newcommand{\areyousure}[1]{{\color{red}\textbf{???} #1 \textbf{???}}}
\newcommand{\todo}[1]{{\color{blue}\textbf{TODO:} #1}}
\makeatletter
\newenvironment{packet}{%
\def\byte{*8mm}%
\newrobustcmd{\member}[3][]{%
\node[packet, minimum width=##3\byte] (##2) {##2};
\node[below=of ##2] {##3};
\node[above=of ##2.north, text depth=0pt] {##1};
\tikzstyle{packet}+=[right=of ##2];
}%
\newrobustcmd{\preamble}[1][15]{%
\node[packet, minimum width=2\byte, dashed] (Preamble) {$\cdots$};
\node[below=of Preamble] {##1};
\node[above=of Preamble.north] {Preambuła};
\tikzstyle{packet}+=[right=of Preamble];
}%
\begin{tikzpicture}[%
packet/.style={draw, auto, align=center, minimum height=7mm, font=\ttfamily, right=of start},
optional/.style={dotted},
every node/.style={text depth=0pt},
node distance=0,
x=8mm, y=1.5cm
]%
\node (start) {};
}{%
\end{tikzpicture}%
\let\byte\undefined
\let\member\undefined
\let\preamble\undefined
}
\makeatother
% opening
\title{Przetwarzanie Rozproszone - Projekt}
\author{\protect\begin{tabular}{ll|l}
Weronika Czarnecka & \textit{165600} & \multirow{4}{*}{\shortstack[l]{Informatyka 2016/2017 \\ Semestr IV, gr. 1}} \tabularnewline
Anna Piliczewska & \textit{165436} & \tabularnewline
Maciej Borzyszkowski & \textit{165407} & \tabularnewline
Kacper Donat & \textit{165581} & %
\protect\end{tabular}}
\floatname{algorithm}{Program}
\begin{document}
\maketitle
\section{Zasady rozgrywki}
Gracze poruszają się po wspólnej planszy czołgami strzelając do siebie i starając się zniszczyć. Każdy gracz kontroluje
tylko i wyłącznie swój czołg.
\subsection{Przechowywany stan}
Aktualny stan rozgrywki $S$ jest determinowany przez aktualne obiekty na mapie. Każda aktualizacja ze strony klienta,
wpływająca na stan $S$ powoduje zwiększenie licznika stanu o 1. Ze względu na trudność synchronizacji kolejności
aktualizacji (np. stan 2 klienta A może być stanem 3 dla klienta B i odwrotnie) zdecydowano się, że każdy klient
przechowuje ilość odebranych aktualizacji stanu od każdego klienta osobno.
Dodatkowo, każdy obiekt na mapie zawiera swój własny stan, zależny od typu obiektu. Przykładowo dla czołgu może to być
pozycja (\texttt{vec2}), kąt (\texttt{double}) oraz ich pierwsza oraz druga pochodna (w celach predykcji).
\section{Wykorzystane technologie}
\begin{itemize}
\item SFML.net - obsługa grafiki oraz wejścia od użytkownika
\item System.Threading - wątki
\item System.Net.Sockets - obsługa sieci
\end{itemize}
\section{Proponowana Architektura}
Z założenia, rozgrywka ma odbywać się w czasie rzeczywistym zatem wykorzystanie protokołu TCP mogłoby
wprowadzać niepotrzebne opóźnienia, wynikające z niepotrzebnych retransmisji pakietów.
Komunikacja będzie odbywała się zatem za pomocą datagramów UDP, które pozwalają na szybszą - acz mniej dokładną
transmisję informacji pomiędzy klientami. W wypadku normalnej gry jednak strata części pakietów jest jak
najbardziej akceptowalna i nie wpłynie na rozgrywkę w znaczący sposób.
W projekcie przyjęto architekturę klient-klient, tj. nie występuje żaden serwer stanowiący źródło prawdy dla rozgrywki.
Zamiast tego jeden z klientów zawsze będzie miał status \textit{mastera}. Poprawnym stanem rozgrywki z definicji jest
stan symulacji \textit{mastera}. \textit{Masterem} zawsze jest klient o najmniejszym \texttt{ClientId}, tj. klient,
który podłączył się najwcześniej. Takie podejście zapewnia rozwiązanie sytuacji, w której aktualny \textit{master} się
rozłączy bez konieczności przeprowadzania negocjacji.
\begin{figure}[H]
\centering
\begin{tikzpicture}[
client/.style = { circle, draw },
master/.style = { very thick },
node distance=2cm
]
\node[client, master] (C1) {$C_1$};
\node[client, right=of C1] (C2) {$C_2$};
\node[client, below=of C2] (C3) {$C_3$};
\node[client, left=of C3] (C4) {$C_4$};
\foreach \u/\v in {C1/C2,C1/C3,C1/C4,C2/C3,C2/C4,C3/C4} \draw[<->] (\u) -- (\v);
\end{tikzpicture}
\caption{Proponowana architektura sieciowa}
\end{figure}
Dodatkowo, przyjmuje się zasadę, że każdy klient jest odpowiedzialny za swoją część symulacji, dla której stanowi źródło
prawdy. W praktyce oznacza to, że każdy gracz wysyła tylko i wyłącznie informacje o zmianach rzeczy, na które on ma
wpływ - np. pozycję własnego czołgu, czy wystrzelenie pocisku. W gestii innych klientów jest przetworzyć te informacje
i zsynchronizować się. Przewidziano również obiekty o zachowaniu deterministycznym, które każdy klient może aktualizować
samodzielnie w ramach swojej symulacji.
Takie podejście zapewnia brak konfliktu o zasoby, ponieważ każdy obiekt jest modyfikowany wyłącznie przez jedną osobę,
zatem nigdy nie nastąpi konflikt. Nie występuje również problem odczytywania zmienionych danych ponieważ wyświetlanie
oraz aktualizacja stanu gry odbywają się w ramach jednego wątku.
Ze względu na możliwość zgubienia niektórych pakietów, oraz chęć uniknięcia skokowej aktualizacji rozgrywki po stronie
każdego klienta następuje predykcja zachowań innych graczy. W wypadku otrzymania pakietu aktualizującego stan gracza,
predykcja jest odrzucana i wykorzystane są otrzymane dane.
Ze względu na niedoskonałość mechanizmu predykcji, predykcje nie mogą modyfikować stanu gry w sposób nieodwracalny, tj
np. predykcja nie może usunąć obiektu z mapy - może go natomiast oznaczyć jako prawdopodobnie usunięty, co za tym idzie
nie będzie on wyświetlany ani predykowany dalej (z punktu widzenia gracza zostanie usunięty) - w takiej sytuacji
przywrócenie obiektu w wypadku błędnej predykcji wciąż będzie możliwe.
Każdy klient będzie obsługiwany przez 4 osobne wątki, których kompetencje nie kolidują ze sobą:
\begin{enumerate}
\item Aktualizacja, predykcja oraz renderowanie stanu symulacji \label{thread:render}
\item Input z sieci (obiór pakietów, kolejkowanie) \label{input:net}
\item Input użytkownika (odczyt klawiatury itd.) \label{input:user}
\item Output sieciowy (wysyłanie pakietów, retransmisje) \label{output:net}
\end{enumerate}
\begin{figure}[H]
\centering
\begin{tikzpicture}[node distance=1cm]
\node[label=below:{Wątek \ref{thread:render}}] (thread-render) {
\begin{tikzpicture}[every node/.style={align=center}, node distance=1cm]
\node[draw] (predict) {Przewidywania};
\node[draw, below=of predict] (update) {Aktualizacja};
\node[draw, below=of update] (render) {Wyświetlanie};
\draw[->] (predict.west) edge [bend right] (update.west)
(update.west) edge [bend right] (render.west)
(render.east) edge [bend right] (predict.east);
\end{tikzpicture}
};
\node[label=below:{Wątek \ref{input:net} i \ref{input:user}}, right=of thread-render] (thread-input) {
\begin{tikzpicture}[every node/.style={align=center}, node distance=1cm]
\node[draw] (udp) {Nasłuchiwanie UDP};
\draw[->] (udp) edge [loop above] (udp);
\node[draw, below=of udp] (user) {Nasłuchiwanie HID};
\draw[->] (user) edge [loop above] (user);
\end{tikzpicture}
};
\node[label=below:{Wątek \ref{output:net}}, left=of thread-render] (thread-output) {
\begin{tikzpicture}[every node/.style={align=center}, node distance=1cm]
\node[draw] (listen) {Wysyłka pakietów};
\draw[->] (listen) edge [loop above] (listen);
\end{tikzpicture}
};
\node[below=of thread-render] (queue-actions) {Kolejka akcji};
\node[above=of thread-render] (queue-output) {Kolejka wysyłki};
\draw[<->, double] (thread-render) edge (queue-actions)
(thread-input) edge (queue-actions);
\draw[<->, double]
(thread-output) edge (queue-output)
(thread-input) edge (queue-output);
\end{tikzpicture}
\end{figure}
Wątki \ref{input:net} oraz \ref{input:user} będą umieszczały akcje do wykonania w jednej kolejce akcji, zatem kolejka ta
musi być \textit{thread-safe} i udostępniać atomowe metody \texttt{enqueue} i \texttt{dequeue}, gwarantujące że stan
kolejki nie zostanie uszkodzony, tj. żadna akcja nie zostanie wykonana 2-krotnie ani żadna nie zostanie zgubiona.
Zostanie to osiągnięte poprzez ustawienie Mutexa\footnote{https://msdn.microsoft.com/en-us/library/system.threading.mutex(v=vs.110).aspx}
w metodach.
Zakładamy, że akcje użytkownika muszą zostać umieszczone na kolejce akcji natychmiastowo, zatem nie ma potrzeby
synchronizacji z siecią. Co za tym idzie, dostęp do kolejki pakietów (potrzebnej ze względu na możliwość otrzymania
pakietów w nieodpowiedniej kolejności) i jej obsługa będzie w całości po stronie wątku \ref{input:net} - czyli problem
wyścigów nie następuje.
Poprzez identyczną kolejkę będzie obsługiwana wysyłka pakietów, stworzona w celu skomunikowania wątków \ref{input:user}
oraz \ref{output:net}. Dodatkowo w wątku \ref{output:net} będzie działać samodzielna kolejka odpowiadająca za
retransmisję pakietów.
Niektóre pakiety zmieniają stan rozgrywki (np. śmierć, wystrzelenie pocisku), a co za tym idzie muszą zostać
przetworzone przez wszystkich klientów w zbliżonej kolejności i w miarę jednakowym czasie (nie ma możliwości
zagwarantowania idealnej synchronizacji). W tym celu wprowadzony zostanie system numerowania kolejnych pakietów, a każdy
pakiet będzie zawierał informację na temat stanu od którego ,,zależy'' - tj. numeru ostatniej aktualizacji.
\subsection{Protokół}
Protokół opiera się o wysyłanie datagramów UDP między wszystkimi klientami. Ze względu na specyfikę UDP (brak gwarancji
dotarcia pakietów, brak gwarancji kolejności, brak gwarancji poprawności) przyjmuje się następujące założenia:
\begin{itemize}
\item nie wszystkie pakiety są ważne, niektóre można pominąć
\item wszystkie pakiety aby zostać przetworzone muszą dojść w całości poprawnie
\item pakiety są numerowane
\item pakiety mogą wymagać konkretnego poziomu stanu gry
\end{itemize}
Ze względu na konieczność szybkiego przesyłania informacji pomiędzy klientami, zaprojektowany protokół jest protokołem
binarnym, o konstrukcji pakietu widocznej poniżej.
\begin{figure}[H]
\center
\begin{packet}
\member{CRC16$_p$}{2}
\member{Type}{1}
\member{Size}{2}
\member{ClientId}{2}
\member{State}{4}
\member{PacketId}{4}
\node[packet, dashed, right=of PacketId, text width=15mm] {CRC16$_d$};
\end{packet}
\caption{Preambuła pakietu}
\end{figure}
\begin{table}[H]
\centering
\begin{tabular}{l|l|l}
\textbf{Właściwość} & \textbf{Typ} & \textbf{Opis} \\ \hline
\texttt{CRC16$_p$} & \texttt{int16} & Suma kontrolna danych w preambule \\
\texttt{Type} & \texttt{uint8} & Rodzaj wysyłanego pakietu \\
\texttt{Size} & \texttt{uint16} & Rozmiar wysłanych danych, razem z pierwszym bajtem \\
\texttt{ClientId} & \texttt{uint16} & Identyfikator klienta \\
\texttt{State} & \texttt{uint32} & Wymagany stan gry do przetworzenia pakietu \\
\texttt{PacketId} & \texttt{uint32} & Timestamp oryginalnego wysłania pakietu \\ \hline
\texttt{CRC16$_d$} & \texttt{int16} & Suma kontrolna danych pakietu (o ile jakieś są) \\
\end{tabular}
\end{table}
Dla ułatwienia implementacji przyjmuje się, że dla pakietów, których najstarszy bit typu (\texttt{Type \& 0x80}) jest
ustawiony na 1 nie zachodzi potrzeba retransmisji - czyli są to pakiety mało istotne bądź szybko przedawniające się.
Przykładem takiego pakietu może być aktualizacja pozycji gracza - nie ma potrzeby retransmisji ponieważ prawdopodobnie i
tak w drodze jest następny pakiet z nowszą pozycją.
W celu zapewnienia poprawności transmisji, każdy pakiet poprzedzany jest 16-bitową sumą kontrolną \texttt{CRC16},
pozwalającą upewnić się, że dane zostały odebrane poprawnie. Suma kontrolna liczona jest ze wszystkich składowych
pakietu poczynając od \texttt{Type} włącznie. Dodatkowo, jeżeli pakiet potrzebuje dodatkowych danych prócz preambuły to
dane te zawierają własną sumę kontrolną \texttt{CRC16}. Rozgraniczenie to wprowadzono, aby uniknąć sytuacji, w której w
preambule zostałoby uszkodzone pole \texttt{Size} uniemożliwiając pobranie całego pakietu poprawnie.
W wypadku niezgodności przesłanej sumy kontrolnej z sumą wyliczoną po stronie odbierającego, odbierający wysyła pakiet
\texttt{RET = 0x81}. Dla pakietów mało ważnych nie zachodzi potrzeba retransmisji.
Aby upewnić się, że transmisja ważnego pakietu się powiodła, każdy klient musi odpowiedzieć pakietem \texttt{ACK}
(\texttt{Type = 0x80}). W wypadku, gdyby odpowiedź \texttt{ACK} nie dotarła w ciągu $t\ \si{ms}$ pakiet zostaje
wysłany ponownie.
W przypadku wielokrotnego otrzymania pakietu o tym samym \texttt{PacketId} (np. w wypadku automatycznej retransmisji),
każdorazowo należy potwierdzić jego otrzymanie odpowiedzią \texttt{ACK} jednak akcja związana z tym pakietem powinna
zostać wykonana tylko raz.
Pakiety \texttt{ACK} oraz \texttt{RET} w polu \texttt{PacketId} zawierają id pakietu, którego dotyczą. Dodatkowo ze
wzgledu na to, że \texttt{ACK} nie jest bezpośrednio związany z rozgrywką, wymagany stan (\texttt{Sate}) zawsze wynosi
0.
\begin{figure}[H]
\center
\begin{packet}
\member{CRC16$_p$}{2}
\member[Type]{0x80}{1}
\member[Size]{15}{2}
\member{ClientId}{2}
\member[State]{0}{4}
\member{PacketId}{4}
\end{packet}
\caption{Pakiet \texttt{ACK}}
\end{figure}
W momencie kiedy po $n$-tej retransmisji pakietu z rzędu nie otrzymamy potwierdzenia \texttt{ACK} uznajemy, że nastąpiło
zerwanie połączenia ze strony tego gracza - dodatkowo, jeżeli jakimś cudem wszyscy pozostali gracze stracili połączenie
zakładamy, że to problem jest po naszej stronie.
W celu sprawdzenia poprawności połączenia z danym graczem można również wysłać pakiet \texttt{PING = 0x00}, za poprawny
\textit{pong} uznajemy pakiet \texttt{ACK}.
\subsection{Zestawienie połączeń}
Ze względu na to, że port na którym nasłuchują klienci nie jest deterministyczny istnieje konieczność wprowadzania
scentralizowanego \textit{lobby} pozwalającego na wymianę tych informacji pomiędzy klientami. Jedynym zadaniem
\textit{lobby} jest przechowywanie informacji o graczach chcących wspólnie zagrać, zatem nie zachodzi potrzeba szybkiego
działania.
Stąd przyjęto, że do realizacji lobby zostanie wykorzystany protokół TCP. Ze względu na to, że lobby stanowi jedynie
pośrednika do zestawiania połączeń, dokładny opis działania zdecydowano się pominąć.
Ponieważ protokół UDP nie zapewnia obsługi połączeń, każdy klient musi wysłać do wszystkich innych pakiet
\texttt{CONNECT = 0x01}, którego odebranie zgodnie z opisem powyżej (najstarszy bit = 0) musi zostać potwierdzone.
Po udanym nawiązaniu połączenia ze wszystkimi klientami, zostaje utworzony czołg gracza, a informacja o tym zostaje
przesłana do pozostałych graczy.
Dokładny opis pakietów służących do aktualizacji stanu rozgrywki (np. aktualizacja pozycji gracza) pomijamy, ponieważ
nie ma związku z problemem synchronizacji klientów.
\section{Potencjalne zmiany}
Założono, że pakiety będą dochodziły \textit{wystarczająco szybko} aby desynchronizacja związana z opóźnieniem w ruchu
sieciowym nie stanowiła problemu. Jednak należy przeprowadzić testy, czy faktycznie jest to dobre założenie. W wypadku,
gdyby jednak desynchronizacja związana z opóźnieniami stanowiła problem należy wprowadzić system pozwalający uwzględniać
opóźnienia odebrania pakietu względem przesłania - np. poprzez wprowadzenie wspólnego, synchronizowanego zegara gry.
Synchronizację zegara można zapewnić w sposób analogiczny do działania protokołu
SNTP\footnote{https://en.wikipedia.org/wiki/Network\_Time\_Protocol}, przy czym synchronizacja następowałaby do zegara
\textit{mastera}. W takim wypadku do pakietów moglibyśmy dołączyć informację na temat momentu w rozgrywce, w którym
pakiet został wysłany i odpowiednio uwzględniać opóźnienie (np. przesuwając obiekt o odpowiednią odległość względem
opóźnienia i dostępnych wektorów prędkości oraz przyspieszenia).
\end{document}