Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML

HTML5 Real-Time Chat with Websockets, jQuery, and Spike-Engine

4.90/5 (40 votes)
23 Jun 2015CPOL3 min read 199.7K   8.6K  
MyChat: a simple real-time client-server chat using fancy HTML5 features and identicons.

Introduction

Everything in the web becomes increasingly real-time and HTML5 finally offers some facilities to build efficient, simple and robust real-time applications on the web. This article serves as a demonstration of such facilities in building a fun and simple client-server chat: 

  1. It uses websockets internally, but abstracted by Spike-Engine, will fallback to flash sockets for older browsers.
  2. It uses HTML5 Data URI for rendering images streamed from the server as byte arrays.
  3. It uses custom fonts and jQuery for animated rendering.
  4. It is cross-platform and with a minimized packet payload and message compression.
  5. The application server is a self-hosted executable and the client is just a plain html file.

Image 1

[view a live demo]  

 Client-Server Protocol    

First of all, we need to define a communication protocol between. This represents the messages which are exchanged between the client and the server. Spike-Engine simplifies the networking implementation and allows expressing this protocol declaratively via an XML file. This part is quite straightforward, check the user guide on their website for more info. 

XML
<?xml version="1.0" encoding="UTF-8"?>
<Protocol Name="MyChatProtocol" xmlns="http://www.spike-engine.com/2011/spml" 
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <Operations>

    <!-- Simple join chat operation that puts the client to the chat room -->
    <Operation Name="JoinMyChat"
               SuppressSecurity="true" />

    <!-- Simple send message operation that broadcast the message to the chat room -->
    <Operation Name="SendMyChatMessage"
               SuppressSecurity="true"
               Compression="Incoming">
      <Incoming>
        <Member Name="Message" Type="String" />
      </Incoming>
    </Operation>

    <!-- A push operation (server to client) that sends the messages to the clients -->
    <Operation Name="MyChatMessages"
               SuppressSecurity="true"
               Compression="Outgoing"
               Direction="Push">
      <Outgoing>
        <Member Name="Avatar" Type="ListOfByte" />
        <Member Name="Message" Type="String" />
      </Outgoing>
    </Operation>
    
  </Operations>
</Protocol> 

Essentially, we've declared 3 message types:

  1. JoinMyChat: initiated by the client who wants to join the chat room. Server will add the client to the room (a list of clients) and generate an identicon to be used as an avatar for that client.
  2. SendMyChatMessage: initiated by the client and contains a string message to send to everyone in the chat room.
  3. MyChatMessages: initiated by the server, sends the avatar and the message to everyone in the room. This is an event of type push, it is pushed to the clients. More info: http://en.wikipedia.org/wiki/Push_technology

Because we are using Spike-Engine to abstract our websockets/flashsockets communication, the network layer is automatically generated, similarly to web services. I won't go into the code generation part here, as a step-by-step explanation can be found here: http://www.spike-engine.com/manual-client and is out of the scope of this article.

Identicons and HTML5 Data URI 

In our chat application, I wanted to use avatars, but I didn't want the users to log-in or chose one, it had to be automatically generated. But not randomly, same IP address should have the same avatar, identifying the person even if he disconnects/reconnects or uses a different computer to chat. Identicons are the right answer for that job.  An Identicon is a visual representation of a hash value, usually of an IP address, that serves to identify a user of a computer system as a form of avatar while protecting the users' privacy. 

Image 2

How to generate them? Jeff Atwood, from stackoverflow wrote a port of such library: http://www.codinghorror.com/blog/2007/01/identicons-for-net.html. And I used their open-sourced library to do this: http://identicon.codeplex.com, with some small alterations. The rendering is straightforward. 

C#
public byte[] Render(IClient client)
{
    using (var stream = new MemoryStream())
    {
        // Get the code from the ip address
        var code = IdenticonUtil.Code(client.Channel.Address.ToString());

        // Generate the identicon
        var image = this.Render(code, 50);

        // Save as png
        image.Save(stream, ImageFormat.Png);
        return stream.ToArray();
    }
} 

The interesting part is that on each message, we want to send the avatar of the user, represented as a byte[]. But how do we actually render it once the JavaScript client received the array? The answer is the new HTML5 feature called Data URI. 

"The data URI scheme is a URI scheme (Uniform Resource Identifier scheme) that provides a way to include data in-line in web pages as if they were external resources. This technique allows normally separate elements such as images and style sheets to be fetched in a single HTTP request rather than multiple HTTP requests, which can be more efficient." - From Wikipedia's entry on Data URI

The following code snipped shows how we render the byte array received by javascript and convert it to a data uri. The final step is just assigning that URI to an <img src=''> element.

JavaScript
// Get the bytes of the image and convert it to a BASE64 encoded string and then
// we use data URI to add dynamically the image data
var avatarUri = "data:image/png;base64," + avatar.toBase64();  

Appendix A: Server-Side Implementation 

For the sake of completeness of this article, here is the full server-side implementation of the chat, exluding the identicon file. 

