The C++ framework for developing highly scalable, high performance servers on Windows platforms.

Example Servers - Echo Server CLR

This example uses the JetByte CLR Hosting Library to host the .Net CLR within the server and passes connection events to .Net code. It allows you to write part of your server in C++ and part of it in any .Net language. The basic structure is similar to the Basic Echo Server example and you should go and read about that first and have a good understanding of how everything fits together. This document will only cover the differences between the Basic Echo Server example and this example.

This example requires the "CLR Hosting" licensing option of The Server Framework and it requires libraries that only ship with that option (see here for licensing options). You can always download the latest version of this example from here; and although you will need the correct libraries to be able to build it you can look at the example code and see how it works and perhaps get ideas from it. A compiled, unicode release, build of this example is available on request if you require it for performance analysis of the framework.

The ServerMain.cpp file in this example includes the construction of a CManagedHost object. This takes an assembly name and a type as construction parameters and these determine the .Net Assembly that is loaded to provide Application Domain management functionality and also the business logic functinality. We start the managed host, which loads the CLR and sets up the Application Domain manager and then we connect the managed host to two instances of our server object.

             const _tstring assembly = _T("ManagedHost, Version=1.0.0.0, PublicKeyToken=24e328fd91cb8ee6");
             const _tstring type = _T("EchoServerCLR.ManagedHost");

             const _tstring welcomeMessage = commandLine.NoWelcomeMessage() ? _T("") : _T("Welcome to echo server\r\n");

             CManagedHost managedHost(
                socketAllocator,
                welcomeMessage,
                assembly,
                type);

             managedHost.Start();

             const CFullAddress address1(
                commandLine.Server(),
                commandLine.Port());

             CSocketServer server1(
                managedHost,
                false,
                address1,
                listenBacklog,
                pool,
                socketAllocator,
                bufferAllocator,
                connectionLimiter);

             const CFullAddress address2(
                commandLine.Server(),
                commandLine.Port() + 1);

             CSocketServer server2(
                managedHost,
                true,
                address2,
                listenBacklog,
                pool,
                socketAllocator,
                bufferAllocator,
                connectionLimiter);

From here on ServerMain.cpp is as you would normally expect.

Our CSocketServer class is fairly straight forward, all of the work is passed off to the CManagedHost object:

 CSocketServer::CSocketServer(
    CManagedHost &managedHost,
    const bool createPerConnectionApplicationDomain,
    const IFullAddress &address,
    const ListenBacklog listenBacklog,
    IIOPool &pool,
    IAllocateStreamSockets &socketAllocator,
    IAllocateBuffers &bufferAllocator,
    ILimitConnections &connectionLimiter)
    :  CStreamSocketServer(address, listenBacklog, *this, pool, socketAllocator, bufferAllocator, NoZeroByteRead, connectionLimiter),
       m_managedHost(managedHost),
       m_createPerConnectionApplicationDomain(createPerConnectionApplicationDomain)
 {

 }

 CSocketServer::~CSocketServer()
 {
    WaitForShutdownToComplete();
 }

 void CSocketServer::OnConnectionEstablished(
    IStreamSocket &socket,
    const IAddress & /*address*/)
 {
    Output(_T("OnConnectionEstablished"));

    m_managedHost.OnConnectionEstablished(m_createPerConnectionApplicationDomain, socket, socket);
 }

 void CSocketServer::OnConnectionClientClose(
    IStreamSocket &socket)
 {
    Output(_T("OnConnectionClientClose"));

    m_managedHost.OnConnectionClientClose(socket);
 }

 void CSocketServer::OnReadCompleted(
    IStreamSocket &socket,
    IBuffer &buffer)
 {
    m_managedHost.ReadCompleted(socket, buffer);
 }

 void CSocketServer::OnSocketReleased(
    IIndexedOpaqueUserData &userData)
 {
    m_managedHost.OnSocketReleased(userData);
 }

We configure the server to run on two ports, on the first we handle all connections in one .Net Application Domain and in the second we create a new Application Domain per connection. This allows us to compare the two levels of separation available to us.

The CManagedHost object is where all of the interfacing between C++ code and .Net code is done. All of this interfacing occurs via COM interfaces. These COM interfaces are defined in IManagedHost.idl which gets built into a type library and a .Net Assembly by the ManagedHostInterface project. We've found that defining the COM interfaces in this way is the best way to avoid duplication between the .Net layer and the C++ layer.

