使用 Websockets、jQuery 和 Spike-Engine 构建的 HTML5 实时聊天
MyChat:一个简单的实时客户端-服务器聊天应用程序,使用了炫酷的 HTML5 功能和 identicons。
引言
Web 中的一切都变得越来越实时,而 HTML5 终于提供了一些工具来构建高效、简单且健壮的 Web 实时应用程序。本文旨在演示这些功能,用于构建一个有趣且简单的客户端-服务器聊天应用程序:
- 它内部使用 WebSocket,但由 Spike-Engine 抽象,对于旧浏览器将回退到 Flash Sockets。
- 它使用 HTML5 Data URI 来渲染服务器作为字节数组流式传输的图像。
- 它使用自定义字体和 jQuery 进行动画渲染。
- 它是跨平台的,具有最小化的数据包负载和消息压缩。
- 应用程序服务器是自托管的可执行文件,而客户端只是一个纯 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 种消息类型:
JoinMyChat
:由想要加入聊天室的客户端发起。服务器会将客户端添加到房间(客户端列表)中,并生成一个 identicon 用作该客户端的头像。SendMyChatMessage
:由客户端发起,包含要发送给聊天室中所有人的字符串消息。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 日 - 初始文章。