Michał Środek

Po prostu devBlog

Witaj na srodek.info

Jest to blog poświęcony nowoczelnym technologiom ułatwiającym tworzenie aplikacji internetowych. Znajdziesz tutaj porady na temat CSS3, JavaScript, designu, web-usability, standardów W3C.

Cześć! Nazywam się Michał Środek. Z zawodu programista php, z zamiłowania gitarzysta oraz fanatyk GNU/Linuksa(openSUSE® w laptopie). W branży aplikacji internetowych od 9 lat. Prywatnie bez dzieci i kota.

Pracuję wciąż nad własnym elastycznym i wydajnym frameworkiem MVC, kilkoma portalami internetowymi oraz mniejszymi bibliotekami php. Czekam na wasze opinie, zgłoszenia błędów oraz pomysły na dalszy rozwój.

Ta część strony jest w trakcie budowy a moje prace tymczasowo niedostępne.

W przypadku pytań, ofert pracy oraz ciekawych pomysłów proszę się ze mną kontaktować. Możesz mnie znaleźć i wysłać PW na php.pl(SHiP), jamendo.com(michalsrodek), goldenLine.pl, facebook.com lub nk.pl

14 Maj 2010

Serwer gry. Część pierwsza.

Filed under: Gry,Moje projekty,PHP — Michał Środek @ 0:47

Pisząc grę inter­ne­tową trzeba zasta­no­wić się nad spo­so­bem komu­ni­ka­cji mię­dzy gra­czami oraz bazą danych. W przy­padku apli­ka­cji prze­glą­dar­ko­wych dużego wyboru nie ma. Prak­tycz­nie wszyst­kie gry wyko­rzy­stują tech­no­lo­gię AJAX. Jak to działa w prak­tyce? Nie za dobrze. Spró­bujmy stwo­rzyć coś dużo bar­dziej wydajniejszego.

Spo­sób dzia­ła­nia tech­no­logi AJAX

AJAX to roz­wią­za­nie jed­no­stronne. Klient prosi ser­wer o pewne infor­ma­cje, a ten je wysyła użyt­kow­ni­kowi. Ser­wer nie posiada dokład­nych infor­ma­cji na temat ilo­ści aktu­al­nie połą­czo­nych użyt­kow­ni­ków oraz nie może wysłać do żadnego z nich samo­dziel­nie żadnej informacji(tj. może jedy­nie odpo­wia­dać na ich zapytania).


Kocham robić te fajne obrazki i choć nie mają one niczego wspól­nego z UML-em mam nadzieję, że jesteś w sta­nie je odczytywać

Prze­ana­li­zujmy przy­kład czatu inter­ne­to­wego w któ­rym mamy trzech użytkowników(Client1, Client2, Client3). Pierw­szy z nich chce wysłać infor­ma­cję do trze­ciego. Wygląda to następująco:

  • (1) Client1 pyta ser­wer czy są jakieś wia­do­mo­ści do niego. Ser­wer zwraca infor­ma­cję o ich braku.
  • (2) Client2 pyta ser­wer czy są jakieś wia­do­mo­ści do niego. Ser­wer zwraca infor­ma­cję o ich braku.
  • (3) Client3 pyta ser­wer czy są jakieś wia­do­mo­ści do niego. Ser­wer zwraca infor­ma­cję o ich braku.
  • (4) Client1 wysyła do ser­wera wia­do­mość prze­zna­czoną dla użyt­kow­nika Client3. Ser­wer zwraca infor­ma­cję o przy­ję­ciu danych.
  • (4,5) Ser­wer zapi­suje wia­do­mość do swo­jego bufora(w tym przy­kła­dzie bazy danych)
  • (5) Client2 pyta ser­wer czy są jakieś wia­do­mo­ści do niego. Ser­wer zwraca infor­ma­cję o ich braku.
  • (6) Client3 pyta ser­wer czy są jakieś wia­do­mo­ści do niego.
  • (6,5) Ser­wer odczy­tuje z bazy danych wia­do­mość oraz zwraca ją użytkownikowi.
  • (7) Client1 pyta ser­wer czy są jakieś wia­do­mo­ści do niego. Ser­wer zwraca infor­ma­cję o ich braku.
  • (8) Client2 pyta ser­wer czy są jakieś wia­do­mo­ści do niego. Ser­wer zwraca infor­ma­cję o ich braku.
  • (9) Client3 pyta ser­wer czy są jakieś wia­do­mo­ści do niego. Ser­wer zwraca infor­ma­cję o ich braku.
  • (…)