C#
public static class MyChatImpl
{
    /// <summary>
    /// A public static function, when decorated with InvokeAt attribute will be
    /// invoked automatically by Spike Engine. This particular one will be invoked
    /// when the server is initializing (only once).
    /// </summary>
    [InvokeAt(InvokeAtType.Initialize)]
    public static void Initialize()
    {
        // First we need to hook the events while the server is initializing, 
        // giving us the ability to listen on those events.
        MyChatProtocol.JoinMyChat += new RequestHandler(OnJoinChat);
        MyChatProtocol.SendMyChatMessage += new RequestHandler<SendMyChatMessageRequest>(OnSendMessage);

        // Since we will add the clients in the chat, we need to remove them from
        // the chat room once they are disconnected. 
        Service.ClientDisconnect += new ClientDisconnectEventHandler(OnClientDisconnect);
    }


    /// <summary>
    /// A list of all clients currently in the chat. This is a concurrent list that 
    /// helps us to avoid most of the concurrency issues as many clients could be 
    /// added/removed to the list simultaneously.
    /// </summary>
    public static ConcurrentList<IClient> PeopleInChat = new ConcurrentList<IClient>();


    /// <summary>
    /// Static method that is ivoked when a client sends a message to the server.
    /// </summary>
    static void OnSendMessage(IClient client, SendMyChatMessageRequest packet)
    {
        // Validate the message
        var message = packet.Message;
        if (message == null || message == String.Empty || message.Length > 120)
            return;

        // We loop through all people in the chat and we broadcast them
        // the incoming message.
        foreach (var person in PeopleInChat)
        {
            // Send the message now
            person.SendMyChatMessagesInform(
                (byte[])client["Avatar"], // The avatar of the client who sends the message
                message            // The message to be sent
                );
                
        }
    }

    /// <summary>
    /// Static method that is ivoked when a client decides to join the chat.
    /// </summary>
    static void OnJoinChat(IClient client)
    {
        // We add the person to the chat room
        if (!PeopleInChat.Contains(client))
        {
            // Generate the avatar for the client
            client["Avatar"] = IdenticonRenderer.Create(client);

            // Add the person in the room
            PeopleInChat.Add(client);

            // Say something nice
            SayTo(client, "Hi, I'm the author of this sample, hope you like it! 
              Just type a message below and see how it works on CodeProject.");
        }
    }

    /// <summary>
    /// Static method that is invoked when a client is disconnected.
    /// </summary>
    static void OnClientDisconnect(ClientDisconnectEventArgs e)
    {
        // Remove the client from the list if he's 
        // disconnected from the chat
        if (PeopleInChat.Contains(e.Client))
            PeopleInChat.Remove(e.Client);
    }

    /// <summary>
    /// Author says something to a particular client in the chat room.
    /// </summary>
    static void SayTo(IClient client, string message)
    {
        client.SendMyChatMessagesInform(Resources.Author, message);
    }

}

Appendix B: Client-Side Implementation   

The client-side implementation is a bit more tricky, as there's some JQuery for visualisation and css files. Everything is included in the source package attached to this codeproject article. 

JavaScript
<script>
    var bubbles = 1;
    var maxBubbles = 8;
    var server;

    function sendMessage() {
        server.sendMyChatMessage($("#msgText").val());
        $("#msgText").val("");
    }

    function addBubble(avatar, text) {

        // Get the bytes of the image and convert it to a BASE64 encoded string and then
        // we use data URI to add dynamically the image data
        var avatarUri = "data:image/png;base64," + avatar.toBase64();

        var bubble = $('<div class="bubble-container"><span ' + 
          'class="bubble"><img class="bubble-avatar" src="' + 
          avatarUri + '" /><div class="bubble-text"><p>' + text + 
          '</p></div><span class="bubble-quote" /></span></div>');
        $("#msgText").val("");

        $(".bubble-container:last")
            .after(bubble);

        if (bubbles >= maxBubbles) {
            var first = $(".bubble-container:first")
                .remove();
            bubbles--;
        }

        bubbles++;
        $('.bubble-container').show(250, function showNext() {
            if (!($(this).is(":visible"))) {
                bubbles++;
            }

            $(this).next(".bubble-container")
                .show(250, showNext);

            $("#wrapper").scrollTop(9999999);
        });
    }

    // On page loaded and ready
    $(window).load(function () {

        // First we need to create a server channel on the given URI, in a form http://IP:PORT
        // For your local test you might try http://127.0.0.1:8002 (or a different IP address/port)
        server = new spike.ServerChannel('http://127.0.0.1:8002');

        // When the browser is connected to the server, we show that we are connected to the user
        // and provide a transport name (websockets, flashsockets etc.).
        server.on('connect', function () {
            // Once connected, we need to join the chat
            server.joinMyChat();

        });


        // Here we hook the room messages inform event so we know when
        // the server sends us the messages. We need to show them properly in the text area.
        server.on('myChatMessagesInform', function (p) {
            addBubble(p.avatar, p.message);
        });

    });

</script> 

History 

  • 23/06/2015 - Source code & article updated to Spike v3
  • 14/09/2013 - Updated source code references & links. 
  • 16/08/2013 - Live demo link. 
  • 15/08/2013 - Initial article.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)