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

Example Servers - WebSockets Simple Text Echo Server

This example shows you how to build WebSockets server which works with the Hixie76 version of the protocol and the HyBi version of the protocol and which can auto detect which protocol vesion the client is connecting with by inspecting the connection handshake. This example uses our WebSocket Tools Library to implement the WebSocket protocol. The basic structure of the server is very 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 "WebSockets" 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.

This server uses a server collection, see here for more details, to manage the 3 endpoints that it exposes.

             CServerCollection servers;

             // Hixie76 server
             {
                OutputEx(_T("Hixie76 server on port: ") + ToString(port));

                const CFullAddress address(
                   commandLine.Server(),
                   port++,
                   addressPreference);

                servers.AddServer(new CHixieSocketServer(
                   address,
                   listenBacklog,
                   pool,
                   socketAllocator,
                   bufferAllocator));
             }

             // HyBi server
             {
                OutputEx(_T("HyBi server on port: ") + ToString(port));

                const CFullAddress address(
                   commandLine.Server(),
                   port++,
                   addressPreference);

                servers.AddServer(new CHyBiSocketServer(
                   address,
                   listenBacklog,
                   pool,
                   socketAllocator,
                   bufferAllocator,
                   connectionLimiter));
             }

             // Auto detect server
             {
                OutputEx(_T("Auto Detect server on port: ") + ToString(port));

                const CFullAddress address(
                   commandLine.Server(),
                   port++,
                   addressPreference);

                servers.AddServer(new CSocketServer(
                   address,
                   listenBacklog,
                   pool,
                   socketAllocator,
                   bufferAllocator,
                   connectionLimiter));
             }

             servers.Start();

The first endpoint is a server that only supports the Hixie76 version of the WebSockets protocol. This version of the protocol supports UTF-8 text messages only and is supported by a wide number of browsers although technically it's pretty much obsolete now.

The server implements a protocol version agnostic WebSocket connection accepting interface and the Hixie76 specific WebSockets interface.

       /// Implement IAcceptWebSocketConnections

       virtual JetByteTools::WebSocket::ConnectionEstablishmentResult OnConnectionHandshake(
          JetByteTools::Win32::IIndexedOpaqueUserData &userData,
          const std::string &uri,
          const bool secure,
          const JetByteTools::WebSocket::CHeaders &requestHeaders,
          JetByteTools::WebSocket::CHeaders &responseHeaders);

       /// Implement Hixie76 IWebSocketServer

       virtual void OnConnectionEstablished(
          JetByteTools::WebSocket::Hixie76::IWebSocket &socket,
          const std::string &uri,
          const bool secure);

      virtual void OnOutboundConnectionEstablished(
         IWebSocket &socket,
         const CHeaders &responseHeaders);

       virtual void OnData(
          JetByteTools::WebSocket::Hixie76::IWebSocket &socket,
          JetByteTools::IO::IBuffer &buffer,
          const JetByteTools::WebSocket::MessageStatus status);

       virtual void OnData(
          JetByteTools::WebSocket::Hixie76::IWebSocket &socket,
          const JetByteTools::Win32::_tstring &text,
          const JetByteTools::WebSocket::MessageStatus status);

       virtual void OnClientClose(
          JetByteTools::WebSocket::Hixie76::IWebSocket &socket);

