Přidej si svou IT školu do profilu a najdi spolužáky zde na síti :)

Diskuze: Async TCP server - vyletí využití CPU 60% po odpojení clienta

C# .NET .NET (C# a Visual Basic) Async TCP server - vyletí využití CPU 60% po odpojení clienta American English version English version

Aktivity (3)
Avatar
Elisse
Člen
Avatar
Elisse:10. listopadu 15:53

Zdravíčko, mám TcpListener využivající asynchroní await async metody pro komunikaci s klienty.

Když ale na strašně klienta zavolám TcpClient.close(), tak server nadále funguje, ale vyletí mu využití CPU na 50 - 60%

Na jiném fóru mi bylo řečeno, že nemám používat pro async metody return void, ale fakt mě nenapadá, proč by to způsobovalo to co říkám.

Netuší někdo prosím co s tím?

Třída pro server:

class Server
{
    private TcpListener server;

    public Server(string hostname, int port = 25000)
    {
        server = new TcpListener(IPAddress.Parse(hostname), port);
    }

    public void ServerStart()
    {
        server.Start();
        WaitForClientConnect();
    }

    private async void WaitForClientConnect()
    {
        TcpClient client = await server.AcceptTcpClientAsync();
        Console.WriteLine("The async connection created for: " + ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString());

        OnClientConnect(client);
    }

    private void OnClientConnect(TcpClient client)
    {
        ClientLowAPI clientReq = new ClientLowAPI(client);

        WaitForClientConnect();
    }
}

Kód pro zachycený jednotlivých klientů:

class ClientLowAPI
{
    private TcpClient client;
    private NetworkStream stream;

    public ClientLowAPI(TcpClient clientConnected)
    {
        client = clientConnected;
        stream = client.GetStream();
        WaitForHeader();
    }

    private async void WaitForHeader()
    {
        byte[] buffer = new byte[4];

        int bytesRead = 0;
        while (bytesRead < 4)
        {
            try
            {
                bytesRead += await stream.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead);
            }
            catch
            {
                stream.Close();
                client.Close();
                return;
            }
        }

        WaitForData(FourBytesToInt(buffer));
    }

    private async void WaitForData(int length)
    {
        byte[] buffer = new byte[length];

        int bytesRead = 0;
        while (bytesRead < length)
        {
            try
            {
                bytesRead += await stream.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead);
            }
            catch
            {
                stream.Close();
                client.Close();
                return;
            }
        }

        ExecuteMessage(buffer);
    }

    private void ExecuteMessage(byte[] binaryData)
    {
        // Do something with message
        WaitForHeader();
    }

    public async void SendMessage(byte[] message)
    {
        byte[] buffer = new byte[message.Length + 4];
        byte[] length = IntToFourBytes(message.Length);

        length.CopyTo(buffer, 0);
        message.CopyTo(buffer, 4);

        try
        {
            await stream.WriteAsync(buffer, 0, buffer.Length);
        }
        catch
        {
            stream.Close();
            client.Close();
            return;
        }
    }

    private int FourBytesToInt(byte[] array)
    {
        int res = 0;
        res += array[0] * 256 * 256 * 256;
        res += array[1] * 256 * 256;
        res += array[2] * 256;
        res += array[3];
        return res;
    }

    private byte[] IntToFourBytes(int intValue)
    {
        byte[] array = new byte[4];
        array[0] = (byte)(intValue >> 24);
        array[1] = (byte)(intValue >> 16);
        array[2] = (byte)(intValue >> 8);
        array[3] = (byte)intValue;
        return array;
    }
}
Editováno 10. listopadu 15:54
 
Odpovědět 10. listopadu 15:53
Avatar
Posix
Člen
Avatar
Odpovídá na Elisse
Posix:10. listopadu 17:43

Problém je ten, že když zavřeš spojení na klientovi, tak na serveru ti to při pokusu číst data vrací 0 Při čtení hlavičky máš podmínku bytes < 4, který je tedy pořád true. Takže se pořád dokola snažíš číst a nedostaneš se z while cyklu -> vysoké využití CPU.