Jak widać wyko­ny­wa­nych jest wiele nie­po­trzeb­nych zapy­tań. Każdy z klien­tów musi stale wysłać zapy­ta­nie do ser­wera co w przy­padku małego czasu może powo­do­wać spore zuży­cie pro­ce­sora ser­wera. W sytu­acji, gdy czas będzie więk­szy w grze poja­wią się lagi. Jak ten pro­blem roz­wią­zuje się w „nor­mal­nych“ grach inter­ne­to­wych? Uży­wane są gniazda.

Jak dzia­łają gniazda


Sze­ro­kie linie ozna­czają nasłu­chi­wa­nie ser­wera oraz klientów

Każdy z klien­tów musi usta­no­wić połą­cze­nie z ser­we­rem. Ser­wer dla każ­dego z nich two­rzy gniazdo za pomocą któ­rego w dowol­nym momen­cie może wysłać infor­ma­cję do jed­nego z użytkowników.

Wygląda to tak:

  • (0) Client1, Client2, Client3 nawią­zują połą­cze­nie z serwerem(tworzone są gniazda)
  • (1) Client1 wysyła wia­do­mość do ser­wera (ser­wer zwraca komu­ni­kat o sukcesie)
  • (2) Ser­wer odnaj­duje gniazdo trze­ciego użytkownika
  • (3) Ser­wer wysyła wia­do­mość do Client3

W odróż­nie­niu do tech­no­logi AJAX w tym przy­padku nasz ser­wer napi­sany w php musi cią­glę nasłu­chi­wać, a więc pro­ces tego skryptu musi być cały czas uru­cho­miony. W tym celu two­rzy się pętlę nie­skoń­czoną, którą cię­gle spraw­dza stan gniazda serwera.

Pierw­sza implementacja

Spró­bujmy stwo­rzyć pierw­szy dzia­ła­jący ser­wer. Nie będzie on pozwa­lał na prze­ka­zy­wa­nie danych innym użyt­kow­ni­kom, a połą­cze­nia nie będą utrzymywane.

<?php 
set_time_limit(0);
 
class GameServer
{
	private $master;
	private $address; 
	private $port; 
	private $maxClients;
 
	public function __construct($address = '127.0.0.1', $port=14117, $maxClients = 10)
	{
		$this->address = $address;
		$this->port = $port;
		$this->maxClients = $maxClients;
 
		$this->master = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); 
		socket_bind($this->master, $this->address, $this->port);
		socket_listen($this->master); 
 
		while(true)
		{
                        // akceptujemy połączenie, tworzymy gniazdo 
			$socket = socket_accept($this->master); 
 
			// odczytujemy informacje z gniazda
			$input = socket_read($socket, 1024); 
 
			// przygotowujemy dane do wysyłki
			$output = 'Wyslales do serwera: '.$input.chr(0); 
 
			// Wysyłamy dane użytkownikowi
			socket_write($socket, $output);
 
                        // zamykamy połączenie z użytkownikiem
			socket_close($socket); 
		}
	}
 
	public function __desctruct()
	{
		// zamknięcie głównego gniazda
		socket_close($this->master); 
	}
}
 
$gameServer = new GameServer();
?>

Two­rzymy obiekt $game­Se­rver. W kon­struk­to­rze możemy podać kilka para­me­trów: adres i port pod jakim ser­wer będzie nasłu­chi­wał oraz mak­sy­malną ilość użytkowników(ta zmienna nie jest uży­wana w tym przy­kła­dzie lecz przyda się później).

Kilka słów o nume­ra­cji portów

W przy­padku IP w wer­sji czwar­tej do dys­po­zy­cji mamy 216 por­tów. Te o nume­rach poni­żej 1024 wyma­gają upraw­nień admi­ni­stra­cyj­nych. W więk­szo­ści przy­pad­ków są one już zare­zer­wo­wane dla innych usług(np. 80 dla http, 23 dla ftp, 21 dla tel­net itd.). Naj­wy­god­niej jest po pro­stu ich nie ruszać.

