356 lines
17 KiB
TeX
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}
|