Getting started with writing a FastCGI Client application (FastCGI Test Client)

I worked on the FastCGI for IIS 6.0 RTM release a while ago and during that time, I worked on developing a FastCGI Client application, in order to test FastCGI at a protocol level. I just decided to blog about it, just in case any of you are interested in getting started with writing a FastCGI client application from scratch. 

You can download the sample client application along with the source code from here. This application can work both over TCP and Named Pipes.

The FastCGI Client application is a generic application that will be used to communicate with the Web Server (FastCGI handler) through FastCGI protocol either over Named Pipes or TCP.  The FastCGI test client communicates in the form of messages with the FastCGI Server.  All these messages are in the form of STDIN, STDOUT and STDERR. The Web Server (FastCGI Handler) creates a Named Pipe or a TCP stream for communication and duplicates these handles onto the stdout, stdin and stderr (parent processes can override the stdin, stdout and stderr of child). By this, the application just has to write it’s protocol messages to stdin, stdout and stderr and the details of the TCP/Named Pipe communication is abstracted out.

As a first step, you can determine if the application was invoked using CGI or FastCGI by calling getpeername on the STD_INPUT_HANDLE.  You should get a socket error if the client was invoked using a FastCGI application. Depending on whether your server uses TCP or Named Pipes, your WSAGetLastError() could return WASENOTCONN or WSAENOTSOCK respectively.

bool IsApplicationInvokedUsingFastCGI( HANDLE s )
{
        // Determine if this application was invoked using CGI or FastCGI
        struct sockaddr nameOfConnectedSocket;
        int nameLenOfConnectedSocket;
        HRESULT hr = S_OK;
        int errs; 

        errs = getpeername( (SOCKET)s, &nameOfConnectedSocket, &nameLenOfConnectedSocket );        
        if ( errs == SOCKET_ERROR )
        {
            hr = HRESULT_FROM_WIN32( WSAGetLastError() );
            if(  hr == HRESULT_FROM_WIN32( WSAENOTCONN ) )
            {
                    //LogMessage( TRACE_MESSAGE, "Application was invoked using FastCGI over TCP, got a WSAENOTCONN on a call to getpeername()" );                    
                    return TRUE;
            }
            else            
            if( hr == HRESULT_FROM_WIN32( WSAENOTSOCK ) ) 
            {                    
                    //LogMessage( TRACE_MESSAGE, "Application was invoked using FastCGI over Named Pipe, got a WSAENOTSOCK on a call to getpeername()" );                    
                    return TRUE;
            }            
            else            
            {                                        
                    return FALSE;            
            }
        }        
        else
        {
            // Quit, this app is not compatible with FastCGI
            //LogMessage( TRACE_MESSAGE, "Application was invoked using CGI, getpeername() successful" );
            return FALSE;
        }        
}
 

Next step to writing a client is to try and determine if the communication with the Web Server should happen over Named Pipes or TCP. Do an accept() socket call on the STDIN and if it succeeds, use TCP, else use Named Pipes. 

bool IsNamedPipeOrSocket( HANDLE hStdin, FASTCGI_PROTOCOL * useNamedPipeOrSocket )
{
        HRESULT hr = S_OK; 

        try
        {
            acceptSocket = accept( (SOCKET)hStdin, NULL, NULL ); 

            if( acceptSocket == INVALID_SOCKET )
            {
                (*useNamedPipeOrSocket) = FcgiConfigProtocolNamedPipe;                
            }
            else
            {
                (*useNamedPipeOrSocket) = FcgiConfigProtocolTcp;
            }            
        }
        catch(...)
        {
            //LogMessage( TRACE_EXCEPTION, "Exception is IsNamedPipeOrSocket() function" );
            return FALSE;
        }
        return TRUE;
}
 

Then Initialize the transport connection and receive and send messages from and to the FastCGI Server respectively. For more information on this exchange please refer the FastCGI protocol specification.

// Initialize the transport connection based on whether the Server is using Named Pipes or TCP.
if( namePipeOrSocket == FcgiConfigProtocolNamedPipe )
{
        pTransport = new TRANSPORT_NAMEDPIPE();
}
else
{
        pTransport = new TRANSPORT_TCP();
}
class TRANSPORT
{
    public: 

        TRANSPORT()
        {}
        ~TRANSPORT()
        {} 

        HRESULT Send( PBYTE pBuffer, DWORD dwSize)
        {
            return SendReceive( TRUE, pBuffer, dwSize );
        }
        HRESULT Receive( PBYTE pBuffer, DWORD dwSize)
        {
            return SendReceive( FALSE, pBuffer, dwSize );
        } 

    protected: 