Our CManagedHost object is derived from JetByteTools::CLRHosting::CHostControl which provides a boiler-plate implementation of the JetByteTools::CLRHosting::IHostControl interface. CManagedHost implements parts of IHostControl and passes itself to its JetByteTools::CLRHosting::CCLRHost member variable when it starts the CLR. CCLRHost is responsible for loading and managing the CLR.

 void CManagedHost::OnConnectionEstablished(
    const bool createPerConnectionApplicationDomain,
    IIndexedOpaqueUserData &userData,
    IStreamSocket &socket)
 {
    IManagedHost *pDefaultManager = GetDefaultManagedHost();

    IManagedHost *pManager = pDefaultManager;

    int id = 0;

    if (createPerConnectionApplicationDomain)
    {
       _bstr_t name(L"Name");

       HRESULT hr = pDefaultManager->CreateAppDomain(name, &id);

       JetByteTools::COM::CException::ThrowOnFailure(_T("CManagedHost::OnConnectionEstablished()"), hr);

       pManager = GetManagedHost(id);
    }

    userData.SetUserPointer(m_managerIndex, pManager);
    userData.SetUserData(m_appDomainIDIndex, id);

    CSocket *pSocket = new CSocket(socket);

    userData.SetUserPointer(m_managedSocketIndex, pSocket);

    SAFEARRAY *pArray = 0;

    HRESULT hr = SafeArrayAllocDescriptorEx(VT_UI1, 1, &pArray);

    JetByteTools::COM::CException::ThrowOnFailure(_T("CManagedHost::OnConnectionEstablished() - SafeArrayAllocDescriptorEx"), hr);

    pArray->pvData = 0;
    pArray->rgsabound[0].lLbound = 0;
    pArray->rgsabound[0].cElements = 0;

    userData.SetUserPointer(m_managedDataIndex, pArray);

    pManager->OnConnectionEstablished(pSocket);
 }

When a connection is established we optionally create a new Application Domain for the connection. We then create some, per connection, helper objects that we use to help us with the C++ to .Net interop; these are a CSocket object which is a simple COM object that we use to expose some socket like functionality to the .Net code. We also create a descriptor for a safe array of bytes that we use to wrap our data buffer when passing it through the COM interface to .Net. Finally we call into the .Net code and pass our CSocket COM object. The .Net side of this call looks something like the following:

       void IManagedHost.OnConnectionEstablished(
          ISocket socket)
       {
          socket.WriteString("Welcome to CLR echo server\r\n");

          socket.Read();
       }

Now that we have all of our helper objects set up for this connection it's relatively easy to dispatch events to our .Net code.

 void CManagedHost::ReadCompleted(
    IIndexedOpaqueUserData &userData,
    const IBufferBase &buffer)
 {
    try
    {
       IManagedHost *pManager = GetManagedHost(userData);

       CSocket *pSocket = GetManagedSocket(userData);

       SAFEARRAY *pArray = GetManagedData(userData, buffer);

       HRESULT hr = pManager->OnReadCompleted(
          pArray,
          pSocket);

       if (FAILED(hr))
       {
          Output(_T("CManagedHost::ReadCompleted() - Managed call failed: ") + GetLastErrorMessage(hr));
       }
    }
    catch(const CException &e)
    {
       Output(_T("CManagedHost::ReadCompleted() - Exception: ") + e.GetDetails());
    }
    catch(...)
    {
       Output(_T("CManagedHost::ReadCompleted() - Unexpected exception"));
    }
 }

When a read completes we extract our per connection state from the user data for the connection, wire up our safe array descriptor and call a method on the COM object and we end up in the following .Net code.

       void IManagedHost.OnReadCompleted(
          byte[] data,
          ISocket socket)
       {
           socket.Write(data);

           socket.Read();
        }

We clean up our per connection state in OnSocketReleased() and the rest of the class consists of data access and helper functions.

Obviously you don't necesarilly need or want to pass a 'socket' into your .Net code. You would, most probably, have a more focused business logic object that needs some work done on it in C++ and some done on it in a .Net language. This example is, of course, deliberately simplistic to demonstrate the setup and interop required. Everything else is just detail ;)

Generated on Thu Jan 28 19:38:44 2021 for The Server Framework - v7.1 by doxygen 1.5.3