首    页 界面/窗口 网络/通讯 数据库 组件开发 图像/多媒体 NET/Web 其它技术 源码下载 资料下载 软件共享 软件外包 曲艺杂谈
栏目导航:  首    页  |  网络/通讯  |  棋牌游戏


包包版网络游戏大厅+桥牌系统 4.终于可以聊天了


原作者:包建强    源出处:博客园    发布者:施昌权    发布类型:转载    发布日期:2009-03-21


  

返回目录   

有了上一章所搭建的网络通信框架,我们就可以自由发挥了。只要把握好HandShake的顺序,就可以了。比如说我下面要介绍的大厅里的聊天机制,就是通过实现了503和504协议的“有问必答”原理。

     重构后的版本,代码在这里下载:PlayCard 2.2

     聊天室的截图如下,以下是一个Server端和两个Client端(注意到,kitty是最后登录的,所以看不到先前的聊天信息):

     

     详细介绍如下:     

     这里,503协议是Client端发送的聊天信息(Request),504协议是Server端将接受到的聊天信息转发(Response)给所有Client端(包括发送聊天信息的Client端,也就是说,即使是自己发送的消息,也要等Server端Response后才能显示)。

     注:在这套源码的自定义协议中,单数协议为Client端发送的Request,双数协议为Server端发送的Response。

     在CommonClassLibrary类库,添加503和504协议的实体类ChatMessage,其中包括发送用户名UserName和发送消息Message两个属性:

Code
     [Serializable()]
     public class ChatMessage : CommonProtocol
     {
         private string message;
         private string userName;

         public ChatMessage()
         {
         }

         public string UserName
         {
             get
             {
                 return userName;
             }
             set
             {
                 userName = value;
             }
         }

         public string Message
         {
             get
             {
                 return message;
             }
             set
             {
                 message = value;
             }
         }
     }

 

     Client端修改:

     登陆成功后,进入MainForm界面,此时会重新建立异步回调的循环

             AsyncCallback GetMsgCallback = new AsyncCallback(GetMsg);
             (Client.GetStream()).BeginRead(recByte, 0, 1024, GetMsgCallback, this);

     这里的GetMsg回调方法和LoginForm的GetMsg方法基本相同,唯一区别在else分支,MainForm界面在处理完接收到的数据包后,继续侦听,于是多了以下两条语句:

                 lock (Client.GetStream())
                 {
                     AsyncCallback GetStreamMsgCallback = new AsyncCallback(GetMsg);
                     Client.GetStream().BeginRead(recByte, 0, 1024, GetStreamMsgCallback, this);
                 }

     而LoginForm的else分支在处理完数据包后,也就是得到验证结果后,不再进行异步回调。说得详细些:验证成功,就跳转到MainForm界面,LoginForm不再继续侦听;验证失败,则立刻终止侦听,直到下一次点击登录按钮,才会重新建立Socket并进行侦听。

     在点击Send按钮后,将携带聊天信息的503协议封装到ChatMessage实体,序列化后发送Request到Server端。

             if (txtMessage.Text.Trim() != "")
             {
                 ChatMessage u = new ChatMessage();
                 u.Protocol = "503";
                 u.Message = txtMessage.Text.Trim(); 

                 SendText(SerializationFormatter.GetSerializationBytes(u));

                 txtMessage.Text = "";
             }

     而处理接收数据包的方法仍然是BuildText,这里是对504协议进行解析:

                 case "504":     //按Hall发送Client的聊天信息
                     ChatMessage chat = (ChatMessage)obj;
                     string message = chat.UserName + " : " + chat.Message + ""r"n";
                     this.Invoke(new DisplayMessage(DisplayText), message); 
                     break;

     BuildText方法所在线程不属于MainForm窗体主线程,但凡是有多线程编程经验的都知道,BuildText方法是不可以直接操作MainForm的控件(DisplayText方法),只能使用this.Invoke技术回调DisplayText的方法指针,正如上面代码所示。

 

     Server端修改

       为消息事件添加MessageEventArgs类,将Client接收到的消息封装到MessageEventArgs参数后传递给MainThread类的方法:

Code
     public class MessageEventArgs : EventArgs
     {
         private string msg;

         public string Message
         {
             get
             {
                 return msg;
             }
             set
             {
                 msg = value;
             }
         }
     }

      Client类

              添加MessageReceived事件