Do zmien­nej $this->master zapi­sy­wane jest główne gniazdo ser­wera two­rzone za pomocą socket_create(). To ono będzie odbie­rało wszyst­kie infor­ma­cje przy­cho­dzące od innych użyt­kow­ni­ków. Co ozna­czają para­me­try wewnątrz?

  • Pierw­szy infor­muje jaka rodzina pro­to­ko­łów będzie uży­wana w przy­padku naszego gniazda. AF_INET okre­śla, że będą to protokoły(TCP oraz UDP) w opar­ciu o IPv4. Możemy rów­nież poin­for­mo­wać o uży­ciu IPv6 zamie­nia­jąc ten para­metr na AF_INET6.
  • Drugi para­metr okre­śla rodzaj trans­mi­sji. Do wyboru mamy:
    • SOCK_STREAM — infor­ma­cje prze­sy­łane są w obu kie­run­kach jed­no­cze­śnie, bez spadku trans­feru, sekwen­cyj­nie oraz nie­za­wod­nie za pomocą stru­mieni baj­tów. Bazo­wym pro­to­ko­łem jest TCP.
    • SOCK_DGRAM — data­gra­mowy pro­to­kół bez­po­łą­cze­niowy z bra­kiem kon­troli prze­pływu i retrans­mi­sji. Bazo­wym pro­to­ko­łem jest UDP(To jest dobry wybór w przy­padku pisa­nia gry internetowej!).
    • SOCK_SEQPACKET — infor­ma­cje prze­sy­łane są w obu kie­run­kach jed­no­cze­śnie, bez spadku trans­feru, sekwen­cyj­nie. Od klienta wyma­gane jest aby odczy­ty­wane były całe pakiety przy każ­dym zapytaniu.
    • SOCK_RAW — gniazdo RAW czyli bez­po­średni do niego dostęp. Umoż­li­wia to stwo­rze­nie wła­snego pro­to­kołu komu­ni­ka­cji. Opcja raczej dla zaawan­so­wa­nych użytkowników.
    • SOCK_RDM — nie­za­wodna trans­mi­sja pakie­tów nie gwa­ran­tu­jąca porząd­ko­wa­nia pakie­tów. Ta opcja nie zadziała w wielu sys­te­mach ope­ra­cyj­nych. Raczej dla zaawan­so­wa­nych użytkowników.
  • Ostatni para­metr to po pro­stu ID pro­to­kołu. W przy­padku TCP oraz UDP możemy sko­rzy­stać ze sta­łych SOL_TCP(6) oraz SOL_UDP(17). Pełną listę iden­ty­fi­ka­to­rów znaj­dziesz w /etc/protocols

W naszym przy­kła­dzie stwo­rzy­li­śmy gniazdo wyko­rzy­stu­jące pro­to­kół TCP. W kolej­nej lini infor­mu­jemy nasze gniazdo pod jakim adre­sem i jaki port ma nasłu­chi­wać. Gdy to okre­ślimy uru­cha­miamy nasłuchiwanie.

Ten pro­ces jest bar­dzo pro­sty. Stwo­rzona została pętla nie­skoń­czona, która przy każ­dym obro­cie akcep­tuje gniazdo przy­cho­dzące. następ­nie odczy­tuje z niego dane, odsyła je do użyt­kow­nika oraz zamyka połą­cze­nie. Ser­wer uru­cho­miony raz będzie dzia­łał w nieskończoność(prawie w nie­skoń­czo­ność ;) ) więc ważne jest aby wyłą­czyć spraw­dza­nie czasu wyko­ny­wa­nia się skryptu. W tym celu w pierw­szej linijce kodu wid­nieje wywo­ła­nie funk­cji set_time_limit()

Testu­jemy serwer

Uru­chom kon­solę z dwoma zakład­kami. Aby uru­cho­mić nasz ser­wer w pierw­szej wpisz:

php server.php

W dru­giej spró­bujmy akty­wo­wał połączenie

