65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Websockets、jQuery 和 Spike-Engine 构建的 HTML5 实时聊天

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.90/5 (40投票s)

2013年8月15日

CPOL

3分钟阅读

viewsIcon

200498

downloadIcon

8603

MyChat:一个简单的实时客户端-服务器聊天应用程序,使用了炫酷的 HTML5 功能和 identicons。

引言

Web 中的一切都变得越来越实时,而 HTML5 终于提供了一些工具来构建高效、简单且健壮的 Web 实时应用程序。本文旨在演示这些功能,用于构建一个有趣且简单的客户端-服务器聊天应用程序: 

  1. 它内部使用 WebSocket,但由 Spike-Engine 抽象,对于旧浏览器将回退到 Flash Sockets。
  2. 它使用 HTML5 Data URI 来渲染服务器作为字节数组流式传输的图像。
  3. 它使用自定义字体和 jQuery 进行动画渲染。
  4. 它是跨平台的,具有最小化的数据包负载和消息压缩
  5. 应用程序服务器是自托管的可执行文件,而客户端只是一个纯 HTML 文件

[查看实时演示]  

 客户端-服务器协议    

首先,我们需要定义客户端和服务器之间的通信协议。这代表了客户端和服务器之间交换的消息。Spike-Engine 简化了网络实现,并允许通过 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> 

基本上,我们声明了 3 种消息类型:

  1. JoinMyChat:由想要加入聊天室的客户端发起。服务器会将客户端添加到房间(客户端列表)中,并生成一个 identicon 用作该客户端的头像。
  2. SendMyChatMessage:由客户端发起,包含要发送给聊天室中所有人的字符串消息。
  3. MyChatMessages:由服务器发起,将头像和消息发送给房间中的所有人。这是一种推送类型的事件,它会被推送到客户端。更多信息: http://en.wikipedia.org/wiki/Push_technology

由于我们使用 Spike-Engine 来抽象我们的 websockets/flashsockets 通信,网络层会自动生成,类似于 Web 服务。我在这里不深入代码生成部分,因为可以在此处找到分步说明:http://www.spike-engine.com/manual-client,这超出了本文的范围。

Identicons 和 HTML5 Data URI 

在我们的聊天应用程序中,我想要使用头像,但不想让用户登录或选择头像,它应该自动生成。但不是随机生成,相同的 IP 地址应该具有相同的头像,即使用户断开/重新连接或使用不同的计算机聊天,也能识别用户。Identicons 是最适合这项工作的解决方案。 Identicon 是哈希值(通常是 IP 地址)的视觉表示,它作为头像用于标识计算机系统的用户,同时保护用户隐私。 

如何生成它们?来自 stackoverflow 的 Jeff Atwood 编写了该库的一个端口:http://www.codinghorror.com/blog/2007/01/identicons-for-net.html。我使用了他们的开源库来实现这一点: http://identicon.codeplex.com,并进行了一些小的修改。渲染非常直接。 

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();
    }
} 

有趣的是,在每条消息中,我们都希望发送用户的头像,它表示为 byte[]。但是,一旦 JavaScript 客户端收到数组,我们如何实际渲染它?答案是名为 Data URI 的新 HTML5 功能。 

“data URI 方案是一种 URI 方案(统一资源标识符方案),它提供了一种将数据内联到网页中的方法,就像它们是外部资源一样。这种技术允许通常独立存在的元素(如图像和样式表)通过一次 HTTP 请求而不是多次 HTTP 请求来获取,这可能更有效。” - 来自维基百科关于 Data URI 的条目。 

以下代码片段展示了我们如何渲染 JavaScript 收到的字节数组并将其转换为数据 URI。最后一步是将其分配给 <img src=''> 元素。

// 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();  

附录 A:服务器端实现 

为了使本文完整,这里是聊天完整的服务器端实现,不包括 identicon 文件。 

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);
    }

}

附录 B:客户端实现  

客户端实现有点棘手,因为有 JQuery 用于可视化和 CSS 文件。所有内容都包含在此 codeproject 文章附带的源代码包中。 

<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> 

历史 

  • 2015 年 6 月 23 日 - 源代码和文章已更新为 Spike v3
  • 2013 年 9 月 14 日 - 更新了源代码引用和链接。 
  • 2013 年 8 月 16 日 - 实时演示链接。 
  • 2013 年 8 月 15 日 - 初始文章。
© . All rights reserved.