专著于富媒体技术~
本站某些作品来源于互联网,如果侵犯了您的利益,请留言说明
      本文将使用FluorineFx和Flex结合介绍一个简单的视频聊天室案例开发,希望通过此篇和大家交流FluorineFx和Flex的相关技术,同时也希望本篇可以帮助到需要使用FluorineFx做及时应用开发的新手朋友。首先列举下本篇中所涉及到的开发环境和相关技术以及简单的需求定义:

      1. Microsoft Visual Studio 2008(VS SP1)+.NET Framework 3.5(SP1)

      2. FluorineFx v1.0.0.15

      3. Adobe Flex Builder 3 + Flex SDK 3.2

      4. Microsoft Office Access 2003

      5. ADO.NET , Linq.....

      本篇的案例的功能需求定义如下:

      1. 用户注册/登录聊天室

      2. 成功登录聊天室后通过选择在线用户进行视频连接完成视频和文字聊天(一对一私聊)

      3. 提供小喇叭功能,实现全服务器发送消息(一对多,一人发送全聊天室的用户都可见)

      4. 提供聊天表情选择窗口,实现聊天表情的发送。

1、服务器端开发

      本篇主要介绍了使用FluorineFx所提供的ApplicationAdapter来建立自己的及时通信应用Adapter,提供及时通信、实况流服务,以及通过远程共享对象管理在线用户列表的功能。

      在我之前的一篇文章里曾经介绍过FluorineFx的ApplicationAdapter的基本步骤,以及使用FluorineFx的ApplicationAdapter来开发及时通信应用的相关知识点。ApplicationAdapter应用最终需要部署到支持FluorineFx服务的Web应用(ASP.NET网站)上,如下图则表示了FluorineFx应用的目录结构。

       对于使用过FluorineFx开发应用的朋友来说,这个图在熟悉不过了。要使用FluorineFx开发及时通信应用,项目结构就必须这样搭建,首先看看本篇案例的解决方案截图:
点击在新窗口中浏览此图片
      从上面的解决方案截图可以看到,整个项目由两部分构成,一个是FluorineFx的类库,封装了远程服务(RemotingService)接口和及时通信应用程序(ApplicationAdapter)。另一个则是FluorineFx网站(和ASP.NET网站没多大的区别,不同的是其中加入了FluorineFx的一些相关配置)。
点击在新窗口中浏览此图片
      为了方便演示案例本篇就直接使用的 Access做为数据库,根据数据库的字段结构建立了一个数据传输对象(DTO),同时为该DTO对象标记[FluorineFx.TransferObject],以使其可以作为FluorineFx的数据传输对象使用。

namespace ChatRoom.Services.DTO
{
    /// <summary>
    /// 用户信息(数据传输对象)
    /// </summary>
    [FluorineFx.TransferObject]
    public class UserInfo
    {
        /// <summary>
        /// 构造方法
        /// </summary>
        public UserInfo() { }

        public int ID { get; set; }

        public string UserName { get; set; }

        public string NickName { get; set; }

        public string Password { get; set; }

        public string HeadImage { get; set; }
    }
}


      远程服务接口(DataService)提供了用户注册,登陆等最基本的通信接口方法,在了解通信接口之前首先学习一个工具类,该类的提供了一个将DataTable类型数据转话为IList<Object>类型返回,详细代码如下:

namespace ChatRoom.Services.Utils
{
    public static class ConvertUtils
    {
        /// <summary>
        /// 提供将DataTable类型对象转换为List集合
        /// </summary>
        /// <param name="table"></param>
        /// <returns></returns>
        public static List<T> ConvertToList<T>(DataTable table) where T : new()
        {
            //置为垃圾对象
            List<T> list = null;

            if (table != null)
            {
                DataColumnCollection columns = table.Columns;
                int columnCount = columns.Count;
                T type = new T();
                Type columnType = type.GetType();
                PropertyInfo[] properties = columnType.GetProperties();

                if (properties.Length == columnCount)
                {
                    list = new List<T>();
                    foreach (DataRow currentRow in table.Rows)
                    {
                        for (int i = 0; i < columnCount; i++)
                        {
                            for (int j = 0; j < properties.Length; j++)
                            {
                                if (columns[i].ColumnName == properties[j].Name)
                                {
                                    properties[j].SetValue(type, currentRow[i], null);
                                }
                            }
                        }
                        list.Add(type);
                        type = new T();
                    }
                }
                else
                {
                    list = null;
                }
            }
            else
            {
                throw new ArgumentNullException("参数不能为空");
            }
            return list;
        }
    }
}
      

      下面是使用FluorineFx提供的远程服务接口的详细定义:

