Named Pipe (Server/Client) Problem



  • Hallo,

    ich habe einen NamedPipe Server und Client, deren Verhalten ich nicht ganz verstehe. Leider ist der Quelltext relativ umfangreich und deshalb poste ich nur relevante Ausschnitte. Die meisten Win32 Funktionsaufrufe sind in ähnliche Namen gekapselt und machen etwas mehr als die WinAPI (zB Fehlerbehandlung, etc). Ich erkläre natürlich gern, was welche Funktion macht bzw. poste den Quelltext dazu, aber für die Beschreibung des Problems ist das anfangs IMO nicht notwendig.

    Der Server ist multithreaded und so sieht die run() Methode des Threads aus, der eingehende Verbindungen akzeptiert. Ich habe einen Sleep Aufruf eingebaut, um auf der Client Seite einen ERROR_PIPE_BUSY Fehler zu provozieren und dort korrekt zu behandeln. Allerdings verhagelt mit dieser Sleep-Aufruf den kompletten Server und ich verstehe nicht, warum.

    unsigned int Win32NamedPipeServer::perform_run()
    {
    	Win32SecurityAttributes wsa = win32_make_security_attributes_everyone();
    	string 		PipeName = create_named_pipe_server_name( PipeName_ );
    	Win32Handle Connect( ::CreateEvent( nullptr, TRUE, FALSE, nullptr ) );
    
    	Win32Handle Pipe = ::CreateNamedPipe( PipeName.c_str(),
    													  PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
    													  PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
    													  PIPE_UNLIMITED_INSTANCES,
    													  32768,
    													  32768,
    													  0,
    													  wsa );
    	// wenn der erste Endpoint nicht erzeugt werden konnte schon hier abbrechen
    	// und den Fehlercode in StartupErrorCode schreiben. Der open()-Aufruf steckt
    	// jetzt noch im WaitForSingleObject und kann den StartupErrorCode überprüfen.
    	bool RunThread 		= static_cast<bool>( Pipe );
    	StartupErrorCode_    = static_cast<bool>( Pipe ) ? 0 : GetLastError();
    	ThreadControlData_.signal_started_event();
    
    	while( RunThread && !ThreadControlData_.stopping_event_signaled() )
    	{
    		::Sleep( 500 ); // Aufruf, um im Client::connect() einen ERROR_PIPE_BUSY zu provozieren
    
    		// Pipe konnte erstellt werden, auf Client warten
    		OVERLAPPED Overlapped = win32_make_overlapped( Connect );
    		unsigned int Result = connect_named_pipe( Pipe, Overlapped );
    		if( Result != ERROR_SUCCESS )
    		{
    			Pipe.reset();
    
    			// Pipe konnte nicht erzeugt werden, kurz CPU abgeben. Solange die
    			// ConnectNamedPipe Aufrufe fehlschlagen wird direkt der nächste
    			// Schleifendurchlauf gestartet und die CPU geht in 100% Auslastung.
    			// Deswegen wird die CPU für 10ms abgegeben
    			SyncWindow_.post_message( PipeWindowMessages::Error, 0, ::GetLastError() );
    			::Sleep( 10 );
    		}
    		else
    		{
    			// darauf warten, dass eins von beiden Events signalisiert wird. Wenn
    			// das 1. Event signalisiert ist (Connect) wurde eine neue Verbindungs-
    			// anfrage gestellt, wenn das 2. Event signalisiert ist wird der Server
    			// beendet.
    TRACE( "Server: Waiting for client" );
    			if( win32_wait_for_one( INFINITE, Connect, ThreadControlData_.stopping_event() ) == Connect )
    			{
    TRACE( "Server: Client connected" );
    				// neuen PipeClient erzeugen
    				PipeClientConnectionPtr_t Connection( new PipeClientConnection( SyncWindow_, Pipe ) );
    				add_connection( Connection );
    
    				// neuen Receive-Endpoint für Pipe erzeugen
    				Pipe = ::CreateNamedPipe( PipeName.c_str(),
    												  PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
    												  PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
    												  PIPE_UNLIMITED_INSTANCES,
    												  32768,
    												  32768,
    												  0,
    												  wsa );
    				if( !Pipe )
    				{
    					SyncWindow_.post_message( PipeWindowMessages::Error, 0, ::GetLastError() );
    				}
    				::ResetEvent( Connect );
    			}
    		}
    	}
    	ThreadControlData_.signal_stopped_event();
    	return 0;
    }
    

    Zeile 7: Erste Instanz einer NamedPipe erzeugen, von der beliebig viele Instanzen erzeugt werden dürfen (PIPE_UNLIMITED_INSTANCES)
    Zeile 22: Abbruchkritieren für den Thread überprüfen
    Zeile 24: Sleep-Aufruf, um auf der Client Seite einen ERROR_PIPE_BUSY Fehler zu provozieren
    Zeile 28: per ConnectNamedPipe im OVERLAPPED Modus auf eingehende Client-Verbindung warten (+Fehlerbehandlung)
    Zeile 47: da ConnectNamedPipe im OVERLAPPED Modus aufgerufen wurde wird hier auf eins von zwei Events gewartet: 1) Client Connect und 2) Thread Stopping Event.
    Zeile 49: das Connect-Event ist signaled und eine Verbindungsanfrage eines Clients wurde festgestellt. Es wird ein neuer Thread für die Behandlung der Pipe erzeugt und anschließend eine neue Instanz der Pipe mit gleichem Namen
    Zeile 67: Connect-Event für neuen Schleifendurchlauf zurücksetzen, weiter bei Zeile 22.

    Der Block Zeile 30-38 wird beim Testen im Debugger nie angesprungen und tritt in dem Fall ein, wenn der Verbindungsaufbau vom Client gestört wird.

    Hier die connect() Methode des Client:

    void Win32NamedPipeClient::connect( const string& Name )
    {
    	if( is_open() )
    	{
    		close();
    	}
    	ReceiveStream_.clear();
    	PipeName_ = Name;
    
    	string PipeName = create_named_pipe_client_name( Name );
    
    	// Solange die Gegenstelle BUSY ist auf eine freie Instanz des Pipe Servers
    	// warten
    	Win32Handle tmpPipe;
    	for( ;; )
    	{
    		tmpPipe = ::CreateFile( PipeName.c_str(), GENERIC_READ|GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr );
    		if( tmpPipe )
    		{
    			// Verbindung ok
    			break;
    		}
    		unsigned int LastError = ::GetLastError();
    TRACE( "Client: GetLastError returned ", LastError );
    
    		if( LastError != ERROR_PIPE_BUSY )
    		{
    			// Anderer Fehler als ERROR_PIPE_BUSY
    			break;
    		}
    		// Sonderfall für ERROR_PIPE_BUSY, auf freie Instanz warten
    		if( !::WaitNamedPipe( PipeName.c_str(), ConnectTimeout_ ) )
    		{
    			// nach Ablauf des Timeouts war keine Instanz verfügbar
    			break;
    		}
    	}
    	if( !tmpPipe )
    	{
    		throw Exception( "Win32NamedPipeClient::open() failed.\n" +
    							  string_to_vcl_string( win32_format_last_error_message() ) );
    	}
    	Win32Handle tmpThread = reinterpret_cast<HANDLE>( _beginthreadex( nullptr, 0, PipeReceiveThread, this, CREATE_SUSPENDED, nullptr ) );
    	if( !tmpThread )
    	{
    		throw Exception( "Win32NamedPipeClient::open() failed.\n" +
    							  string_to_vcl_string( win32_format_last_error_message() ) );
    	}
    	Pipe_ 	= tmpPipe;
    
    	ThreadControlData_.assign_thread( tmpThread );
    	ThreadControlData_.resume_thread();
    	ThreadControlData_.wait_for_started_event( INFINITE );
    }
    

    Zeile 15: Schleife bis Verbindung steht oder in einen Timeout läuft
    Zeile 17: clientseitige Verbindung zur NamedPipe aufbauen
    Zeile 23: Verbindungsaufbau war nicht erfolgreich, Sonderfall für ERROR_PIPE_BUSY behandeln
    Zeile 32: Auf freie Instanz der NamedPipe warten

    Mein Testfall baut in einer Schleife ohne Verzögerung 10 Client Verbindungen auf. Das ::Sleep( 500 ) im Server Quelltext soll dafür sorgen, dass nach jedem Verbindungsaufbau für 0.5sec keine frei Instanz der NamedPipe zur Verfügung steht und der CreateFile[/c Aufruf im Client fehlschlägt. Das funktioniert genau so wie erwartet, [c]LastError in Zeile 23 hat den Wert 231 (ERROR_PIPE_BUSY). ConnectTimeout_ hat den Werte 2000 und WaitNamedPipe soll max. 2sec auf eine frei Instanz warten. Allerdings kehrt der Aufruf nie mit TRUE zurück und WaitNamedPipe läuft immer in den Timeout. Wenn ich das break in Zeile 35 auskommentiere liefert mir jeder CreateFile Aufruf FALSE und GetLastError() 231 zurück, d.h. ich kriege hier nie eine Instanz der NamedPipe.

    Der Server akzeptiert die erste Verbindung und erzeugt auch eine neue Instanz der NamedPipe. Die Ausgaben beim Programmlauf sind
    [Win32NamedServer.cpp]: Server: Waiting for client
    [Win32NamedServer.cpp]: Server: Client connected
    [Win32NamedServer.cpp]: Server: Waiting for client
    Er steht also auf dem win32_wait_for_one() Aufruf, aus dem er allerdings nie zurückkommt ( win32_wait_for_one() ist ein Wrapper um WaitForMultipleObjects ).

    Eigentlich sollte sich Server und Client so verhalten:

    1. Server erzeugt NamedPipe und wartet per ConnectNamedPipe auf eine eingehende Verbindung
      1. Client startet Verbindungsaufbau
        => Server ConnectNamedPipe gelingt (Zeile 47)
        => Server erzeugt neue Instanz (Zeile 55)
        => Server macht neuen Schleifendurchlauf und legt sich 0.5s schlafen
      1. Client startet Verbindungsaufbau
        => Server schläft noch
        => Client CreateFile schlägt fehl und GetLastError liefert 231 zurück (Zeilen 17 + 23)
        => Client ruft WaitNamedPipe mit 2s Timeout auf (Zeile 32)
        => Server wacht auf und erzeugt neue Pipe Instanz
        => Client WaitNamedPipe kehrt mit TRUE zurück
        => Client startet neuen Schleifendurchlauf und verbindet sich mit neuer Instanz

    Aber irgendwie passiert da was ganz anderes, nachdem der Client ein Mal eine Verbindung aufbauen möchte während der Server schläft kehrt der Server später nie wieder aus dem wait_for_one Aufruf zurück. Genausowenig liefert WaitNamedPipe im Client irgendwann mal TRUE zurück und der Client verbindet sich.

    Der Server soll bestehende Verbindungen halten, bis sie vom Client beendet werden. Es ist also nicht so, dass die Verbindungen nur zum Übertragen von Daten geöffnet und direkt nach der Übertragung wieder geschlosssen werden. Falls es relevant sein sollte: Der Client schließt die Verbidung mit CloseHandle , der Server mit DisconnectNamedPipe und anschließendem CloseHandle .

    Hat da jemand ne Idee, was ich falsch mache?


  • Mod

    Hast Du nur einen Client oder mehrere?
    Das erkenne ich hier nicht.

    Wird Win32NamedPipeClient::connect endlos oft aufgerufen?
    Und was sagt denn der Client, wo steht der?
    Du kannst ja beide Prozesse gleichzeitig debuggen.



  • Hallo Martin,

    ja, es gibt mehrere Clients, im Testfall werden in einer Schleife 10 Verbindungen aufgebaut. Der Testfall besteht aus zwei separaten Anwendungen, eine für den Server und eine für die Clients.

    Ich habe sowohl Client als auch Server debuggt und den ersten Client-Verbindungsversuch erst dann gestartet, wenn der Server in Zeile 47 steht und auf Vervollständigung der Verbindung wartet. Dann klappt der Verbindungsaufbau auch, der Server erzeugt eine neue Pipe Instanz, startet einen zweiten Schleifendurchlauf und legt sich in Zeile 24 für 500ms schlafen.
    Der zweite Client findet jetzt keine freie Pipeinstanz und der CreateFile Aufruf in Zeile 17 schlägt fehl. GetLastError() in Zeile 23 liefert den Fehlercode 231 (ERROR_PIPE_BUSY) zurück und anschließend wird in Zeile 32 WaitNamedPipe aufgerufen mit einem Timeout von 2s aufgerufen. Vor Ablauf des WaitNamedPipe Timeouts wacht der Server wieder auf und wartet in Zeile 47 auf eine neue Clientanfrage.

    Soweit läuft alles wie erwartet, was mich jetzt nur verwundert ist:
    Im Client läuft WaitNamedPipe immer in einen Timeout, auch wenn der Server in Zeile 47 auf Verbindungen wartet. Kein anderer Client kann eine Verbindung aufbauen, selbst dann nicht, wenn ich das Testprogramm beende und neu starte. Alle folgenden Verbindungsaufbauversuche des Client schlagen fehl. CreateFile gelingt nie und GetLastError in Zeile 23 liefert ERROR_PIPE_BUSY zurück. Der anschließende WaitNamedPipe Aufruf läuft immer in einen Timeout und bricht damit den Versuch ab.

    Der Server steht im zweiten Schleifendurchlauf nach dem Sleep in Zeile 47 und wartet in win32_wait_for_one auf eingehende Verbindungen. Weil die Verbindungsaufbauversuche der Clients aber alle fehlschlagen kommt er aus diesem Aufruf nie wieder zurück.


  • Mod

    Ich habe mir das mal bei mir angesehen.

    Ich prüfe den Rückgabewert von WaitNamedPipe gar nicht. 😉
    Ich warte mit der Funktion ca. eine Sekunden und gehe gleich wieder und Versuche den CreateFile.
    Aktuell ist der Code so, in allen meinen 4 Diensten, die named Pipes benutzen. Wahrscheinlich CPP (Copy&Paste Programming).

    Nach der Doku, kann ConnectNamedPipes 0 zurückgegeben, aber dennoch einen Connect herbeiführen. In diesem Fall ist aber der Event NICHT gesetzt, weil Overlapped I/O nicht eingetreten ist.

    MSDN:

    If the operation is asynchronous, ConnectNamedPipe returns immediately. If the operation is still pending, the return value is zero and GetLastError returns ERROR_IO_PENDING. (You can use the HasOverlappedIoCompleted macro to determine when the operation has finished.) If the function fails, the return value is zero and GetLastError returns a value other than ERROR_IO_PENDING or ERROR_PIPE_CONNECTED.

    If a client connects before the function is called, the function returns zero and GetLastError returns ERROR_PIPE_CONNECTED. This can happen if a client connects in the interval between the call to CreateNamedPipe and the call to ConnectNamedPipe. In this situation, there is a good connection between client and server, even though the function returns zero.

    In meinem Code. sieht das in etwa so aus:

    if (::ConnectNamedPipe(hPipe,&ovl)==0)
    	{
    		// check what happens
    		dwError = ::GetLastError();
    		if (dwError==ERROR_IO_PENDING)
    		// The overlapped connection in progress. 
    			;
    		else if (dwError==ERROR_PIPE_CONNECTED)
    			// Client is already connected, so signal an event. 
    			::SetEvent(ovl.hEvent);
    		else
    			// Other unknown error
    			break;
    	}
    	else
    	{
    		dwError = ::GetLastError();
    		break;
    	}
    

    HTH

    PS: Deine Schreibweise des Codes und das Du alles kapselst und man die Win32 API nicht sieht ist nicht sonderlich hilfreich finde ich.
    Ich finde es extrem irritierend.



  • Danke,

    den Fall ohne Overlapped I/O gucke ich mir mal an.
    Ich kapsel halt nur die Sachen weg oder bau mir Funktionen dafür, wenn ich WinAPI Sachen für umständlich halte.

    Zum Beispiel

    OVERLAPPED make_overlapped( HANDLE Event )
    {
       OVERLAPPED retVal = { 0 };
       retVal.hEvent = Event;
       return retVal;
    }
    
    void f1()
    {
       HANDLE Pipe = ::CreateNamedPipe( .... );
       HANDLE Event = ::CreateEvent( nullptr, TRUE, FALSE, nullptr );
       OVERLAPPED ovl;
       ::SecurezeroMemory( &ovl, sizeof( ovl ) );
       ovl.hEvent = Event:
       ::ConnectNamedPipe( Pipe, &ovl );
       ::CloseHandle( Pipe );
       ::CloseHandle( Event );
    }
    
    void f2()
    {
       Win32Handle Pipe  = ::CreateNamedPipe( ... );
       Win32Handle Event = ::CreateEvent( nullptr, TRUE, FALSE, nullptr );
       OVERLAPPED ovl = win32_make_overlapped( Event );
       ::ConnectNamedPipe( Pipe, &ovl );
    }
    

    Ich finde f2 übersichtlicher, da es auf die Funktion reduziert ist und keine "störenden" mehrzeiligen Initialisierungen mitbringt. Insbesondere ewige GetLastError() Fehlerüberprüfungskaskaden finde ich unübersichtlich.
    Außerdem erlauben die Kapselungen RAII, was mit C nicht so geht. Alles in allem eine Frage des persönlichen Stils und Gewohnheit, würde ich sagen...


Anmelden zum Antworten