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


包包版网络游戏大厅+桥牌系统 3.从登录说起


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


  

返回目录   

本文讲解了网络游戏大厅的登录部分的Server端实现,包括:自定义网络协议、MemoryStream流的序列化技术、多线程有状态地与客户端通信、异步接收网络包等多种技术。并附有一个Server端的登录模块代码,可以配合着同时发布的Client端exe文件一起使用,来模拟登录的效果。

     源码下载PlayCard.rar(.NET 2.0版)

     安装说明:请执行压缩包中的sql文件——创建数据库PlayCard2,并在其中创建PlayerList表和导入5笔数据,然后手动修改Server端App.Config配置文件中的数据库连接字符串。

     演示方法:请用VS2005打开解决方案,按F5运行Server端程序。同时打开多个Client端程序,分别使用不同的帐号进行登录。可用的5个帐号(密码都是1):jax;dx;kitty;Anders和Jeffrey。

      讲解如下:

 

     Server端编程     

     首先启动Server端的Monitor,在其构造函数中,创建了一个线程,并调用了startListen方法:

        public MainThread()
        {
            clientTable = new Hashtable();

            serverThread = new Thread(new ThreadStart(startListen));
            serverThread.Start();

            AddLog("Socket Server Started");
        }


        public void startListen()
        {
            try
            {
                //Start the tcpListner 
                serverListener = new TcpListener(5151);
                serverListener.Start();
                do
                {
                    //Create a new class when a new Chat Client connects 
                    Client newClient = new Client(serverListener.AcceptTcpClient());

                    newClient.Connected += OnConnected;
                    newClient.Disconnected += OnDisconnected;

                    //Connect to the clients 
                    newClient.Connect();
                }

                while (true);
            }

            catch(Exception ex)
            {
                AddLog("Error: " + ex.Message);
                serverListener.Stop();
            }

        }

     在上面这个startListen方法中,侦听自身的5151端口,这里使用到了do{} while (true);这个死循环,在一般编程中,这是不允许的,也只有在Socket通讯中可以这么使用。死循环中,只要收到来自客户端的请求:serverListener.AcceptTcpClient(),就会创建一个Client实例:

Client newClient = new Client(serverListener.AcceptTcpClient());

     同时还会将两个方法OnConnected、Disconnected绑定到Client实例的两个相应的事件上,分别用来处理连接、离线的情况:

newClient.Connected += OnConnected;
newClient.Disconnected += OnDisconnected;

     接下来一条语句是非常关键的:

 newClient.Connect();

     这就开始了对消息进行接收并解析的过程,让我们一步步地分析。

 

     话说,Client实例的这个Connect方法,导致了Server端异步接收网络包:

        public void Connect()
        {
            AsyncCallback GetStreamMsgCallback = new AsyncCallback(myReadCallBack);
            myClient.GetStream().BeginRead(recByte, 0, 1024, GetStreamMsgCallback, null);
        }

     我们看到这个异步方法,从0位置开始,每次读取1024个字节,然后就会异步回调方法myReadCallBack,构成一个循环,直至读取到所有字节,并将其放入SplitBytes实例中并对其进行解析:

        BuildText(sb.ReceiveAllByte);

     解析完成后,要做好善后工作:销毁SplitBytes实例,并开始读取新的网络包。

  

     以上大多是很底层的技术,尤其是这个异步接收网络包的设计,耗费了我不少心血和调试时间。大家可以借鉴一下这个框架。

     注意到,这个myReadCallBack方法从逻辑上分为三部分:

         1.如果第一次读取到的字节数小于1,表示客户端失去了连接,为此关闭这个连接,触发Disconnected事件后直接返回:

                lock (myClient.GetStream())
                {
                    numberOfBytesRead = myClient.GetStream().EndRead(ar);

                    if (numberOfBytesRead < 1)
                    {
                        //If a value less than 1 received that means that client disconnected 
                        myClient.Close();
                        //raise the Disconnected Event 
                        if (Disconnected != null)
                        {
                            EventArgs e = new EventArgs();
                            Disconnected(this, e);
                        }

                        return;
                    }

                }

         2.接下来就可以读取字节了:

                sb.AddBytes(recByte, numberOfBytesRead);
                recByte = new byte[1024];

         3.之后我们要判断,这个Socket上的数据流是否读取完了。如果没读取完,也就是if条件为真,就会异步调用myReadCallBack方法,再次读取1024位字节,从而形成循环,直到读取完所有数据流,此时进入if条件的另一分支。

                if (myClient.GetStream().DataAvailable)
                {
                    myClient.GetStream().BeginRead(recByte, 0, recByte.Length, new AsyncCallback(myReadCallBack), myClient.GetStream());
                }

                else
                {
                    BuildText(sb.ReceiveAllByte);
                    sb.Dispose();

                    if (isLogin)
                    {
                        lock (myClient.GetStream())
                        {
                            AsyncCallback GetStreamMsgCallback = new AsyncCallback(myReadCallBack);
                            myClient.GetStream().BeginRead(recByte, 0, 1024, GetStreamMsgCallback, null);
                        }

                    }

                }

     在另一分支里,会使用BuildText方法反序列化接收到的字节数组,并进行登录验证。如果验证成功,isLogin标记被置为true,从而等待这个Client端线程发送新的数据包;否则,不会再等待这个Client端,而且也不会存储这个线程,如果再次登陆,则一切重头再来。

 

     下面让我们进入BuildText解析函数。

        public void BuildText(byte[] dataByte)
        {
            BinaryFormatter formatter = new BinaryFormatter();
            MemoryStream stream = new MemoryStream(dataByte);
            CommonProtocol obj = (CommonProtocol)formatter.Deserialize(stream);
            stream.Close();

            string Protocol = obj.Protocol;

            switch (Protocol)
            {
                case "501":
                    LoginUser u = (LoginUser)obj;
                    CheckUserName(u.UserID, u.Password, u.IP);
                    break;
            }

        }

     分为两个阶段:

     1.将字节数组进行反序列化成对象obj

     2.根据对象obj的Protocol属性,执行不同的逻辑。如501协议代表请求登录,则将obj进一步转化为LoginUser对象,使用CheckUserName方法,对其进行检验:

      LoginUser u = (LoginUser)obj;
      CheckUserName(u.UserID, u.Password, u.IP);

     *注:之所以能将obj转化为LoginUser对象,是因为Client端发送501协议时,就是将其封装在LoginUser对象中并进行序列化的。

 

     接下来的CheckUserName方法,分为两部分逻辑:

     1.在DB中进行验证,成功与否封装在LoginUser对象的IsLogin属性中,

     2.建立502协议,序列化LoginUser对象u,并发送:

     byte[] message = SerializationFormatter.GetSerializationBytes(u);
     Send(message);

     SerializationFormatter类封装了序列化的过程,而下面的Send方法则将序列化后的字符数组发送给原先发送501协议的Client端——这就是有状态连接的好处,轻松的实现了HandShake机制。

        public void Send(byte[] byteMessage)
        {
            lock (myClient.GetStream())
            {
                BinaryWriter writer = new BinaryWriter(myClient.GetStream()); ;
                writer.Write(byteMessage);
                writer.Flush();
            }

        }

     注意到,在CheckUserName方法中,我们触发了Connected事件:

        if (Connected != null)
        {
            EventArgs e = new EventArgs();
            Connected(this, e);
        }

     从而进一步调用MainThread类的OnConnected方法

        public void OnConnected(object sender, EventArgs e)
        {
            //Get the client that raised the event 
            Client temp = (Client)sender;

            //Add the client to the Hashtable 
            clientTable.Add(temp.UserID, temp);

            AddLog("Client Connected:" + temp.UserID);
        }

     这里,在MainThread类中,维护了一个哈希表clientTable,里面存储了所有的Client线程。而OnConnected方法负责将登录成功的Client线程添加到clientTable中。这样,就实现了有状态连接,当这个Client线程再次发送消息到Server端,就会从clientTable中将这个线程找出来。

     不光是在MainThread类存储这些线程信息。此外,Server端还维护着一个DataTable,存储着所有在线用户的信息——也就是位于ClientList单例类的clientTable。

     在登录成功之后,这个Client端的线程就存储在Server端的clientTable这个哈希表中了。以后Client和Server进行交互,Server都会检查这个哈希表,从而正确找出这个线程,并在上面写消息发送到Client。

 

     最后,讲解一下离线机制,有状态的。MainThread类的OnDisconnected方法,附属到Client对象的OnDisconnected事件。这个事件什么时候被触发呢?在myReadCallBack这个回调方法中,也就是在接收不到数据包的情形下,这意味着用户断开了Socket连接,包括正常退出或拔掉网线的非法退出,那么Server端会立刻得到通知:numberOfBytesRead < 1,于是就触发OnDisconnected事件,执行OnDisconnected方法。

     在该方法中,我们会在DB中将这个用户的IsLogin字段修改为0以表示它的离开:

            DB.UpdateStatus(temp.UserID, "0");

     然后分别去除哈希表中相应的线程和DataTable中该用户的相关资料,

                clientTable.Remove(temp.UserID);

                DataRow findRow = ClientList.Instance().FindUserRow(temp.UserID);
                ClientList.Instance().RemoveClient(temp.UserID);

     最后,遍历哈希表中剩下的用户,逐一向它们发送有人离线的消息,这是一个510协议:

                Disconnect u = new Disconnect();
                u.Protocol = "510";
                u.UserID = temp.UserID;
                u.DeskNumber = DeskNumber;
                u.DeskPosition = DeskPosition;

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

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

 

     记住,这里使用的HandShake不是对等的,因为是基于TCP而不是UDP的。也就是说,永远是Client端主动发request到Server端。而Server端则永远是被动的接收request,处理后再response到Client端。

     应该说,大部分通信都是这样设计的。举个例子,登录体系的设计,Client端主动发501协议请求Server端;Server端接收后,去数据库验证,将成功与否的信息作为502协议再回复给Client端。

     当然也有例外,就是Server端失去与Client端的连接,这时,Server端会接收不到数据包,于是就认为Client端离开了,从而进行一些逻辑处理,向其它Client发送该Client离开的消息(510协议)——这也可以认为是一种退化的HandShake,Server端仍然是被动的。

 

   Client端编程

     讨论过Server端编程,再来看Client端的代码,就容易多了,大部分都是重复的技术。带大家走一遍流程。

     对于登录而言,可以说Server端编程是在等候501协议,然后去数据库验证,发送502协议;而Client端则是发送501协议,然后等候502协议,然后根据验证结果,进行逻辑处理。Client端使用到的主要函数如GetMsg、BuildText、SendText都和Server端差不多,这里就不多介绍了。

     如果Client端的GetMsg接收不到数据包,也会执行Disconnect方法,这同于Server端。 

     此外,需要注意Client端的TcpClient对象,被封装为一个SocketHelper单件。在进行身份验证的时候,如果成功,会继续沿用这个单件;否则,就销毁它——因为失败的登录没有必要再保留这个单件,这样再次登录就可以建立新的SocketHelper单件了。

     我们看到,TcpClient类型对象myClient单独出现在很多地方,比如说接收数据包:myClient.GetStream()。既然这样,为什么还要建立这个单件呢?