hellson@hellson:~> telnet 127.0.0.1 14117
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
Hello
Wyslales do serwera: Hello
Connection closed by foreign host.
hellson@hellson:~>

W przy­padku zamknię­cia skryptu wyko­nany zosta­nie destruk­tor zamy­ka­jący gniazdo główne.

Mam nadzieję, że ten arty­kuł uła­twił ci zro­zu­mie­nie idei gniazd. Już jutro kon­ty­nu­acja tematu. Ulep­szę kod ser­wera tak aby odbie­rał infor­ma­cje od róż­nych użyt­kow­ni­ków jed­no­cze­śnie. Połą­cze­nie będzie utrzy­my­wane aż do momentu wpi­sa­nia „exit“ przez użytkownika.

Dodaj arty­kuł do:

  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • Gwar
  • Reddit
  • Technorati
  • Twitter
  • Wykop

Komentarzy: 14 »

  1. To nie jest dobry spo­sób na utrzy­ma­nie trwa­łego połą­cze­nia z ser­we­rem, weź pod uwagę że np. Apa­che dla każ­dego klienta/połączenia two­rzy nowy wątek, teraz niech połą­czy się 200–500 klien­tów i … admi­ni­stra­tor pomy­śli że ktoś mu DOSa robi ;) — ogól­nie zaje­dziesz tym maszynę.

    Komentarz by pfugiel — 14 maja 2010, 11:40

  2. ps. czemu nie ma obsługi Gra­va­tar (http://gravatar.com) ? :P

    Komentarz by pfugiel — 14 maja 2010, 11:42

  3. a) Teo­re­tycz­nie tak ale powiedz mi w któ­rym momen­cie uży­łem ser­wera apa­che? :D Wyko­nuję skrypt php jako skrypt powłoki pomi­ja­jąc tym samym całą otoczkę apa­chową. Ser­wer się nie zapcha. Zadziała to jak zwy­kły ser­wer np. gry counter-strike insta­lo­wany czę­sto na maszy­nach linuksowych(a przy­naj­mniej kie­dyś tak było).

    PS: Gra­va­tar działa. Spraw­dzi­lem i oka­zało się, że poda­łeś złego maila(tj. hashe się nie zga­dzają z tym lin­kim, który poda­łeś jako adres URL).

    Komentarz by Michał Środek — 14 maja 2010, 12:26

  4. Nie wiem czy pisa­nie ser­wera w php to dobry pomysł, ja bym wybrał raczej c lub python, no chyba, że pph wybra­łeś tylko na potrzeby edu­ka­cyjne ;)
    Ciężko będzie też zna­leść hosting, który pozwoli Ci utrzy­my­wać wła­sną instan­cje ser­wera, potrzebny będzie albo dedyk albo vps a to tro­che kosz­tuje ;)

    Komentarz by Maciej Wiczołek — 14 maja 2010, 12:35

  5. Ser­wer zosta­nie prze­pi­sany naj­praw­do­po­dob­niej do Javy lub Pythona. Aktu­al­nie po pro­stu nie mam czasu zagłę­biać się w ten temat. Na począ­tek PHP wystar­czy tym bar­dziej, że radzi sobie cał­kiem nieźle.

    Komentarz by Michał Środek — 14 maja 2010, 12:37

  6. Java to dobry język ale na ser­wer za mało wydajny ;)

    Komentarz by Maciej Wiczołek — 14 maja 2010, 14:27

  7. Wyko­rzy­stu­jesz ten ser­wer w jakiejś kon­kret­nej pro­duk­cji, czy to czy­sto „aka­de­micki“ test?

    Komentarz by Tomasz Kowalczyk — 15 maja 2010, 0:10

  8. Opie­ram to o moje aktu­alne postępy przy pisa­niu wła­snej gry. Tak więc jest to sen­sowny i dzia­ła­jący kod ;) .

    Komentarz by Michał Środek — 15 maja 2010, 0:35

  9. Wiem, że pew­nie jestem „nie­do­in­for­mo­wany“, ale można to gdzieś zoba­czyć, jakiś ser­wer testowy, czy coś podob­nego? Co to za gra, napi­szesz coś o niej?

    Komentarz by Tomasz Kowalczyk — 15 maja 2010, 0:58

  10. http://srodek.info/blog/215/nowy-projekt-na-biurku

    W skró­cie: gra dla wielu gra­czy w cza­sie rze­czy­wi­stym napi­sana w PHP + HTML5 + miej­scami xHTML(jego będę sta­rał się uni­kać). Mam pomysł na jedną małą grę i jedną dużą. Co z tego wyj­dzie zoba­czymy :) .

    Komentarz by Michał Środek — 15 maja 2010, 1:18

  11. Myślę, że PHP jak naj­bar­dziej nadaje się do tego typu pro­jektu. W końcu i tak naj­węż­szym gar­dłem będzie baza danych, a kon­kret­nie dyski twarde. Nie ważne czy gra będzie w PHP, Java czy nawet C++

    Zasta­na­wia mnie jak roz­wią­żesz kwe­stie ska­lo­wal­no­ści. Bo jeden ser­wer wystar­czy na 20tyś use­rów max. A przy takiej ilo­ści gra­czy koszty mogą sie nie zwrócić.

    Roz­wa­ża­łeś roz­wia­za­nia w stylu Ama­zon Web Services?

    Komentarz by kalkulator kredytowy — 15 maja 2010, 23:32

  12. Ja nie mam aż takiego roz­ma­chu ;) aby mieć 20tys gra­czy wiec o AWS w ogóle nie myśla­łem. Co do obcią­żeń — infor­ma­cje gry będę trzy­mał w pamięci współ­dzie­lo­nej tak aby nie obcią­żać dys­ków. Aktu­ali­za­cje do bazy nie będą robione z każdą akcją bo będzie ich po pro­stu za dużo. Raczej będę to robił par­tiami — np. po zakoń­cze­niu jakie­goś etapu na danej mapie(np. zabi­cie jed­nostki wroga) lub w przy­padku roz­łą­cze­nia się gracza(co łatwo wykryć, w końcu soc­ket sie zamyka). Ale i tak nie ma co gdy­bać — wszystko wyj­dzie w pra­niu :D .

    Komentarz by Michał Środek — 16 maja 2010, 0:09

  13. Jak na to co zaczą­łeś tutaj opi­sy­wać paso­wało by zarzu­cić defi­ni­cją czym jest AJAX… gdyż AJAX to nie tylko odpy­ty­wa­nie ser­wera co 1s… mia­łem się tro­chu roz­pi­sy­wać ale… i u Cie­bie nie ma spój­no­ści… piszesz o AJAX i temat ury­wasz, zmie­niasz na stronę ser­we­rową a pyta­nie?? co zadaje tu zapy­ta­nie?? flash?, AJAX — czyli w dużym uprosz­cze­niu JS lub DHTML??… mniej­sza o tym…

    w komen­ta­rzach tro­chę pisa­łem o AJAX’ie [i jak to kolega Bat­man oce­nił wygląda to jak soc­kety]:
    http://wilgucki.blogspot.com/2010/04/komunikacja-z-serwerem-w-czasie.html

    Komentarz by zegarek84 — 17 maja 2010, 2:04

  14. Wycho­dzę z zało­że­nia, że każdy się orien­tuję czym jest AJAX(a jeśli nie wie to zawsze jest dostępna Wiki­pe­dia). Po pro­stu chcia­łem zro­bić jakiś wstęp, pokrótce poka­zać, że jest lep­sze roz­wią­za­nie tj. bar­dziej opty­malne w porów­na­niu do tego czego używa więk­szość pro­gra­mi­stów gier www. Oczy­wi­ście nie ozna­cza to, że AJAX-a nie będę w ogóle uży­wał. Jak łatwo się domy­śleć pojawi się trze­cia część arty­kułu, w któ­rej opi­szę zapro­gra­mo­wa­nie klienta ;) .

    Co do mojego ser­wera — zapy­ta­nia będzie zada­wał czy­sty Java­Script. Nie lubię fle­xow itp. Uni­kam zamknię­tych roz­wią­zań jak ognia.

    Komentarz by Michał Środek — 17 maja 2010, 2:26

Kanał RSS z komentarzami do tego wpisu. TrackBack URL

Dodaj komentarz