Dále tam máš nepřímé rekurze (WaitForClien­tConnect->OnClientConnect a WaitForHeader->WaitForData->ExecuteMessage). Docela divný návrh. Pokud by server běžel dlouho a připojovalo se hodně lidí, tak by ti časem vyskočila vyjmka StackOverflowEx­ception-

Ti druzí mají s void a async pravdu. Protože tam máš hned na začátku pouze WaitForClientCon­nect, tak hned po zavolání metody se pokračuje dál, tj. nečeká se na připojení. Když změníš async void na async Task a při volání použiješ await, tak se bude čekat. Takže buď používej správně async/await nebo nepoužívej vůbec async metody.

Malý tip na konec. Zkus zvážit použití BinaryReaderu a BinaryWriteru, ať se nemusíš starat o práci s bytama.

Editováno 10. listopadu 17:44
Akceptované řešení
+20 Zkušeností
+1 bodů
Řešení problému
Nahoru Odpovědět 10. listopadu 17:43
Proč to dělat jednoduše, když to jde složitě.
Avatar
Elisse
Člen
Avatar
Odpovídá na Posix
Elisse:10. listopadu 17:47

Když bych nepoužíval async/await, tak asi vytvářím pro každého uživatele nový Task za což jsem dostal vyhubováno na jiném fórku ještě víc :)

Jinak k tomu prvnímu mám pocit, že když ten connection uzavřu, tak by to mělo skočit při pokusu o čtení do vyjímky, ne? Ale určitě to otestuju až se k tomu dostanu.

A s tou rekurzí si bohužel nevím rady jak ten návrh udělat jinak :( jsem bohužel amatér zatím

 
Nahoru Odpovědět 10. listopadu 17:47
Avatar
Posix
Člen
Avatar
Odpovídá na Elisse
Posix:10. listopadu 17:50

Tak jednoduše nepoužívej AcceptTcpClien­tAsync, ReadAsync a WriteAsync. Použij sync metody.

Při pokusu o čtení ti nevyskočí vyjímka. Viz. dokumentace

Nahoru Odpovědět 10. listopadu 17:50
Proč to dělat jednoduše, když to jde složitě.
Avatar
Elisse
Člen
Avatar
Odpovídá na Posix
Elisse:10. listopadu 17:58

Vyskočí když je socket zavřený, právě otestováno

IOException

když nepoužiju async metody, tak nemám ponětí jak to udělat rozumně aby to dokázalo obsluhovat více klientů a do toho šlo zároveň psát do konzole.

 
Nahoru Odpovědět 10. listopadu 17:58
Avatar
Elisse
Člen
Avatar
Odpovídá na Posix
Elisse:10. listopadu 18:04

Ha tak už vím jak to je, když klienta na tvrdo killnu, tak to vyhodí exception, ale když na klientovi zavolám TcpClient.close() tak ji to nevyhodí.

 
Nahoru Odpovědět 10. listopadu 18:04
Avatar
Elisse
Člen
Avatar
Odpovídá na Posix
Elisse:10. listopadu 18:27

To čtení když vrací nulu si ošetřím to je tedy v pořádku, ale rád bych se vrátil celkově k tomu špatnému návrhu, zda by jsi mi ty nebo někdo mohl pomoci s tím jak to udělat do přijatelné varianty.

Jinak část kódu

private async void WaitForClientConnect()
{
    TcpClient client = await server.AcceptTcpClientAsync();
    Console.WriteLine("The async connection was created for: " + ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString());

    OnClientConnect(client);
}

čeká dokud se někdo nepřipojí :-O

Editováno 10. listopadu 18:28
 
Nahoru Odpovědět 10. listopadu 18:27
Avatar
Posix
Člen
Avatar
Odpovídá na Elisse
Posix:10. listopadu 20:07

Pokud nanapíšeše await před volání WaitForClientCon­nect(), tak opravdu nečeká. Měl bys to nastudovat.

Nahoru Odpovědět 10. listopadu 20:07
Proč to dělat jednoduše, když to jde složitě.
Avatar
Elisse
Člen
Avatar
Odpovídá na Posix
Elisse:10. listopadu 20:38

A proč se tedy:

The async connection was created for...

vypíše až po tom co se někdo připojí?

Editováno 10. listopadu 20:38
 
Nahoru Odpovědět 10. listopadu 20:38
Avatar
Posix
Člen
Avatar
Odpovídá na Elisse
Posix:10. listopadu 20:44

Zkus přidat výpis do těchto metod:

public void ServerStart()
    {
        server.Start();
        WaitForClientConnect();
        Console.WriteLine(".....ServerStart");
    }



    private void OnClientConnect(TcpClient client)
    {
        ClientLowAPI clientReq = new ClientLowAPI(client);

        WaitForClientConnect();
        Console.WriteLine(".....OnClientConnect");
    }

Uvidíš, že ten výpis bude dřív, než se někdo stihne připojit, tedy bude to před "The async connection created for....".

Vedlejší účinek tvého návrhu je to, že to vypadá, že to funguje :D

Editováno 10. listopadu 20:45
Nahoru Odpovědět 10. listopadu 20:44
Proč to dělat jednoduše, když to jde složitě.
Avatar
Elisse
Člen
Avatar
Odpovídá na Posix
Elisse:13. listopadu 8:07

Omlouvám se o víkendu "nepracuju" a nejsem většinou ani na PC :)