The server uses the standard JetByteTools::Socket::IStreamSocketServerExCallback::OnConnectionEstablished() and JetByteTools::Socket::IStreamSocketServerExCallback::OnReadCompleted() callbacks to connect up the WebSockets protocol parser to the new connection and then feed data into it as reads complete. Note that calling JetByteTools::WebSocket::Hixie76::CProtocolHandler::AcceptHandshake() issues the first read on the connection and so you should have finished preparing your socket before calling this as read completions can begin to arrive as soon as this function is called.

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

    CProtocolHandler *pHandler = m_protocolHandlerAllocator.Allocate(*this, socket, CProtocolHandler::AccumulateCompleteMessage);

    socket.SetUserPointer(m_protocolHandlerIndex, pHandler);

    pHandler->AcceptHandshake();
 }

 void CHixieSocketServer::OnReadCompleted(
    IStreamSocket &socket,
    IBuffer &buffer)
 {
    try
    {
       IProtocolHandler *pHandler = reinterpret_cast<IProtocolHandler *>(socket.GetUserPointer(m_protocolHandlerIndex));

       pHandler->OnDataReceived(buffer);
    }
    catch(const CException &e)
    {
       OutputEx(_T("ReadCompleted - Exception - ") + e.GetDetails());
       socket.AbortConnection();
    }
    catch(...)
    {
       OutputEx(_T("ReadCompleted - Unexpected exception"));
       socket.AbortConnection();
    }
 }

The JetByteTools::WebSocket::Hixie76::CProtocolHandlerAllocator allocates an instance of JetByteTools::WebSocket::Hixie76::CProtocolHandler for each new connection. You can customise the protocol handler using the JetByteTools::WebSocket::Hixie76::CProtocolHandler::HandlerConfigurationFlags here we ask the protocol handler to accumulate complete WebSocket messages where possible before passing them to us; whether or not the protocol handler can do this will depend on the size of the buffers that we supply it with and the size of the messages but if each of your messages can fit within a single buffer then you will only ever get complete messages passed to your JetByteTools::WebSocket::Hixie76::IWebSocketServer::OnData() callbacks.

Once the protocol handler is connected to the socket the first read may complete and the data is fed into the protocol handler via the standard JetByteTools::Socket::IStreamSocketServerExCallback::OnReadCompleted() callback.

As the first data arrives in the protocol handler the handler parses the handshake request and, assuming it's a valid Hixie76 handshake request, calls JetByteTools::WebSocket::IAcceptWebSocketConnections::OnConnectionHandshake() to notify you of a new WebSocket connection.

 ConnectionEstablishmentResult CHixieSocketServer::OnConnectionHandshake(
    IIndexedOpaqueUserData & /*userData*/,
    const string &uri,
    const bool secure,
    const CHeaders &requestHeaders,
    CHeaders &responseHeaders)
 {
    Output(_T("OnConnectionHandshake - : ") + CStringConverter::AtoT(uri));

    size_t index = 0;

    string header;

    Output(_T("Request headers"));

    while (requestHeaders.GetRawHeader(index, header))
    {
       Output(CStringConverter::AtoT(header));
    }

    Output(_T("Protocol Version: Hixie76"));

    const string protocol = secure ? "wss" : "ws";

    responseHeaders.Add("Sec-WebSocket-Location: " + protocol + "://" + requestHeaders.GetValue("host") + uri);

    if (requestHeaders.Contains("sec-websocket-protocol"))
    {
       throw CException(_T("CHixieSocketServer::OnConnectionEstablished()"), _T("Unknown protocol"));
    }

    static const string webSocketOriginHeader = "sec-websocket-origin";

    static const string originHeader = "origin";

    if (requestHeaders.Contains(webSocketOriginHeader))
    {
       responseHeaders.Add("Sec-WebSocket-Origin: " + requestHeaders.GetValue(webSocketOriginHeader));
    }
    else if (requestHeaders.Contains(originHeader))
    {
       responseHeaders.Add("Sec-WebSocket-Origin: " + requestHeaders.GetValue(originHeader));
    }

    if (requestHeaders.Contains(originHeader))
    {
       responseHeaders.Add("Origin: " + requestHeaders.GetValue(originHeader));
    }

    return ::ConnectionEstablished;
 }

You can do pretty much whatever you need here, examining request headers and the uri for validity, setting response headers and deciding whether to accept the connection or not. Once you return the appropriate handshake response is generated and sent to the client and the JetByteTools::WebSocket::Hixie76::IWebSocketServer::OnConnectionEstablished() callback is called.

 void CHixieSocketServer::OnConnectionEstablished(
    IWebSocket &socket,
    const string &uri,
    const bool /*secure*/)
 {
    Output(_T("OnConnectionEstablished - Hixie76: ") + CStringConverter::AtoT(uri));

    socket.TryRead();
 }