如果只有一个LoginForm窗体,那么用不用单件都是一样的。但是接下来进入大厅——也就是MainForm窗体也要用到TcpClient对象进行消息通信——使用同样的端口,所以有一个全局TcpClient对象就非常必要的,所以在这里我对TcpClient对象进行了封装,从而在Client端任何窗体中都是可以访问到的。

 

     一些零零碎碎的技术

     通信协议

     话说,通信协议这东西,也就是Protocol,是国际组织定的而我们要遵守的,比如说SOAP协议,但是我们也可以定义自己的通信协议。在“包包游戏大厅”中,我将这个自定义协议OO为一个CommonProtocol类,这是一个基类,所有的通信协议都从这个基类派生,如LoginUser实体类就派生于此。为了支持序列化,要在类头加上[Serializable]以及在类中添加一个空的构造函数:

    [Serializable()]
    public class CommonProtocol
    {
        private string protocol;

        public CommonProtocol()
        {
        }


        public string Protocol
        {
            get
            {
                return protocol;
            }

            set
            {
                protocol = value;
            }

        }

    }

     注意到,这个类具有一个Protocol属性,代表自定义协议的编号,例如501协议请求登陆,502协议验证登陆成功与否,而我们使用到了派生于这个基类的实体类LoginUser作为传输的对象。更多通信协议(及相关实体),请参见:包包版网络游戏大厅 附录1 通信协议。我会在后续章节逐一介绍这些通信协议。

 

     序列化机制

     这里我使用到了BinaryFormatter,从而使序列化速度比较快;而流则选用了MemoryStream,专门用于Socket通信,通过

          byte[] message = stream.ToArray();

     直接将内存流转换为字节数组。

     代码如下,注意到,我将其封装成一个静态方法,并放入CommonClassLibrary项目,以供Client端和Server端同时使用。

    public class SerializationFormatter
    {
        private SerializationFormatter()
        {

        }


        public static byte[] GetSerializationBytes(object obj)
        {
            BinaryFormatter formatter = new BinaryFormatter();
            MemoryStream stream = new MemoryStream();
            formatter.Serialize(stream, obj);
            byte[] message = stream.ToArray();
            stream.Close();

            return message;
        }

    }

 

     通信机制

     下面介绍通信机制。其实,用WebService是最简单的,可以穿透防火墙,同时也不需要额外的解析。用Remoting也不错。但是2年前,我正好接触到Socket编程,所以想直接在通信的最底层进行编程,达到练手的目的,于是,便有了上面若干思路。

 

     补充说明:老怪这家伙批评我这篇文章是新手入门级别的,我想了想,也倒是,对于那些网络编程玩家而言,这确实很简单。但是,作为“包包游戏大厅”系列的第3章,此文的作用十分重大。我花了几天时间,把原先8000行代码的游戏大厅精简到现在这个简单的登录程序,就是为了让读者先掌握基本框架,然后带领读者逐步扩展功能。话说,当前的这套框架,是整个游戏大厅的通信基础,再往下,只要写通信协议就可以了,而不需要再关心Socket底层的数据包处理。有兴趣的朋友可以拿我这套框架去开发别的应用程序,而不只局限于游戏大厅。

 

     下一节,让我们沿着这个思路,考察游戏大厅中聊天机制的实现。


