Špiónská aplikace v C# - Klient - 4. díl

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

Vítejte u mého dalšího článku, kde si naprogramujeme klienta pro naši špiónskou aplikaci. Nebudeme tentokrát zbytečně mluvit a analogicky podle serveru naprogramujeme našeho klienta :).

Kód

Opět si uděláme event, že jsme dostali data a také event, že jsme se přihlásili na server (u odesílání se to bude hodit). Dále potřebujeme heslo k serveru, jméno, pod kterým na něm vystupujeme, TcpClient a jeho SslStream abychom mohli posílat data. Pak také List toho, co chceme odesílat a bool, jestli jsme přihlášeni k serveru, nebo ne. Nemělo by smysl, kdyby se tato proměnná dala měnit zvenčí, takže ji dáme private a obalíme. Stejně tak obalíme IP adresu a port, abychom je snadno 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;
    }
}

Dále uděláme konstruktor. Bude o trochu těžší, než u serveru, neboť konstruktor SslStreamu u klienta je trochu složitější. Ze všeho nejdříve si nastavíme jméno a heslo, pak zavoláme konstruktor TcpClienta a pokusíme se připojit k serveru. Pak se pokusíme přečíst long značící, jak dlouhý je certifikát. Pak přečteme i jej, jako vždy bajt po bajtu. Poté zavoláme konstruktor SslStreamu, kterému dáme NetworkStream z TcpClienta, řekneme, že chceme původní NetworkStream zavřít a předáme mu dvě metody: první má za úkol zkontrolovat vzdálený certifikát a říct, jestli mu věříme nebo ne. My však vždy vrátíme true, protože jsme si jistí, že server ví, co nám posílá. (Pokud bychom zkontrolovali proměnnou sslPolicyErrors, která obsahuje informace o tom, co je špatně, vždy bychom dostali RemoteCertifi­cateChainError­s, pokud bychom první daný certifikát nenainstalovali do systému a RemoteCertifi­cateNameMismat­ch, pokud bude server běžet na jiné adrese, než pro kterou byl certifikát vygenerován.)

Druhá metoda dostane jako jeden z parametrů seznam lokálních certifikátů, ze kterých má jeden vybrat. Já zde prostě vrátím první certifikát, ale metodu si pochopitelně můžete předělat, pokud chcete. Dále si vytvoříme kolekci certifikátů, vložíme do ní ten, který jsme dostali od serveru (server nám posílá klientský certifikát, ještě než se začne šifrovat) a pak zavoláme metodu AuthenticateAs­Client, která, jak název napovídá, nás autentizuje jako klienta do spojení. Na serveru by se tou dobou měla zavolat metoda AuthenticateAs­Server. Pokud budou obě metody zavolány a nikde se nevyskytne výjimka, nastavíme náš securedStream na tento nastavený SslStream, eventu LoggedIn přiřadíme metodu, která nám spustí odesílací vlákno (pochopitelně nemůžeme odesílat, dokud nejsme přihlášeni) a na závěr spustíme přijímací 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.: pokud chceme nechat uživatelský kód odchytávat všechny výjimky, smažeme try-catch blok v konstruktoru.

Další je na řadě přijímací vlákno. Napřed si inicializujeme BinaryWriter, BinaryReader a MemoryStream. Podle naší konvence pak nejdříve přečteme long, značící délku zprávy a dva bajty hlavičky. Pak zkontrolujeme, zda jsme přihlášeni. Pokud ne, jediná zpráva, která nás zajímá, je ta, která indikuje, že už přihlášeni jsme. Použijeme tedy naše konstanty, které jsme si definovali už v serveru, abychom zkontrolovali, zda aktuální zpráva je ta, na kterou čekáme.

Pokud je, nastavíme proměnnou _loggedIn na true, spustíme event LoggedIn a znovu spustíme tuto metodu. Nezávisle na tom, jak dopadly předchozí řádky kódu, pak ještě musíme přečíst vlastní zprávu. Pokud je zpráva prázdná, nemá cenu provádět další kód a tak rovnou vrátíme prázdný MemoryStream a hlavičku. V opačném případě přečteme naši zprávu bajt po bajtu a nacpeme do MemoryStreamu. Na závěr resetujeme náš MemoryStream a nastartujeme event, že jsme dostali zprá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);
    }
}

Další věc, kterou musíme ještě naprogramovat, je odesílací metoda. Je, myslím, docela jednoduchá. Napřed zamkneme kolekci našich úkolů, a pak do ní přidáme náš odesílací kód. Pokud je délka zprávy větší než nula, zapíšeme do našeho SslStreamu její délku (long), pak hlavičku, a pak do něj zkopírujeme Stream, obsahující data k odeslání. Pokud je Stream prázdný, zapíšeme jen nulu (stále jako long!) a hlavičku. Metodu si také přetížíme, abychom mohli snadno posílat 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);
}