Again, you can do pretty much whatever you want here. In this server we expect the client to send the first data message, so we simply issue a read request. We could equally well decide that we send the first message and send it here.

When data arrives it is passed to us via the JetByteTools::WebSocket::Hixie76::IWebSocketServer::OnData() callback. We configured our protocol handler to accumulate complete messages and so in this server we will only get a callback when either a message is complete or the supplied buffer is full. There are two versions of JetByteTools::WebSocket::Hixie76::IWebSocketServer::OnData(), one that provides the data as UTF-8 in an instance of JetByteTools::IO::IBuffer and one that provides you with text in string form. The buffer interface is more flexible though you need to do your own UTF-8 conversion when you need to access the data, you can do that using JetByteTools::Win32::CStringConverter. The text version of the callback is slightly more convenient for simple servers and is enabled by passing JetByteTools::WebSocket::Hixie76::CProtocolHandler::DispatchTextAsStrings as one of the flags when you create the protocol handler.

 void CHixieSocketServer::OnData(
    IWebSocket &socket,
    IBuffer &buffer,
    const MessageStatus status)
 {
    // For this to be called you need to specify CProtocolHandler::DispatchTextAsStrings in the flags that are passed
    // during construction

    if (status != MessageStatusComplete)
    {
       throw CException(
          _T("CHixieSocketServer::OnData()"),
          _T("Unexpected: message larger than buffer size"));
    }

    // For this to be called you need to NOT specify CProtocolHandler::DispatchTextAsStrings in the flags that are passed
    // during construction

    DEBUG_ONLY(Output(_T("OnMessage - ") + ToString(buffer.GetUsed()) + _T(" bytes\r\n") + DumpData(buffer.GetMemory(), buffer.GetUsed())));

    // if we wanted to convert the buffer to text we could do this

    //const _tstring text = CStringConverter::UTF8toT(buffer.GetMemory(), buffer.GetUsed());

    //DEBUG_ONLY(Output(_T("OnMessage - \"") + text+ _T("\"")));

    socket.TryWriteText(buffer);

    socket.TryRead();
 }

 void CHixieSocketServer::OnData(
    IWebSocket &socket,
    const _tstring &text,
    const MessageStatus status)
 {
    // For this to be called you need to specify CProtocolHandler::DispatchTextAsStrings in the flags that are passed
    // during construction

    if (status != MessageStatusComplete)
    {
       throw CException(
          _T("CHixieSocketServer::OnData()"),
          _T("Unexpected: message larger than buffer size"));
    }

    DEBUG_ONLY(Output(_T("OnData - \"") + text+ _T("\"")));

    socket.TryWriteText(text);

    socket.TryRead();
 }

Due to the nature of WebSocket messages, there's no size limit to a Hixie76 message and no way of knowing how big the message is until all of it has arrived, the OnData() interface supplies a JetByteTools::WebSocket::MessageStatus enum which can tell you when you have a complete message. In this simple server we only handle complete messages. See the later server examples for details of how to deal with messages that are larger than your buffer size.

Our server simply echoes the message back to the client. Note that the data that we receieve here is, of course, just the message text that the client sent, all framing has been removed by the protocol handler.

When the client closes the connection it sends a close noticiation which we display in our debug log. We then issue our own close.

 void CHixieSocketServer::OnClientClose(
    IWebSocket &socket)
 {
    Output(_T("OnClientClose"));

    socket.Close();
 }

