Zamzrani Serveru TCP/IP – .NET – Fórum – Programujte.com
 x   TIP: Přetáhni ikonu na hlavní panel pro připnutí webu

Zamzrani Serveru TCP/IP – .NET – Fórum – Programujte.comZamzrani Serveru TCP/IP – .NET – Fórum – Programujte.com

 

TD
~ Anonymní uživatel
28 příspěvků
25. 2. 2016   #1
-
0
-

Ahojte, stretel sa nekdo s tym že ked pisal server pro TCP/IP komunikaci se "soketama" tak mu to zamrzalo? dočital sem sa na nete že to je tym že ked čekam na klienta musim čekat v jinem vlakne. ale ni to mi nejak moc nepomohlo. nemate nekdo odkaz na nejaky dobry tutorial kde je to dost dobre vysvetlene? našel sem same nedokonale a nedotahnute dokonca. Keby sa mi podarylo spravit fungujuci program na Server aj Client rad bych na to napisal nejaky važne prehladny tutorial s najčastejšíma chybama. Diky

Nahlásit jako SPAM
IP: 89.103.90.–
TD
~ Anonymní uživatel
28 příspěvků
25. 2. 2016   #2
-
0
-

ešte take doplnujuce mam videro verzii programu a u každeho je nejaka vada na kteru nemožu dojit u jedneho pracuju cez backgroundworker .... a u dalšich cez Thread
 

Nahlásit jako SPAM
IP: 89.103.90.–
p3can
~ Anonymní uživatel
312 příspěvků
26. 2. 2016   #3
-
0
-

#1 TD
mas nejaky kod kde ti to zamrza? pokud vim tak se pouziva pristup budto z hlavniho vlakna pomoci asynchronich metod nebo si nastartujes jine vlakno a tam vsecko delas synchrone.

Nahlásit jako SPAM
IP: 195.178.92.–
peter
~ Anonymní uživatel
4014 příspěvků
26. 2. 2016   #4
-
0
-

Resim neco jineho, ale treba je princip mozna podobny. Resim program pro komunikaci, neco jako chat. Mam 2 asynchronni servery, datovy (udp) a exchange (http). Pres exchange probiha propojeni uzivatelu, resi mistnost, kdo je v mistnosti, kdy naposledy psal a pod. S nim komunikuje uzivatel tak 1x za 17s (jestli nekdo vstoupil do mistnosti, treba). Uzivatele se propojuji jako perr, dva spolu. Takze 5 uzivatelu si kazdy u sebe udela 4 kanaly, kde se propoji vzdy jen dva uzivatele v kanalu. Program pak sleduje jen svuj kanal, jestli tam neco prislo nebo ne. Data server to pak prerozdeluje podle cisel portu. Uzivatel tedy data server kontaktuje pres 4 udp kanaly v rychlem intervalu.

Cili, exchange server resi posilani takovych zprav, jako objevil se novy uzivatel, posilam certifikaty (seznam portu a klicu), aby jste si mne pripojili, poslete mi svoje klice.
Data server pak posila mezi uzivateli sifrovana data. Sifruje to program pomoci certifikatu, co si vymenili uzivatele pres exchange server.

Treba resis neco podobne, potrebujes pomocny kanal/server pro obsluhu a druhy pro samotne datove prenosy. A nebo mas chybne nastavenou synchronni komunikaci, kdy server ceka, az se odpoji uzivatel a do te doby nepusti nikoho dalsiho.

Nahlásit jako SPAM
IP: 2001:718:2601:26c:1886:41...–
Ovrscout
~ Anonymní uživatel
113 příspěvků
26. 2. 2016   #5
-
0
-

#1 TD
Mno nějaký pěkný a dobrý tutoriál teď v záložkách nemám, v tom ti neporadim. Každopádně pokud ti to zamrzá tak je prostě nejlepší se podívat kde, použij debuger, logy, příznaky, cokoliv co je po ruce. :)

JInak klasicky čtení,zápis, příjem a někdy i ukončení spojení mohou docela dlouho trvat. tj. Buď je dobré použít Async volání, nebo pěkně jednoduše pro každé TcpSpojení vytvořit vlákno. Teoreticky můžeš zkusit i Socket.Blocking=false, ale to už bych raději doporučil Ten async přístup.

