The Socket Framework provides support for real-time two-way communication between Verint Community and the browser. This enables receiving and sending messages directly to specific users or scoped groups of users.
[toc]
When would I want to use sockets or presence?
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. Socket plugins can be developed to easily power other functionality like real-time whiteboards, analytics, or even games.
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 can also provide:
- 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 viewed 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 across a single connection between the server and browser. Additionally, the browser shares the same, single, connection across all of its windows and tabs, transparently identifying a single window or tab at a time to serve as the connection's host.
- Socket connections are reliably maintained and reconnect after any disconnections with randomized, exponential, retry backoffs.
- To support scale-out to multiple application instances and delivery of socket messages from plugins running outside of web contexts in the job server, Verint Community automatically distributes message sending across a bus. 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 bus for use as a way to keep real-time application state in synchronization across multiple instances as needed.
API
The server-side API exists 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 from the browser. The IClientsController also supports sending messages to a user, to all users present to specific content or services, or broadcasting to all users.
Each enabled socket plugin yields a corresponding client-side API.
ISocketMessageBus Plugin
Multiple-application instance scale-out and message delivery from non-web contexts like job server plugins is automatically supported by the enablement of plugins which implement the ISocketMessageBus interface. Verint Community bundles two default implementations to connect to either the Socket Message Bus Service or the Database Message Bus, but also 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 => "mySocket"; // ... }
Then the following JavaScript API will be automatically exposed:
// receive and handle a message with optional data from the server jQuery.telligent.evolution.sockets.mySocket.on(messageName, function(data) { // handle }); // send data to the server an optional object message jQuery.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.
jQuery.telligent.evolution.messaging.subscribe('socket.connected', function() { // set up message listeners or send messages });
Simple 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 => "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 = Telligent.Evolution.Extensibility.Apis.Get<Telligent.Evolution.Extensibility.Api.Version1.IUsers>().Users.Get(new Telligent.Evolution.Extensibility.Api.Version1.UsersGetOptions { Username = (string)e.MessageData.Recipient }); // get the sender user var from = Telligent.Evolution.Extensibility.Apis.Get<Telligent.Evolution.Extensibility.Api.Version1.IUsers>().Get(new Telligent.Evolution.Extensibility.Api.Version1.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
In Widget Studio, create a new 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 => "Sample Greeter Socket Plugin"; public string Description => "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 => "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 = Telligent.Evolution.Extensibility.Apis.Get<Telligent.Evolution.Extensibility.Api.Version1.IUsers>().Users.Get(new Telligent.Evolution.Extensibility.Api.Version1.UsersGetOptions { Username = (string)e.MessageData.Recipient }); // get the sender user var from = Telligent.Evolution.Extensibility.Apis.Get<Telligent.Evolution.Extensibility.Api.Version1.IUsers>().Get(new Telligent.Evolution.Extensibility.Api.Version1.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>
Presence Services
Being able to send messages directly to individual users is a great start. However, there is often a need to send messages to specific groups of users, i.e. messages about new replies to all users viewing a given forum thread so that it may update its UI in realtime. To that end, presence services enable sending messages to users scoped by their presence to pieces of content or abstract service groupings.
Content Presence
The Content Presence service tracks which content users are currently viewing. This enables the platform to deliver updates to users in real time regarding changes to content they are currently viewing. Importantly, this can mean in whatever or wherever way that the content is presented. For example, a user viewing a forum thread's question in a group's activity stream is considered and tracked as present to that forum thread the same as a user on the forum thread's own page. Also importantly, content presence even takes into account whether the content's UI is scrolled into view. For example, a user is not be present to another forum thread whose activity story is scrolled out of browser view in the activity stream.
Content Presence Mappings
To understand content presence APIs, it is important to first understand the ways in which content presence mappings are created and removed:
- Page Context
- The service uses URL context to track what content the user is currently viewing. As long as a user is on a URL with content context, the user is present to those content records. For example, if a user is viewing a blog post, the content service would be aware that the user is present to that blog post, the blog, and even the post's author's content records. So, sending a socket message to any of those three content IDs would deliver it to the viewing user.
- HTML presence attributes
- In addition to overall page context, specific portions of a page's UI can identify themselves as representing specific content, so that while a user is viewing that UI, the user is also mapped as present to the content. These identifications are represented as attributes added to the HTML element. The activity stream uses such HTML attributes to wrap stories in the stream so that the user is present to stories' corresponding content, and only when those stories are scrolled into view.
- APIs
- In-Process, Rest and Script APIs are available to directly add and remove presence mappings, though these are rarely needed.
Content presence mappings are automatically removed when they expire. As long as a user is on a page, the page regularly pings back to the server to identify that the user is online, and to identify the list of content records the user is present to, whether they were identified by the overall page's context, or by the currently scrolled elements decorated with HTML presence attributes. Presence mappings that have not been refreshed for across a few of these pings are considered expired and unmapped, thereby removing the user from receipt of socket messages sent to users present to the content. Since all windows and tabs of a single browser share a single socket connection, the single connection sends a ping identifying present content records across all open windows and tabs.
Content Presence API
Content presence APIs enable sending messages, querying total presence counts, and registering specific presence mappings.
To send messages, use IClientsController.SendToUsersPresentToContent(Guid contentId, string messageName).
To query metadata summaries about users present to a given piece of content via IContentPresence
or core_v2_contentPresence.GetSummary().
This metadata is just a count summary, and may be delayed.
To register specific HTML elements as representing content so that the user is mapped to the content while viewing the element, use core_v2_contentPresence.RenderAttributes()
. This method will render the proper attributes to include inline with any HTML element, such as a <div> wrapping an activity stream story, or a <span> surrounding a like button.
Service Presence
Service Presence is nearly identical to Content Presence in that it enables mapping a user's presence and sending socket messages to users based on those mappings. However, while Content Presence is designed specifically for Content IDs and Content Type IDs and automatically maps users to content by their page context, service presence allows mapping users to any arbitrary Guid. As such, there are no automatically-created service presence mappings. Service presence mappings can only be registered by specific HTML presence attributes or the APIs (rarely used). (Please see the above discussion on Content Presence Mappings for a discussion on how such attributes work.)
In this way, a widget can define an HTML element to contain a service presence mapping of any arbitrary ID. Any user viewing the element in that widget will be considered present to that ID "service". And a socket plugin can then deliver a message to any user viewing that widget by the same arbitrary service presence ID.
Similar to content presence, service presence mappings automatically expire.
While less frequently needed than content presence, service presence provides flexibility for edge cases.
Service Presence API
To send messages, use IClientsController.SendToUsersPresentToService(Guid serviceId, string messageName).
To query metadata summaries about users present to an ID via IServicePresence
or core_v2_servicePresence.GetSummary().
This metadata is just a count summary, and may be delayed.
To register specific HTML elements as services mapping ID so that the user is mapped to the ID while viewing the element, use core_v2_servicePresence.RenderAttributes()
. This method will render the proper attributes to include inline with any HTML element.
User Presence
User presence is a set of related UIs and APIs which track and render a user's overall presence to the site. User Presence is built on sockets and content presence. After all, user records are content records, too.
UI
The service is responsible for the presence indicator often displayed alongside the user names.
API
Live-updating user presence indicators can be easily rendered with core_v2_ui.UserPresence()
. These are an easy way to add benefits of realtime-updating presence to your UI to improve engagement in your community.
Of note, core_v2_ui.UserPresence is, itself, built on top of these APIs. It renders a UI component which presents an indicator dot wrapped with content presence HTML attributes registering the accessing user as present to the user being indicated so that socket messages sent on users' presence change events can be delivered to anyone viewing the changed users' indicators.
A user's current presence is an attribute of the user's entity, returned from REST, Script, or In-Process APIs. Like other user property changes, changes to their presence can be handled with events.
Similar to content and service presence mappings, users' presences are automatically set to offline whenever the user closes all of their browser windows and tabs or after the last of their content presence mappings expire.
Privacy
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.
Example Socket using Content Presence
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>
Scale-out
Overview
While multiple users connected to a single web node are easy enough to reason about, this becomes more complicated when there are multiple app nodes and a job server. Consider the case of User A on Server X and User B on Server Y where User A intends to send a real-time chat socket message to User B. The socket message must not only be sent to a different user, it must be sent to that user from a different server entirely. Or consider the case of a plugin running in the context of a job on the job server instead of a web node which also intends to send a socket message to a user, such as a socket-based message that some delayed process has completed. The job server is not connected directly to any users, and so needs to communicate with the web nodes in order to deliver socket messages.
These scenarios are already automatically and transparently supported through the use of the message bus. Verint Community requires the use of a message bus. Verint Community comes with two possible message bus services built in, the Socket Message Bus Windows Service as well as the lower performance Database Message Bus. At least one of these services or another conforming message bus is required to be enabled as part of the installation of Community.
Socket Message Bus Service
Verint Community comes with the Socket Message Bus Windows Service and its corresponding ISocketMessageBus
plugin-based connector, Socket Message Bus Service available in Administration > Site > Message Buses. This is simply a Windows Service which echoes messages it receives to all connected Verint Community instances.
Community also includes an alternative Database Message Bus which has lower performance, but requires no separate service installation.
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 directly. Generic messages can be useful for keeping small amounts of state synchronized in realtime across application instances.
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 // ... } } };
Additional Notes
- Socket connections are only established for authenticated, non-system, users
- User disconnection events are raised whenever a user's last window or tab for a browser closes, as they all share a single connection. Disconnection events can also raise when plugins refresh.