That's all there is to the Hixie server. The HyBi server is similar but the callback interface provided by the JetByteTools::WebSocket::HyBi::CProtocolHandler is more complex as HyBi supports binary messages as well as UTF-8 as well as other more advanced functionality.

       /// Implement IAcceptWebSocketConnections

       virtual JetByteTools::WebSocket::ConnectionEstablishmentResult OnConnectionHandshake(
          JetByteTools::Win32::IIndexedOpaqueUserData &userData,
          const std::string &uri,
          const bool secure,
          const JetByteTools::WebSocket::CHeaders &requestHeaders,
          JetByteTools::WebSocket::CHeaders &responseHeaders);

       // Implement HyBi IWebHyBiSocketServer

       virtual void OnOutboundConnectionEstablished(
          JetByteTools::WebSocket::HyBi::IWebSocket &socket,
          const JetByteTools::WebSocket::CHeaders &responseHeaders);

       virtual void OnConnectionEstablished(
          JetByteTools::WebSocket::HyBi::IWebSocket &socket,
          const std::string &uri,
          const bool secure);

       virtual void OnData(
          JetByteTools::WebSocket::HyBi::IWebSocket &socket,
          const JetByteTools::Win32::_tstring &text,
          const JetByteTools::WebSocket::MessageStatus status,
          const __int64 messageBytesOutstanding);

       virtual void OnData(
          JetByteTools::WebSocket::HyBi::IWebSocket &socket,
          JetByteTools::IO::IBuffer &buffer,
          const JetByteTools::WebSocket::MessageType type,
          const JetByteTools::WebSocket::MessageStatus status,
          const __int64 messageBytesOutstanding);

       virtual void OnPingResponse(
          JetByteTools::WebSocket::HyBi::IWebSocket &socket,
          const BYTE *pData,
          const BYTE length);

       virtual void OnClientClose(
          JetByteTools::WebSocket::HyBi::IWebSocket &socket,
          const WORD status,
          const JetByteTools::Win32::_tstring &text);

Once again everything starts in the standard JetByteTools::Socket::IStreamSocketServerExCallback::OnConnectionEstablished() callback, which looks identical to the Hixie server in this case as were using the same flags. Note that the allocator is HyBi specific and that JetByteTools::WebSocket::HyBi::CProtocolHandler::HandlerConfigurationFlags supports more configuration options for tuning your protocol handler.

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

    CProtocolHandler *pHandler = m_protocolHandlerAllocator.Allocate(*this, socket, CProtocolHandler::AccumulateCompleteMessage);

    socket.SetUserPointer(m_protocolHandlerIndex, pHandler);

    pHandler->AcceptHandshake();
 }

As before, there is a buffer based callback and a string based one for convenience. We're using the buffer based callback due to the fact that we didn't pass JetByteTools::WebSocket::HyBi::CProtocolHandler::DispatchTextAsStrings in our configuration flags when we allocated the handler.

 void CHyBiSocketServer::OnData(
    IWebSocket &socket,
    const _tstring &text,
    const MessageStatus status,
    const __int64 messageBytesOutstanding)
 {
    // For this to be called you need to specify CProtocolHandler::DispatchTextAsStrings in the flags that are passed
    // during construction

    if (status != MessageStatusComplete)
    {
       throw CException(
          _T("CHyBiSocketServer::OnData()"),
          _T("Unexpected: message larger than buffer size"));
    }

    if (messageBytesOutstanding != 0)
    {
       throw CException(
          _T("CHyBiSocketServer::OnData()"),
          _T("Unexpected: message larger than buffer size: ") + ToString(messageBytesOutstanding) + _T(" outstanding"));
    }

    DEBUG_ONLY(Output(_T("OnMessage - \"") + text+ _T("\"")));

    socket.TryWriteText(text);

    socket.TryRead();
 }

 void CHyBiSocketServer::OnData(
    IWebSocket &socket,
    IBuffer &buffer,
    const MessageType type,
    const MessageStatus status,
    const __int64 messageBytesOutstanding)
 {
    // For this to be called you need to NOT specify CProtocolHandler::DispatchTextAsStrings in the flags that are passed
    // during construction

    if (type == MessageTypeText)
    {
       if (status != MessageStatusComplete)
       {
          throw CException(
             _T("CHyBiSocketServer::OnData()"),
             _T("Unexpected: message larger than buffer size"));
       }

       if (messageBytesOutstanding != 0)
       {
          throw CException(
             _T("CHyBiSocketServer::OnData()"),
             _T("Unexpected: message larger than buffer size: ") + ToString(messageBytesOutstanding) + _T(" outstanding"));
       }

       DEBUG_ONLY(Output(_T("OnMessage - ") + ToString(buffer.GetUsed()) + _T(" bytes\r\n") + DumpData(buffer.GetMemory(), buffer.GetUsed())));

       // if we wanted to convert the buffer to text we could do this

       //const _tstring text = CStringConverter::UTF8toT(buffer.GetMemory(), buffer.GetUsed());

       //DEBUG_ONLY(Output(_T("OnMessage - \"") + text+ _T("\"")));

       socket.TryWriteText(buffer);

       socket.TryRead();
    }
    else
    {
       throw CException(
          _T("CHyBiSocketServer::OnData()"),
          _T("Unexpected: we don't handle binary frames"));
    }
 }