No tak situace, kterou popisuješ bohužel nenastala :-O viz. screenshot.

Každopádně pokud budeš mít čas a náladu budu velmi rád, když mi to pomůžeš dostat do rozumného stavu a pochopit ten async způsob, přepsat do to sync řešení jen protože to neumím bych nerad :)

 
Nahoru Odpovědět 13. listopadu 8:07
Avatar
Roman
Člen
Avatar
Roman:13. listopadu 19:36

Mozna bude lepsi si vykopirovat kod z MS a poradne si odkrokovat a pochopit funkci async volani

 
Nahoru Odpovědět 13. listopadu 19:36
Avatar
Elisse
Člen
Avatar
Odpovídá na Roman
Elisse:14. listopadu 8:30

No tak pokud vím, tak na awaitu se kód uspí a program pokračuje před async metodou a až se vrátí odpověď z awaitnuté metody tak se vyvolá na tom místě ne?

Kdybych to plně chápal z nějakého examplu, tak tu nezakládám thready na fórku :) Nejsem ten co se ptá ještě než napíše řádek kódu nebo googluje :)

 
Nahoru Odpovědět 14. listopadu 8:30
Avatar
Taskkill
Šéfredaktor
Avatar
Odpovídá na Elisse
Taskkill:14. listopadu 9:05

Async/await nemá s uspáváním nic společného. Ať už je to c# nebo JavaScript.
Představ si to tak, že když dáš před volání async funkce await, tak se počká na její vyhodnocení a až pak se pokračuje sekvenčně dolů.
Ještě lepší představa by byla, kdybys náhodou znal a chápal koncept promise. Async await je v podstatě sugar pro promisy.
Každopádně představovat si to jako uspání je špatně, takhle to nefunguje.

 
Nahoru Odpovědět 14. listopadu 9:05
Avatar
Elisse
Člen
Avatar
Odpovídá na Taskkill
Elisse:14. listopadu 9:12

Promiň špatná terminologie, uspí jsem myslel jako počká, nicméně to hlavní je ,ale to ,že pokračuje kód před tou async metodou, ne?

 
Nahoru Odpovědět 14. listopadu 9:12
Avatar
Marian Benčat
Redaktor
Avatar
Odpovídá na Taskkill
Marian Benčat:14. listopadu 9:27

"Představ si to tak, že když dáš před volání async funkce await, tak se počká na její vyhodnocení a až pak se pokračuje sekvenčně dolů." - todle neni tak uplne 100% pravda. Na nic se nečeká. Právě, že kdyby se čekalo, tak je to k ničemu (a to hlavni vlakno ti vytuhne). Samozřejmě záleží hodně na configure await. (U STA i s await muze dispatch UI thread čekat, pokud je to špatně nastavené).Už z toho důvodu, to není stejné jako promise,..

