Špiónská aplikace v C# - Server - 2.díl

C# .NET Pro pokročilé Špiónská aplikace v C# - Server - 2.díl

Vítejte u mého druhého článku, kde si naprogramujeme server pro naši špiónskou aplikaci.

Jako první si vytvoříme strukturu s názvem Connection, neboť musíme najednou ukládat TpcClient a jeho SslStream. Jelikož u struktury nemůžeme mít null, vytvoříme si konstantu Empty. Zároveň si pro jistotu přetížíme operátory.

public struct Connection
{
    public static readonly Connection Empty = new Connection(null, null);

    public SslStream securedStream;
    public TcpClient client;

    public Connection(SslStream stream, TcpClient client)
    {
        this.securedStream = stream;
        this.client = client;
    }

    public static bool operator ==(Connection c1, Connection c2)
    {
        if (c1.client == c2.client && c1.securedStream == c2.securedStream)
            return true;
        else
            return false;
    }

    public static bool operator !=(Connection c1, Connection c2)
    {
        if (c1.client == c2.client || c1.securedStream == c2.securedStream)
            return false;
        else
            return true;
    }
}

Každého klienta připojeného k serveru bude tvořit jeho Connection a jméno, pod jakým vystupuje na serveru. Zároveň potřebujeme nějaký TcpListener, ke kterému se budou moci klienti připojit:

public Dictionary<string, Connection> connections = new Dictionary<string, Connection>();
private TcpListener tcpListener;

Není důvod, aby někdo přímo na tyto vlastnosti koukal zvenčí, takže je nastavíme private. Zároveň si ale vytvoříme indexer, aby se k jednotlivým klientům šlo dostat (ale kolekce nešla zvenku měnit) a taky by bylo dobré, aby byla zvenčí vidět IP adresa a port:

public IPAddress address
{
    get
    {
        return ((IPEndPoint)tcpListener.LocalEndpoint).Address;
    }
}

public int port
{
    get
    {
        return ((IPEndPoint)tcpListener.LocalEndpoint).Port;
    }
}

public Connection this[int i]
{
    get
    {
        return connections.ElementAt(i).Value;
    }
    protected set { }
}

Budeme také potřebovat nějaké události – když se klient připojí, odpojí a pošle zprávu. Jelikož zpráva se skládá z délky, dvou-bajtové hlavičky a těla, bude delegát vypadat takto:

public delegate void DataRecieved(byte[] header, MemoryStream message, string name, Connection sender);
public delegate void ClientEvent(string name, Connection connection);
public delegate void NewBPS(long uncompressed);

public event DataRecieved MessageRecieved;
public event ClientEvent ClientLoggedIn;
public event ClientEvent ClientLeft;
public event NewBPS NewBytesPerSecond;

Proč jsem pro tělo zprávy použil MemoryStream a ne byte[] si vysvětlíme za chvíli. Poslední event budeme vyvolávat každou sekundu a bude sloužit k tomu, abychom mohli měřit, kolik dat nám serverem proteče.

Nyní se můžeme vrhnout na konstruktor. Nastavíme si heslo, které budeme vyžadovat po klientovi, abychom s ním vůbec komunikovali. Dále si vytvoříme naslouchací a odesílací vlákno, obě nastavíme na pozadí, aby nebránily vypnutí aplikace. Pak ještě vytvoříme Timer, který každou sekundu spustí event, že máme k dispozici nové číslo, a pak promaže naši hodnotu, kolik bajtů nám proteklo serverem, aby se počítalo od znova.

public readonly string password;

