IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Špiónske aplikácie v C # - Server - 2.diel

Vitajte u môjho druhého článku, kde si naprogramujeme server pre našu špiónske aplikáciu.

Ako prvé si vytvoríme štruktúru s názvom Connection, lebo musíme naraz ukladať TpcClient a jeho SslStream. Keďže u štruktúry nemôžeme mať null, vytvoríme si konštantu Empty. Zároveň si pre istotu preťažíme operátorov.

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 pripojeného k serveru bude tvoriť jeho Connection a meno, pod akým vystupuje na serveri. Zároveň potrebujeme nejaký TcpListener, ku ktorému sa budú môcť klienti pripojiť:

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

Nie je dôvod, aby niekto priamo na tieto vlastnosti pozeral zvonku, takže je nastavíme private. Zároveň si ale vytvoríme Indexer, aby sa k jednotlivým klientom išlo dostať (ale kolekcia nešla zvonku meniť) a tiež by bolo dobré, aby bola zvonku vidieť 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 tiež potrebovať nejaké udalosti - keď sa klient pripojí, odpojí a pošle správu. Keďže správa sa skladá z dĺžky, dvoch-bajtové hlavičky a tela, bude delegát vyzerať 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;

Prečo som pre telo správy použil MemoryStream a nie byte [] si vysvetlíme za chvíľu. Posledné event budeme vyvolávať každú sekundu a bude slúžiť k tomu, aby sme mohli merať, koľko dát nám serverom pretečie.

Teraz sa môžeme vrhnúť na konštruktor. Nastavíme si heslo, ktoré budeme vyžadovať od klienta, aby sme s ním vôbec komunikovali. Ďalej si vytvoríme načúvacie a odosielajúcej vlákno, obe nastavíme na pozadí, aby nebránili vypnutie aplikácie. Potom ešte vytvoríme Timer, ktorý každú sekundu spustí event, že máme k dispozícii nové číslo, a potom premaže našu hodnotu, koľko bajtov nám pretieklo serverom, aby sa 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;
}

Ako prvý si ukážeme, ako urobíme odosielacie vlákno. Aby sme zaistili, že sa správy budú odosielať v správnom poradí (aby sa napr. Nestalo, že kým budeme asynchrónne posielať veľký súbor, začne sa posielať už iná správa), metóda Send () bude iba dáta (konkrétne metódy, čo sa majú vykonať) pridávať do kolekcie, odkiaľ je potom bude jednu po druhej vykonávať odosielajúcej vlákno.

V celej tejto "knižnici" sa budeme striktne vyhýbať práci s poľami bajtov, ak si nebudeme absolútne istí, že dáta nebudú veľká (napr. Pri hlavičke), namiesto nich budeme vždy používať MemoryStream. Jednak sa s ním pracuje tiež veľmi ľahko a po druhé používanie streamy miesto polí s bajty sa nám veľmi osvedčí, až budeme programovať jednotlivé moduly a predovšetkým sa tým vyhneme výnimke OutOfMemoryEx­ception, ktorá by nás inak otravovala každú chvíľu.

Než budeme upravovať kolekciu, musíme si ju zamknúť, pretože musíme počítať s tým, že sa odosielacia vlákno môže pokúsiť kolekciu upraviť zrovna, keď to robíme my. Potom podľa našej konvencie zapíšeme najprv BinaryWriterem long značiace dĺžku správy, potom dva bajty hlavičky a potom vlastné dáta. Ak je dĺžka dát menšia alebo rovná nule, nemá cenu vykonávať predchádzajúce kód, stačí jednoducho zapísať nulu ako veľkosť, potom hlavičku a už nič. Metóde tiež odovzdáme SslStream klienta, ktorému chceme správu poslať.

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);
            }
        });
    }
}

Metódu si tiež preťažíme, aby sme si uľahčili posielanie stringov. Vytvoríme si tiež statické metódy GetBytes a GetString, aby sme toho nemuseli vždy toľko písať.

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);
}

Ešte ale potrebujeme vlastné vlákno, ktoré bude dáta odosielať. Je veľmi jednoduché - zamkne kolekciu, potom všetko v nej vykoná a vyčistí ju.

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

                tasks.Clear();
            }
        }
    }
}

Ďalej si podľa našej konvencie, ako budeme posielať a prijímať dáta, urobíme ešte metódu na čítanie. Pre ňu nevytvárame samostatné vlákno, pretože ju budeme volať z vlákna obsluhujúceho konkrétneho klienta. Než sa pustíme do čítacích metód, bolo by dobré definovať si nejakú konštantu, ktorú budeme označovať dáta, čo si má spracovávať server, nie je posielať ďalej. Ja zvolil 255.

public static readonly byte SERVER_INTERNAL = 255;

Potom ešte konštantu, ktorá bude značiť, že sa klient chce korektne odpojiť.

public static readonly byte BYE = 247;

V tejto chvíli vám to možno prestáva dávať zmysel, to je preto, že do tutoriálu Kopírujem kusy kódu z hotového projektu, ale vysvetlím to. Ako sme si povedali, prvý bajt značí, aké časti programu je správa určená, a druhý, čo správa obsahuje. Definujeme si teda konštantu pre správy určené priamo pre server a konštantu, ktorá značí, že sa klient chce odpojiť, aby sme si nemuseli pamätať čísla.

Z dôvodu obmedzenia maximálnej veľkosti článku musím tento článok opäť rozdeliť, budúci článok začne presne, kde tento skončil.


 

Všetky články v sekcii
C # - Pre pokročilých
Článok pre vás napísal jiri.sada
Avatar
Užívateľské hodnotenie:
Ešte nikto nehodnotil, buď prvý!
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
Aktivity