The Socket Framework provides support for real-time two-way communication between Verint Community and the browser. This allows for both receiving and sending messages directly to the user or scoped groups of users.
[toc]
When would I want to use sockets?
Sockets are useful when you need to push messages from the server to the browser. An easy example to consider is a notification popup that is pushed to a user in the event of an action occurring on the server. Sockets are the mechanism through which built-in platform features including notifications, chat, live activity streams, forum threads, comment threads, theme previews, presence indicators, tracing, and more communicate with the browser.
Overview
Built on top of SignalR, the Socket Framework inherits all of SignalR's performance, stability, and diversity of browser support over underlying WebSocket, server-sent event, or long polling transports. Because it is Verint Community-specific, the Socket Framework also provides:
- plugin based extensibility
- Both built-in and third-party plugin can implement their own sockets which each define a pair of server and client methods and events for bi-directional communication, all multiplexed over a single underlying connection to the server.
- Presence
- Users' site presence as well as their presence to individual pieces of content regardless of how and where that content is presented is tracked to enable delivery of messages scoped only to users viewing specific content. Additionally, Service Presence supports a similar, but non-content-based scoping of messages.
- Scale-out and reliability
- All sockets are multiplexed to over a single connection to a browser. And the browser shares the same, single, connection across all windows or tabs.
- Socket connections are reliably maintained, and reconnect after disconnections with randomized, exponential, retry backoffs.
- To support scale-out to multiple application instances as well as sending of socket messages from jobs running outside of web contexts in the job server, Verint Community bundles two possible message bus servers: the Socket Message Bus Service as well as the lower-performing Database Message Bus. Alternatively, Verint Community can use any message bus service through plugin extensibility. The configuration of which message bus server to use, if any at all, is defined by plugin-based connectors, which can be enabled or disabled through standard plugin management without restarting the application. Socket plugins also expose the underlying message bus for use as a way to keep real-time application state in synchronization across multiple instances.
The socket framework powers notifications, chat, live activity streams, forum threads, comment threads, theme previews, presence indicators, tracing, and more in Verint Community. Socket plugins can be developed to easily power other functionality like real-time whiteboards, analytics, or even games.
[DIAGRAM: Sockets]
API
The API exists solely as a set of interfaces implementable by plugins.
ISocket Plugin
A socket is defined by a plugin which implements the ISocket interface. A single socket plugin represents a single channel of communication between the server and browser. For example, a socket plugin may represent all functionality related to sending and receiving realtime chat messages. A single socket plugin can send and receive arbitrary messages and optional data with those messages, but can only send or receive messages sent by or to the same socket plugin.
Socket plugins are injected with instances of ISocketController.
The ISocketController exposes an instance of an IClientsController which contains events for when a user connects, disconnects, or sends a message. The IClientsController also supports sending messages to a user or broadcasting to all users.
Each enabled socket plugin yields a corresponding client-side API.
Socket plugins will often also implement the IHtmlHeaderExtension interface to register JavaScript which handles or sends socket messages along with custom logic required by the plugin.
ISocketMessageBus Plugin
Multiple-application instance scale-out is achieved through usage of plugins which implement the ISocketMessageBus interface. Verint Community bundles a default implementation, but supports integrating with other message busses as well. For details, see the discussion on Scale-Out.
Client-Side
Socket Endpoints
Each ISocket
plugin implements a SocketName
. This one-word string becomes the name of the client-side API. For example, given the socket:
public class MySocket : ISocket { // ... string SocketName { get { return "mySocket"; } } // ... }
Then the following JavaScript API will be automatically exposed:
// receive and handle a message with optional data from the server $.telligent.evolution.sockets.mySocket.on(messageName, function(data) { // handle }); // send data to the server an optional object message $.telligent.evolution.sockets.mySocket.send(messageName, data);
Initiation
Before attempting to send or receive messages from the client side, the Socket Framework must initiate itself. This happens automatically, and when complete, it publishes a socket.connected client-side message.
$.telligent.evolution.messaging.subscribe('socket.connected', function() { // set up message listeners or send messages });
Example Socket
The following shows a basic example of a socket which routes greetings from one user to another. A user provides the user name of another community member, clicks 'greet', and an alert is displayed to the other user.
Greetings are initiated from a user via a widget, sent via the client-side socket API to the ISocket
plugin, and then delivered to the recipient user where the same widget instance for the other user will display the message in a JavaScript alert()
.
Server Side
Create a new plugin which implements the ISocket interface. Its Name
, Description
, and Initialize()
are unimportant. For SocketName
and SetController()
, use the following:
public string SocketName { get { return "greeter"; } } public void SetController(ISocketController controller) { controller.Clients.Received += (sender, e) => { // if this was a greting message, let's process it if (e.MessageName == "greet") { // get the associated recipient user // data passed from the client side is available on the dynamic MessageData object var to = PublicApi.Users.Get(new UsersGetOptions { Username = (string)e.MessageData.Recipient }); // get the sender user var from = PublicApi.Users.Get(new UsersGetOptions { Id = e.UserId }); // send a new message to the recipient controller.Clients.Send(to.Id.GetValueOrDefault(), "greet", new { Message = String.Format("Hello {0}, from {1}", to.DisplayName, from.DisplayName) }); } }; }
Client Side
Create a new Studio Widget with content:
## set up some unique ids for interface elements #set($recipientInputId = $core_v2_widget.UniqueId('recipient')) #set($sendLinkId = $core_v2_widget.UniqueId('greet')) ## basic interface <input type="text" id="$recipientInputId" /> <a href="#" id="$sendLinkId">Greet</a> <script type="text/javascript"> // Wait for socket to be connected jQuery.telligent.evolution.messaging.subscribe('socket.connected', function() { // when 'Greet' is clicked, send a message to be delivered to the recipient jQuery('#$sendLinkId').on('click', function(e) { e.preventDefault(); jQuery.telligent.evolution.sockets.greeter.send('greet', { Recipient: jQuery('#$recipientInputId').val() }); }); // when a 'greet' message is received from the socket, display it as an alert jQuery.telligent.evolution.sockets.greeter.on('greet', function(data){ alert(data.Message); }); }); </script>
Note use of the socket.connected client-side message as well as the automatically-generated jQuery.telligent.evolution.sockets.testSocket
API.
The result is a simple UI that enables sending a greeting to another user by their user name.
Download
The complete sample can be downloaded:
using System; using Telligent.Evolution.Extensibility.Api.Version1; using Telligent.Evolution.Extensibility.Sockets.Version1; namespace Samples { public class Greeter : ISocket { #region IPlugin public string Name { get { return "Sample Greeter Socket Plugin"; } } public string Description { get { return "This plugin will demonstrate a simple socket plugin which supports sending a message to a user"; } } public void Initialize() { } #endregion #region ISocket public string SocketName { get { return "greeter"; } } public void SetController(ISocketController controller) { controller.Clients.Received += (sender, e) => { // if this was a greting message, let's process it if (e.MessageName == "greet") { // get the associated recipient user // data passed from the client side is available on the dynamic MessageData object var to = PublicApi.Users.Get(new UsersGetOptions { Username = (string)e.MessageData.Recipient }); // get the sender user var from = PublicApi.Users.Get(new UsersGetOptions { Id = e.UserId }); // send a new message to the recipient controller.Clients.Send(to.Id.GetValueOrDefault(), "greet", new { Message = String.Format("Hello {0}, from {1}", to.DisplayName, from.DisplayName) }); } }; } #endregion } }
<scriptedContentFragments> <scriptedContentFragment name="Greeter UI" version="9.0.0.0" description="Sample UI corresponding to the Greeter Socket Plugin Demo" instanceIdentifier="b6261cfed4f4466591e39743c153725e" theme="" isCacheable="false" varyCacheByUser="false" showHeaderByDefault="true" cssClass=""> <contentScript><![CDATA[## set up some unique ids for interface elements #set($recipientInputId = $core_v2_widget.UniqueId('recipient')) #set($sendLinkId = $core_v2_widget.UniqueId('greet')) ## basic interface <input type="text" id="$recipientInputId" /> <a href="#" id="$sendLinkId">Greet</a> <script type="text/javascript"> // Wait for socket to be connected jQuery.telligent.evolution.messaging.subscribe('socket.connected', function() { // when 'Greet' is clicked, send a message to be delivered to the recipient jQuery('#$sendLinkId').on('click', function(e) { e.preventDefault(); jQuery.telligent.evolution.sockets.greeter.send('greet', { Recipient: jQuery('#$recipientInputId').val() }); }); // when a 'greet' message is received from the socket, display it as an alert jQuery.telligent.evolution.sockets.greeter.on('greet', function(data){ alert(data.Message); }); }); </script>]]></contentScript> </scriptedContentFragment> </scriptedContentFragments>
Scale-out
Overview
The socket message bus is used both internally by the Socket Framework to push messages to multiple application instances when sending messages as well as exposed as a generic message bus for ISocket plugins to consume through the injected IMessageBusController instance.
When only a single application instance is needed, the Socket Message Bus uses an internal memory-only bus. When multiple application instances are needed, the in-memory bus cannot be used. Instead a Plugins which implements ISocketMessageBus must be enabled.
Socket Message Bus Server
Verint Community 7.5 comes with the optionally-installed Socket Message Bus Windows Service and its corresponding ISocketMessageBus
plugin-based connector, Socket Message Bus Service Connector. This is simply a Windows Service which echoes messages it receives to all connected Verint Community instances.
Custom Message Bus
In cases where a community would prefer to use an existing third party message bus solution, custom ISocketMessageBus plugins can be written against it and deployed. An example could be a message bus service which supports multiple Verint Community communities on a single bus server by potentially adding an identifier to the string messages sent and received by the plugin for routing the message on the bus server.
Generic Messages
Custom logic in ISocket
plugins can also make use of the message bus. Generic messages can be useful for keeping realtime state synchronized across application instances. For example, chat presence may be persisted in a database, but cached locally in multiple instances. Since it is important that these caches remain synchronized, presence change messages can be sent and received across the message bus for other app instances' plugins to handle and process.
The ISocketController injected into ISocket plugins contains an instance of an IMessageBusController. The IMessageBusController exposes events for when generic messages are received from instances of the application along with a method for sending them.
A useful pattern for state change is to send a generic message to the bus, and only handle it once it is re-received. This allows all instances to know about the state. Additionally, the instance that sends it receives the message immediately without having to wait for the round-trip, and is made aware of the local origin of the message. In this manner, the sender can also be exclusively responsible for potentially persisting the state change elsewhere.
controller.MessageBus.Publish("something.happened",""); controller.MessageBus.Received += (sender, e) => { if (e.MessageName == "something.happened") { // update some local caches // ... if (e.Source == BusMessageSource.Local) { // update DB // ... } } };
Presence Services
User Presence
The User Presence service tracks which users are online. The service is responsible for the presence indicator often displayed along side the user's name. In conjunction with the Content Presence service (discussed below), the service sends socket messages as users go offline and online to update the presence indicators in near real-time for users who are currently viewing the user whose online status has changed.
The platform has the options for Enable Presence Tracking, Allow Members To Toggle Presence Tracking Enablement and Default Presence Tracking Enablement Value available under Membership in the Membership Options panel in Administration. Enable Presence Tracking controls whether user presence tracking is available for the community. If tracking is enabled, Allow Members To Toggle Presence Tracking Enablement controls whether or not each user should have the option to disable tracking of their online status. If that option is enabled the Default Presence Tracking Enablement Value is also available. If users are giving the option to control their presence tracking, the Enable presence tracking option will be available in the membership settings.
Content Presence
The Content Presence service tracks which content users are currently viewing. This enables the platform to update users in real time to changes in content they are currently viewing. The API provides a quantity of users who are currently viewing each content item.
How is content presence determined?
- Page Context: The service uses the page content to track what content the using is currently viewing. For instance if a user is currently viewing a blog post, the content service would be aware that user is currently viewing that blog post, its author, the blog the post belonged to and the group the blog belonged to.
- Html attributes: Html attributes can be added to through the
core_v2_contentPresence.RenderAttributes
method. This method will render the proper attributes to automatically track a user's content presence, when the decorated element is currently visible on the page they are viewing. The comment stream and activity stream are two examples where this method is in use. - APIs: In Process, Rest and Widget APIs are available to directly add and remove which content a user is present for. Additionally, a summary of the quantity of users viewing each content item is available.
Service Presence
The Service Presence service tracks what services a user is currently viewing. When a user is present for a service, messages about changes in that service can be sent to the user's browser so that browser can be updated to show the latest changes. The activity story service is an example of how this service is used. When an activity story is created, deleted or updated any user that is present to the activity story service will be see that updated of that change and their activity story stream can be updated as needed.
How do you define a Service Presence service?
Widgets can define themselves as services, using the core_v2_servicePresence.RenderAttributes
method and passing a unique service identifier. For instance, the activity stream widget can define itself as a service, doing allows users who are currently viewing an activity stream to receive socket messages for new activity stories. Using the Content Presence service alone, would only allow users to receive socket messages to for content that already exists in the activity stream.
How is service presence determined?
- Html attributes: Html attributes can be added to through the
core_v2_servicePresence.RenderAttributes
method. This method will render the proper attributes to automatically track a user's presence to a service, when the decorated element is currently visible on the page they are viewing. - APIs: In Process, Rest and Widget APIs are available to directly add and remove which service a user is present for. Additionally listing which users are currently present to a service is available in the same APIs.
Combining the Content and Service Presence Services with Sockets creates new opportunities to enhance community functionality. The new Live Comment and Thread Forums are two features that make use of the Socket Bus and the Presence Services.
Example Socket using Content Presence Service
This example will create an ISocket
implementation that will send socket messages to users when a vote is received or changed on an idea. By combining the socket implementation with the content presence service, the socket messages can be sent only to those users who are currently viewing the idea that received the vote.
Server Side
The ISocket
implementation will use the ideavotes for the SocketName
and maintain a reference to the SocketController
for later use in our example.
ISocketController _sockets; public string SocketName => "ideavotes"; public void SetController(ISocketController controller) { _sockets = controller; }
The example class will need to send a message whenever an idea is voted on, this can be accomplished by attaching to the Idea Vote Events for these actions in our plugin's Initialize
method. In each event handler, we will call the same method SendVoteCounts passing the idea id for the vote.
public void Initialize() { _ideas = Telligent.Evolution.Extensibility.Apis.Get<IIdeas>(); _ideaVotes = Telligent.Evolution.Extensibility.Apis.Get<IVotes>(); _ideaVotes.Events.AfterCreate += VoteCreated; _ideaVotes.Events.AfterDelete += VoteDeleted; _ideaVotes.Events.AfterUpdate += VoteUpdated; } private void VoteCreated(VoteAfterCreateEventArgs e) { SendVoteCounts(e.IdeaId); } private void VoteUpdated(VoteAfterUpdateEventArgs e) { SendVoteCounts(e.IdeaId); } private void VoteDeleted(VoteAfterDeleteEventArgs e) { SendVoteCounts(e.IdeaId); }
In the SendVoteCounts method, the number of up and down votes will be retrieved and the _sockets.Clients.SendToUsersPresentToContent
method will be used to send a message to all users who are currently present to that idea.
void SendVoteCounts(Guid ideaId) { var idea = _ideas.Get(ideaId); if (idea != null && !idea.HasErrors()) { var upVotes = _ideaVotes.List(new VotesListOptions() {IdeaId = idea.ContentId, PageIndex = 0, PageSize = 1, Value = true }); var downVotes = _ideaVotes.List(new VotesListOptions() { IdeaId = idea.ContentId, PageIndex = 0, PageSize = 1, Value = false }); // send socket message with updated totals _sockets.Clients.SendToUsersPresentToContent(idea.ContentId, "ideavoted", new { contentId = idea.ContentId, contentTypeId = _ideas.ContentTypeId, yesVotes = upVotes?.TotalCount.ToString() ?? "0", noVotes = downVotes?.TotalCount.ToString() ?? "0" }); } }
Client Side
Once this plugin is compiled and enabled on the community site, each time an idea is voted on any user who is viewing that idea will receive an ideavoted socket message. For this example, we can use a widget to see these messages. The example will make use of the $.telligent.evolution.sockets.ideavotes
API that is automatically generated for our ISocket
implementation. That API will enable the widget to listen for messages from the custom socket. In this case, the message's data will output to the browser's console window.
<script type="text/javascript"> jQuery.telligent.evolution.messaging.subscribe('socket.connected', function() { jQuery.telligent.evolution.sockets.ideavotes.on('ideavoted', function(data) { console.log(data); }); }); </script>
The widget can be placed on the Idea page or Idea List page, you will be able to see the messages for the ideas you are currently viewing when any vote is received on the idea(s) you are currently viewing.
Completed Sample
using System; using Telligent.Evolution.Extensibility.Ideation.Api; using Telligent.Evolution.Extensibility.Sockets.Version1; using Telligent.Evolution.Extensibility.Version1; namespace Telligent.Evolution.Examples { public class VoteSocket : IPlugin, ISocket { ISocketController _sockets; IIdeas _ideas; IVotes _ideaVotes; public string Name => "Idea Vote Sockets"; public string Description => "Enables live updates to idea votes."; public string SocketName => "ideavotes"; public void Initialize() { _ideas = Telligent.Evolution.Extensibility.Apis.Get<IIdeas>(); _ideaVotes = Telligent.Evolution.Extensibility.Apis.Get<IVotes>(); _ideaVotes.Events.AfterCreate += VoteCreated; _ideaVotes.Events.AfterDelete += VoteDeleted; _ideaVotes.Events.AfterUpdate += VoteUpdated; } public void SetController(ISocketController controller) { _sockets = controller; } private void VoteCreated(VoteAfterCreateEventArgs e) { SendVoteCounts(e.IdeaId); } private void VoteUpdated(VoteAfterUpdateEventArgs e) { SendVoteCounts(e.IdeaId); } private void VoteDeleted(VoteAfterDeleteEventArgs e) { SendVoteCounts(e.IdeaId); } void SendVoteCounts(Guid ideaId) { var idea = _ideas.Get(ideaId); if (idea != null && !idea.HasErrors()) { var upVotes = _ideaVotes.List(new VotesListOptions() {IdeaId = idea.ContentId, PageIndex = 0, PageSize = 1, Value = true }); var downVotes = _ideaVotes.List(new VotesListOptions() { IdeaId = idea.ContentId, PageIndex = 0, PageSize = 1, Value = false }); // send socket message with updated totals _sockets.Clients.SendToUsersPresentToContent(idea.ContentId, "ideavoted", new { contentId = idea.ContentId, contentTypeId = _ideas.ContentTypeId, yesVotes = upVotes?.TotalCount.ToString() ?? "0", noVotes = downVotes?.TotalCount.ToString() ?? "0" }); } } } }
<?xml version="1.0" encoding="utf-8"?> <scriptedContentFragments> <scriptedContentFragment name="Idea Socket Presence Test" version="10.0.0.0" description="" instanceIdentifier="c7e0a2c7d2b44ab08f22d8938ef03a06" theme="" isCacheable="false" varyCacheByUser="false" showHeaderByDefault="false" cssClass="" provider="7bb87a0cc5864a9392ae5b9e5f9747b7"> <contentScript><![CDATA[ <script type="text/javascript"> jQuery.telligent.evolution.messaging.subscribe('socket.connected', function() { jQuery.telligent.evolution.sockets.ideavotes.on('ideavoted', function(data) { console.log(data); }); }); </script> ]]></contentScript> </scriptedContentFragment> </scriptedContentFragments>
Tips and Tricks
- Sockets can only be used by authenticated, non-system users