HyBi WebSocket messages are even more complex than Hixie ones. Each message can be sent either as a complete message, in which case the size of the message is known when the message header is complete, or it can be sent as a series of message fragments, where the size of each fragment is known when the header is complete but there's no way to determine the size of the message until the final fragment arrives. Servers should not rely on clients only sending complete messages as 'intermediaries' beyond the control of the client or server developers can, in many cases, arbitrarily fragment messages.

The JetByteTools::WebSocket::HyBi::IWebSocketServer::OnData() callbacks provide you with as much information about the state of your message as the protocol handler can. There's the familar JetByteTools::WebSocket::MessageStatus enum which will tell you when you have a complete message (or the final fragment of a fragmented message) and when this is JetByteTools::WebSocket::MessageStatusComplete then the value of messageBytesOutstanding will be 0 when the message is actually complete or else contain the number of bytes still pending. Since we only deal in complete messages which fit in a buffer in this example we check for JetByteTools::WebSocket::MessageStatusComplete and messageBytesOutstanding == 0 to determine if we have a complete message.

Finaly the HyBi versions of the WebSocket protocol provide support for UTF-8 text and binary messages and we can tell which kind of message the client sent from the JetByteTools::WebSocket::MessageType enum.

The HyBi versions of the protocol support the sending of Ping/Pong messages between the client and server, this example does not send any Ping messages and therefore expects no Pong responses.

 void CHyBiSocketServer::OnPingResponse(
    IWebSocket & /*socket*/,
    const BYTE * /*pData*/,
    const BYTE /*length*/)
 {
    throw CException(
       _T("CHyBiSocketServer::OnPingResponse()"),
       _T("Unexpected: we don't send pings"));
 }

HyBi close notifications are also more complex, providing an optional reason code and reason.

 void CHyBiSocketServer::OnClientClose(
    IWebSocket &socket,
    const WORD status,
    const _tstring &reason)
 {
    Output(_T("OnClientClose:") + ToString(status) + _T(": ") + reason);

    socket.Close(CloseStatusNormal);
 }

It's important to tell the protocol handler when the connection is closed. The protocol handler takes a reference to the underlying stream socket and it only releases this once a close notification has been received or when you tell it that the connection is closed. Failure to call OnConnectionClosed() may cause sockets to be leaked and connections to stay in memory.

 void CSocketServer::OnConnectionReset(
    IStreamSocket &socket,
    const DWORD /*lastError*/)
 {
    IProtocolHandler *pHandler = static_cast<IProtocolHandler *>(socket.GetUserPointer(m_protocolHandlerIndex));

    pHandler->OnConnectionClosed();
 }

 void CSocketServer::OnConnectionClosure(
    IStreamSocket &socket,
    const ConnectionClosureReason /*reason*/)
 {
    IProtocolHandler *pHandler = reinterpret_cast<IProtocolHandler *>(socket.GetUserPointer(m_protocolHandlerIndex));

    pHandler->OnConnectionClosed();
 }