namespace ChatRoom.Services
{
    [RemotingService]
    public class DataService
    {
        public DataService()
        {
        }

        /// <summary>
        /// 用户注册
        /// </summary>
        /// <param name="info"></param>
        /// <returns></returns>
        public bool Register(UserInfo info)
        {
            info.Password = FormsAuthentication.HashPasswordForStoringInConfigFile(info.Password, "MD5");
            StringBuilder CommandText = new StringBuilder("insert into UserInfo values (");
            CommandText.Append(string.Format("'{0}','{1}','{2}','{3}')", info.UserName, info.Password, info.NickName, info.HeadImage));

            try
            {
                int row = DataBase.ExecuteSQL(CommandText.ToString());
                if (row > 0)
                    return true;
                return false;
            }
            catch (Exception ex)
            {
                return false;
            }
        }

        /// <summary>
        /// 用户登录
        /// </summary>
        /// <param name="info"></param>
        /// <returns></returns>
        public UserInfo Login(UserInfo info)
        {
            info.Password = FormsAuthentication.HashPasswordForStoringInConfigFile(info.Password, "MD5");
            StringBuilder CommandText = new StringBuilder("select * from UserInfo where ");
            CommandText.Append(string.Format("UserName='{0}' and Password='{1}'", info.UserName, info.Password));

            try
            {
                DataTable table = DataBase.ExecuteQuery(CommandText.ToString());
                if (table != null)
                {
                    return ConvertUtils.ConvertToList<UserInfo>(table)[0];
                }
                return null;
            }
            catch (Exception ex)
            {
                return null;
            }
        }
    }
}


      FluorineFx.Messaging.Adapter.ApplicationAdapter做为新的应用程序的基类,提供了客户端与应用程序之间通信的接口方法、数据流服务以及共享对象服务等。它能够时时处理应用程序的启动、停止、客户端的连接和断开等及时操作。

      在本篇案例项目中,我所建立的ApplicationAdapter为ChatRoomApplication,用来提供启动及时应用服务器,接受客户端连接和断开以及视频流服务等功能。这里需要注意一点,在接受客户端连接的时候业务处理流程,在应用程序启动的时候就将会创建一个共享对象,用来保存在线用户列表。

public override bool AppStart(IScope application)
{
    //用户共享对象
    CreateSharedObject(application, "OnLineUsers", false);

    return base.AppStart(application);
}


      当用户通过客户端连接服务器的时候,将用户信息传递到服务器,服务器端接受到数据后将会对用户数据进行验证(验证用户名和密码是否匹配),如果验证失败则直接返回false表示连接服务器失败,如果验证通过则设置当前连接的属性并连接到共享对象,然后取出共享对象里的在线用户列表数据,判断当前连接到服务器的用户是否在线,如果用户已在线则返回false表示用户登录失败,并给于提示当前用户已经登录。如果用户没有存在于共享里则表示当前连接的用户没有登录,那么就将当前连接的用户信息写入共享对象,客户端会通过异步事件处理函数接收到最新的在线用户列表数据。