public event EventHandler<MessageEventArgs> MessageReceived;

              在BuildText方法中添加对503协议的处理

                 case "503":
                     if (MessageReceived != null)
                     {
                         ChatMessage meg = (ChatMessage)obj;
                         MessageEventArgs e = new MessageEventArgs();
                         e.Message = meg.Message;

                         MessageReceived(this, e);
                     } 
                     break;

       MainThread类

              将OnMessageReceived方法附属到新添加的事件MessageReceived上:

              newClient.MessageReceived += OnMessageReceived; 

              而添加OnMessageReceived方法如下,从而把这条聊天信息转发给所有在线用户: 

         public void OnMessageReceived(object sender, MessageEventArgs e)
         {
             //Message sender client 
             Client temp = (Client)sender;
             AddLog(temp.UserName + " :" + e.Message);

             ChatMessage chat = new ChatMessage();
             chat.Protocol = "504";
             chat.UserName = temp.UserName;
             chat.Message = e.Message;

             byte[] message = SerializationFormatter.GetSerializationBytes(chat);

             Client tempClient;
             DataTable dt = ClientList.Instance().GetUserList();
             foreach (DataRow row in dt.Rows)
             {
                 string uid = (string)row["UserID"];
                 tempClient = (Client)clientTable[uid];
                 tempClient.Send(message);
             }
         }

     以上是Client端和Server端的修改,归结出“程咬金三板斧”,以后每次添加新协议都如法炮制:

       1.携带新信息的协议,就在CommonClassLibrary类库添加相应的实体类,派生于CommonProtocol基类。

       2.永远是Client端先发Request请求,也就是一个单数协议,如501(登录)、503(发聊天消息),以后还会有很多。这是一个主动的动作,来自UI的的操作,注意,这里只发送消息就可以,而不要等待结果——所谓异步编程的思路。让我们再来看一下点击发送按钮的方法:

             if (txtMessage.Text.Trim() != "")
             {
                 ChatMessage u = new ChatMessage();
                 u.Protocol = "503";
                 u.Message = txtMessage.Text.Trim();

                 SendText(SerializationFormatter.GetSerializationBytes(u));

                 txtMessage.Text = "";
             }

       3.Server端永远是被动的接收来自Client的Request请求——一个单数协议,在BuildText方法中对其进行解析后,触发主线程的相应事件,于是在相应的方法中,发送偶数协议,也就是Response。这里,可能是发给原先Request的Client端(如502协议登录验证结果),也可能是群发给其他Client端(如504协议转发聊天信息)

       4.无论Server端还是Client端,都是在BuildText方法中对接收到的数据包进行反序列化,然后根据协议的不同进行不同的处理。以后我们每次添加新协议,都要这里加上case分支语句,注意到Server端处理单数协议,Client处理偶数协议。

       相应的,在这些处理模块中,要进行方法回调,从而操作主线程或UI。这里,Server端使用了事件机制;而Client端使用了委托回调机制。

 

     补充:小赵指出我使用了HashTable这个老古董存储client对象不是很好,于是我将其改造为Dictionary<KValue, TValue>范型:

         private Dictionary<string, Client> clientTable;
         clientTable = new Dictionary<string, Client>();

     也许有人会问,游戏大厅需要聊天么?是的,可以没有这个功能。我演示的目的是承上启下,介绍一下在我这个框架下如何轻松地开发新功能,制定一个套路,为下面的大厅通信打下基础。

     此外,在Client端,目前还没有显示其他用户进入或离开的消息,以及显示用户列表的功能。

 

     下一章,我要搭建游戏大厅,并对聊天功能进行改进。


发表评论
  回复  引用  查看    
#1楼 2008-07-31 17:55 | Anders Liu      
不要拿我们两口子说事,赶紧翻译IL Assembly,等着看呢。
  回复  引用  查看    
#2楼 [楼主]2008-07-31 18:02 | 包建强      
@Anders Liu
别吵别吵,晚上开始翻译第四章!
天天翻译也挺无聊的,N多人抱怨看不懂。
话说,哪天我也培养出了MVP,就不用你们夫妇了。
  回复  引用    
#3楼 2008-07-31 18:30 | qq1 [未注册用户]
肉麻噢..................
  回复  引用  查看    
#4楼 2008-07-31 18:46 | 横刀天笑      
你这聊天信息上的话是真的几个人聊,还是就你一个人在那白唬啊,呵呵
  回复  引用  查看    
