使用ASP.NET 2.0构建基于AJAX的Web聊天应用程序






4.41/5 (13投票s)
ASP .NET 聊天应用程序
引言
本文将为您介绍一个基于著名的开源AjaxPro.NET库的Web聊天应用程序。在这里,我们将构建一个类似著名MSN Messenger的多用户聊天应用程序。我们将大量使用JavaScript编程(这是由AjaxPro.NET Framework决定的,它支持通过客户端JavaScript访问.NET库),运行在ASP.NET 2.0和SQL Server Express Edition环境下。
测试环境
- Windows XP Professional(已安装IIS)
- Visual Studio 2005(加上SQL Server 2005 Express Edition)
- AjaxPro.NET Framework
此外,您需要熟悉ASP.NET 2.0 C#编程、SQL Server编程和JavaScript+ DOM。为了跟上示例进度,您应该下载本文附带的源代码文件(参见文章末尾的下载URL)。
本文中的名为AjaxProChat的示例项目将实现以下基本目标。
- 实时查看在线聊天好友
- 即时发送消息给在线好友
- 实时接收消息并弹出类似于MSN的友好提示窗口
- 在在线和离线状态之间切换
因此,我们可以很容易地描述这个聊天应用程序的总体流程图,如图1所示。

毫无疑问,关键在于实时功能——实时状态切换、即时发送和接收消息等。使用传统的Web技术处理这些会遇到很多困难,但现在AJAX派上了用场——所有的问题都可以迎刃而解。现在,让我们卷起袖子,从数据库设计开始。
数据库设计
在介绍了总体目标之后,我们可以将注意力转移到数据库设计上。
在本例中,我们将设计一个名为AjaxProChat的数据库,其中包含三个表——users用于存储用户信息,message用于存储详细的聊天信息,global_info用于记录users表中的更改时间。
表设计
由于本文源代码文件提供了完整的数据库,我将省略相关的CREATE TABLE SQL脚本。所以,让我们直接深入到表结构。以下三个表分别给出其相应的结构定义和字段说明。
表1:Users表结构
字段名 | 类型 | 注释 |
用户名 | varchar(50) | NOT NULL, Primary Key 用户登录名 |
nickname | varchar(50) | Can be null 上面定义的用户名关联的昵称 |
密码 | varchar(50) | NOT NULL |
状态 | int | NOT NULL, DEFAULT (0) 用户登录状态—0:离线;1:在线;2:隐身 |
logintime | datetime | Can be NULL 用户上次登录的日期时间 |
logouttime | datetime | Can be NULL 用户上次登出的日期时间 |
last_msg_id | int | NOT NULL, DEFAULT (0) 用户的最后一条接收消息ID |
表2:Message表结构
字段名 | 类型 | 注释 |
msg_id | int | IDENTITY (1,1) NOT NULL, Primary Key 自动递增的消息ID |
sender | varchar(50) | NOT NULL 发送者的用户名 |
receiver | varchar(50) | NOT NULL 接收者的用户名 |
content | varchar(255) | NOT NULL 相应的聊天内容 |
sendtime | datetime | NOT NULL 发送消息的日期时间 |
表3:global_info表结构
字段名 | 类型 | 注释 |
UserChanged | int | NOT NULL, DEFAULT (0) |
除了这些表,我们还在users表中定义了一个触发器。这意味着,当users表中的记录发生插入、删除或更新时,触发器将被调用,将global_info表中UserChanged字段加1。
存储过程设计
为了提高效率并简化数据库访问,我们在AjaxProChat数据库中设计了五个存储过程。下面我们列出它们及其相应的功能。
- GetNewMessage—获取符合指定条件的最新的消息
- GetRecentMsg—返回两个指定用户之间最近的聊天信息
- SendMessage—将发送的消息(发送和接收的)保存为新记录到数据库
- UserLogin—登录系统—成功返回零,否则返回1
- UserLogout—执行UserLogin的相反操作
由于您将踏上漫长的旅程,并且我已经为您提供了带注释的源代码供下载,因此在此不再列出它们的SQL脚本。但随着故事的进展,您将很快得出结论:几乎所有的.aspx页面都充满了如图2所示的调用路径。
详细解决方案
在开始使用AjaxPro.NET之前,让我先做一个简要介绍。
AjaxPro.NET是一个知名的开源框架,它基于服务器端技术,并支持构建不同版本的.NET Web应用程序。该框架通过多种方式支持服务器端的.NET SDK,通过客户端JavaScript。它可以将JavaScript请求定向到相关的服务器端.NET方法,然后服务器将新生成的特殊JavaScript返回给浏览器。其主要功能如下:
- 从客户端JavaScript访问Session和Application数据
- 缓存所需结果
- 自由使用源代码
- 在框架中添加和修改新的方法和属性,而无需修改任何源代码
- 所有类都支持客户端JavaScript返回数据,并且可以在JavaScript内部使用DataSet
- 使用HTML控件访问和返回数据
- 无需重新加载页面,使用事件委托访问数据
- 只提供一个调用方法,显著降低CPU使用率
现在,要在我们的ASP.NET 2.0 Web应用程序中使用AjaxPro.NET,我们需要正确配置web.config文件。由于下载的资料已经附带了指南和一个优秀的Visual Studio插件—AjaxProVSTemplate.vsi,我们将不再过多讨论此框架的用法,而是专注于主要主题。
在此演示应用程序中,我们主要提供了三个网页—login.aspx、main.aspx和chatroom.aspx。但是,首先有一个小技巧值得一提。
Index.html—一个代理
为了隐藏我们聊天页面浏览器菜单栏和工具栏,我们使用了一些技巧——构建一个临时的代理页面index.html。下面列表1中的客户端JavaScript展示了如何实现。
列表 1
<script language="javascript">
//open a new window
window.open("Login.aspx", "_blank", "location=no;menubar=no;status=no;toolbar=no");
//close the parent window
Close();
// close the window without any prompt
function Close()
{
var ua = navigator.userAgent;
var ie = navigator.appName == "Microsoft Internet Explorer" ? true : false;
if (ie)
{
var IEversion = parseFloat(ua.substring(ua.indexOf("MSIE ") + 5, ua.indexOf
(";", ua.indexOf("MSIE "))))if (IEversion < 5.5)
{
var str = '<object id=noTipClose classid=
"clsid:ADB880A6-D8FF-11CF-9377-00AA003B7A11">';
str += '<param name="Command" value="Close"></object>';
document.body.insertAdjacentHTML("beforeEnd", str);
document.all.noTipClose.Click();
}
else
{
window.opener = null;
window.close();
}
}
else
{
window.close()
}
}
</script>
Login.aspx
接下来是普通的登录页面login.aspx,图3展示了它的初次印象。
该网页是此Web版本应用程序的入口点,它仅调用前面描述的UserLogin存储过程来完成系统登录。这个过程是一个典型的例子,我们不在此详述,但有一点应该注意:如果输入的用户名不为空,并且数据库中没有旧的用户名,那么我们将直接存储它,无论其相关的密码是否为空。更多细节请查阅存储过程的SQL脚本。接下来,我们将进入login.aspx.cx文件中的关键代码。
列表2:登录页面的典型ASP.NET 2.0服务器端数据库连接编程
public partial class login: System.Web.UI.Page { private string myConnectionString = ConfigurationManager.ConnectionStrings[ "msn_Data_ConnStr"].ConnectionString; protected System.Data.SqlClient.SqlConnection sqlConnection; protected System.Data.SqlClient.SqlCommand sqlCommand; protected void Page_Load(object sender, EventArgs e){} protected override void OnInit(EventArgs e) { InitializeComponent(); base.OnInit(e); } private void InitializeComponent() { this.sqlConnection = new SqlConnection(myConnectionString); this.sqlCommand = new System.Data.SqlClient.SqlCommand(); this.sqlCommand.CommandText = "dbo.[UserLogin]"; this.sqlCommand.CommandType = System.Data.CommandType.StoredProcedure; this.sqlCommand.Connection = this.sqlConnection; //specify the parameters of the stored procedure this.sqlCommand.Parameters.Add(new System.Data.SqlClient.SqlParameter( "@RETURN_VALUE", System.Data.SqlDbType.Int, 4, System.Data.ParameterDirection.ReturnValue, false, ((System.Byte)(0)), ( (System.Byte)(0)), "", System.Data.DataRowVersion.Current, null)); this.sqlCommand.Parameters.Add(new System.Data.SqlClient.SqlParameter( "@username", System.Data.SqlDbType.VarChar, 50)); this.sqlCommand.Parameters.Add(new System.Data.SqlClient.SqlParameter( "@password", System.Data.SqlDbType.VarChar, 50)); this.sqlCommand.Parameters.Add(new System.Data.SqlClient.SqlParameter( "@status", System.Data.SqlDbType.Int, 4)); } protected void btnLogin_Click(object sender, EventArgs e) { int iRet = - 1; sqlCommand.Parameters["@username"].Value = tbUsername.Text; sqlCommand.Parameters["@password"].Value = tbPassword.Text; sqlCommand.Parameters["@status"].Value = Convert.ToInt32 (ddlStatus.SelectedValue); try { sqlConnection.Open(); sqlCommand.ExecuteNonQuery(); iRet = Convert.ToInt32(sqlCommand.Parameters["@RETURN_VALUE"].Value); } catch (SqlException) { Response.Write("<script>alert('Here,in SqlException');</script>"); //merely for debugging purpose } finally { sqlConnection.Close(); } if (iRet == 0) { FormsAuthentication.RedirectFromLoginPage(tbUsername.Text, false); Response.Redirect("Main.aspx"); } if (iRet == 2) { FormsAuthentication.RedirectFromLoginPage(tbUsername.Text, false); Response.Redirect("Main.aspx"); } else { lblMessage.Text = "iRet =" + iRet.ToString() + "The login failed, please check out your password!"; } } }
Main.aspx—主控页
现在,让我们关注聊天系统的主要控制页面main.aspx,其运行时快照如图4所示。
从开发者的角度来看,我们可以将此部分分为三个不同的组件:登录状态切换、用户列表查看和最新消息提示。所以,让我们逐一深入了解它们内部的工作原理。
登录状态切换
在主页面的顶部显示当前用户名及其登录状态。用户可以通过ListBox控件轻松地在“在线”和“离线”之间切换。当用户单击“退出”按钮时,用户将注销,控件将切换回初始登录页面。现在,让我们检查相关的内部逻辑。首先,我们在服务器端定义了一个Ajax方法—SetUserStatus,它根据传递的参数—strUsername和iStatus直接更新users表中的用户登录状态。列表3给出了相应的代码。
列表 3
[AjaxPro.AjaxMethod]
public int SetUserStatus(string strUsername, int iStatus) {
//hold the returned value
int iRet = 0;
//Obtain the database connection string from the file web.config and set up the connection
string strConnection = ConfigurationManager.ConnectionStrings["msn_Data_ConnStr"].ConnectionString;
SqlConnection conn = new SqlConnection(strConnection);
//create a new SQLCommand object
SqlCommand cmd = conn.CreateCommand();
cmd.CommandText = string.Format(
"UPDATE users SET status = {0} WHERE username='{1}'",
iStatus, strUsername
);
try{
//open the data connection
conn.Open();
//execute the SQL command-- set the user's status
iRet = cmd.ExecuteNonQuery();
}
catch (SqlException){}
finally
{
//close the database connection
conn.Close();
}
return iRet;
}
接下来是相关的客户端函数setUserStatus,它将调用服务器端方法setUserStatus来更新当前用户登录状态,如列表4所示。
列表4:客户端JavaScript方法调用服务器端Ajax方法
Collapse Copy Codefunction setUserStatus()
{
//user name
var username = el("lblUsername").innerText;
//status
var status = el("user_status").value;
//call the relevant server-side Ajax method
AjaxProChat.SetUserStatus(username, status);
}
至于注销,这是通过常规的ASP.NET事件响应机制在按钮Logout相关的事件处理程序lblExit_Click中执行的。这里我们省略了代码列表。
查看用户列表
请注意,在本例中,我们采用了实时查询数据库的操作来列出所有在线用户。因此,我们必须认真权衡用户实时特性与服务器端负载之间的成本。显然,如果查询数据库的间隔越短,应用程序的实时特性就越好,但这会给服务器带来更重的负担。然而,如果间隔越长,服务器负载就越轻,客户端的实时特性也会相对较差。所以,我们这里使用了一个小技巧:在数据库中添加一个标志—global_info表中的UsersChanged字段,用于记录users表更改的次数。因此,在代码中,我们可以实时判断该标志是否已更改,以决定是否向服务器端发送请求来更新在线用户列表。这里,为了简洁起见,我们选择省略简单的服务器端代码,而为客户端编程留出空间。
列表 5
Collapse Copy Code//update the users list
function refreshUserlist()
{
//get the value of field UsersChanged from inside table users on the server side
var changed = AjaxProChat.StatusChanged().value;
//if table users has changed
if (changed > changedTimes)
{
//write down the current value of field UsersChanged
changedTimes = changed;
//obtain the DataTable object to hold the users info
var arUserlist = AjaxProChat.GetOnlineUserList().value.Tables[0];
//the <div> object to show the users list
var divUserlist = el("userlist");
//remove the old contents
while (divUserlist.childNodes.length > 0)
{
divUserlist.removeChild(divUserlist.childNodes[0]);
}
//show the users list
for (var i = 0; i < arUserlist.Rows.length; i++)
{
//the login user name
var username = arUserlist.Rows[i].username;
//nickname
var nickname = arUserlist.Rows[i].nickname;
//create a <div> object for showing one user message
var result = document.createElement("div");
//set the cursor shape hand-like
result.style.cursor = "pointer";
//inner patches
result.style.padding = "2px 0px 2px 0px";
//mouse click handler--open the chat room window
result.onclick = function()
{
//if the chat room window has not been opened
if (!sendWindow)
{
// open the chat room window--note the passed parameters
window.showModelessDialog("chatroom.aspx?username=" + username,
window, "help:no;unadorned:yes;edge:sunken;resizable:yes;status:no")
;
}
};
//the texts for the user name and the nickname are to be shown inside the <span> object
var result1 = document.createElement("span");
//set the mouse in-and-out effect
result1.onmouseover = function()
{
this.style.color = "#205288";
};
result1.onmouseout = function()
{
this.style.color = "#000000";
};
//set show style
result1.style.textAlign = "left";
result1.style.fontWeight = "bold";
result1.style.fontFamily = "Arial, Verdana";
//show the user name plus the nickname
result1.innerHTML = username + " (" + nickname + ")";
//attach the <div> object to DOM
result.appendChild(result1);
divUserlist.appendChild(result);
}
}
}
由于我们在重要的行上添加了许多注释,我们只总结一下总体流程:通过Ajax方法—AjaxProChat.GetOnlineUserList()获取数据库中的最新用户列表;依次填充用户名和相关昵称。从上面可以看出,我们使用了许多围绕DOM对象—div和span的精巧JavaScript编程。
类似MSN的弹出窗口
为了方便接收新消息,我们设计了一个小巧友好的类似MSN的提示窗口。

main.aspx页面负责实时检查是否有新消息进来;如果有,则弹出这个小的提示窗口,当前用户可以单击其中的链接进行回复。与主页面的功能一样,完成此提示操作也需要两个步骤——服务器端和客户端。首先,我们定义了一个Ajax方法mainGetNewMessage,它用于获取最新聊天消息的DataSet。我们可以从列表6中看到ASP.NET 2.0中典型的数据库连接。
列表6:服务器端方法mainGetNewMessage的代码
Collapse Copy Code[AjaxPro.AjaxMethod]
public DataSet mainGetNewMessage()
{
DataSet ds = new DataSet();
SqlConnection conn = new SqlConnection
(ConfigurationManager.ConnectionStrings["msn_Data_ConnStr"].ConnectionString);
SqlCommand cmd = conn.CreateCommand();
cmd.CommandText = string.Format("GetNewMessage '{0}'", User.Identity.Name);
SqlDataAdapter da = new SqlDataAdapter(cmd);
try
{
da.Fill(ds);
}
catch (SqlException){}
finally
{
conn.Close();
}
return ds;
}
由于上面的代码是常见的数据库操作,并且带有许多注释,我们不再提供不必要的详细信息。
在客户端,相应地定义了一个方法checkNewMessage,它调用服务器端方法mainGetNewMessage,并在有新消息时弹出提示框。列表7显示了相关的代码片段。
列表7:客户端方法checkNewMessage的代码
Collapse Copy Codefunction checkNewMessage()
{
if (!sendWindow)
{
var dt = AjaxProChat.mainGetNewMessage().value.Tables[0];
if (dt.Rows.length > 0)
{
var sender = dt.Rows[0].sender;
var content = DealBrackets(dt.Rows[0].content);
var MSG1 = new CLASS_MSN_MESSAGE("aa", 200, 120, "Hint:", sender +
" says: ", content);
MSG1.oncommand = function()
{
if (!sendWindow)
{
window.showModelessDialog("chatroom.aspx?username=" + sender, window,
"help:no;unadorned:yes;edge:sunken;status:no");
}
};
MSG1.rect(null, null, null, screen.height - 50);
MSG1.speed = 10;
MSG1.step = 5;
MSG1.show();
}
}
}
这里展示了从客户端JavaScript调用服务器端Ajax方法的典型流程。正如您可能已经猜到的,这个提示窗口的真正秘密在于以下代码片段。
列表 8
Collapse Copy Codevar MSG1 = new CLASS_MSN_MESSAGE("aa",200,120,"Hint:",sender + " says: ",content);
在可下载的源代码中,我们在文件CLASS_MSN_MESSAGE.js中定义了一个JavaScript弹窗类。我们正在创建一个CLASS_MSN_MESSAGE类的实例,最后一行显示了通过调用实例的show方法来显示弹窗。更多详情请查阅相关的JavaScript编码。
Chatroom.aspx
最后,我们来到了这个基于AJAX的聊天应用程序的核心部分。真正重要的用于开始聊天的窗口是chatroom.aspx页面。让我们先看一下它在图6中的运行时截图。

从图6可以看出,它相当简单。页面大部分是用于与朋友聊天和显示相应消息的区域,底部有一行用于输入要发送的消息的TEXTBOX控件和一个基本的发送按钮。您可能已经猜到,使用类似的JavaScript编程技巧来组织<div>对象及其内部子项。
为了更容易理解,我们还将这部分分为三个组件:查看最近消息、发送消息和接收消息。再次,让我们逐一分析它们。
查看最近消息
当聊天室窗口加载时,会加载指定数量的最新聊天消息。为此,我们在服务器端定义了一个AJAX方法—GetRecentMsg,然后该方法调用GetRecentMsg存储过程,最后返回所需的聊天记录项。列表9给出了方法GetRecentMsg的具体代码。
列表 9
Collapse Copy Code[AjaxPro.AjaxMethod]
public DataSet GetRecentMsg(string strUsername)
{
DataSet ds = new DataSet();
SqlConnection conn = new SqlConnection
(ConfigurationManager.ConnectionStrings["msn_Data_ConnStr"].ConnectionString);
SqlCommand cmd = conn.CreateCommand();
cmd.CommandText = string.Format("GetRecentMsg '{0}','{1}', {2}",
User.Identity.Name, strUsername, 8);
SqlDataAdapter da = new SqlDataAdapter(cmd);
try
{
da.Fill(ds);
}
catch (SqlException){}
finally
{
conn.Close();
}
return ds;
}
发送消息
为此,我们在服务器端定义了一个AJAX方法—SendMessage,然后该方法调用GetRecentMsg存储过程,最后返回所需的聊天记录项。列表10给出了方法GetRecentMsg的具体代码。
列表 10
Collapse Copy Code[AjaxPro.AjaxMethod]
public DataSet GetRecentMsg(string strUsername)
{
DataSet ds = new DataSet();
SqlConnection conn = new SqlConnection
(ConfigurationManager.ConnectionStrings["msn_Data_ConnStr"].ConnectionString);
SqlCommand cmd = conn.CreateCommand();
cmd.CommandText = string.Format("GetRecentMsg '{0}','{1}', {2}",
User.Identity.Name, strUsername, 8);
SqlDataAdapter da = new SqlDataAdapter(cmd);
try
{
da.Fill(ds);
}
catch (SqlException){}
finally
{
conn.Close();
}
return ds;
}
接收消息
在这里,我们当然应该实时接收消息,而整个过程与主页面的非常相似,所以我们不必多说。但仍有一点需要注意,如果聊天室窗口已打开,新消息将直接显示在其中;否则,将弹出一个小的提示窗口(参见图5)来显示此新消息。这就是为什么全局变量sendWindow在main.aspx页面的客户端JavaScript中被定义的原因,如下所示。
列表11:变量sendWindow的初始定义
Collapse Copy Code//……(omitted, inside main.aspx)
<script language="javascript">
//used to mark whether the chat room is open
var sendWindow = false;
//check out whether the user status has been changed
var changedTimes = 0;
//the main procedure
mainLoop();
//……
虽然变量sendWindow在main.aspx页面中定义用于标识聊天室是否已打开,但它也在chatroom.aspx页面中使用。结合上面的定义,在chatroom.aspx页面中存在以下代码片段。
列表12:变量sendWindow也在chatroom.aspx页面中使用
Collapse Copy Code//……(omitted, inside page <span class=Italic>chatroom.aspx</span>)
<body onunload="dialogArguments.sendWindow=false;" onload="dialogArguments.sendWindow=true;">
//……
当chatroom.aspx页面首次打开时,变量sendWindow被设置为true,此时main.aspx页面中的checkNewMessage函数将不会查询新消息,消息将直接显示在聊天室中。但是,当chatroom.aspx网页关闭时,变量sendWindow被设置为false,此时main.aspx页面中的checkNewMessage函数将调用服务器端的相应方法GetNewMessage,该方法将通过GetNewMessage存储过程查询新消息,并最终在弹出的提示框中显示结果。
作者说明:到目前为止,我们已经介绍了本文中所有的网页及其大致功能。由于SQL Server 2005 Express Edition(当然包括SQL Server 2005)有严格的要求,我建议您下载并解压源代码,然后仔细配置数据库引擎。至于AjaxPro.NET框架,您可能对它不熟悉,但掌握其通用用法肯定不会有多大困难。只需注意一点——我在本示例中使用了最新版本的AjaxPro.NET框架,但使用相关方案跟着我做也是可以的。最后但同样重要的是,我在托管模式和Microsoft IE的Web模式下都测试了演示项目。就是这样!
结论
好的,在这篇文章中,我们实现了最终目标——一个简单的类似MSN的基于Web的聊天应用程序。另一方面,正如我们在本文中所看到的,只有通过流行的WEB 2.0技术—AJAX—才能成功开发此类即时软件并最终投入实际使用。并且为了简洁起见,还有许多相关技术有待深入研究,例如更严格和实用的登录方法、更深入的JavaScript代码分析、AjaxPro.NET提供的功能才刚刚触及皮毛、严格的调试和部署等。令人高兴的是,本文仅展示了一个基于Web 2.0的演示,因此还有很多内容供您深入研究。