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 # - Klient - 4. diel

Vitajte u môjho ďalšieho článku, kde si naprogramujeme klienta pre našu špiónske aplikáciu. Nebudeme tentoraz zbytočne hovoriť a analogicky podľa serveru naprogramujeme nášho klienta :) .

Kód

Opäť si urobíme event, že sme dostali dáta a tiež event, že sme sa prihlásili na server (u odosielanie sa to bude hodiť). Ďalej potrebujeme heslo k serveru, meno, pod ktorým na ňom vystupujeme, TcpClient a jeho SslStream aby sme mohli posielať dáta. Potom tiež List toho, čo chceme odosielať a bool, či sme prihlásení k serveru, alebo nie. Nemalo by zmysel, keby sa táto premenná dala meniť zvonku, takže ju dáme private a obalíme. Rovnako tak obalíme IP adresu a port, aby sme ich ľahko dostali.

public event DataRecieved MessageRecieved;
public event Server.ClientEvent LoggedIn;

public string password = "";
public string name = "";
private TcpClient connection;
private SslStream securedStream;
private List<Action> tasks = new List<Action>();
private bool _loggedIn = false;

public bool loggedIn
{
    get
    {
        return _loggedIn;
    }
    protected set { }
}

public IPAddress address
{
    get
    {
        return ((IPEndPoint)connection.Client.LocalEndPoint).Address;
    }
}

public int port
{
    get
    {
        return ((IPEndPoint)connection.Client.LocalEndPoint).Port;
    }
}

Ďalej urobíme konštruktor. Bude o trochu ťažšie, než u servera, pretože konštruktor SslStreamu u klienta je trochu zložitejšia. Zo všetkého najskôr si nastavíme meno a heslo, potom zavoláme konštruktor TcpClienta a pokúsime sa pripojiť k serveru. Potom sa pokúsime prečítať long značiace, ako dlhý je certifikát. Potom prečítame aj ho, ako vždy bajt po bajtu. Potom zavoláme konštruktor SslStreamu, ktorému dáme NetworkStream z TcpClienta, povieme, že chceme pôvodné NetworkStream zavrieť a odovzdáme mu dve metódy: prvá má za úlohu skontrolovať vzdialený certifikát a povedať, či mu veríme alebo nie. My však vždy vrátime true, pretože sme si istí, že server vie, čo nám posiela. (Ak by sme skontrolovali premennú sslPolicyErrors, ktorá obsahuje informácie o tom, čo je zle, vždy by sme dostali RemoteCertifi­cateChainError­s, ak by sme prvý daný certifikát nenainštalovali do systému a RemoteCertifi­cateNameMismat­ch, ak bude server bežať na inej adrese, než pre ktorú bol certifikát vygenerovaný.)

Druhá metóda dostane ako jeden z parametrov zoznam lokálnych certifikátov, z ktorých má jeden vybrať. Ja tu proste vrátim prvý certifikát, ale metódu si pochopiteľne môžete prerobiť, ak chcete. Ďalej si vytvoríme kolekciu certifikátov, vložíme do nej ten, ktorý sme dostali od servera (server nám posiela certifikát klienta, ešte než sa začne šifrovať) a potom zavoláme metódu AuthenticateAs­Client, ktorá, ako názov napovedá, nás autentizuje ako klienta do spojenia. Na serveri by sa tou dobou mala zavolať metóda AuthenticateAs­Server. Pokiaľ budú obe metódy zavolanie a nikde sa nevyskytne výnimka, nastavíme náš securedStream na tento nastavený SslStream, eventu LoggedIn priradíme metódu, ktorá nám spustí odosielajúcej vlákno (pochopiteľne nemôžeme odosielať, kým nie sme prihlásený) a na záver spustíme prijímacie vlákno.

public Client(IPAddress serverIp, int serverPort, string name, string password)
{
    this.password = password;
    this.name = name;

    try
    {
        connection = new TcpClient();
        connection.Connect(new IPEndPoint(serverIp, serverPort));

        BinaryWriter certWriter = new BinaryWriter(new MemoryStream());
        BinaryReader reader = new BinaryReader(connection.GetStream());
        long certLenght = reader.ReadInt64();

        while (certLenght != 0)
        {
            certWriter.Write(reader.ReadByte());
            certLenght--;
        }

        SslStream stream = new SslStream(connection.GetStream(), false, new RemoteCertificateValidationCallback(CertificateValidationCallback), new LocalCertificateSelectionCallback(CertificateSelectionCallback));
        X509Certificate2Collection certs = new X509Certificate2Collection();

        certs.Add(new X509Certificate2(((MemoryStream)certWriter.BaseStream).ToArray(), password));

        stream.AuthenticateAsClient(serverIp.ToString(), certs, SslProtocols.Default, false);

        securedStream = stream;

        LoggedIn += Client_LoggedIn;

        Thread recievingThread = new Thread(RecievingThread);
        recievingThread.Name = "recievingThread";
        recievingThread.IsBackground = true;
        recievingThread.Start();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}

private static X509Certificate CertificateSelectionCallback(object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertifica-te, string[] acceptableIssuers)
{
    return localCertificates[0];
}

private static bool CertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    return true;
}
private void Client_LoggedIn(string name, Connection connection)
{
    Thread sendThread = new Thread(SendThread);
    sendThread.Name = "sendThread";
    sendThread.IsBackground = true;
    sendThread.Start();
}