public override bool AppConnect(IConnection connection, object[] parameters)
{
    UserInfo info = ((Dictionary<string, object>)parameters[0])["0"] as UserInfo;

    connection.Client.SetAttribute("UserName", info.UserName);

    //连接到共享对
    ISharedObject users = GetSharedObject(connection.Scope, "OnLineUsers");
    if (users == null)
    {
        this.CreateSharedObject(connection.Scope, "OnLineUsers", false);
        users = GetSharedObject(connection.Scope, "OnLineUsers");
    }
    else
    {
        List<UserInfo> onLineUserList = users.GetAttribute("UserInfo") as List<UserInfo>;
        if (onLineUserList != null)
        {
            List<UserInfo> tempList = (from u in onLineUserList
                                       where u.UserName == info.UserName
                                       select u
                                       ).ToList();
            if (tempList.Count > 0)
            {
                //Server RPC
                this.CallClientMethod(connection, "onLogined", "当前用户已经登录");
                return false;
            }
            else
            {
                onLineUserList.Add(info);
                users.SetAttribute("UserInfo", onLineUserList);
            }
        }
        else
        {
            onLineUserList = new List<UserInfo> { info };
            users.SetAttribute("UserInfo", onLineUserList);
        }
    }
    return base.AppConnect(connection, parameters);
}


      OK,到这里就完成了对用户进行有效验证和接受与拒绝连接的功能,那么如果是用户下线了或是无意之间关闭了浏览器呢?这种情况应当如何去处理,如果不处理的话那么用户是不是会一直卡在线上呢?答案是肯定的。FluorineFx为此也提供了接口来处理用户下线的操作,那么在这里我们需要做什么呢?需要做的就是取出当前退出系统的客户端连接属性值,然后在取出共享对象里的在线用户列表数据,根据用户连接时所设置的属性进行循环判断,找到当前退出系统的用户数据,最后将他从共享对象里删除,服务器端的共享对象一但改变,所有连接到服务器的客户端都会通过共享对象的异步事件得到最新的数据,用来更新客户端的显示列表。

public override void AppDisconnect(IConnection connection)
{
    string userName = connection.Client.GetAttribute("UserName") as string;

    ISharedObject users = GetSharedObject(connection.Scope, "OnLineUsers");

    if (users != null)
    {
        List<UserInfo> onLineUserList = users.GetAttribute("UserInfo") as List<UserInfo>;
        if (onLineUserList != null)
        {
            List<UserInfo> tempList = onLineUserList.FindAll(c => c.UserName.Length > 0);
            foreach (var item in tempList)
            {
                if (item.UserName.Equals(userName))
                {
                    onLineUserList.Remove(item);
                }
            }
            users.SetAttribute("UserInfo", onLineUserList);
        }
    }
    base.AppDisconnect(connection);
}


      到这里服务器端的开发就基本完成了,现在就需要将这个ApplicationAdapter配置好,然后启动通过网站启动服务,Flex或者是Flash就可以通过rtmp协议连接了。首先需要配置的是rtmp协议通信信道:

<channel-definition id="my-rtmp" class="mx.messaging.channels.RTMPChannel">
    <endpoint uri="rtmp://{server.name}:2777" class="flex.messaging.endpoints.RTMPEndpoint"/>
    <properties>
        <idle-timeout-minutes>20</idle-timeout-minutes>
    </properties>
</channel-definition>


      然后就是在应用目录下面配置ApplicationAdapter,指定当前这个应用又那一个Adapter来处理。建立app.config文件进行如下配置:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <application-handler type="ChatRoom.Services.ChatRoomApplication"/>
</configuration>


2、客户端开发

首先就是客户实现用户登录,通过FluorineFx提供的RemotingService的接口方法进行数据验证。

private function onLogin(event:MouseEvent):void
{
    remoteConn = new RemotingConnection("http://localhost:2020/ChatRoom.FluorineFxWeb/Gateway.aspx",ObjectEncoding.AMF3);
    myInfo = new UserInfo();
    myInfo.UserName=this.txtUserName.text;
    myInfo.Password=this.txtPassword.text;
    remoteConn.RemotingCall("ChatRoom.Services.DataService.Login",onLoginResult,onLoginFault,myInfo);
}

private function onLoginResult(result:UserInfo):void
{
    if(result != null)
    {
        this.myInfo = result;
        this.viewStack.selectedChild = chatView;
        rtmpnc = new RtmpConnection("rtmp://localhost:2777/VideoChat",ObjectEncoding.AMF3,onNetStatusHandler,myInfo);
    }
    else
    {
        this.lbState.text = "登陆失败,用户名或密码错误!";
    }
}

private function onLoginFault(event:Object):void
{
    this.lbState.text = "登陆失败,请重试!";
}

private function onClear(event:MouseEvent):void
{
    this.txtUserName.text="";
    this.txtPassword.text="";
    this.lbState.text="";
    this.txtUserName.setFocus();
}


      RemotingConnection和RtmpConnection是我自己扩展的NetConnection类,功能和NetConnection一样,不同的是封装后的使用相对来说比较方便点。首先通过RemotingService的接口进行用户名和密码验证,通过了则创建一个到RTMP服务器的连接RtmpConnection(等同于NetConnection)。

