Š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 RemoteCertificateChainErrors, pokud bychom první daný certifikát nenainstalovali do systému a RemoteCertificateNameMismatch, 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 AuthenticateAsClient, která, jak název napovídá, nás autentizuje jako klienta do spojení. Na serveru by se tou dobou měla zavolat metoda AuthenticateAsServer. 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žením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 97x (8.92 kB)
Aplikace je včetně zdrojových kódů v jazyce C#