Nahoru Odpovědět 14. listopadu 9:27
In Smalltalk, everything is an object, In Clojure, everything is a list, In Javascript, everything is fucking mistake
Avatar
Taskkill
Šéfredaktor
Avatar
Odpovídá na Marian Benčat
Taskkill:14. listopadu 10:06

Jen jsem se snažil přiblížit z nějaký běžný stránky věci. Vím, že to není takhle, ale vždycky jsem to tak viděl lidí vysvětlovat. Musím být přesnější, já vím.

Editováno 14. listopadu 10:07
 
Nahoru Odpovědět  +1 14. listopadu 10:06
Avatar
Taskkill
Šéfredaktor
Avatar
Odpovídá na Elisse
Taskkill:14. listopadu 10:08

Tomhle úplně nerozumím, jak může pokračovat něco před? To před, už se provedlo ne?

 
Nahoru Odpovědět 14. listopadu 10:08
Avatar
Elisse
Člen
Avatar
Odpovídá na Taskkill
Elisse:14. listopadu 10:14

pseudo:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("1");
        AsyncMethod();
        Console.WriteLine("2");

    }

    public static async void AsyncMethod()
    {
        await MyMethod();
    }
}

Výstup bude:
1
2

a po returnu z awaitnuté metody:

2

Takto jsem to myslel.

Editováno 14. listopadu 10:15
 
Nahoru Odpovědět 14. listopadu 10:14
Avatar
Taskkill
Šéfredaktor
Avatar
Odpovídá na Elisse
Taskkill:14. listopadu 10:21

Tohle ale je úplně jinej případ.
Já mluvím o tomhle pseudokódu:

class program
  main()
    asyncFoo()

async asyncFoo()
  print "první"
  await asyncBar()
  print "poslední"

async asyncBar()
  print "druhý"

Protože tvůj kód sice awaituje, ale jako příklad použití je to zvláštní. Možná to tak potřebuješ, to nezpochybňuju.

A z tvého kódu nevidím, jestli se vypíše ještě jedna 2ka na konci.

Editováno 14. listopadu 10:22
 
Nahoru Odpovědět 14. listopadu 10:21
Avatar
Elisse
Člen
Avatar
Odpovídá na Taskkill
Elisse:14. listopadu 10:31

Už se v tom ztrácím, uvedomu trošku v obraz co vlastně od svého kódu očekávám.

Pustím program.

Vytvoří se server, který poslouchá pro nové klienty asynchroně, protože potřebuji aby se tok programu dostal dále k ovládací konzoli serveru.

při vytvoření z vyvolaného returnu z await nové spojení, jakož to smyčku pro nového klienta, kde se už komunikuje tam a zpátky stále dokola.

Zkusím uvést obrázek, který snad ušetří mnoho otázek a nedorozumnění :)

Editováno 14. listopadu 10:32
 
Nahoru Odpovědět 14. listopadu 10:31
Avatar
Elisse
Člen
Avatar
Odpovídá na Elisse
Elisse:14. listopadu 10:33

Očividně nejde v úpravě příspěvku změnit obrázek, přidávám upravený (jedna šipka na víc)

 
Nahoru Odpovědět 14. listopadu 10:33
Avatar
Elisse
Člen
Avatar
Odpovídá na Posix
Elisse:14. listopadu 12:21

No zkusil jsem to změnit a vytvořit něco takového? Je to lepší směr nebo stále úplně špatně?
Hlavně jsem se chtěl zbavit té rekurze jak jsi říkal.

