NOVINKA – Víkendový online kurz Software tester, který tě posune dál. Zjisti, jak na to!
NOVINKA - Online rekvalifikační kurz Java programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.

Š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.


 

Všechny články v sekci
Špiónská aplikace v C# .NET
Článek pro vás napsal jiri.sada
Avatar
Uživatelské hodnocení:
6 hlasů
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