private function onNetStatusHandler(event:NetStatusEvent):void
{
    trace(event.info.code);
    switch(event.info.code)
    {
        case "NetConnection.Connect.Success":onConnSuccess();break;
        case "NetConnection.Connect.Failed":onConnError();break;
    }
}

private function onConnSuccess():void
{
    //将自己的视频数据发布到RTMP服务器,这里使用的是FluorineFx
    var mic:Microphone = Microphone.getMicrophone();
    var publishNs:NetStream = new NetStream(rtmpnc);
    publishNs.attachCamera(cam);
    publishNs.attachAudio(mic);
    publishNs.client = this;
    publishNs.publish(myInfo.ID.toString()); //将用户ID作为流名进行发布实况流
    
    userSO = SharedObject.getRemote("OnLineUsers",rtmpnc.uri,false);
    userSO.addEventListener(SyncEvent.SYNC,onSyncHandler);
    userSO.client = this;
    userSO.connect(rtmpnc);
    
    timer = new Timer(1000);
    timer.addEventListener(TimerEvent.TIMER,onTimerHandler);
    timer.start();
}

private function onConnError():void
{
    trace("login error");
    writeMessage("<font color=\"#FF0000\">系统提示:连接视频服务器失败</font>");
}


      创建连接的同时指定了由那一个方法(onNetStatusHandler)来处理连接状态,通过判断连接状态如果连接成功则将自己的视频数据发布到RTMP服务器(特别提醒:在发布流的时候是使用的用户ID作为流名,在建立视频聊天的时候需要根据这个ID才能查看到视频),同时还连接到服务器上的远程共享对象(作用:通过异步事件处理函数实现在线用户列表),最后建立了一个Timer是不断的调用服务器方法获取当前系统时间(注意:实际开发中不建议这样做);如果连接服务器失败则在聊天消息显示区输入一条提示信息。

      在线用户列表使用共享对象来实现,可以及时的处理用户上线下线功能和实现客户端数据同步更新等。下面是共享对象的异步事件处理函数:

private function onSyncHandler(event:SyncEvent):void
{
    var array:Array = event.target.data.UserInfo as Array;
    if(array != null)
    {
        userArray.removeAll();
        for(var i:Number=0; i<array.length; i++)
        {
            var info:UserInfo = array[i] as UserInfo;
            userArray.addItem(info);
        }
        trace("userArray length:" + userArray.length);
    }
}


     从异步事件中取出当前最新的数据,然后添加到用户界面的显示列表数组(userArray)里,Flex直接使用List组件显示在线用户列表,通过绑定userArray设置数据源,当userArray改变后List组件的显示也会同步更新显示。

       那么怎么去建立视频聊天查看到对方的视频呢?其实实现也很简单,这里还是要从用户列表出发,通过点击用户列表上的在线用户,然后建立与该用户的视频连接。同时判断是否选择的是怎么,本案例中我没有将自己从在线列表里屏蔽而是通过判断当前选择的是否为自己,如果是自己则不进行视频连接,也不能发送文本聊天信息。

private function onUserItemHandler(event:Event):void
{
    info = List(event.target).selectedItem as UserInfo;  //把当前选择的用户信息通过变量保存下来
    this.lbNickName.text = info.NickName;
    
    if(info.UserName == myInfo.UserName)
    {
        writeMessage("<font color=\"#FF0000\">系统提示:不能和自己进行视频聊天</font>");
    }
    else
    {
        //建立视频流的连接
        if(this.ns)
        {
            this.ns.close();
        }
        this.ns = new NetStream(this.rtmpnc);
        ns.client = this;
        sound = this.ns.soundTransform;
        var v1:Video = new Video();
        v1.width = 320;
        v1.height = 240;
        v1.attachNetStream(ns);
        this.videoDisplay.addChild(v1);
        ns.play(info.ID.toString());  //当前选择的用户的ID
    }
}


      OK,到这里就成功的完成了用户登录,建立与RTMP服务器的连接,发布视频流,接收指定的视频流等功能,接下来就是实现文字聊天的功能了。这里我使用的是第二篇文章里所介绍的方法(
提示:该方法就是直接使用SharedObject的send()方法)来实现文字聊天功能。

