Example 6

We are going to create a chat room in this example. It introduces the items below:

  • publish-subscribe feature
  • extend observer pattern to the interprocess space.
  • get callback from remote process.

We are not going to explore any new API in this example but a design technique using the remote library. We will create a chat room application where multiple clients can join chat room by connecting to the server and then start chatting with each other.

In our class design, we are going to create a remote_reader interface and a remote_topic interface. The remote_reader subscribes to the remote_topic class to get notification when any new message is posted to the remote_topic. At the server side, we will create a topic class that implemnents the remote_topic interface. The topic class take pointer to remote_reader interface directly in it's subscribe/unsubscribe methods.

This application can be implemented using the intrusive way and non-intrusive way. This example will show the intrusive way where the target class take a remote pointer directly. For the non-intrusive way, I will let you to explore it yourself.

Here is the code.

Note: Only the class definition code fragment is shown here.

// remote_reader.hpp
REMOTE_CLASS_BEGIN(remote_reader)               \
REMOTE_METHOD_M1(void, on_notify, std::string)  \
REMOTE_CLASS_END
// remote_topic.hpp
REMOTE_CLASS_BEGIN(remote_topic)                                        \
REMOTE_METHOD_C0(std::string, about)                                    \
REMOTE_METHOD_M1(void, post, std::string)                               \
REMOTE_METHOD_M1(void, subscribe, boost::shared_ptr<remote_reader>)     \
REMOTE_METHOD_M1(void, unsubscribe, boost::shared_ptr<remote_reader>)   \
REMOTE_CLASS_END

Below is the topic class to bind with remote_topic interface. Note that it takes pointer to remote_reader directly in it's method (intrusive way). In actual application, we will also need to handle error condition by catching exception but error handling code is not shown here.

// topic.hpp
#include "remote_reader.hpp"

class topic
{
    typedef boost::shared_ptr<remote_reader> remote_reader_ptr;
    
    std::string m_about;
    std::set<remote_reader_ptr> m_readers;

    static void notify(remote_reader_ptr rdr, std::string msg)
    {
        rdr->on_notify(msg);
    }
    
public:
    topic(std::string str) : m_about(str) {}

    std::string about() const { return m_about; }

    void subscribe(remote_reader_ptr rdr)
    {
        m_readers.insert(rdr);
    }

    void unsubscribe(remote_reader_ptr rdr)
    {
        m_readers.erase(rdr);
    }

    void post(std::string msg)
    {
        auto fn = boost::bind(&notify, _1, msg);
        std::for_each(m_readers.begin(), m_readers.end(), fn);
    }
};

In server, we just need to bind the topic class as remote_topic service.

// server.cpp
#include "topic.hpp"
#include "remote_topic.hpp"
int main()
{
    std::string about("Sharing CppRemote techniques");
    topic tp(about);

    remote::server server;
    server.bind<remote_topic>(&tp, "cppr");

    server.start(remote::make_tcp_binding(8888));
    std::cout << "serving topic \"" << about << "\"" << std::endl;

    std::cin.get();
    return 0;
}

At the client application, just implement the remote_reader interface and subscribe to the topic class through remote_topic interface.

// client.cpp
#include "remote_topic.hpp"

class reader
{
public:
    void on_notify(std::string msg)
    {
        // handle on notify here
        std::cout << msg << std::endl;
    }
};

int main()
{
    remote::session session;
    session.start(remote::make_tcp_binding("127.0.0.1", 8888));

    // get user name while waiting for the session to connect
    std::string name;
    std::cout << "Enter your name: ";
    std::getline(std::cin, name);

    if(session.wait_for_ready() != remote::session::started)
        return -1;

    // create a remote_reader pointer.
    // we can also use 'remote_cast' instead of 'bind' here
    auto rdr = session.bind<remote_reader>(boost::make_shared<reader>());

    // get a remote_topic interface
    auto tp = session.get<remote_topic>("cppr");

    // subscribe to the topic
    tp->subscribe(rdr);
    
    // get the topic description and print it.
    std::cout << "*** \"" << tp->about() << "\" ***" << std::endl;

    // repeatedly get input from user and post to the chat room until we get "bye"
    std::string input;
    while(input != "bye")
    {
        std::getline(std::cin, input);
        tp->post(name + ": " + input);
    }

    // leave the room
    tp->unsubscribe(rdr);
    std::cout << "*** unsubscribed ***" << std::endl;

    std::cin.get();
    return 0;
}

That's it. The intrusive way of implementing remote observer pattern. We have just designed a publish-subscribe feature that is normally available in most middleware framework. But we have more way of expressing how we want to achieve this feature in our design and not only the framework way.

How about if we already have an observer pattern class model where the topic class accepts a pure abstract ireader class that is not remote pointer? And we want to extend this model without modifying existing classes. We can also design our remote model using the non-intrusive way.

I will leave this as exercise to you. Here are the hints:

  • inherit ireader in remote_reader interface.
  • create an adaptor class to adapt topic to remote_topic interface.

Try it, it's fun. :)

Next Example