可伸缩的 COMET 结合 ASP.NET - 第二部分






4.91/5 (17投票s)
一个演示 COMET 和 ASP.NET 可重用 API 的聊天应用程序(接续上一篇文章)。
引言
如果您阅读了我之前的文章 可伸缩的 COMET 结合 ASP.NET,那么您应该理解我想要实现的目标。我解释了 COMET 以及如何从 ASP.NET 中获得最佳的可伸缩性能;然而,我认为上一篇文章有点过于简略。它足够好地演示了技术,但实际上并没有包含任何有用的代码。所以,我想写一个 API,将上一篇文章的功能封装成一组类,可以包含在典型的 Web 项目中,让您有机会利用(并测试)这个想法。
我不会过多详细介绍线程模型,因为它在前一篇文章中已经基本涵盖;我只会介绍 API 以及如何在您的 Web 应用程序中使用它。
我决定编写一个轻量级的消息传递 API,它在消息交换方面类似于 Bayeux 协议;然而,它并不是该协议的实现,因为我认为对于让这个 API 工作来说,它过于复杂,而且它也只是一个草案。
我最初的文章提到我将制作一个井字棋游戏;不幸的是,我发现用一个简单的聊天应用程序来演示这个想法会更容易。该应用程序使用 COMET 通道接收消息,并使用 WCF 服务发送消息。
术语表
以下是我在这份文档中使用的术语列表,以及它们所代表的含义
- 通道 (Channel) - 这是 COMET 客户端可以连接的端点。发送给客户端的任何消息都必须通过通道传递。
- 超时 (Timeout) - 当客户端连接到通道一段时间后,如果没有收到消息。客户端可以在“超时”时重新连接。
- 空闲客户端 (Idle Client) - 这是客户端未连接到服务器的时间段,空闲客户端将在预设时间后被断开连接。
- 消息 (Message) - 通过通道发送给客户端的 JSON 消息。
- 已订阅 (Subscribed) - 已订阅通道的客户端。它们已连接并准备好接收消息。
核心项目
核心项目包含使 ASP.NET 应用程序支持 COMET 所需的所有类。该代码的设计在很大程度上与原始文章中的代码相似,但我扩展了功能,以支持客户端和服务器之间传输通用消息。
控制 COMET 机制的主要类是 `CometStateManager`。这个类管理您应用程序中的单个通道。这个类聚合了一个 `ICometStateProvider` 实例,该实例以特定方式为您的应用程序管理状态。在该 API 中,有一个内置的 `InProcCometStateProvider` 实现,它将状态存储在服务器内存中。显然,这对于负载均衡的环境不好,但可以实现一个自定义的提供程序,使用数据库或自定义状态服务器。
为了将您的通道暴露给外部世界,它需要被包装在一个 `IHttpAsyncHandler` 实现中。我实际上尝试使用 WCF 中的异步模型,但发现它并没有像异步处理程序那样释放 ASP.NET 工作线程,这有点遗憾,而且完全出乎意料。
下面的代码演示了如何设置一个 `IHttpAsyncHandler` 来为您的 COMET 通道提供一个端点
public class DefaultChannelHandler : IHttpAsyncHandler
{
// this is our state manager that
// will manage our connected clients
private static CometStateManager stateManager;
static DefaultChannelHandler()
{
// initialize the state manager
stateManager = new CometStateManager(
new InProcCometStateProvider());
}
#region IHttpAsyncHandler Members
public IAsyncResult BeginProcessRequest
(HttpContext context, AsyncCallback cb, object extraData)
{
return stateManager.BeginSubscribe(context, cb, extraData);
}
public void EndProcessRequest(IAsyncResult result)
{
stateManager.EndSubscribe(result);
}
#endregion
#region IHttpHandler Members
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
throw new NotImplementedException();
}
public static CometStateManager StateManager
{
get { return stateManager; }
}
#endregion
}
上面的代码非常简单。我们有一个 `CometStateManager` 的静态实例,它使用 `ICometStateProvider` 的一个实现来构造。在这个例子中,我们使用了内置的 `InProcCometStateProvider` 实现。
该类的其余实现只是将 `BeginProcessRequest` 和 `EndProcessRequest` 方法映射到我们 `CometStateManager` 实例的 `BeginSubscribe` 和 `EndSubscribe` 方法。
我们还需要 `web.config` 文件中的条目来启用该处理程序。
<add verb="POST"
path="DefaultChannel.ashx"
type="Server.Channels.DefaultChannelHandler, Server" />
就这样,通道现在就可以由客户端订阅了。
CometClient 类
通道需要跟踪客户端,每个客户端在某种缓存中都由 `CometClient` 类的一个实例表示。我们不希望任何不认识的客户端连接到服务器或订阅通道而没有任何身份验证机制,因此我们将实现一个身份验证机制,可能是标准的 ASP.NET 登录表单,或者可能是一个 WCF 调用到一个可以验证某些凭据然后初始化通道中客户端的服务。
下面的代码显示了包含的聊天应用程序中 `default.aspx` 文件的登录操作
protected void Login_Click(object sender, EventArgs e)
{
try
{
DefaultChannelHandler.StateManager.InitializeClient(
this.username.Text, this.username.Text, this.username.Text, 5, 5);
Response.Redirect("chat.aspx?username="
+ this.username.Text);
}
catch (CometException ce)
{
if (ce.MessageId == CometException.CometClientAlreadyExists)
{
// ok the comet client already exists, so we should really show
// an error message to the user
this.errorMessage.Text =
"User is already logged into the chat application.";
}
}
}
我们不验证密码或其他任何东西,我们只是直接从页面获取用户名并用它来标识我们的客户端。COMET 客户端有两个由 API 消费者提供的令牌
- `PrivateToken` - 这是客户端私有的令牌,用于订阅该客户端的消息。
- `PublicToken` - 这是用于将客户端标识给其他客户端的令牌。通常在向特定客户端发送消息时使用。
我们使用公共和私有令牌的原因是,私有令牌可以用来订阅通道并接收该用户的消息。我们不希望任何其他客户端能够这样做,除了原始客户端(例如,我们不希望消息被伪造!)。出于这个原因,如果我们想在客户端之间发送消息,我们会使用公共令牌。
我还包括了一个名为 `DisplayName` 的客户端属性,可以用来存储用户名;这只是为了简单起见。
要在通道中设置客户端,您需要调用 `InitializeClient`。如上所示。此方法接受以下参数
- `publicToken` - 客户端的公共令牌
- `privateToken` - 客户端的私有令牌
- `displayName` - 客户端的显示名称
- `connectionTimeoutSeconds` - 已连接客户端等待消息的秒数,直到它响应超时消息
- `connectionIdleSeconds` - 服务器等待客户端重新连接的秒数,然后才会杀死空闲客户端
在上面的例子中,调用了 `InitializeClient`,并将表单中的用户名指定为 `publicToken`、`privateToken` 和 `displayName`。虽然这不太安全,但对于示例来说已经足够了。为了使其更安全,我本可以为 `privateToken` 生成一个 GUID,并将公共令牌保留为用户名。
`InitializeClient` 调用将通过一个新初始化的 `CometClient` 类调用 `ICometStateProvider.InitializeClient`,并期望它将其存储在缓存中。
当 `CometClient` 现在在通道中可用时,客户端可以使用其 `privateToken` 订阅该通道。
客户端 JavaScript
为了启用客户端功能,核心项目中的 WebResource `Scripts/AspNetComet.js` 包含订阅通道所需的所有 JavaScript(以及一个来自 这里的公共域 JSON 解析器)。为了方便起见,我在 `CometStateManager` 上包含了一个名为 `RegisterAspNetCometScripts` 的静态方法,它接受一个 `Page` 作为参数并在该页面上注册脚本。
protected void Page_Load(object sender, EventArgs e)
{
CometStateManager.RegisterAspNetCometScripts(this);
}
有了这个调用,我们就可以自由地使用可用的非常基本的客户端 API 了。下面的示例摘自 Web 项目中的 `chat.aspx`,它展示了在初始化客户端后如何订阅特定通道。
var defaultChannel = null;
function Connect()
{
if(defaultChannel == null)
{
defaultChannel =
new AspNetComet("/DefaultChannel.ashx",
"<%=this.Request["username"] %>",
"defaultChannel");
defaultChannel.addTimeoutHandler(TimeoutHandler);
defaultChannel.addFailureHandler(FailureHandler);
defaultChannel.addSuccessHandler(SuccessHandler);
defaultChannel.subscribe();
}
}
客户端 API 的所有功能都封装在一个名为 `AspNetComet` 的 JavaScript 类中。此类的一个实例用于跟踪已连接客户端的状态。订阅所需的一切就是 COMET 端点处理程序的 URL、`CometClient` 的 `privateToken` 以及用于在客户端上标识通道的别名。一旦我们构造了一个 `AspNetComet` 实例,我们就设置了一系列在 COMET 生命周期特定时间调用的处理程序。
- `addTimeoutHandler` - 添加一个处理程序,当客户端等待预设时间但未收到消息时调用。
- `addFailureHandler` - 添加一个在 COMET 调用失败时调用的处理程序;失败的例子包括 COMET 客户端未被识别。
- `addSuccessHandler` - 为发送给客户端的每条消息调用一个处理程序。
以下代码显示了每个处理程序方法的签名
function SuccessHandler(privateToken, channelAlias, message)
{
// message.n - This is the message name
// message.c - This is the message contents
}
function FailureHandler(privateToken, channelAlias, errorMessage)
{
}
function TimeoutHandler(privateToken, channelAlias)
{
}
`SuccessHandler` 的 `message` 参数是 `CometMessage` 类的一个实例。下面的代码显示了该类及其 JSON 合同
[DataContract(Name="cm")]
public class CometMessage
{
[DataMember(Name="mid")]
private long messageId;
[DataMember(Name="n")]
private string name;
[DataMember(Name="c")]
private object contents;
/// <summary>
/// Gets or Sets the MessageId, used to track
/// which message the Client last received
/// </summary>
public long MessageId
{
get { return this.messageId; }
set { this.messageId = value; }
}
/// <summary>
/// Gets or Sets the Content of the Message
/// </summary>
public object Contents
{
get { return this.contents; }
set { this.contents = value; }
}
/// <summary>
/// Gets or Sets the error message if this is a failure
/// </summary>
public string Name
{
get { return this.name; }
set { this.name = value; }
}
}
发送消息
在聊天 Web 应用程序中,我包含了一个 AJAX 启用的 WCF Web 服务,它充当聊天应用程序“发送消息”功能的端点。下面的代码显示了发送消息按钮单击事件的客户端处理程序
function SendMessage()
{
var service = new ChatService();
service.SendMessage(
"<%=this.Request["username"] %>",
document.getElementById("message").value,
function()
{
document.getElementById("message").value = '';
},
function()
{
alert("Send failed");
});
}
该代码构造了一个由 ASP.NET Web 服务框架创建的 `ChatService` 客户端对象的一个实例,然后只需调用 `SendMessage` 方法,并将客户端的 `privateToken` 和他们的消息传递过去。
`SendMessage` 的服务器端代码然后接收参数,并将消息写入所有客户端;下面的代码对此进行了演示
[OperationContract]
public void SendMessage(string clientPrivateToken, string message)
{
ChatMessage chatMessage = new ChatMessage();
//
// get who the message is from
CometClient cometClient =
DefaultChannelHandler.StateManager.GetCometClient(clientPrivateToken);
// get the display name
chatMessage.From = cometClient.DisplayName;
chatMessage.Message = message;
DefaultChannelHandler.StateManager.SendMessage(
"ChatMessage", chatMessage);
// Add your operation implementation here
return;
}
此方法根据私有令牌查找 `CometClient`,然后创建一个 `ChatMessage` 对象,该对象用作发送给每个连接客户端的消息内容,通过 `CometStateManager` 实例上的 `SendMessage` 方法。这将触发连接到 `chat.aspx` 中 `SuccessHandler` 方法回调的任何客户端,该方法将消息写入页面上的聊天区域。
function SuccessHandler(privateToken, alias, message)
{
document.getElementById("messages").innerHTML +=
message.c.f + ": " + message.c.m + "<br/>";
}
Using the Code
解决方案中包含的网站无需任何配置更改即可运行,只需连接几个客户端到应用程序并使用所需的用户名登录,然后进行聊天。消息应实时接收,并立即显示给每个用户。
使用该 API 将使您能够在 AJAX 启用的应用程序中使用 COMET 风格的方法。使用 WCF 对于向服务器发送消息可能很方便,所有这些都会自动为您整齐地包装起来,然后只需回调到 COMET 通道上的已连接客户端。
历史
- 2008 年 7 月 11 日 - 创建。