The two servers that we have presented so far are ideal if you either want to explicitly support one version of the WebSocket protocol only, or if you want to support each version on a different port. When writing a server that deals solely with text messages and for which you wish to support as many different browser clients as possible it's more likely that you will want to support all variants of the WebSocket protocol with a singler server on a single port. We demonstrate this with the final server in this example.

The main final server includes all of the protocol handler callbacks from each of the previous two servers and uses a JetByteTools::WebSocket::CAutoDetectProtocolHandlerAllocator to allocate the correct protocol handler based on the initial client handshake. We configure the protocol handler allocator in our server's constructor so that we can control which protcol versions we support.

 CSocketServer::CSocketServer(
    const IFullAddress &address,
    const ListenBacklog listenBacklog,
    IIOPool &pool,
    IAllocateSequencedStreamSockets &socketAllocator,
    IAllocateBuffers &bufferAllocator,
    ILimitConnections &connectionLimiter)
    :  CStreamSocketServerEx(address, listenBacklog, *this, pool, socketAllocator, bufferAllocator, NoZeroByteRead, connectionLimiter),
       m_protocolHandlerIndex(socketAllocator.RequestUserDataSlot(_T("m_protocolHandlerIndex"))),
       m_nextBufferIndex(bufferAllocator.RequestUserDataSlot(_T("m_nextBufferIndex"))),
       m_bufferAllocator(bufferAllocator),
       m_protocolHandlerAllocator(*static_cast<JetByteTools::WebSocket::HyBi::IWebSocketServer *>(this), bufferAllocator)
 {
    m_protocolHandlerAllocator.SupportHixie76(
       *this,
       CHixie76ProtocolHandler::AccumulateCompleteMessage);

    m_protocolHandlerAllocator.SupportHyBiVersions(
       CAutoDetectProtocolHandlerAllocator::HyBiProtocolAllVersions,
       *this,
       CHyBi08ProtocolHandler::AccumulateCompleteMessage);
 }

Note that we can fine tune the supported HyBi versions if required but you're unlikely to want to use anything but JetByteTools::WebSocket::CAutoDetectProtocolHandlerAllocator::HyBiProtocolAllVersions to support the maximum number of browser versions.

The rest of the server is pretty much an amalgamation of the previous two servers except that we can use the common JetByteTools::WebSocket::IWebSocket interface to factor out the code that deals with our messages into something like this.

 void CSocketServer::OnMessage(
    IWebSocket &socket,
    const _tstring &text)
 {
    // For this to be called you need to specify CProtocolHandler::DispatchTextAsStrings in the flags that are passed
    // during construction

    DEBUG_ONLY(Output(_T("OnMessage - \"") + text+ _T("\"")));

    socket.TryWriteText(text);

    socket.TryRead();
 }

 void CSocketServer::OnMessage(
    IWebSocket &socket,
    IBuffer &buffer)
 {
    // For this to be called you need to NOT specify CProtocolHandler::DispatchTextAsStrings in the flags that are passed
    // during construction

    DEBUG_ONLY(Output(_T("OnMessage - ") + ToString(buffer.GetUsed()) + _T(" bytes\r\n") + DumpData(buffer.GetMemory(), buffer.GetUsed())));

    // if we wanted to convert the buffer to text we could do this

    //const _tstring text = CStringConverter::UTF8toT(buffer.GetMemory(), buffer.GetUsed());

    //DEBUG_ONLY(Output(_T("OnMessage - \"") + text+ _T("\"")));

    socket.TryWriteText(buffer);

    socket.TryRead();
 }

This example has only scratched the surface of the WebSockets support within The Server Framework. Later examples will explore binary message support, secure connections and the large message and explicit fragment interfaces provided by the JetByteTools::WebSocket::HyBi::IWebSocket interface.

Generated on Sun Sep 12 19:06:45 2021 for The Server Framework - v7.4 by doxygen 1.5.3