public Server(IPAddress address, int port, string password)
{
    try
    {
        this.password = password;
        tcpListener = new TcpListener(address, port);

        Thread listenThread = new Thread(ListenThread);
        listenThread.Name = "listenThread";
        listenThread.IsBackground = true;
        listenThread.Start();

        Thread sendThread = new Thread(SendThread);
        sendThread.Name = "sendThread";
        sendThread.IsBackground = true;
        sendThread.Start();

        Timer timer = new System.Timers.Timer();
        timer.Elapsed += timer_Elapsed;
        timer.Interval = 1000;
        timer.Enabled = true;
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}

void timer_Elapsed(object sender, ElapsedEventArgs e)
{
    if (NewBytesPerSecond != null)
        NewBytesPerSecond(uncompressedBPS);

    uncompressedBPS = 0;
}

Jako první si ukážeme, jak uděláme odesílací vlákno. Abychom zajistili, že se zprávy budou odesílat ve správném pořadí (aby se např. nestalo, že zatímco budeme asynchronně posílat velký soubor, začne se posílat už jiná zpráva), metoda Send() bude pouze data (konkrétně metody, co se mají provést) přidávat do kolekce, odkud je pak bude jednu po druhé provádět odesílací vlákno.

V celé této „knihovně“ se budeme striktně vyhýbat práci s poli bajtů, pokud si nebudeme absolutně jistí, že data nebudou velká (např. u hlavičky), místo nich budeme vždy používat MemoryStream. Jednak se s ním pracuje také velmi snadno a druhak používání Streamů místo polí s bajty se nám velmi osvědčí, až budeme programovat jednotlivé moduly a především se tím vyhneme výjimce OutOfMemoryEx­ception, která by nás jinak otravovala každou chvíli.

Než budeme upravovat kolekci, musíme si ji zamknout, protože musíme počítat s tím, že se odesílací vlákno může pokusit kolekci upravit zrovna, když to děláme my. Pak podle naší konvence zapíšeme nejprve BinaryWriterem long značící délku zprávy, pak dva bajty hlavičky a pak vlastní data. Pokud je délka dat menší nebo rovna nule, nemá cenu provádět předchozí kód, stačí prostě zapsat nulu jako velikost, pak hlavičku a už nic. Metodě také předáme SslStream klienta, kterému chceme zprávu poslat.

private List<Action> tasks = new List<Action>();

public void Send(Stream securedStream, Stream data, byte[] header)
{
    lock (tasks)
    {
        tasks.Add(delegate
        {
            try
            {
                if (data.Length > 0)
                {
                    BinaryWriter writer = new BinaryWriter(securedStream);
                    writer.Write(data.Length);
                    writer.Write(header);
                    data.Position = 0;
                    data.CopyTo(securedStream);
                }
                else
                {
                    BinaryWriter writer = new BinaryWriter(securedStream);
                    writer.Write((long)0);
                    writer.Write(header);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        });
    }
}

Metodu si také přetížíme, abychom si usnadnili posílání stringů. Vytvoříme si také statické metody GetBytes a GetString, abychom toho nemuseli vždy tolik psát.

public void Send(Stream securedStream, string data, byte[] header)
{
    Send(securedStream, new MemoryStream(GetBytes(data)), header);
}

public static string GetString(byte[] message)
{
    return Encoding.UTF8.GetString(message);
}

public static byte[] GetBytes(string message)
{
    return Encoding.UTF8.GetBytes(message);
}

Ještě ale potřebujeme vlastní vlákno, které bude data odesílat. Je velmi jednoduché – zamkne kolekci, pak vše v ní provede a vyčistí ji.

private void SendThread()
{
    while (true)
    {
        if (tasks.Count != 0)
        {
            lock (tasks)
            {
                foreach(Action task in tasks)
                {
                    task();
                }

                tasks.Clear();
            }
        }
    }
}

Dále si podle naší konvence, jak budeme posílat a přijímat data, uděláme ještě metodu na čtení. Pro ni nevytváříme samostatné vlákno, neboť ji budeme volat z vlákna obsluhujícího konkrétního klienta. Než se pustíme do čtecích metod, bylo by dobré definovat si nějakou konstantu, kterou budeme označovat data, co si má zpracovávat server, nikoli je posílat dál. Já zvolil 255.

public static readonly byte SERVER_INTERNAL = 255;

Pak ještě konstantu, která bude značit, že se klient chce korektně odpojit.

public static readonly byte BYE = 247;

V tuto chvíli vám to možná přestává dávat smysl, to je proto, že do tutoriálu kopíruji kusy kódu z hotového projektu, ale vysvětlím to. Jak jsme si řekli, první bajt značí, jaké části programu je zpráva určena, a druhý, co zpráva obsahuje. Definujeme si tedy konstantu pro zprávy určené přímo pro server a konstantu, která značí, že se klient chce odpojit, abychom si nemuseli pamatovat čísla.

Z důvodu omezení maximální velikosti článku musím tento článek opět rozdělit, příští článek začne přesně, kde tento skončil.


 

  Aktivity (3)

Článek pro vás napsal jiri.sada
Avatar
Autor se věnuje programování v C#, stříhání a úpravou videí a efektů do nich, trollení svých kamarádů drobnými viry a obecně všemu okolo počítačů a elektroniky s Androidem

Jak se ti líbí článek?
Celkem (6 hlasů) :
4.666674.666674.666674.666674.66667


 


Miniatura
Všechny články v sekci
C# - Pro pokročilé

 

 

Komentáře

Avatar
Martin Dráb
Redaktor
Avatar
Martin Dráb:

Nevím, proč často vidím tento obrat:

if (podminka)
  return true;
else return false;

Proč není lepší napsat prostě:

return (podminka);

Jinak se mi zdá, že by také bylo rozumné, aby odesílací vlákno nezatěžovalo procesor, když nemá co na práci. Tady to vypadá, že "nikdy nepsí". Přitom by v C# určitě měly být implementované semafory. Pak tam ještě bez zámku saháš na položku tasks.count, ale to v tomto případě asi vadit nebude.

Pokud bude mít odesílací vlákno hodně práce (dlouhou frontu úkolů nebo časově náročné úkoly), volání metody Send zablokuje ostatní (protože jednotlivé úkoly jsou vykonávány pod zamknutou kolekcí). Ale tohle je spíš otázka výběru, co člověk chce. Opačný extrém také nemusí být zrovna dobrý.

Odpovědět  +4 23. dubna 23:37
2 + 2 = 5 for extremely large values of 2
Avatar
Odpovídá na Martin Dráb
David Podeszwa:

Možná proto že dost lidí neví že vlastně sama podmínka v závorce vrací bool. Jinak C# semafory má ale jejich použití se teda mně zdá chaotické a navíc pokud jsi to myslel použít na to že by odesílací vlákno spalo do té doby dokud mu nepříjde další Task tak na tohle já hoodně používám ManualResetEvent příklad použití:

public class Trida
    {
        //Vytvoří instanci ManualResetEvent s počátečním stavem na false
        ManualResetEvent mre = new ManualResetEvent(false);

        public void Vlakno1()
        {
            while (true)
            {
                //Vykoná nějaký kod
                int i = 1 + 1;
                //Počká na nejake jine vlakno ktere mu rekne ze muze pokracovat
                mre.WaitOne();
                //Take se muze pouzit mre.WaitOne(int); kde integer jako parametr je počet milisekund po kterem se thread vzbudi pokud ho nikdo nevzbudil manualne
            }
        }
        public void NejakaMetoda()
        {
            //Tady jednoduse probudime Vlakno1 ze "spanku"
            mre.Set();
        }
    }

Je to pouze příklad já toto používám často hodí se to při různých asynchronních operacích

Editováno 24. dubna 10:03
 
Odpovědět 24. dubna 10:02
Avatar
Martin Dráb
Redaktor
Avatar
Odpovídá na David Podeszwa
Martin Dráb:

ManualResetEvent se hodí, pokud potřebuješ indikovat, že se něco stalo (obvykle něco, co se tak úplně libovolně neopakuje). Kdo ji resetuje do nesignálního stavu (stavu, kdy WaitOne čeká)?

Ohledně fronty by spíš bylo lepší použít samoresetující se eventu, což je v podstatě binární semafor, ta manuální je lehce nebezpečná.

Odpovědět  +1 24. dubna 10:45
2 + 2 = 5 for extremely large values of 2
Avatar
Odpovídá na Martin Dráb
David Podeszwa:

No to máš asi pravdu já fronty takhle jak on nepoužívám já jsem si udělal na Tcp komunikaci vlastní splácanou knihovnu jak serverovou tak klientskou přímo s pomocí Socketů a při každém posílání si věci zamykám a pokud z venčí posílám velké soubory tak si to většinou rozkouskuju a posílám postupně

 
Odpovědět 24. dubna 13:45
Avatar
jiri.sada
Redaktor
Avatar
jiri.sada:

Stále mi neschválili druhou část tohohle článku :-(

 
Odpovědět 27. dubna 16:21
Avatar
jiri.sada
Redaktor
Avatar
Odpovídá na Martin Dráb
jiri.sada:

K metodě Send: Je mi to jasné, ale to měl být záměr, aby se jednotlivé věci zařadily za sebe a posílaly se přesně v pořadí, jak volám Send

 
Odpovědět 27. dubna 18:08
Avatar
Martin Dráb
Redaktor
Avatar
Odpovídá na jiri.sada
Martin Dráb:

To podle mě nic nemění na tom, že se zamyká po zbytečně dlouhou dobu. Ono by stačilo, kdyby vlákno obsluhující požadavky na odeslání pracovalo následovně:

  1. zamklo,
  2. odebralo požadavek z fronty,
  3. odemklo,
  4. zpracovalo požadavek.

Fronta (implementovaná kolekcí) zajistí, že se požadavky budou zpracovávat v pořadí příchodu a zároveň se zbytečně nebude blokovat volání metody Send.

Samozřejmě to může vést k extrému, že fronta naroste do neuvěřitelných rozměrů, pokud je snaha odeslat víc požadavků, než vlákno stíhá zpracovávat, ale vše má holt své výhody a nevýhody. A pořád se to dá ještě nějak řešit; tam už hodně záleží, co konkrétně je potřeba.

Odpovědět 27. dubna 19:41
2 + 2 = 5 for extremely large values of 2
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 7 zpráv z 7.