发表评论
  回复  引用    
#1楼 2008-07-27 22:49 | good [未注册用户]
sf.


  回复  引用  查看    
#2楼 2008-07-27 22:56 | 非空      
newClient.Connected += OnConnected;
newClient.Disconnected += OnDisconnected;
如果这样是不是在do while里给事件挂好多方法啊?
这样写是不是会好一点
:if(newClient.Connected == null)
newClient(sender, e);
  回复  引用  查看    
#3楼 [楼主]2008-07-27 23:03 | 包建强      
@非空
这倒不会,每个客户端一个newClient,下次再进来,直接使用这个newClient,不会再调用这里“给事件挂好多方法”。你设置个断点调试一下,就知道了。
  回复  引用  查看    
#4楼 2008-07-27 23:17 | 非空      
噢,我设置看下^_^,还有就是请教包哥一下,在回调方法myReadCallBack里又调了BeginRead 我看MSDN上说 BeginRead中的回调方法用的是线程池的线程,如果是这样的话当数据量大的时候就一直在BeginRead 完了回调 的循环会不会把线程耗光啊?
  回复  引用  查看    
#5楼 [楼主]2008-07-27 23:23 | 包建强      
@非空
这倒没想过。我这套程序,当初只是写来玩玩,把一些灵感实现出来。

博客园有没有异步调用的高手啊,解释解释这个问题啊!
  回复  引用  查看    
#6楼 2008-07-27 23:30 | 非空      
--引用--------------------------------------------------
包建强: @非空
这倒没想过。我这套程序,当初只是写来玩玩,把一些灵感实现出来。

博客园有没有异步调用的高手啊,解释解释这个问题啊!
--------------------------------------------------------
明天写个死循环,再打开任务管理器看看回调方法会不会耗光 o(∩_∩)o...哈哈!
好好休息天天向上!
  回复  引用  查看    
#7楼 2008-07-28 09:27 | WilsonWu      
支持ing...
  回复  引用  查看    