Každopádně pár obecnějších rad k Tcp co mne tak napadá:
-Zkontroluj si nastavení timeoutů, Bývá jich víc(read,write) a na více místech(třeba pokud používáš stream nebo tak).
- Kontroluj návratové hodnoty a při chybě se dle nich řiď, případně si je někde zaloguj.
  Konkrétně třeba Send vrací kolik byte se podařilo zapsat, některé tutoriály to ignorují.
  Obyčejně se to dělá tak že se program zkusí odeslat neodeslaná data znova v nějaké smyčce s vlastním timeoutem.
-nespoléhej se na to že co pošleš jedním zápisem tak na druhé straně vyčteš jedním čtením. Tcp není packetový protokol! i když většinou se to tak chová. Některé tutoriály jsou v tomhle fuj.
-zkontroluj a zajisti řádné uvolňování prostředků(sockety, streamy, async věci) né všeho je v systému nekonečné množství
-Tcp port clienta by mněl být dynamický, statický(pevný) Tcp port je na serveru. To je ověřená konfigurace.
  Pokud se budeš nastavovat oba porty, může ti to fungovat, ale také se můžeš dočkat nepěkných věcí kolem TIME_WAIT a chyb při obnovování spojení(pokud se z nějakého důvodu rozpadne).

-Tcp není vhodné používat stylem připojit-dotaz-odpověď-odopojit. V rámci vteřin nebo i desítek vteřin a více spojení najednou pak může být spousta spojení v TIME_WAIT stavu nebo, což může být horší, k vyčerpání "volných" tcp portů(ok, tohle je trochu zjednodušné ale v kostce je to tak)

Teď trochu více zaměřené na propustnosti a odezvy (na LAN asi není nutné ale pro GPRS, to je jiné káva, ale třeba 3G/4G už zas bývá docela přijatelné):

-Zkontroluj si nastavení velikosti bufferu pro čtení a zápis aby nebyli zbytečně malé(pro většinu  čtených a odesílaných dat), ale nedělej je zas ukrutně velké.

- Data pro odeslání si pokud možno připrav do bufferu a odešli najednou(pokud mají rozumnou velikost do pár kB, 100MB balík dat asi bude lepší posílat po částech nebo přes stream).
Pomůže to snížit počet packetů které v tcp létají sem a tam(i pokud se jen odesílá, tak příchozí strana potvrzuje přijatá data aodeslaných a nepotvrzených dat je jen omezené množství)
-Pokud je komunikace typu dotaz-odpověď tak v kombinaci s předchozím bodem může být vhodné vypnout nagle algoritmus, viz Socket.NoDelay. Někde to zas vhodné být nemusí, např pokud by jsi přímo posílal stisky kláves, tak bych to nechal na nagle a nepsal bych vlastní buffer s timeoutem.

Mno a když nevíš, tak prozkoumej dokumentaci pro použité funkce na msdn, občas se tam najde pár typů nebo komentářů.

Nahlásit jako SPAM
IP: 193.165.79.–
TD
~ Anonymní uživatel
28 příspěvků
27. 2. 2016   #6
-
0
-

 Zdravím :) diky za super rady preštudujem to ale tady postujem asi najlepší kod ktery sa mi podaryli :) neukamenujte ma ked to bude moc zle sem v c# total začatečnik. Diky za každu radu :)

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.Net;
using System.Net.Sockets;
using System.IO;

namespace Zeus
{
    public partial class Form4_TCP_SERVER : Form
    {
        private TcpClient client;
        public StreamReader STR;
        public StreamWriter STW;
        public string receive;
        public string text_to_send;


        public Form4_TCP_SERVER()
        {
            InitializeComponent();

            IPAddress[] localIP = Dns.GetHostAddresses(Dns.GetHostName());  //získat vlastní IP
            foreach (IPAddress addres in localIP)
            {
                if (addres.AddressFamily == AddressFamily.InterNetwork)
                {
                    textBox_IP.Text = addres.ToString();  //po načitani IP zapisani do textboxu
                }
            }
        }