Dále si ještě musíme udělat vlastní odesílací vlákno. Je velmi jednoduché – zamkne kolekci, pak vše v ní provede (pokud v ní něco je) a vyčistí ji.

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

                tasks.Clear();
            }
        }
    }
}

Jako předposlední věc si ještě uděláme metodu na přihlášení k serveru. Není to vlastně nic jiného, než odeslání hesla a jména jako běžnou zprávu na server. Oboje tedy metodou Send pošleme, s hlavičkou, která značí, že jde o zprávu přímo pro server. Metoda Send vloží odesílací kód do kolekce úkolů, ale odesílací vlákno nám ještě neběží, proto manuálně vše v kolekci provedeme a kolekci 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ávěr si ještě uděláme tzv. destruktor. Jde o speciální metodu, která se spustí při destrukci dané instance třídy. Označuje se tildou (~) a nebere žádné parametry. Destruktor si uděláme proto, že když nám klient nějak spadne, budeme moci ještě informovat server o našem odpojení. Opět využijeme naše konstanty.

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

No a to je pro dnešek všechno! Nyní už máme plně funkční a na použití jednoduchý server a klienta, takže si je v příštích dílech nějak šikovně obalíme a vytvoříme rozhraní pro jednotlivé moduly.

Klienta si opět můžete stáhnout v archivu pod článkem.


 

Stáhnout

Staženo 36x (8.92 kB)
Aplikace je včetně zdrojových kódů v jazyce C#

 

  Aktivity (4)

Č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 (1 hlasů) :
333 33


 



 

 

Komentáře

Avatar
Jan Vondra
Člen
Avatar
Jan Vondra:

Fakt super články , jen jak jsem se díval do tvého "bia" na konci stránky
" stříhání a úpravou videí a efektů do nich "
Nenapadlo tě udělat video-verzi těchto článků ?

 
Odpovědět 4. května 14:48
Avatar
Martin Dráb
Redaktor
Avatar
Martin Dráb:

Jestli to dobře chápu, tak klient předpokládá, že stroj je vždy připojen k internetu a že server je vždy dostupný. Nebo jsem si alespoň nevšiml, že by se nějak řešily případy, kdy vypadne spojení či jej nelze navázat (aby tyto celkem běžné události klient přežil). Pro ukázkové účely je to samořezjmě jedno.

Odpovědět  +3 4. května 15:07
2 + 2 = 5 for extremely large values of 2
Avatar
jiri.sada
Redaktor
Avatar
Odpovídá na Martin Dráb
jiri.sada:

To si musí odchytit sám uživatelský kód

 
Odpovědět 4. května 15:56
Avatar
jiri.sada
Redaktor
Avatar
jiri.sada:

Opravenou verzi článku jsem odeslal, ale musí mi ji schválit

 
Odpovědět 4. května 16:11
Avatar
Tomáš Svoboda:

Zatraceně, kde jsi se tohle všechno naučil ? ( myslím tím C# celkově ) Nedoporučil bys o C# knihu ? A co znamená to přetížení funkce a proč :D

Odpovědět 5. května 6:59
Alea iacta est.
Avatar
Ondřej Krsička
Redaktor
Avatar
Odpovídá na Tomáš Svoboda
Ondřej Krsička:

Podívej se na zdejší tutoriál...

 
Odpovědět  +1 5. května 7:09
Avatar
jiri.sada
Redaktor
Avatar
jiri.sada:

Programuju v C# tak od 13ti let a naučil jsem se to všechno z internetu a tím, že jsem prostě stále a stále zkoušel nové věci. Základy jsem se naučil na téhle síti (tehdy ještě nebyly články za peníze) cca za dva týdny a ten zbytek... prostě z internetu, googlu a stack overflow, knížku o C# jsem nikdy neměl :D :D :D

 
Odpovědět 5. května 9:07
Avatar
Odpovídá na Ondřej Krsička
Tomáš Svoboda:

Samozřejmě, že zde jsem již hledal a snad i našel ten správný článek. Jen mít kredity na přečtení celého.
Jak se tomu přetížení říká anglicky ? Google mi pomůže. Dík :)

Odpovědět 5. května 11:16
Alea iacta est.
Avatar
Martin Dráb
Redaktor
Avatar
Odpovědět  +2 5. května 12:48
2 + 2 = 5 for extremely large values of 2
Avatar
Ondřej Krsička
Redaktor
Avatar
Odpovídá na Tomáš Svoboda
Ondřej Krsička:

Jinak placený články o OOP (a hlavně cvičení) doporučuju. Dlouho jsem se to učil jinde, ale až tady jsem to pochopil.

 
Odpovědět  +2 5. května 16:18
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 10 zpráv z 10.