#8楼 2008-07-28 09:32 | HedgeHog      
博主不简单,自己开发的东西,拿出来分享.很不简单啊!
  回复  引用  查看    
#9楼 2008-07-28 09:47 | Vincent Zhou      
嗯,很赞!有时间我要好好学习下
  回复  引用  查看    
#10楼 2008-07-28 09:59 | 斧头帮少帮主      
先打包...
  回复  引用  查看    
#11楼 2008-07-28 09:59 | 斧头帮少帮主      
哦,不是打楼主的意思...
  回复  引用  查看    
#12楼 2008-07-28 10:07 | 光年      
我刚想做个局域网的天黑请闭眼呢.
  回复  引用  查看    
#13楼 2008-07-28 10:17 | 逍遥海盗船      
留个名,关注一下。
  回复  引用  查看    
#14楼 2008-07-28 18:14 | 破碎虚空      
支持,没接触过,先留个记号,有时间学习下!
  回复  引用    
#15楼 2008-07-29 10:29 | 程序一生 [未注册用户]
学习了,包包好人啊
  回复  引用  查看    
#16楼 2008-07-29 10:29 | myjia      
App.config 中
<add key="ServerUrl" value="http://218.242.80.116:4501"/>
干什么用的?我怎么连不上去呀.
  回复  引用  查看    
#17楼 [楼主]2008-07-29 10:34 | 包建强      
@myjia

PlayCard的2.1版本没有这个文件和这个设置,请你确定下载的版本是正确的。

  回复  引用  查看    
#18楼 2008-07-29 10:52 | myjia      
用vs2008能运行吗?我运行错误.
  回复  引用  查看    
#19楼 [楼主]2008-07-29 11:02 | 包建强      
@myjia
刚刚测试过,用VS2008打开时需要转换。打开后,按F5编译,仍然可以运行。查看Project属性,发现仍然是使用.NET2.0编译的,所以运行良好。
  回复  引用    
#20楼 2008-07-29 11:26 | renhaojie [未注册用户]
学习中》》》》
  回复  引用  查看    
#21楼 2008-07-30 11:50 | 火星人.NET      
强烈建议,包包游戏大厅,改名为“老包”游戏大厅

包包这个名字,我念着都肉麻~~
  回复  引用  查看    
#22楼 2008-07-31 08:56 | 横刀天笑      
老大,难道是我看错了?这篇文章不是曾经发表过?
  回复  引用  查看    
#23楼 2008-07-31 08:59 | 炮灰向前冲      
我想请教一下,在大厅里,如何实现桌子与人物点击的结合呢?就是点击一个位置,人物头像出现。桌子是用控件绘制的,还是操作GDI呢,如果是IMG控件,在桌子多时,其性能是非常低的
  回复  引用  查看    
#24楼 [楼主]2008-07-31 09:01 | 包建强      
@横刀天笑
今天要在此基础上,介绍聊天的实现,还有大厅的建立。所以重发旧文,以免读者摸不着头脑。
  回复  引用  查看    
#25楼 2008-07-31 09:03 | cumt吴波      
收藏下,自己也有打算做这样的一个东西,不过不是游戏用
  回复  引用  查看    
#26楼 [楼主]2008-07-31 09:05 | 包建强      
@炮灰向前冲
桌子是一个控件,里面每个位置是一个button,桌子布是图片,对于Client端,这点性能算是还不算什么。而且,button因为是.NET内部支持的控件,所以性能会好一些。
  回复  引用    
#27楼 2008-07-31 09:18 | 大明天下 [未注册用户]
我也就笑纳了,包哥!
  回复  引用  查看    
#28楼 2008-07-31 09:21 | 姜敏      
@LZ能不能下次做游戏的时候不要以桥牌来说啊,说个什么斗地主的都行啊,通俗点,有几个人会玩桥牌啊,看包兄写文章的意思,好像就是写给少数人看的,不写的新奇的东西出来觉的没有成就感啊?
  回复  引用  查看    
#29楼 [楼主]2008-07-31 09:23 | 包建强      
@姜敏

两年前写的代码,没考虑现在普及的事情,所以选择了我最喜欢的桥牌。不过这次重构,准备实现象棋或拱猪的游戏,这样大家容易接受一些。
  回复  引用  查看    