#5楼 [楼主]2008-07-31 18:51 | 包建强      
@横刀天笑
这张图是自己同时开三个客户端,做的一个测试。
以后打牌也是这样测试的,因为没有那么多机器,也没有硬盘安装虚拟机。
以后要限制一台机器同时打开多个客户端的,但是现在不需要,所以数据库里面有IP和Port两个字段,就是干这个用的。
  回复  引用  查看    
#6楼 [楼主]2008-07-31 18:56 | 包建强      
@横刀天笑
听说你是做Winform的,想向你请教如何绕过路由器,实现网络的通信。
  回复  引用  查看    
#7楼 2008-07-31 19:32 | jillzhang      
@包建强
绕过路由?
没有路由,如何实现网络链路层?
这个不是软件的范畴了吧?
  回复  引用  查看    
#8楼 2008-07-31 19:40 | 横刀天笑      
@jillzhang

他的意思不是绕过路由,是防火墙



你的服务器端必须放在一个公网服务器上,这样才行
  回复  引用  查看    
#9楼 [楼主]2008-07-31 19:57 | 包建强      
@横刀天笑
我下面要实现这么一个逻辑:
自动选择一个Client端作代理Server,由其建立Socket与其他三个打牌人通信。但是这个代理Server如果是局域网内用户,怎么处理?难道其他三个人和路由器通信?
  回复  引用  查看    
#10楼 2008-07-31 20:08 | jillzhang      
@包建强
内网和外网之间的主动通讯是标准socket通讯
外网到内网的主动通讯要打洞,make hole!
打洞原理很简单,到网上搜索一把就知道了
  回复  引用  查看    
#11楼 2008-07-31 20:09 | jillzhang      
而且打洞前提也必须有一台公网IP的服务器才行
  回复  引用  查看    
#12楼 [楼主]2008-07-31 20:24 | 包建强      
@jillzhang
这个技术给我一个链接,我想想。
现在的问题是,Server端看到的这个ProxyServer客户端的IP是路由器的,怎么能还知道发给内部哪个IP?
  回复  引用  查看    
#13楼 2008-07-31 20:56 | 横刀天笑      
@包建强
嗯,这个就是NAT
首先这个Server必须记录下这个ProxyServer请求Server的时候的IP和Port的,这个IP和Port都是路由器的,以后所有发送到这个IP和这个Port的数据都会转发到这个ProxyServer的,这就是传说的打洞
  回复  引用  查看    
#14楼 [楼主]2008-07-31 21:00 | 包建强      
@横刀天笑
但是客户端在局域网内的IP怎么获取?没有这个IP,怎么识别?
  回复  引用  查看    
#15楼 2008-07-31 21:11 | 横刀天笑      
这个你就不用关心了,发送到路由器上这个端口的数据包都会被转发到开始的那个内网客户端,因为这里已经有了一个“洞”,呵呵
  回复  引用  查看    
#16楼 2008-07-31 21:12 | 横刀天笑      
你可以找找关于NAT和UDP打洞的相关知识,应该都有介绍
  回复  引用  查看    
#17楼 2008-07-31 21:12 | Angel Lucifer      
这些问题其实都很简单。

仔细翻阅下《TCP/IP详解:协议》就知道了。
  回复  引用  查看    
#18楼 2008-07-31 21:13 | Caling Xie      
赵吉力 在你的文章图片上面是 田鼠! 呵呵.
  回复  引用    
#19楼 2008-08-01 08:47 | renhaojie [未注册用户]
学习中》》期待包哥的下一篇!
  回复  引用  查看    
#20楼 2008-08-01 09:36 | 西就东城      
不错

问一个:
502,503,504协议是你自己写的??编写这些协议你是用什么来定义语义的??普通文档还是伪代码还是其他?

谢谢指教!
  回复  引用  查看    
#21楼 [楼主]2008-08-01 09:48 | 包建强      
将协议做成一个带有503的实体,序列化这个实体并发送到Server;Server接收这个消息并反序列化成原始体对象。
  回复  引用  查看    
#22楼 2008-08-01 11:07 | airwolf2026      
可以考虑另外一种代理主机的处理方法,那样应该不用考虑'打洞'了吧?
就是类似浩方,或者VS等对战平台,把客户机加进入类似虚拟局域网的环境.
打过魔兽争霸的,应该有这种体会,就是当局域网内主机掉线的时候,不一定会游戏一起结束,而是会自动有一台客户机编程server角色...

