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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.41/5 (13投票s)

2009年2月23日

CPOL

13分钟阅读

viewsIcon

86642

downloadIcon

3576

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所示。

image001.jpg
图 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所示的调用路径。

image002.jpg

图 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展示了它的初次印象。

image003.jpg

图 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所示。

image004.jpg

图 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的提示窗口。

image005.jpg
图5:类似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中的运行时截图。

image006.jpg
图 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的演示,因此还有很多内容供您深入研究。

© . All rights reserved.