#30楼 2008-07-31 09:24 | 姜敏      
博客园首页固然不能说什么文章都放,也并不一定要放些别人一看就觉的特别高深的东西.它是一个技术沟通平台,只要是园友精心写的code,我觉的通俗一点并没有什么坏处,并不会因为通俗而失去文章的意义,所谓简约而不简单,不知道包兄对我的看法是否同意呢?如有冒犯的地方还望谅解.
  回复  引用  查看    
#31楼 2008-07-31 09:34 | Jeffrey Zhao      
小包子为什么还用Hashtable啊?
  回复  引用  查看    
#32楼 2008-07-31 09:36 | 丁学      
posted @ 2008-07-31 08:46 包建强 阅读(2153) 评论(30)
#1楼 61.48.45.*2008-07-27 22:49 | good [未注册用户]

你这算啥?
  回复  引用  查看    
#33楼 [楼主]2008-07-31 09:36 | 包建强      
@Jeffrey Zhao
小赵说的是,我忘了重构这个数据结构了,下一版加聊天功能时改一下。
  回复  引用  查看    
#34楼 [楼主]2008-07-31 09:38 | 包建强      
@丁学
别有用意!
  回复  引用    
#35楼 2008-07-31 09:54 | 飘过 [未注册用户]
为什么修改了文章的发布时间,这算怎么回事?!!!
  回复  引用  查看    
#36楼 [楼主]2008-07-31 09:57 | 包建强      
@飘过
因为今天讲聊天机制实现。
  回复  引用  查看    
#37楼 2008-07-31 10:12 | StillWartersRunDeep      
跟踪
  回复  引用  查看    
#38楼 2008-07-31 10:31 | zhuds      
好内容!!!
  回复  引用  查看    
#39楼 2008-07-31 10:33 | 客家网络      
如何处理消息的边界?
  回复  引用  查看    
#40楼 [楼主]2008-07-31 10:47 | 包建强      
@客家网络
请仔细读我的myReadCallBack方法,因为是异步循环的机制,这就确保了接受的两个消息不会混在一起,会被先后处理。
  回复  引用  查看    
#41楼 2008-07-31 11:50 | Michael Xu      
好东西 学习
  回复  引用  查看    
#42楼 2008-11-12 13:54 | 韦恩卑鄙      
这个机制和我原来写的一个游戏大厅的机制几乎一样
但是 如果一个client 的所有异步read 都是自己上一次read结束后开始的
lock (myClient.GetStream()) 就显得没有必要了

我的游戏大厅完全没有lock过client的 stream 也从未因为多线程访问而出过错误(50-1000连接 6x24 运行半年左右) 所以是不是lock了没有必要的部分造成了多余的开销呢
  回复  引用  查看    
#43楼 2008-12-16 14:19 | 韦恩卑鄙      
--引用--------------------------------------------------
包建强: @非空
这倒没想过。我这套程序,当初只是写来玩玩,把一些灵感实现出来。

博客园有没有异步调用的高手啊,解释解释这个问题啊!
--------------------------------------------------------

对了 不是线程池 是完成端口
  回复  引用  查看    
#44楼 2009-02-11 02:49 | xiaotie      
挑刺:
(1) 赞同韦恩卑鄙的意见,不必要lock,不存在冲突的可能性;
(2) recByte 太小,不够容纳一个packet;
(3) 每次监听完了再new一个recByte 无必要 recByte = new byte[1024];
(4) SplitBytes需要动大手术,性能问题
  回复  引用  查看    
#45楼 2009-02-11 03:24 | xiaotie      
@包建强
TCP协议和OS协议栈都无法保证2个消息不能混在一起。同时,也无法保证你一次异步处理的是一个完整消息。比如,一个消息是2000byte长,分两次发,第一个packet收到了,OS回调,读取清空了TCP Socket缓冲区,在缓冲区中没数据了,NetworkStream.DataAvailable 傻傻的为 false,于是就当成整条消息处理了,而此时消息的后半部分还没到呢。局域网内压力小时这种现象很少,压力大时或者非局域网就会出现这类问题。




关于我们 版权声明 广告服务 联系我们 友情链接 加入收藏
站长:施昌权    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号