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

Everything you need to know about per connection user data

Often when you're writing a server or client application with the framework you will find yourself needing to pass your own information around with each connection. To remove the need to continually reinvent the wheel and store per connection data in maps of pointers keyed by sockets the framework supports the concept of per connection 'user data'. The user in user data is 'user of the framework' and it's a way for the application developer to add arbitrary pieces of data to a socket so that they can be retrieved during the handling of the various socket callbacks.

Since you may wish to add a number of different pieces of information to a socket, or more likely, various layers of code may wish to add a single pointer to a data structure to a socket, sockets support an arbitrary number of user data 'slots'. A slot is a single user data storage location which can be accessed by index via the IIndexedOpaqueUserData interface which the socket supports. Before you can access a user data slot you need to obtain an index to a slot. You do this by calling the RequestUserDataSlot() method of an object that supports the IProvideUserData interface. In general, where an instance of collection of objects supports IIndexedOpaqueUserData the collection or factory for those objects will support IProvideUserData. So, with the sockets, the socket allocator supports IProvideUserData and it is from the socket allocator that you request your user data slot index.

You must request all the user data slots that you need before you allocate your first socket. This is because each socket must have the same number of user data slots (so that you can be given any one of them and your slot is valid) and to achieve this the socket allocator must know how many slots are required before it allocates the first socket. Essentially you request your slot when you are created and the first socket is allocated when the server starts or the connection manager initiates the first outbound connection.

User data slots are allocated by name and the name must be unique for your slot to be truly yours! To that end it's often good to use the name (and namespace!) of your class when requesting a user data slot. If you need more than one than simply add some other name to the end of the class name. Once you have requested a slot you can store it away for later use.

 CSocketServer(
    IAllocateStreamSockets &socketAllocator)
    : m_readSeqencingFilter(socketAllocator)
  {

 ...

 CReadSequencingStreamSocketConnectionFilter::CReadSequencingStreamSocketConnectionFilter(
    IProvideUserData &dataProvider)
    :  m_userDataIndex(dataProvider.RequestUserDataSlot(_T("CReadSequencingStreamSocketConnectionFilter"))),
       m_pFilterManager(0)
 {

 }
The framework provides callback functions that make it easy for you to manage the lifetime of your user data. The most commonly used for server code are OnConnectionEstablished() to allocate your per connection data and store it in the socket and OnSocketReleased() to remove you data and clean up. Whereas, with client code, OnPreOutgoingConnect() can be used to allow the caller of Connect() to pass user data through to store in the socket.

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

    CPerConnectionData *pPerConnectionData = new CPerConnectionData(m_allocator);

    socket.SetUserPointer(m_userDataIndex, pPerConnectionData);

    m_pool.DispatchConnectionEstablished(socket, address);
 }

 void CSocketServer::OnSocketReleased(
    IIndexedOpaqueUserData &userData)
 {
    Output(_T("OnSocketReleased"));

    CPerConnectionData *pPerConnectionData = userData.GetUserPointer(m_userDataIndex);

    delete pPerConnectionData;

    m_pool.OnSocketReleased(userData);
 }
If you're writing a filter then you may also use FilterSocketAttached() and FilterSocketReleased(), these have the advantage of being isolated in your filter, you don't have to worry about the callback interfaces or deal with routing callback calls to clients of your code. FilterSocketAttached() also has the advantage of being passed a pointer to "filter data" which can be passed to the server and connection manager's internal Connect() calls (you need to derive from the server or connection manager class to access these protected methods).

 CSmartStreamSocket CStreamSocketConnectionManager::SecureConnect(
    const IFullAddress &address,
    const void *pUserData)
 {
    return Connect(address, pUserData, reinterpret_cast<void*>(1));
 }

 ...

 void CStreamSocketConnectionManager::FilterConnect(
    IFilterableStreamSocket &socket,
    const IFullAddress & /*address*/,
    const void * /*pUserData*/,
    const void *pFilterData)
 {
    // allocate an ssl connector for this connection
    // associate it with the socket
    // tell it we're a server...

    const bool connectSecure = (1 == reinterpret_cast<int>(pFilterData));

    if (m_pContext && connectSecure)
    {
       CAsyncSocketConnector *pConnector = new CAsyncSocketConnector(*m_pContext, m_verifyPeer, *this, m_bufferAllocator, socket);

       pConnector->Connect();
       pConnector->SetAuthContext(0);

       socket.SetUserPointer(m_SSLConnectorIndex, pConnector);
    }
 }
If none of these options work for you, then you could hook into the socket allocator directly using IMonitorSocketAllocation...

So, now that we know how to allocate and deallocte your user data, how do you access it? Generally you'd retrieve the data at the start of each callback in which you need it. So, for example...

 IBuffer *CStreamSocketConnectionManager::FilterReadCompleted(
    IFilterableStreamSocket &socket,
    IBuffer &buffer)
 {
    IBuffer *pBuffer = &buffer;

    CAsyncSocketConnector *pConnector = socket.GetUserPointer(m_SSLConnectorIndex);

    if (pConnector)
    {
       pConnector->ReadCompleted(buffer);

       m_pFilterManager->RequestRead(socket, 0, *this);

       pBuffer = 0;
    }

    return pBuffer;
 }


It's also possible to associate user data with instances of IBuffer, after requesting a slot from the buffer allocator, however since buffers are only valid for a single read or write operation it's not especially useful to do so. Buffer user data is used, sparingly, internally to the framework.

Key points for using per connection user data
  • Always request your user data slot from a socket allocator before the first socket is created, so for a server that's before you call Start() and for a connection manager it's before you initiate the first outbound connection.
  • Allocate your user data structure and associate it with the socket in one of the allocation functions detailed above and clean up after the connection in the matching "socket released" function.
  • Access your user data in any callback where you need it.
  • You probably don't have a good reason to associate user data with buffers, even if you think you do.

More information

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