private function onSendMessage(event:MouseEvent):void
{
    if(info!=null)
    {
        userSO.send("chatMessage", this.txtMessage.text, myInfo, info);
        this.txtMessage.text="";
    }
    else
    {
        writeMessage("系统提示:请选择聊天对象");
    }
}

public function chatMessage(message:String, sayUser:UserInfo, recUser:UserInfo):void
{
    if(recUser.UserName==this.myInfo.UserName)
    {
        message = sayUser.NickName + "对你说:"+message;
        writeMessage(message);
    }
    if(sayUser.UserName==this.myInfo.UserName)
    {
        message = "我对"+recUser.NickName + "说:"+message;
        writeMessage(message);
    }
}

private function writeMessage(message:String):void
{
    this.txtDisMessage.htmlText += message + "\n";
    this.txtDisMessage.verticalScrollPosition = this.txtDisMessage.maxVerticalScrollPosition;
}


      OK,大功告成,现在是集视频和文字聊天的多人在线聊天室就实现了,不足的是只能一对一聊天。如果我要对大家说话怎么办呢?于是我在本案例中设计了一个小喇叭功能,通过发送小喇叭实现全服务器喊话。下边是下喇叭组件代码:

<?xml version="1.0" encoding="utf-8"?>
<mx:TitleWindow xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="540" height="50"
    headerHeight="8" roundedBottomCorners="true" borderColor="#000000">
    <mx:TextInput x="2" y="3" width="400" id="txtMessage"/>
    <mx:Button x="466" y="3" label="关闭" click="onClose(event)"/>
    <mx:Button x="410" y="3" label="发送"
        enabled="{txtMessage.text.length > 0 ? true : false}"
        click="onSend(event)"/>
    
    <mx:Script>
        <![CDATA[
            import mx.core.Application;
            import mx.events.CloseEvent;
            import mx.managers.PopUpManager;
            private function init():void
            {
                this.txtMessage.setFocus();
            }
            
            private function onClose(event:MouseEvent):void
            {
                onCloseHandler(null);
            }
            
            private function onCloseHandler(event:CloseEvent):void
            {
                Application.application.speakFlag = false;
                PopUpManager.removePopUp(this);
            }
            
            private function onSend(event:MouseEvent):void
            {
                Application.application.userSO.send("speakMessage",txtMessage.text,Application.application.myInfo);
                this.txtMessage.text = "";
                onCloseHandler(null);
            }
        ]]>
    </mx:Script>
</mx:TitleWindow>


      同样通过远程共享对象的send()方法实现发送小喇叭功能,在客户端定义一个方法(speakMessage)来接受小喇叭发送的消息内容,然后显示在用户聊天界面上。

public var speakFlag:Boolean = false;
private function onSpeaker(event:MouseEvent):void
{
    if(!speakFlag)
    {
        var dis:Speaker = new Speaker();
        dis.x = 230;
        dis.y = 505;
        PopUpManager.addPopUp(dis,this,false);
        speakFlag = true;
    }
}
public function speakMessage(message:String,info:UserInfo):void
{
    message = "【小喇叭】:" + info.NickName + "说:" + message;
    writeMessage(message);
}


      貌似这一整篇都是代码,除了代码我也不知道该怎么去介绍更容易说得清楚了,下面来看看上面的劳动成功,启动服务器后运行多个客户端来聊天测试看看。
点击在新窗口中浏览此图片
点击在新窗口中浏览此图片
点击在新窗口中浏览此图片

      现在还差一个重要的功能没有实现了,前面提到过画中画功能,也就是说在和在线朋友进行视频聊天的同时,需要将自己的视频以小视频窗口的方式显示在聊天窗口,实现所谓的画中画功能,显示自己的视频通过初始化方法,程序启动后就直接显示出自己的视频。

private function init():void
{
    //将自己的视频显示在画中画中
    cam = Camera.getCamera();
    if(cam != null)
    {
        this.myVD.attachCamera(cam);
    }
    else
    {
        writeMessage("未能找到视频设备,请检测是否正确安装设备!");
    }
}


      本文就介绍到这里,关于聊天表情的实现这里就不作介绍了,由于时间关系本案例里也没有实现这个功能,有兴趣的朋友可以下载源代码自己去扩展实现聊天表情这个功能。这里我将实现的原理简单说一下,通过TileList组件加载表情图片或动画信息显示出来。