        virtual HRESULT SendReceive( bool fSend, PBYTE pBuffer, DWORD dwSize ) = 0;
}; 
class TRANSPORT_NAMEDPIPE : public TRANSPORT
{
}
class TRANSPORT_TCP : public TRANSPORT
{
}
class TRANSPORT_NAMEDPIPE : public TRANSPORT
{
    public: 

        TRANSPORT_NAMEDPIPE()
        {}
        ~TRANSPORT_NAMEDPIPE()
        {}
    protected:            

        HRESULT SendReceive( bool fSend, PBYTE pBuffer, DWORD dwSize )
        {
            BOOL fRet  = FALSE;
            DWORD dwLastError = 0;
            DWORD nNumberOfBytesRead = 0; 

            if( fSend == TRUE )
            {        
                fRet = WriteFile( hStdin, pBuffer, dwSize, &nNumberOfBytesRead, NULL );
            }
            else
            {
                fRet = ReadFile( hStdin, pBuffer, dwSize, &nNumberOfBytesRead, NULL );
            } 

            if(!fRet)
            {
                dwLastError = GetLastError();
            } 

            return HRESULT_FROM_WIN32( dwLastError );
        }
}; 

class TRANSPORT_TCP : public TRANSPORT
{
    public: 

        TRANSPORT_TCP()
        {}
        ~TRANSPORT_TCP()
        {} 

    protected: 

        HRESULT SendReceive( bool fSend, PBYTE pBuffer, DWORD dwSize )
        {
            int iRet = 0;
            WSABUF winsockBuffer;
            DWORD   dwBytesTransferred  = 0;
            DWORD   dwFlags             = 0;
            DWORD   dwLastError            = 0; 

            winsockBuffer.len = dwSize;
            winsockBuffer.buf = (char *)pBuffer; 

            if( fSend == TRUE )
            {
                iRet = WSASend( acceptSocket, &winsockBuffer, 1, &dwBytesTransferred, 0, NULL, NULL ); 
            }
            else
            {
                iRet = WSARecv( acceptSocket, &winsockBuffer, 1, &dwBytesTransferred, &dwFlags, NULL, NULL );
            } 

            if( SOCKET_ERROR == iRet )
            {
                dwLastError = WSAGetLastError();
            } 

            return HRESULT_FROM_WIN32( dwLastError );
        }
};
 

A while ago, Rick who was the developer of the FastCGI core module, published a fake fastcgi server that works only over Named Pipes. To quickly see the working of this client application, you can run it with the fake server as below. The highlighted messages are returned by the client application TestClient.exe.

D:\fake>fakefcgi.exe d:\php\hello.php TestClient.exe
Fake FastCGI web server
FCGI_PARAMS sent
FCGI_STDIN sent
Launching receive loop
FCGI_STDOUT: FCGIClient : Obtained FCGI_BEGIN_REQUEST message from Server
FCGI_STDOUT: FCGIClient : Obtained FCGI_PARAMS from Server
FCGI_STDOUT: FCGIClient : Obtained FCGI_STDIN from Server
FCGI_STDOUT: FCGIClient : Sending FCGI_END_REQUEST to Server
FCGI_END_REQUEST received
killing app
FastCGI process exited with 0

7 Comments

  • great post! I know there have been several people looking for this. Welcome to blogosphere!

  • Thanks, FastCGI is Great!

  • Nice blog...

  • Nice post! Thanks! http://www.iadjustablebeds.com

  • Do you think this part is all okay?

    || catch(...)
    {
    //LogMessage( TRACE_EXCEPTION, "Exception is IsNamedPipeOrSocket() function" );
    return FALSE;
    }
    return TRUE; ||

  • have a great working

    http://www.oyunoynatr.org/pet-rescue-saga-oyna.html

    Pet Rescue Saga

  • Hi,

    According to FastCGI specification http://www.fastcgi.com/drupal/node/6?q=node/22 application can handle multiple requests over one single transport connection (simple speaking... per one process)

    “3.3 (…)
    All data that flows over the transport connection is carried in FastCGI records. FastCGI records accomplish two things. First, records multiplex the transport connection between several independent FastCGI requests. This multiplexing supports applications that are able to process concurrent requests using event-driven or multi-threaded programming techniques. Second, records provide several independent data streams in each direction within a single request.
    (...)”

    Are FastCGI modules for IIS (http://www.iis.net/downloads/microsoft/fastcgi-for-iis) able to handle FastCGI application that way ?

    I've checked several configuration on *nix environments and each of them can handle requests that way... (one application process can handle concurrent multiple requests so it's not necessary to spawn new process to handle concurrent request)

    - Apache + mod_fastcgi
    - Nginx (has native fastcgi support)
    - lighttpd + spawn_fastcgi

    Any help appreciated.

    /Adam

Comments have been disabled for this content.