Introduction
I have had some experiences trying to build a chat application—most of the time I failed, or I had some other important stuff to deal with. A few weeks back I wanted to check some services on Microsoft Azure, and Azure Redis Cache got my attention. As much as the name sounds crazy, the service itself is dreat, and runs on the Redis protocol; read more about the protocol here. So Azure Redis Cache is a service offering by Microsoft Azure, to provide you with:
- The in-memory key-value data structure for caching purposes.
- Pub-sub implementation for any purpose—we will use this for chat messaging.
- Much more services, such as clustering or sharding of the contents, which are beyond the scope of this article.
Most of you might have heard the name of Redis, for the use of in-memory caching services. However, I am more interested in the publish-subscribe model of Redis, much more than the in-memory caching services and utilities. The pub-sub model allows us to:
- Subscribe to a specific topic; Redis supports pattern-based subscription too, but I will not talk about that!
- Wait for someone to "Publish" a message to that specific topic.
As a subscriber, we can then preview what that message was all about. That is exactly where we can perform the most complex operations to slice-dice the message and preview the message in a neat, user-friendly manner.
I would expect that you know the basics of Redis and some basics of C# programming. I will be using a WPF application, because I was facing a few problems in using a simple console application, and I will explain what went wrong and what was the reason I had to chose WPF; otherwise this would have been my .NET Core article. Anyhow, we can now dig into some basics of the technology that are going to cover and then we proceed with the development of the application.
Note
The images in the article look smaller than they are, right-click and open them in new tab for preview them properly.
The article’s code is also available on GitHub, you can capture it from, here.
Azure Redis Cache Setup
Before I ask you to get a free Azure account, note that you can use your own locally deployed Redis instances too. So if you want to install the Redis instance locally, consider downloading and setting up the server for the rest of article to work. Visit the downloads page, and continue from there. Most of the steps required to setup the server locally are already explained pretty neatly on the internet and you can check a few blogs to find out how to set it up. Also, the connection string to be passed would then depend on your local instance and not the one I will be using, which is from Microsoft Azure of course.
Anyways, in my instance, I will be going ahead to set up an instance of Azure Redis Cache online in the Azure Portal to access the service and then proceed with the development part. On Azure, search for Redis Cache and you will be provided with the Redis Cache instance to be created on the platform.
Figure 1: Redis Cache service on Azure.
There would be other services there too, but you only need to select this one—unless of course trying out other services. Once on the form, fill in with the details that you find suitable and then create the instance of the service on Azure.
Figure 2: Redis Cache creation form on Azure.
Azure will take a while, and let you know once the service has been deployed for you to start using it. A few things to consider, Azure does support clustering of Redis servers. I will not do that because that will only increase the complexity of the stuff. Secondly, since we won't be using the caching, we don't necessarily need the clustering and sharing services. However, the storage persistence and other features might be useful but please read the Redis vs Apache Kafka section for more on this topic.
You will need a few settings from the Azure portal, that you will use to configure the Redis clients.
- Keys
- Port to use (Redis doesn't support SSL natively).
- The endpoint to connect to (You might have the local one installed, in that case, the server where it is listening to).
We are going to use StackExchange.Redis library, which is also recommended by Microsoft to use with Azure. I will cover that part in the next sections when we start working on the project itself.
End results
What we are focusing on, is a simple chat application where everyone has a username and they communicate using the usernames to forward the messages to a specific node on the communication grid. There are a lot of other solutions that we can use, such as socket.io on Node.js app, and much more solutions. I found that Redis can be helpful as it can take care of a lot of critical problems itself—problems like mapping sockets to channels and channels to sockets for faster iteration and processing.
For a sample chat application with Socket IO and Node.js, consider visiting their own web page where they explain the overall process of doing so, https://socket.io/get-started/chat/.
In our current requirement and workflow, we want to reach the following workflow and support chat on the platform for multiple users. Please remember, this is just a demonstration there are a lot of other features that are missing and are not implemented because I did not have time to do so, and neither was it in my consideration to do. But, cherish what you got!
Figure 3: Preview of the applications running and service 3 users in the chat.
Now, without further ado, let's dive deeper into the development of this project and the explanation of why I thought this can be done using Redis, and more especially through WPF.
Developing the WPF client
Let's now start developing the WPF application. I chose WPF when console application did not work out quite well. WPF is a good platform to develop graphical applications, instead of using the Windows Forms application development platform, for several reasons that I do not want to talk about. Here, I am using the WPF as a client for my domain, where the server is hosted on Azure.
If I divide the entire client app into sections, we will capture the following aspects that are to be developed.
- Redis client.
- Front-end to present any messages.
- Service to publish the messages for the user to receive the message.
All of this can easily be done using a simple console application. I did start with a .NET Core application acting as a client for the Redis service hosted on Azure.
Problem with a Console app
But the problem with console application is, that it doesn't distinguish when you are entering a message, and when you are reading a string from the console. The entire program was working quite fine, until I brought in four users to talk randomly. Upon receipt of a message, the console began to show me messages, that I no longer wish to remember.
The code for that was like this, and yes, it did work quite fine for 2 users. You can give it a try for sure, no problem at all. :-)
- class Program
- {
- private static ConnectionMultiplexer redis;
- private static ISubscriber subscriber;
-
- static void Main(string[] args)
- {
- connect();
-
- if (redis != null && subscriber != null)
- {
- Console.WriteLine("Connected to Azure Redis Cache.");
- Console.WriteLine("Enter the message to send; (enter 'quit' to end program.)");
-
- string message = "";
- while (message.ToLower() != "quit")
- {
- message = Console.ReadLine().Trim();
-
- if (!string.IsNullOrEmpty(message) && message.Trim().ToLower() != "quit")
- {
- sendMessage(message);
- }
- }
- }
- else
- {
- Console.WriteLine("Something went wrong.");
- }
-
- Console.WriteLine("Terminating program...");
- Console.Read();
- }
-
- static void connect()
- {
- redis = ConnectionMultiplexer.Connect("<your-connection-string>");
-
- subscriber = redis.GetSubscriber();
- subscriber.Subscribe("messages", (channel, message) =>
- {
- Console.WriteLine($"{channel}: {(string)message}.");
- });
- }
-
- static void sendMessage(string value)
- {
- subscriber.Publish("messages", value);
- }
- }
Sorry for no commentary on the code, that was just the code I was trying out. The errors in the code made me think of an alternate platform to write the code on. Apart from UWP, WPF was the one that came to my mind and I started porting the code from .NET Core to .NET framework's WPF framework.
Also, to those who figured it out: Yes, the code was different and used a global channel to send messages to everyone. This WPF app uses a different scheme and forwards messages to only specific users.
The architecture of the app—the idea
The difference in the console app and WPF app enabled me to support a different approach to using the chat message. In this, I was able to send the messages to specific users as needed. The previous console app was using the fact that we can use a single channel and broadcast the messages to everyone. The change was made, to make things a bit clearer. As you have seen in the image above, our app is capable of sending the messages only to the users that are intended, recipients. Redis supports multiple subscribers/channel too, and we can definitely use that, for "group chatting".
The idea behind the app was to support every member to have a separate channel to listen to. Think of it, as their own inbox and everyone can dump a message in the channel. This way, we can separate our the members and who listens to a specific channel. A rough sketch of the architecture is something like this,
Figure 4: App workflow overview.
The thing is, that now you can visualize how each user can send a message, but they will more likely listen to the stream they are subscribing to—which we do so use their own username that they specify on the app start.
Lastly, as a secondary feature, I wanted to make sure that we are able to send the messages to the right recipient. One of the ways to do that was, using the list of active clients and then forwarding the messages to the ones we are interested in. Too complicated in Redis. Why? Because Redis doesn't support active users list, and their channels or other similar information that can be used to access the active user and send a message to them—unless of course, you are admin of the service, which kills the purpose of a chat app and allowing users to gain access to such sensitive information. We can do this if we consider using a middleware that takes care of user profiles and authentication, or friends, let's say.
In that case, we would be using the user's ID as the channel-title, such as, "user_1234" and then enabling the messaging. That can help us send a message to anyone who is known on the platform. Once again, this does not guarantee that the message was sent to the user unless we take care of the channels we are writing to, such as selecting the user from a list provided by our server that maintains the list of our contacts. Because we are merely publishing the message on a channel, there is no TCP-like acknowledgment of the message. But, but, but, there is a response in Redis protocol that you can capture and use, in the StackExchange.Redis package library, it is the return type of the Publish(Async) function. The service is provided by Redis protocol and can be used to check how many users got the message, in our case, it should be one or two as needed by your business logic. In case the return message is zero, we can conclude that the recipient is offline or does not exist. I will explain this in a minute.
In other words, we would have to manage a database that tracks who did we contact and what are the usernames for the contacts that we wish to contact. Redis won't be of any use in that part.
Writing the code
Since all the explanation has been made, it is time we start coding. Start off with the creation of a new WPF project, you can do that in Visual Studio itself. Once you have the WPF application up and running, you can modify the project to mimic what we need.
The front end of the application was made simple and neat. For responsiveness, and since we are using WPF, I used the grid templates and made the application responsive through the Grid control instead of StackPanel. The front-end of the app was like,
Figure 5: App's UI is shown for demonstration.
And, I used the following XAML code to build the front-end, you can download the archives or consider cloning the project from GitHub to try it out.
- <Window x:Class="RedisDemo.Chat.WPF.MainWindow"
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
- xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:local="clr-namespace:RedisDemo.Chat.Wpf"
- mc:Ignorable="d"
- Title="Sample App" Height="500" Width="750">
- <Grid>
- <Grid.RowDefinitions>
- <RowDefinition Height="45" />
- <RowDefinition />
- <RowDefinition Height="80" />
- </Grid.RowDefinitions>
- <StackPanel Orientation="Horizontal" Margin="5">
- <TextBlock Text="Username: " Margin="0, 8, 5, 0" />
- <TextBox Padding="2" IsEnabled="False" Height="25" Margin="0, 0, 10, 0" Width="250" Name="username" TextChanged="username_TextChanged" />
- <Button IsEnabled="False" Content="Connect" Height="30" HorizontalAlignment="Right" Name="setUsernameBtn" Click="setUsernameBtn_Click" Padding="5" />
- </StackPanel>
- <Grid Grid.Row="1">
- <ListView Name="messagesList">
- <ListView.ItemTemplate>
- <DataTemplate>
- <StackPanel Margin="10, 0, 0, 10">
- <TextBlock Text="{Binding Sender}" FontWeight="SemiBold" FontSize="15" />
- <TextBlock Text="{Binding Content}" />
- <TextBlock Text="{Binding ReceivedAt}" Foreground="Gray" />
- </StackPanel>
- </DataTemplate>
- </ListView.ItemTemplate>
- </ListView>
- </Grid>
- <Grid Grid.Row="2">
- <Grid.RowDefinitions>
- <RowDefinition />
- <RowDefinition Height="25" />
- </Grid.RowDefinitions>
- <Grid.ColumnDefinitions>
- <ColumnDefinition />
- <ColumnDefinition Width="70" />
- </Grid.ColumnDefinitions>
- <TextBox IsEnabled="False" TextChanged="message_TextChanged" VerticalAlignment="Top" Height="25" Margin="10" Name="message" />
- <Button IsEnabled="False" VerticalAlignment="Top" Margin="8" Content="Send" Height="30" Name="sendMessageBtn" Click="sendMessageBtn_Click" Padding="5" Grid.Column="1" />
- <TextBlock Grid.Row="1" Margin="10, 0, 0, 0" Text="Messages should be forwarded using @username scheme to be delivered to a specific member." />
- </Grid>
- </Grid>
- </Window>
I used Grid controls, and created columns and rows, to support responsive-scaling of the app's UI. The benefit is that it has a cleaner look on scaling of the application. Aside from that, I did use a few hardcoded Margin values and some other stuff but that was needed to maintain somewhat consistency. The full-screen view on my monitor here looks like this,
Figure 6: Full screen view of the application's UI.
The major part was to manage the code and write the backend code. My primary interest was to make the code cleaner, and better in the terms of, performance and efficiency. I used the async/await modifiers wherever possible. Utilized global variables, instead of local-short-lived variables that need to be created again in every function call.
Tidbits of the code are as follows, the primary settings that are involved in the establishment of the connection and the final function that closes the connections are as follows,
-
- private ObservableCollection<Message> collection;
- private ConnectionMultiplexer redis;
- private ISubscriber subscriber;
-
- public MainWindow()
- {
- InitializeComponent();
- Closing += MainWindow_Closing;
-
-
- setThingsUp();
- }
-
- private async void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
- {
-
- if (redis != null)
- {
- var sub = redis.GetSubscriber();
- await sub.UnsubscribeAsync(username.Text.Trim().ToLower());
- await redis.CloseAsync();
- }
- }
-
-
- private void setThingsUp()
- {
- collection = new ObservableCollection<Message>();
- username.IsEnabled = true;
- setUsernameBtn.IsEnabled = false;
-
- username.Focus();
-
-
- messagesList.ItemsSource = collection;
- }
Now that we have the basic workflow defined, our users will see the main text box as that will have the focus. Later, they will enter their usernames and establish a connection to the Azure Redis Cache service using the connection string we have. Once the user inputs the username, they can press the "Connect" button and establish the connection, the code is as follows,
- private async void setUsernameBtn_Click(object sender, RoutedEventArgs e)
- {
-
- setUsernameBtn.IsEnabled = false;
-
-
- redis = await ConnectionMultiplexer.ConnectAsync("chat.redis.cache.windows.net:6380,password=ekWIDeEpdZYf3/xo551Ai6YDWQJ4SFhWGRZgYgMr6MA=,ssl=True,abortConnect=False");
-
- if (redis != null)
- {
- if (redis.IsConnected)
- {
-
- subscriber = redis.GetSubscriber();
- await subscriber.SubscribeAsync(username.Text.Trim().ToLower(), (channel, value) =>
- {
- string buffer = value;
- var message = JsonConvert.DeserializeObject<Message>(buffer);
- message.ReceivedAt = DateTime.Now;
-
-
- Dispatcher.Invoke(() =>
- {
- collection.Add(message);
- });
- });
-
-
- message.IsEnabled = true;
- message.Focus();
-
- Title += " : " + username.Text.Trim();
-
- username.IsEnabled = false;
- }
- else
- {
- MessageBox.Show("We could not connect to Azure Redis Cache service. Try again later.");
- setUsernameBtn.IsEnabled = true;
- }
- }
- else
- {
- setUsernameBtn.IsEnabled = true;
- }
- }
The basic checks are to make sure that we were able to connect to the service. If so, we then proceed and subscribe to our username. One thing to note here is that we need to check how our usernames are used as a channel.
- Trim any white spaces, to avoid any trouble of extra spaces in the channel name.
- Lower the username (or you can use Upper as well, I leave that on you!) so that you only push the messages to the same user—I consider Afzaal, AFZAAL, afzaal and aFzaal to be same users and a mistake in typing only.
So that was done using the code:
- await subscriber.SubscribeAsync(username.Text.Trim().ToLower(), ...
The code was pretty neat, and the StackExchange.Redis authors did a great job in implicitly converting RedisChannel to string and vice versa, that is why I was using a string value instead of an object representing RedisChannel object. Inside the same function, we provide a lambda that gets called when a message is published on the channel we subscribe to.
- string buffer = value;
- var message = JsonConvert.DeserializeObject<Message>(buffer);
- message.ReceivedAt = DateTime.Now;
-
-
- Dispatcher.Invoke(() =>
- {
- collection.Add(message);
- });
I used a special class, to contain the message. This class contains the properties that show,
- Who—the sender
- What—the message
- When—the time
Of the messages that the user has just received, Json.NET library was used to deserialize and serialize the message on both ends. The message type is defined as the following type,
- public class Message
- {
- public int Id { get; set; }
- public string Sender { get; set; }
- public string Content { get; set; }
- public DateTime ReceivedAt { get; set; }
- }
Pretty neat and simple. Right? The last point in that is that we are using Dispatcher.Invoke function because our subscriber listens to the messages and calls a lambda that runs on a background thread. That is why, we need to execute the code using the Dispatcher. And that is pretty much it.
Now, to send the message, I used the following code to Publish the message to the channel of the user I had shown interest in, using the @username notation.
-
- private async void sendMessageBtn_Click(object sender, RoutedEventArgs e)
- {
- var content = message.Text.Trim();
-
-
- var recipient = content.Split(' ')[0].Replace("@", "").Trim().ToLower();
-
-
- var blob = new Message();
- blob.Sender = username.Text.Trim();
- blob.Content = content;
-
-
- var received = await subscriber.PublishAsync(recipient, JsonConvert.SerializeObject(blob));
-
-
- if (received == 0)
- {
- MessageBox.Show($"Sorry, '{recipient}' is not active at the moment.");
- }
- message.Text = "";
- }
Again, this is the same code that had been talked about before. We are trying to capture the username of the person we are interested in and then we write the message on their channel. One thing to note here is, that Redis can write a string message on the channel too. We are not doing that, instead, we are writing a serialized object of our type of message. This gets deserialized on the other end and we see the message on the screen.
Also, take a look here:
- var received = await subscriber.PublishAsync(recipient,...
We are trying to capture the number of clients that received our message. If that number is zero, we are showing a message stating that the user is offline. This does not mean that the user does not exist if we had a complete platform of chat, merely that the user was not online. Have a look at this behavior here,
Figure 7: An error message showing that the recipient is offline.
These are a few of the common aspects that I wanted you guys to take a look at. Redis so far has been useful in many cases and has helped us in utilizing a framework/platform to build a complex application environment.
Run the program
Now that we have got all the code running, we can run the program and see how it works. The code has already been shown in the image above, and the problem was also shown in the image in the previous section. You can take a look at it there.
However, one thing that I want you to consider is that you must always dispose the resources you are using, and in the case of this application, there is a lot more to do than just a dispose call. You need to unsubscribe from the Redis channel too, I am doing that in the Closing event.
- private async void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
- {
-
- if (redis != null)
- {
- var sub = redis.GetSubscriber();
- await sub.UnsubscribeAsync(username.Text.Trim().ToLower());
- await redis.CloseAsync();
- }
- }
This is optional, but can help a lot. One way of help is that it can provide a result of "zero", when the user signs out of the application and other users can know that the recipient is offline. Otherwise, it is upto Redis to consider a socket closed after a while when it runs a check.
Redis or Apache Kafka
Apache Kafka is a great platform for streaming data. Apache Kafka focuses more on streaming of the messages through a queue. Queue, similarly, can have multiple subscribers and multiple publishers. The primary feature is that once a message is read it can persist in the queue in Kafka, whereas in Redis it is cleared out.
Redis also is a featured platform and supports fast message forwarding. Kafka has to manage and persist the data and thus can be forgiven for storage and persistence, but Kafka still is faster and provides a lower latency in many cases. For the speed benchmarking please consider reading this blog post, https://bravenewgeek.com/benchmarking-message-queue-latency/.
For the data persistence, Redis supports data storage, where it stores the data (caching data) to the hard drives. The channel queues are cleared as soon as a data is sent to the recipient. While I was trying out the application, I got the following chart on Azure, telling me that none of the messages were stored and each time a message was captured it was sent directly and not cached.
Figure 8: Azure portal showing the messages being missed in the channel broadcast.
I would need to replicate this scenario in Apache Kafka and see how that works, and I might consider writing a comparison scenario, but since I do not have any information on how Kafka treats this, I have less to talk about.
Consider reading this thread on SO for a bit more as well, https://stackoverflow.com/questions/37990784/difference-between-redis-and-kafka.
What's next?
Well in this article we merely scratched the complex surface of building a chat application. There are a lot of features missing from this, you can definitely check the source code on GitHub and apply some changes to it. I might also work on a few changes soon as I feel them necessary.
The grouping of the users and the signage of them to be a group member is a great feature and should be implemented as well. Besides this, there was a feature of tabs in an application I once worked on. I really like that feature, but I did not have time for that much of a complex chat application.
Apart from these, you need to think about the model Redis supports. How many connections can it support? How many channels can it support? Moreover, how many connections/channels or how many channels/connection can it provide? Also, what is the limitation of Azure in this concern, all of these questions can greatly help you out in understanding whether you should consider Redis or not. Also, take a look at the Redis or Apache Kafka section above to find a few ways in which Redis can help or when Kafka might be a better solution for you!
Lastly, this was never meant to be a great chat application. Merely a trial of Redis service, and Azure's solution to hosted Redis cache implementation. I hope this might have helped you out in a few ways, and that you learned how Redis' pub-sub works, with a demonstration. If it is unclear, reach out to me in the comments section below, or install and setup everything on your local machine and try it out.
Can't wait to see what you come up with!