不知道是不是这个原理哈.不对之处还望指点,另外就是至今还没有研究过浩方等的实现原理....
  回复  引用  查看    
#23楼 2008-08-01 16:00 | 文炽城      
哈哈。
发现一个文章BUG。

注:在这套源码的自定义协议中,单数协议为Client端发送的Request,单数协议为Server端发送的Response。
  回复  引用  查看    
#24楼 [楼主]2008-08-01 16:37 | 包建强      
@文炽城
已经改正,后者改为双数。
  回复  引用    
#25楼 2008-08-01 21:37 | nh022 [未注册用户]
都是高手……
  回复  引用    
#26楼 2008-08-02 00:00 | alenboy [未注册用户]
我是菜鸟,问一下,用C#写跟用C++写的,服务器运行性能上会有大的差别吗?特别是客服端连接数多的情况下。。。
  回复  引用  查看    
#27楼 2008-08-03 16:52 | 绿蚂蚁      
我觉得这个通信框架还可以在抽象一下,把协议,协议的处理抽象出来,增加新协议的时候,就不必去动这个框架
  回复  引用  查看    
#28楼 2008-08-05 16:59 | 英雄      
牛,正在照着学习呢,学到了不少东西!包哥,期待你的下个作品》》》
  回复  引用  查看    
#29楼 2008-08-05 17:00 | 英雄      
好像你好几天没有写了,期待着游戏大厅的搭建!
  回复  引用  查看    
#30楼 2008-08-12 17:13 | brilliance-Nick      
UDP打洞..不过也需要一台有公网IP的服务器...

刚好看到一篇文章

http://www.cnblogs.com/wmj/archive/2008/08/06/1262220.html
  回复  引用  查看    
#31楼 2008-08-12 17:15 | brilliance-Nick      
听说浩方是通过把tcp/ip协议转换为ipx协议实现虚拟局域网的~不知道是真是假
  回复  引用  查看    
#32楼 2008-11-12 11:19 | 韦恩卑鄙      
--引用--------------------------------------------------
brilliance-Nick: 听说浩方是通过把tcp/ip协议转换为ipx协议实现虚拟局域网的~不知道是真是假
--------------------------------------------------------
绝对是假的
每个浩方游戏大厅都是虚拟的局域网 但是不限于ipx协议
目前大多是一个250ip的子网
  回复  引用  查看    
#33楼 2008-11-12 14:02 | 韦恩卑鄙      
@包包
---------------------------
补充:小赵指出我使用了HashTable这个老古董存储client对象不是很好,于是我将其改造为Dictionary<KValue, TValue>范型:
-------------------------------
在我的大厅工程中 原来也打算用Dictionary<KValue, TValue>
但是印象里因为 Dictionary<KValue, TValue> 不支持 IColonable 而放弃了

在一些线程中需要枚举的情况下 用lock粗暴的锁定 Dictionary<KValue, TValue>显然不智
所以我最终觉得 Hashtable的clone 是很有用的。
----如果有必要可以做一个特殊的强类型读写属性或方法 但是clone不能舍弃.

hashtable 被我用在各个房间的子房间列表、各个房间的人员等多线程访问的部分。
但是聊天部分的暂存区域 我采用了web聊天室常用的短期内容缓存 每一个频道用了一个强类型的链表
---------------------------
下一章,我要搭建游戏大厅,并对聊天功能进行改进。
---------------------------
格外期待您的实现方式



http://tank.0u.cn/ 这个是我做的大厅 但是目前因为我已经离职 项目停止运营 上面应该勉强还有个10多人在线
客户端需要j2me的模拟器
跟风搞手网 结果运营不善3天流失率90%
见笑了
  回复  引用  查看    
#34楼 [楼主]2008-11-12 14:46 | 包建强      
@韦恩卑鄙
多谢指教!可以加我MSN,进一步探讨.



关于我们 版权声明 广告服务 联系我们 友情链接 加入收藏
站长:施昌权    Email:scq2099yt@163.com    MSN:scq2099yt@live.cn    QQ:14046300    本站QQ群:67202409
Copyright © 2008     卓为VC(www.joyvc.cn)    All Rights Reserved    建议分辨率 1024×768
本站由施昌权制作维护
京ICP备09012297号