        private void click_connect(object sender, EventArgs e)  // tlačitko na aktivovani serveru
        {
            try
            {
                if (textBox_Port.Text == "" || textBox_IP.Text == "") // ošetreni ked neni zadane jedna z dvoch veci bud IP alebo port
                {
                    MessageBox.Show(this, "Trotle zvol Port alebo IP adresu.", "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error);  //vypisani vyskakujuceho okna
                }
                else
                {

                    TcpListener listener = new TcpListener(IPAddress.Any, int.Parse(textBox_Port.Text));  // dva radky na zahajeni komunikace po TCP/IP naslouchani
                    listener.Start();
                   
                    
                    client = listener.AcceptTcpClient();

                    STR = new StreamReader(client.GetStream());
                    STW = new StreamWriter(client.GetStream());
                    STW.AutoFlush = true;


                    backgroundWorker1.RunWorkerAsync();                     //zahájení příjmu dat
                    backgroundWorker2.WorkerSupportsCancellation = true;    //Možnost zrušit toto vlákno
                }
            }
            catch (UnauthorizedAccessException)
            {
                MessageBox.Show(this, "Neautorizovany pristup.", "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }
    

        private void click_send(object sender, EventArgs e)
        {
            if (textBox_send.Text == "")
            {
                MessageBox.Show(this, "Nejsou zadané žadne parametry na poslaní.", "WARNING", MessageBoxButtons.OK, MessageBoxIcon.Warning);
            }
            else
            {
                text_to_send = textBox_send.Text;
                backgroundWorker2.RunWorkerAsync();
            }

            textBox_send.Text = "";
        }

        private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
        {
            while (client.Connected)
            {
                try
                {
                    receive = STR.ReadLine();
                    this.textBox_message.Invoke(new MethodInvoker(delegate() { textBox_message.AppendText("You : " + receive + "\n"); }));
                    receive = "";
                }
                catch (Exception x)
                {
                    MessageBox.Show(x.Message.ToString());
                }
            }
        }

        private void backgroundWorker2_DoWork(object sender, DoWorkEventArgs e)
        {
            try
            {

                if (client.Connected)
                {
                    STW.WriteLine(text_to_send);
                    this.textBox_message.Invoke(new MethodInvoker(delegate() { textBox_message.AppendText("Me : " + text_to_send + "\n"); }));
                }
                else
                {
                    MessageBox.Show("send failed!");
                }
                backgroundWorker2.CancelAsync();
            }
            catch (Exception x)
            {
                MessageBox.Show(x.Message.ToString());
            }
        }

        private void Click_clear(object sender, EventArgs e)
        {
            textBox_message.Clear();
        }


      }
}
Nahlásit jako SPAM
IP: 89.103.90.–
p3can
~ Anonymní uživatel
312 příspěvků
28. 2. 2016   #7
-
0
-

#6 TD
no a jake problemy mas stim kodem co jsi sem postnul? co se tak divam tak jediny problem by mel byt v metode
click_connect protoze se provadi na UI vlakne a uprostred je metoda AcceptTcpClient ktera je blokujici.

Nahlásit jako SPAM
IP: 77.92.213.–
TD
~ Anonymní uživatel
28 příspěvků
28. 2. 2016   #8
-
0
-

no prave že u tehotoi sa mi podarylo snad vychytat všetko okrem teho zamrzana a to je to cos napsal asi

Nahlásit jako SPAM
IP: 89.103.90.–
Ovrscout
~ Anonymní uživatel
113 příspěvků
29. 2. 2016   #9
-
0
-

Takováhle práce se socketem v ručně vytvořeném vlákně je spíš vhodná pokud těch tcp spojení není mnoho a potřebuješ průběžně na pozadí komunikovat přes tcp a jen sem tam aktualizovat GUI.

Při zběžném kouknutí mi v tom příkladu chybí ošetření přístupu z více vláken k receive a  text_to_send.
Zatím to asi tak moc nevadí(rozumněj, asi ti to nebude moc často padat) ale jakmile s tím začneš nějak pracovat můžeš se dočkat nepěkného chování a pádů.


Zkus najít přklady s BeginAccept , BeginReceive,BeginSend ... . V podstatě to funguje podobně jak to máš, také je tam takový skrytý worker thread, ale nemusíš ho ručně psát, jen uděláš funkce které se zavolají při dokončeném načtení nebo zápisu dat, nebo při příjmu spojení. Pro tvůj jednoduchý příklad (příjem a odeslání na základě povelu z GUI) to bude dost možná ještě jednodušší než to máš teď. I když obecně mi to vychází tak asi stejně složité jako ruční worker thread, ale zase se to lépe škáluje kdybys to snad potřeboval. Každopádně doporučuji se na to alespoň podívat.

Nahlásit jako SPAM
IP: 193.165.79.–
Ovrscout
~ Anonymní uživatel
113 příspěvků
29. 2. 2016   #10
-
0
-

#9 Ovrscout

taji takový základní příklad od ms

https://msdn.microsoft.com/en-us/library/bew39x2a%28v=vs.110%29.aspx

Nahlásit jako SPAM
IP: 193.165.79.–
TD
~ Anonymní uživatel
28 příspěvků
31. 3. 2016   #11
-
0
-

Tak sem se pokoušel jak se jen dalo ale stale sem zmaten s te prace s vlaknama. Ked sem se pokoušel metodu AcceptTcpClient poslat do jineho vlakna tak to stale nejde.

moje pokusy : 

 private void click_connect(object sender, EventArgs e)
        {
            try
            {
                if (textBox_Port.Text == "" || textBox_IP.Text == "") 
                {
                    MessageBox.Show(this, "bad IP or port", "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error);  
                }
                else
                {                  
                    Thread thd_AcceptTcpClient = new Thread(new ThreadStart(AcceptTcpClient_Thread));
                    thd_AcceptTcpClient.Start();
                   


                    backgroundWorker1.RunWorkerAsync();                     
                    backgroundWorker2.WorkerSupportsCancellation = true;    
                }
            }
            catch (UnauthorizedAccessException)
            {
                MessageBox.Show(this, "Neautorizovany pristup.", "ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error);
            }
        }

a Pak volam ve vlaknu

public void AcceptTcpClient_Thread()
            {
                 TcpListener listener = new TcpListener(IPAddress.Any, int.Parse(textBox_Port.Text));  
                        listener.Start();

                        client = listener.AcceptTcpClient();

                        STR = new StreamReader(client.GetStream());
                        STW = new StreamWriter(client.GetStream());
                        STW.AutoFlush = true;
            }


Dekuju moc za každu radu a za každu pripominku sem važne zufaly s tych vlaken :/

Nahlásit jako SPAM
IP: 89.103.90.–
p3can
~ Anonymní uživatel
312 příspěvků
31. 3. 2016   #12
-
0
-

#11 TD
1ni vlakno bude na UI (standartni hlavni vlakno jak je ted)

2he vlakno bude obsahovat nekonecny while a metodu AcceptTcpClient.

pokud 2 he vlakno prijme klienta tak nastartujes dalsi 2 vlakna jedno na cteni z klienta a druhe na zapis do klienta

vlakna startuj pres Task.Factory.StartNew(()=>{}); a parametry si predavej pres closure v lambdach

kapis?
 

Nahlásit jako SPAM
IP: 77.92.213.–
TD
~ Anonymní uživatel
28 příspěvků
31. 3. 2016   #13
-
0
-

Tak trochu ano ale jak poznam že sem prijal klienta? a tento prikaz vidim Prvy-krat v živote Task.Factory.StartNew(()=>{}); ani podla examlu na msdn sem to moc nepobral a kde bych mnel umistit OUT.Text=RemoteIpEndPoint.Address.ToString() + " je pripojeni."; ?? a ten konec už vubec nekapiš :/ ... ja sem sa dluho dobu učil C a C++ ale včil sem prešel na c# a cele sa mi to plete a sem s teho zamotany

Nahlásit jako SPAM
IP: 89.103.90.–
TD
~ Anonymní uživatel
28 příspěvků
1. 4. 2016   #14
-
0
-

Tak s tych vlaken sem zmateny že vic to už nejde ale ked človek patlá cedno cez druhe tak neco občas jede :D Je moc zle kombinovat backgroundworker a Thread ?? alebo sa mam rozhodnut a použivat iba jedno?

po stlačeni tlačitka zavolám :  

Thread thd_AcceptTcpClient = new Thread(new ThreadStart(AcceptTcpClient_Thread));
 thd_AcceptTcpClient.Start();

V temto vlakne spuštám TCP a čekám na klienta ked sa klient pripoji pustí dalši vlákno :

 public void AcceptTcpClient_Thread()
        {          
                TcpListener listener = new TcpListener(IPAddress.Any, int.Parse(textBox_Port.Text));
                listener.Start();
                while (true)
                {
                    client = listener.AcceptTcpClient();
                    Thread thd_connect = new Thread(new ThreadStart(connect_Thread));
                    thd_connect.Start();

                }
        }


a toto vlakno služí na prijatie a poslanie dat : 

public void connect_Thread()
        { 
STR = new StreamReader(client.GetStream());
                STW = new StreamWriter(client.GetStream());
                STW.AutoFlush = true;

                backgroundWorker1.RunWorkerAsync();
                backgroundWorker2.WorkerS
upportsCancellation = true;}

no a background workey su take jak sem už hore daval.

Moja otazná znie je to moc zmatlane? Je možne nejak pomenovat klienta? a jakym spusobem sa potom rušá ty spustene vlakna? našel sem že cez .abort ale kam to dat? a nejake disconnect sem na netu niekde nenašel to nejde spravit u TCP??

Nahlásit jako SPAM
IP: 89.103.90.–
p3can
~ Anonymní uživatel
312 příspěvků
1. 4. 2016   #15
-
0
-

#14 TD
jelikoz uz sem to psal mockrat tak uz se me to nechce psat nejak extra osetrene tady je nastrel jak to ma vypadat. jediny problem je tam v tom ze TCP neumoznuje detekovat vypadek spojeni nekde mezi clientem a serverem. na to se pouziva mechanismus "heartbeat" pripadne vyssi trida nez je tcp.

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace Kecalek
{
    public partial class Form1 : Form
    {
        private Dictionary<string, StreamWriter> clients = new Dictionary<string, StreamWriter>();
        private TcpListener server;
        private TcpClient client1;
        private StreamWriter client1writer;
        private TcpClient client2;
        private StreamWriter client2writer;
        public Form1()
        {
            InitializeComponent();
        }

        private void serverStart_Click(object sender, EventArgs e)
        {
            server = new TcpListener(789);
            Task.Factory.StartNew(() =>
            {
                int i = 0;
                server.Start(10);
                Write("Server started");
                while (true)
                {
                    var clientAcceptedByServer = server.AcceptTcpClient();
                    var name = "Client" + ++i;
                    clients.Add(name,new StreamWriter(clientAcceptedByServer.GetStream()) {AutoFlush=true});
                    StartListening(clientAcceptedByServer, name);
                }
            });
        }

        private void StartListening(TcpClient serverClient, string name)
        {
            Write("Server accept " + name);
            Task.Factory.StartNew(() =>
            {
                using (var reader = new StreamReader(serverClient.GetStream()))
                {
                    while (true)
                    {
                        var data = reader.ReadLine();
                        if (data == null)
                            break;
                        Write(name + ": " + data);
                    }
                    Write("Server detect " + name + " is disconnected");
                    clients.Remove(name);
                }
            });
        }

        private void Write(string text)
        {
            if (InvokeRequired)
                this.Invoke((Action) (() => listBox1.Items.Insert(0,text)));
            else
                listBox1.Items.Insert(0,text);
        }

        private void client1Start_Click(object sender, EventArgs e)
        {
            Task.Factory.StartNew(() =>
            {
                client1 = new TcpClient("localhost", 789);
                client1writer = new StreamWriter(client1.GetStream());
                client1writer.AutoFlush = true;
                using (var reader = new StreamReader(client1.GetStream()))
                {
                    while (true)
                    {
                        var data = reader.ReadLine();
                        if (data == null)
                            break;
                        Write("Client1  recieved:"  + data);
                    }
                }
                Write("Client1 disconected");
            });
        }

        private void serverBroadcast_Click(object sender, EventArgs e)
        {
            Write("Server broadcast message");
            foreach (var client in clients.Values)
            {
                client.WriteLine("Message text");
            }
        }

        private void client1Send_Click(object sender, EventArgs e)
        {
            Write("Client 1 sending message");

            client1writer.WriteLine("Client1 text");
        }

        private void client2Start_Click(object sender, EventArgs e)
        {
            Task.Factory.StartNew(() =>
            {
                client2 = new TcpClient("localhost", 789);
                client2writer = new StreamWriter(client2.GetStream());
                client2writer.AutoFlush = true;
                using (var reader = new StreamReader(client2.GetStream()))
                {
                    while (true)
                    {
                        var data = reader.ReadLine();
                        if (data == null)
                            break;
                        Write("Client2  recieved:" + data);
                    }
                }
                Write("Client2 disconected");
            });
        }

        private void client2Send_Click(object sender, EventArgs e)
        {
            Write("Client 2 sending message");

            client2writer.WriteLine("Client2 text");
        }

        private void serverStop_Click(object sender, EventArgs e)
        {
            server.Stop();
            foreach (var client in clients.Values)
                client.Dispose();
        }

        private void client1Stop_Click(object sender, EventArgs e)
        {
            client1.Close();
        }

        private void client2Stop_Click(object sender, EventArgs e)
        {
            client2.Close();
        }
    }
}
Nahlásit jako SPAM
IP: 77.92.213.–
TD
~ Anonymní uživatel
28 příspěvků
2. 4. 2016   #16
-
0
-

diky za vzorovy kod, hodil sem si ho visualka, testoval, študoval vytvoreni serveru prijati a odeslani dát sem pochopil ale tych dvoch klientu vubec totalne ne kažfy muj pokus s nima neco spravyt mi vyvolalo len plno vynimek ktere jak pisals nejsu ošetrene tak je to samozrejme že to bude nadavat neco. ale vidim to že neostava nic jine než stale dookola študovat a študovat dokat na neco nedojdu ... ale Dekuju moc za rady a trpezlivost :)

Nahlásit jako SPAM
IP: 89.103.90.–
p3can
~ Anonymní uživatel
312 příspěvků
3. 4. 2016   #17
-
0
-

#16 TD
kliknu serverStart_Click pro nastartovani serveru pak client1Start_Click, client2Start_Click. server posila zpravy pomoci serverBroadcast_Click a klienti pomoci client1Send_Click a client2Send_Click. tam nic nepada a vsechno je asynchroni a nezamrza UI. neni osetrene to ze posilas zpravy na zavrene spojeni.


 

Nahlásit jako SPAM
IP: 77.92.213.–
Zjistit počet nových příspěvků

Přidej příspěvek

Toto téma je starší jak čtvrt roku – přidej svůj příspěvek jen tehdy, máš-li k tématu opravdu co říct!

Ano, opravdu chci reagovat → zobrazí formulář pro přidání příspěvku

×Vložení zdrojáku

×Vložení obrázku

Vložit URL obrázku Vybrat obrázek na disku
Vlož URL adresu obrázku:
Klikni a vyber obrázek z počítače:

×Vložení videa

Aktuálně jsou podporována videa ze serverů YouTube, Vimeo a Dailymotion.
×
 
Podporujeme Gravatara.
Zadej URL adresu Avatara (40 x 40 px) nebo emailovou adresu pro použití Gravatara.
Email nikam neukládáme, po získání Gravatara je zahozen.
-
Pravidla pro psaní příspěvků, používej diakritiku. ENTER pro nový odstavec, SHIFT + ENTER pro nový řádek.
Sledovat nové příspěvky (pouze pro přihlášené)
Sleduj vlákno a v případě přidání nového příspěvku o tom budeš vědět mezi prvními.
Reaguješ na příspěvek:

Uživatelé prohlížející si toto vlákno

Uživatelé on-line: 0 registrovaných, 21 hostů

Podobná vlákna

TCP server, TCP klient v Linuxu — založil kocourOggy

TCP/IP — založil vojtano_k

Tcp/Ip C++ — založil Kenvelo

TCP — založil Petr

TCP Sniffer v C# — založil CZechBoY

 

Hostujeme u Českého hostingu       ISSN 1801-1586       ⇡ Nahoru Webtea.cz logo © 20032024 Programujte.com
Zasadilo a pěstuje Webtea.cz, šéfredaktor Lukáš Churý