Pozn .: ak chceme nechať užívateľský kód odchytávať všetky výnimky, zmažeme try-catch blok v konstruktoru.

Ďalšia je na rade prijímacie vlákno. Najprv si inicializujeme BinaryWriter, BinaryReader a MemoryStream. Podľa našej konvencie potom najskôr prečítame long, značiaci dĺžku správy a dva bajty hlavičky. Potom skontrolujeme, či sme sa prihlásili. Ak nie, jediná správa, ktorá nás zaujíma, je tá, ktorá indikuje, že už prihlásení sme. Použijeme teda naše konštanty, ktoré sme si definovali už v servera, aby sme skontrolovali, či aktuálna správa je tá, na ktorú čakáme.

Ak je, nastavíme premennú _loggedIn na true, spustíme event LoggedIn a znovu spustíme túto metódu. Nezávisle na tom, ako dopadli predchádzajúce riadky kódu, potom ešte musíme prečítať vlastné správu. Ak je správa prázdna, nemá cenu vykonávať ďalšie kód a tak rovno vrátime prázdny MemoryStream a hlavičku. V opačnom prípade prečítame našu správu bajt po bajtu a napcháme do MemoryStreamu. Na záver resetujeme náš MemoryStream a naštartujeme event, že sme dostali správu.

private void RecievingThread()
{
    while (true)
    {
        MemoryStream result = new MemoryStream();
        BinaryReader networkReader = new BinaryReader(securedStream);
        BinaryWriter memoryWriter = new BinaryWriter(result);
        long bufferSize = networkReader.ReadInt64();
        byte[] header = new byte[2];
        header[0] = networkReader.ReadByte();
        header[1] = networkReader.ReadByte();

        if (!_loggedIn)
        {
           if (header[0] == OwnerID.SERVER_INTERNAL && header[1] == TypeID.AUTH_OK)
           {
               _loggedIn = true;

               if(LoggedIn != null)
                   LoggedIn(name, new Connection(securedStream, connection));

               RecievingThread();
           }
        }

        if (bufferSize == 0)
            if (MessageRecieved != null)
                MessageRecieved(header, result);

        while (bufferSize != 0)
        {
            memoryWriter.Write(networkReader.ReadByte());
            bufferSize--;
        }

        result.Position = 0;

        if (MessageRecieved != null)
            MessageRecieved(header, result);
    }
}

Ďalšia vec, ktorú musíme ešte naprogramovať, je odosielacie metóda. Je, myslím, celkom jednoduchá. Napred zamkneme kolekciu našich úloh, a potom do nej pridáme náš odosielajúcej kód. Ak je dĺžka správy väčšia ako nula, zapíšeme do nášho SslStreamu jej dĺžku (long), potom hlavičku, a potom do neho skopírujeme Stream, obsahujúce dáta na odoslanie. Ak je Stream prázdny, zapíšeme len nulu (stále ako long!) A hlavičku. Metódu si tiež preťažíme, aby sme mohli ľahko posielať stringy.

public void Send(Stream data, byte[] header)
{
    lock (tasks)
    {
        tasks.Add(delegate
        {
            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);
            }
        });
    }
}

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

Ďalej si ešte musíme urobiť vlastný odosielací vlákno. Je veľmi jednoduché - zamkne kolekciu, potom všetko v nej vykoná (pokiaľ v nej niečo je) a vyčistí ju.

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

                tasks.Clear();
            }
        }
    }
}

Ako predposledný vec si ešte urobíme metódu na prihlásenie na server. Nie je to vlastne nič iné, než odoslanie heslá a mená ako bežnú správu na server. Oboje teda metódou Send pošleme, s hlavičkou, ktorá značí, že ide o správu priamo pre server. Metóda Send vloží odosielacej kód do kolekcie úloh, ale odosielacie vlákno nám ešte nebeží, preto manuálne všetko v kolekcii vykonáme a kolekciu vyčistíme.

public void Login()
{
    try
    {
        Send(password, new byte[2] { OwnerID.SERVER_INTERNAL, 0 });
        Send(name, new byte[2] { OwnerID.SERVER_INTERNAL, 0 });

        foreach (Action task in tasks)
        {
            task();
        }

        tasks.Clear();
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
    }
}

Na záver si ešte urobíme tzv. Destruktor. Ide o špeciálnu metódu, ktorá sa spustí pri deštrukcii danej inštancie triedy. Označuje sa tilda (~) a neberie žiadne parametre. Destruktor si urobíme preto, že keď nám klient nejako spadne, budeme môcť ešte informovať server o našom odpojenie. Opäť využijeme naše konštanty.

~Client()
{
    Send(new MemoryStream(), new byte[2] { OwnerID.SERVER_INTERNAL, TypeID.BYE });
}

No a to je pre dnešok všetko! Teraz už máme plne funkčné a na použitie jednoduchý server a klienta, takže si ich v najbližších dielach nejako šikovne obalíme a vytvoríme rozhranie pre jednotlivé moduly.

Klienta si opäť môžete stiahnuť v archíve pod článkom.


 

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 97x (8.92 kB)
Aplikácia je vrátane zdrojových kódov v jazyku C#

 

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