最近有点闲,于是漫无目的的在网上找资料学习。正好赶上WebQQ2.0发布,发现做的还蛮不错的,于是我也有了来模仿一下的想法。在博客园找了很久的资料,终于确定用COMET来作为聊天系统的主要连接技术,然后在加上一点jquery,本人把这个微型群聊天初步做了出来。本篇文章还参考了卢春城的 一步一步打造WebIM (额 其实COMET方面 主要还是他的代码)
1 用例图
本人的画图水平有待提高,先将就着看吧。 图里演示了两个用户的登录情况。
2 实现方式
我们看到上图中有个Message Management的东西,他是处理所有信息的类,不管是发送消息处理,接收消息处理都是由他的操作指挥。而他直接管辖的就是listener,
客户端以及服务器端的消息都会经过listener,他实现一个传达者的角色。
接下来我主要说下send.aspx, receive.aspx,disconnect.aspx在这个聊天程序中的作用与基本实现。我在web.config中对这几个aspx进行了映射,实际的实现是通过几个
IHttpAsyncHandler, IHttpHandler来做的。
<add path="recevie.aspx" verb="*" type="Will.ChatApp.WebCore.ConnectionHandler"/>
<add path="send.aspx" verb="*" type="Will.ChatApp.WebCore.SendMsgHandler"/>
<add path="disconnect.aspx" verb="*" type="Will.ChatApp.WebCore.DisconnectHandler"/>
</httpHandlers>
a) receive.aspx (ConnectionHandler)
这个httpHandler是负责实现COMET连接的,他是一个IHttpAsyncHandler。主要方法有BeginProcessRequest,EndProcessRequest。我在BeginProcessRequest中接受客户端的请求,只要ConnectionAsyncResult的IsComplete属性不为true,这个请求将永远保持下去,这样就实现了我们想要的客户端http 长连接。这里我们做的是个群聊天,所以当任何一个客户端发来请求,都需要通知其他客户有消息到来。
于是我们就需要把这个 ConnectionAsyncResult保存起来,当有其他客户发送消息,再及时把这个ConnectionAsyncResult.IsComplete设为true,释放连接通知客户端。而这正是我们需要listener的地方。
代码
{
_context = context;
ConnectionAsyncResult asyncResult = new ConnectionAsyncResult(cb, extraData);
string receiver = "*";
string sender = context.Request["Sender"];
string from = context.Request["From"];
string type = context.Request["Type"];
if (!MessageManagement.Instance.AddListener(receiver, sender, asyncResult, type))
{
//已有消息,发送消息并结束链接
asyncResult.Complete();
}
return asyncResult;
}
下面我高亮了几个方法。第一亮点是如果当用户是新登录,于是会将用户添加到用户列表,并放回一个新用户标记。第二个亮点是将这个用户的活动时间更新,后面我们判断用户是否掉线会用到。第三个亮点是新起一个线程,如果10s内用户没有接收到任何消息或者操作,这个连接将会被关闭。第四个亮点判断是新用户登陆后,就会把更新所有客户端的用户列表并通过监听器发送出去。
/// 添加消息监听器,如果查找到符合监听器条件的消息,返回false,此时不会添加监听器
/// 如果没有查找到符合监听器条件的消息,返回true,此时监听器将被添加到m_Listeners中
/// 如果是已连接用户 就更新该用户的活动时间
/// </summary>
public bool AddListener(String receiver, String sender, ConnectionAsyncResult asynResult, String type)
{
MessageListener listener = new MessageListener(receiver, sender, asynResult);
lock (m_Lock)
{
if (!m_Listeners.ContainsKey(receiver))
{
m_Listeners.Add(receiver, new List<MessageListener>());
}
List<MessageListener> listeners = m_Listeners[receiver] as List<MessageListener>;
1 bool flag = IsNewUserLogin(listeners, sender, type);
//if reconnect, update the status as alive
2 onlineUserList.OnlineUserList.ForEach(p =>
{
if (p.UserName == listener.Sender)
{
p.Status = ConnectionStatus.Alive;
p.UpdateTime = DateTime.Now;
}
});
//查找消息
ChatMessage messages = Find(receiver, sender);
if (messages.ChatMessageList.Count == 0)
{
//插入监听器
listeners.Add(listener);
3 new Timer(TimerCallBack, listener, 10000, 0);
}
else
{
//发送消息
listener.Send(messages);
}
//通知所有人上线消息
if (flag)
{
4 SpreadOtherListener(listeners);
}
return messages.ChatMessageList.Count == 0;
}
}
b) send.aspx(SendMsgHandler)
这个比较简单,只是单纯的处理用户发送的消息。当Message Management 接到消息后,会通知所有的listener有消息到。
代码
{
context.Response.ContentType = "text/plain";
string receiver = context.Request["Receiver"];
string sender = context.Request["Sender"];
string message = context.Request["Message"];
SoleChatMessage result = new SoleChatMessage();
result.ChatContent = new MessageContext() { MessageContent = message };
result.ChatTime = DateTime.Now;
result.Sender = sender;
result.SenderIP = context.Request.UserHostAddress;
result.Receiver = receiver;
MessageManagement.Instance.NewMessage(result);
}
b) disconnect.aspx(DisconnectHandler.cs)
这个httpHandler主要是处理当用户点击关闭窗口按钮时做的操作。 此时Message Management接到消息后会遍历所有listener,发现时当前客户端发来的请求,则关闭该客户端的连接,移除listener。 而listener的查找标识就是登录客户端的名称,这个还是有点不大安全的。
代码
{
string sender =context.Request["Sender"];
string recevier =context.Request["Receiver"];
MessageManagement.Instance.DisposeDisconnectListener(recevier,sender);
}
/// 当用户点击关闭窗口按钮后触发 把用户从用用列表中踢出 并且释放监听器
/// </summary>
/// <param name="recevier"></param>
/// <param name="sender"></param>
public void DisposeDisconnectListener(string recevier, string sender)
{
List<MessageListener> listeners = m_Listeners[recevier] as List<MessageListener>;
List<MessageListener> removeListeners = new List<MessageListener>();
var user = onlineUserList.OnlineUserList.Find(p => p.UserName == sender);
if (user != null)
{
onlineUserList.OnlineUserList.Remove(user);
}
foreach (MessageListener listener in listeners)
{
removeListeners.Add(listener);
if (String.Compare(listener.Sender, sender, true) == 0)
{
System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(listener.Complete));
}
else
{
listener.Send(onlineUserList);
System.Threading.ThreadPool.QueueUserWorkItem(new System.Threading.WaitCallback(listener.Complete));
}
}
foreach (MessageListener listener in removeListeners)
{
//移除监听器
listeners.Remove(listener);
}
}
3 问题
这次主要讲解下,大致整个流程,其实还有很多没提到。比如前台的实现,还有前台的安全,后台的安全验证等等。 我的程序还在修改,有些东西会后期加上去的。
最后,希望我不要写的烂尾 谢谢
4 代码提供 ChatApp.rar