class ClientLowAPI
    {
        private TcpClient client;
        private NetworkStream stream;
        private string ip;

        public ClientLowAPI(TcpClient clientConnected)
        {
            client = clientConnected;
            stream = client.GetStream();
            ip = string.Copy(((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString());

            Listening();
        }

        private async void Listening()
        {
            while (true)
            {
                try
                {
                    int length = await GetHeader();
                    byte[] message = await GetData(length);
                    byte[] asnwerMessage = ExecuteMessage(message);
                    SendMessage(asnwerMessage);
                }
                catch
                {
                    stream.Close();
                    client.Close();
                    Console.WriteLine("The async connection was closed for: " + ip + " (Username: " + HighAPI.GetUsername() + ")");
                    return;
                }

            }
        }

        private async Task<int> GetHeader()
        {
            byte[] buffer = new byte[4];

            int bytesRead = 0;
            while (bytesRead < 4)
            {
                bytesRead += await stream.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead);
                if (bytesRead == 0)
                {
                    throw new System.IO.IOException();
                }
            }

            return FourBytesToInt(buffer);
        }

        private async Task<byte[]> GetData(int length)
        {
            byte[] buffer = new byte[length];

            int bytesRead = 0;
            while (bytesRead < length)
            {
                bytesRead += await stream.ReadAsync(buffer, bytesRead, buffer.Length - bytesRead);
                if (bytesRead == 0)
                {
                    throw new System.IO.IOException();
                }
            }

            return buffer;
        }

        private byte[] ExecuteMessage(byte[] binaryData)
        {
            // Do something with message
            return answer
        }

        public async void SendMessage(byte[] message)
        {
            byte[] buffer = new byte[message.Length + 4];
            byte[] length = IntToFourBytes(message.Length);

            length.CopyTo(buffer, 0);
            message.CopyTo(buffer, 4);

            await stream.WriteAsync(buffer, 0, buffer.Length);
        }

        private int FourBytesToInt(byte[] array)
        {
            int res = 0;
            res += array[0] * 256 * 256 * 256;
            res += array[1] * 256 * 256;
            res += array[2] * 256;
            res += array[3];
            return res;
        }

        private byte[] IntToFourBytes(int intValue)
        {
            byte[] array = new byte[4];
            array[0] = (byte)(intValue >> 24);
            array[1] = (byte)(intValue >> 16);
            array[2] = (byte)(intValue >> 8);
            array[3] = (byte)intValue;
            return array;
        }
    }
Editováno 14. listopadu 12:22
 
Nahoru Odpovědět 14. listopadu 12:21
Avatar
Posix
Člen
Avatar
Odpovídá na Elisse
Posix:14. listopadu 20:24

Radši založ nové vlákno s konkrétní otázkou. Problém s využitím CPU se zdá být vyřešený.

Mimochodem to while (true) v constructoru ti dává smysl? Takto nikdy nedojde k vytvoření...

Nahoru Odpovědět 14. listopadu 20:24
Proč to dělat jednoduše, když to jde složitě.
Avatar
Marian Benčat
Redaktor
Avatar
Odpovídá na Taskkill
Marian Benčat:14. listopadu 23:00

Ty mas novou profilovku? Ty jo, sluší.

Nahoru Odpovědět 14. listopadu 23:00
In Smalltalk, everything is an object, In Clojure, everything is a list, In Javascript, everything is fucking mistake
Avatar
Taskkill
Šéfredaktor
Avatar
Taskkill:15. listopadu 10:31

Díky :-D :-D skoro takhle vypadám i real life.

 
Nahoru Odpovědět 15. listopadu 10:31
Avatar
Marian Benčat
Redaktor
Avatar
Odpovídá na Taskkill
Marian Benčat:15. listopadu 16:17

Jen míň barevnej, že? A to nejsem rasista :D

Nahoru Odpovědět 15. listopadu 16:17
In Smalltalk, everything is an object, In Clojure, everything is a list, In Javascript, everything is fucking mistake
Avatar
Taskkill
Šéfredaktor
Avatar
Odpovídá na Marian Benčat
Taskkill:15. listopadu 17:54

Ja jsem takhle bledej. :-D Hlavne ma jiny vlasy.

 
Nahoru Odpovědět 15. listopadu 17:54
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 28 zpráv z 28.