3、应用程序部署

      首先需要需要注意一点就是Flex的配置文件service-config.xml,后面的flash将通过service-config.xml里配置的RTMP通信信道与FluorineFx的RTMP服务器连接连接,详细配置如下:

<channel-definition id="my-rtmp" class="mx.messaging.channels.RTMPChannel">
    <endpoint uri="rtmp://{server.name}:2777" class="flex.messaging.endpoints.RTMPEndpoint"/>
    <properties>
        <idle-timeout-minutes>20</idle-timeout-minutes>
    </properties>
</channel-definition>


     在网站中添件一新Web页面(FluorineChatRoom.aspx)来承载flash(.swf)的运行,将新发布的.swf负载到该页面。

<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
        id="VideoChat" width="520" height="520"
        codebase="http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab">
        <param name="movie" value="flash/VideoChat.swf" />
        <param name="quality" value="high" />
        <param name="bgcolor" value="#869ca7" />
        <param name="allowScriptAccess" value="sameDomain" />
        <embed src="flash/VideoChat.swf" quality="high" bgcolor="#869ca7"
            width="520" height="520" name="VideoChat" align="middle"
            play="true"
            loop="false"
            quality="high"
            allowScriptAccess="sameDomain"
            type="application/x-shockwave-flash"
            pluginspage="http://www.adobe.com/go/getflashplayer">
        </embed>
</object>


小提示:另外也建议使用swfobject.js来加载swf!

      然后将宿主FluorineFx提供的RemotingService和ApplicationAdapter的网站发布出来部署到IIS上。
点击在新窗口中浏览此图片
      部署过程中我单独分配了一个Web端口88,OK,现在运行Geteway.aspx就会得到一个地址:http://localhost:88/Gateway.aspx 这个地址就是flash中需要用来作为RPC通信的通信网关地址,简单的理解就是通过这个地址来访问FluorineFx提供的远程服务(RemotingService)。现在回到客户端开发代码中去,修改通信网关的地址,将原来在VS下调试的地址修改为部署到IIS后的访问地址:

remoteConn = new RemotingConnection("http://localhost:2020/ChatRoom.FluorineFxWeb/Gateway.aspx",ObjectEncoding.AMF3);


修改为如下:

remoteConn = new RemotingConnection("http://localhost:88/Gateway.aspx",ObjectEncoding.AMF3);


      现在重新发布flash,将新发布的.swf复制到IIS中部署的站点对应的目录下,到这里在本地测试基本就完成了。但是这样做只能做到本地单机访问,如果要在局域网内访问则需要在IIS中为该应用站点分配IP,然后通过IP访问就OK了。

      服务器主机上又怎么部署呢?通过上面这些步骤或许已经体会到,Flash客户端需要的只是两个地址,一个Gateway.aspx网关地址,一个是RTMP服务器访问地址,其实随便你将这个Web应用这么部署,只要保证这两个地址有效就行了。比如你的服务器IP是:61.128.128.68,在此服务器上部署了FluorineFx网站(绑定的域名为www.mydomain.com);那么客户端访问RemotingService的地址则为:"http://www.mydomain.com/Gateway.aspx ",RTMP服务器的连地址则为:"rtmp://61.128.128.68:2777/VideoChat"。通过这两地址修改flash端然后重新发布一个.swf覆盖以前的flash文件,这样就可以通过域名在公网上使用本篇所介绍的视频聊天室案例项目了。

      本案例并不完善,还有许多地方都做得不到位。比如用户上线下线广播功能也没完成(内部我已经提供了接口和方法,只是没有调用而已),有兴趣的朋友可以下载源代码去参考或帮忙完成这些功能。


            


出处:Bēniaǒ成长笔记
转载时必须以链接形式注明出处及本声明!



发表评论
表情
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
emotemotemotemotemot
打开HTML
打开UBB
打开表情
隐藏
昵称   密码   游客无需密码
网址   电邮   [注册]
               

验证码 不区分大小写