• 如果您想对本站表示支持,请随手点击一下广告即可~
  • 本站致力于提供原创、优秀的技术文章~
  • 有任何疑问或建议 均可以在站点右侧栏处 通过各种方式联系站长哦~
  • Corba接口学习笔记

    网络协议 EXP 145阅读 0评论

    By EXP 2013-03-26



    1. CORBA简介

    CORBA(Common Object Request Broker Architecture),公用对象请求代管者体系结构,它是为了实现分布式计算而引入的。为了说明CORBA在分布计算上有何特点,我们从它与其它几种分布计算技术的比较中进行说明。

    与过去的面向过程的RPC(Remote Procedure Call)不同,CORBA是基于面向对象技术的,它能解决远程对象之间的互操作问题。

    MicroSoft的DCOM(Distributed Component Object Model)也是解决这一问题的,但它基于Windows操作系统,虽然DCOM已有在其他操作系统如Sun Solaris,Digital Unix,IBM MVS上的实现,但毫无疑问,只有在微软的操作系统上才会实现得更好。而只有CORBA是真正跨平台的,平台独立性正是CORBA的初衷之一。

    另一种做到平台无关性的技术是Java RMI(Remote Method Invocation),但它只能用JAVA实现。CORBA与此不同,它通过一种叫IDL(Interface Definition Language)的接口定义语言,能做到语言无关,也就是说,任何语言都能制作CORBA组件,而CORBA组件能在任何语言下使用。

    因此,可以这样理解CORBA:CORBA一种异构平台下的语言无关的对象互操作模型

    2. CORBA的基本概念

    2.1. ORB(Object Request Broker)对象请求代理

    CORBA体系结构的核心就是ORB。

    下图为ORB的基本模型,它作为一个”软件总线”来连接网络上的不同对象,提供对象的定位和方法调用,可以这样简单理解:ORB就是使得客户应用程序能调用远端对象方法的一种机制

    图1 ORB基本模型

    图1 ORB基本模型

    具体来说就是:当客户程序要调用远程对象上的方法时,首先要得到这个远程对象的引用,之后就可以像调用本地方法一样调用远程对象的方法。当发出一个调用时,实际上ORB会截取这个调用(通过客户Stub完成),因为客户和服务器可能在不同的网络、不同的操作系统上甚至用不同的语言实现,ORB还要负责将调用的名字、参数等编码成标准的方式(称为Marshaling)通过网络传输到服务器方(实际上在同一台机器上也如此),并将参数通过Unmarshaling的过程传到正确的对象上(这个过程叫重定向,Redirecting),服务器对象完成处理后,ORB通过同样的Marshaling/Unmarshaling方式将结果返回给客户。

    因此,ORB是一种功能,它具备以下能力:

    • 对象定位(根据对象引用定位对象的实现)
    • 对象定位后,确信Server能接受请求
    • 将客户方请求通过Marshaling/Unmarshing方式重定向到服务器对象上
    • 如果需要,将结果以同样的方式返回

    注:
    Marshaling一个对象的过程就是一个序列化(deflating)的过程,相应的Unmarshaling就可以看作是反序列化(inflating)的过程。


    2.2. IDL(Interface Definition Language)接口定义语言

    如果说ORB使CORBA做到平台无关,那么IDL则使CORBA做到语言无关

    正像其名字中显示的那样,IDL仅仅定义接口,而不定义实现,类似于C中的头文件。实际上它不是真正的编程语言。要用它编写应用,需要将它映射它相应的程序设计语言上去,如映射到C++或JAVA上去。映射后的代码叫Client Stub Code和Server Skeleton Code。

    IDL的好处是使高层设计人员不必考虑实现细节而只需关心功能描述。IDL可以说是描述性语言。设计IDL的过程也是设计对象模型的过程。它是编写CORBA应用的第一步,在整个软件设计过程中至关重要。

    IDL的语法很像C++,当然也像Java。很难想像一个程序设计人员是不懂C或Java的,所以,几乎所有的程序设计人员都能迅速理解IDL。而这正是IDL设计者所希望的。


    2.3. OA(Object Adapter)对象适配器

    OA用于构造对象实现与ORB之间的接口。它给框架发送方法,调用并且支持服务器对象的生命周期,完成对象引用的生成、维护,对象定位等功能。对象适配器有各种各样的,常用的有两种:

    • BOA(Basic Object Adapter)基本对象适配器:负责激活对象,即当客户请求对象的服务时,激活对象实现的能力。
    • POA(Portable Object Adapter)可移植对象适配器:是BOA的替代方式,提供大量可扩展的接口,来处理一些对于BOA来说不合理的要求。

    2.4. GIOP(General Inter-ORB Protocol)通用ORB协议

    我们知道,客户和服务器是通过ORB交互的,那么,客户方的ORB和服务器方的ORB又是通过什么方式通信呢?通过GIOP(General Inter-ORB Protocol)。也就是说,GIOP是一种通信协议,它规定了客户和服务器的ORBs间的通信机制

    GIOP设计的尽可能简单,开销最小,同时又具有最广泛的适应性和可扩展性,以适应不同的网络。它定义了以下两个方面:

    • The Common Data Representation (CDR) definition.(通用数据表示定义):它实际上是IDL数据类型在网上传输时的编码方案。它对所有IDL数据类型的映射都作了规定。
    • GIOP Message Formats(GIPO消息格式):它规定了Client和Server两个角色之间要传输的消息格式。主要包括Request和Reply两种消息。

    GIOP因为是一种通用协议,所以不能直接使用。在不同的网络上需要有不同的实现。目前使用最广的便是Internet上的GIOP,称为IIOP(Internet Inter-ORB Protocol),IIOP把GIOP消息数据映射为TCP/IP连接行为和输入/输出流读/写。

    注:
    IIOP不是完全从GIOP分离出来的协议,它更像是GIOP的一个实例。


    2.5. 其他常见概念

    • DII(Dynamic Invocation Interface)动态调用接口:位于客户端,发送客户端的调用请求。
    • DSI(Dynamic Skeleton Interface)动态框架接口,位于服务器端,传送客户端的调用请求。
    • SII(Static Invocation Interface)静态调用接口:位于客户端,客户与ORB之间的静态接口。
    • SSI(Static Skeleton Interface)静态框架接口:位于服务器端,ORB与服务器之间的静态接口。
    • stub存根:位于客户端,由IDL编译器编译IDL文件生成,其功能类似一个客户代理。
    • skeleton框架:位于服务器端,由IDL编译器编译IDL文件生成,其功能是负责发送一个操作调用给能实现此操作的服务。
    • IR(Interface Repository)接口存储库:存储运行时所需要的IDL规范。
    • IMR(Implementation Repository)实现存储库:存储对象实现(一个服务器)的详细信息(即一个执行程序需要被放置在哪一个服务器上)。
    • IOR(Interoperable Object Reference)可操作对象引用:它包括所有客户与服务器联系所需的各种信息(包括CORBA服务器对象进程的IP地址和TCP端口等),ORB将通过它产生在网络上唯一标识那个将被分布对象的消息。
    • ORBAservices(CORBA服务):在ORB级别之上,定义了大多数分布式企业对象利用的公共服务,如命名服务、交易对象服务、关系服务、生命周期服务、外表化服务、持久性服务、查询服务、对象集合服务、属性服务、事件服务、许可证服务、时间服务、事务服务、并发控制服务和安全服务等。
    • CORBAfacilities(CORBA工厂):位于CORBAservices之上,定义了更高层次的分步式服务与框架。如:打印、电子邮件、文档管理等。

    3. CORBA的体系结构

    图2 CORBA体系结构

    图2 CORBA体系结构

    上图为CORBA的体系结构图,它描述了以下内容:

    • CORBA规范中定义了IDL语言及其向其他高级语言的映射。类似于COM中的IDL语言,OMG的IDL语言通过说明对象的接口来定义对象,它也是一种描述性语言。一个接口同样包括一组命名的操作和相应于这些接口的参数。
    • ORB核心提供了客户与对象间实现透明通信的方法,它可以屏蔽对象实现位置、实现方式、状态和通信机制等细节以及不同实现间可能存在的差异。
    • 对象适配器位于ORB核心和对象实现之间,它负责服务对象的注册、对象引用的创建和解释、对象实现的服务进程的激活和去活、对象实现的激活和去活以及客户请求的分发。
    • IDL存根为客户提供了静态调用方式,IDL构架为客户提供了静态实现方式。IDL编译器编译描述服务对象接口的IDL文件,生成对应于具体编程语言的IDL存根和IDL构架程序。IDL存根负责把用户的请求进行编码,发送到对象实现端,并对接收到的处理结果进行解释,把结果或异常信息返回给用户;IDL构架对用户请求进行解码,定位所请求的对象的方法,执行该方法,并把执行结果或异常信息编码后发送给客户。
    • 动态调用接口DII(Dynamic Invocation Interface)和动态构架接口DSI(Dynamic Skeleton Interface)提供了动态调用方法和动态实现方法。某些情况下客户预先不知道服务对象的接口信息,需要通过查询或者采用其他的手段获得服务对象的接口描述信息,然后使用DII动态调用ORB核心接口的方法来构造客户请求并发送到对象实现。在对象实现方可以使用DSI动态分发用户请求的机制,以便动态的处理客户方的请求。客户和对象实现所采用的方式并不一定要对应,也就是说,客户方支持的静态和动态两种调用方式,对象实现方支持的静态和动态两种实现方式,经过组合后得到的4种方式都可能出现。例如,客户方可能使用静态调用方式,而对象实现方使用动态构架接口,反之亦然。
    • 在动态方式下,需要查询相应的服务对象的接口描述信息(在静态方式下,这些信息由IDL文件来描述),这些信息由接口库提供。接口库通常以IDL描述文件为其输入,将接口描述信息进行处理后存放在文件、数据库或者其他形式的存储机制中,并提供一组标准的调用接口供客户查询使用。服务对象的描述信息也由接口库提供。

    4. CORBA的应用程序结构

    图3 CORBA应用程序结构

    图3 CORBA应用程序结构

    上图为CORBA的应用程序结构,它显示了CORBA应用程序各部件间的调用关系,ORB在CORBA客户和服务器之间传递方法调用和相关信息。

    在CORBA应用系统中主要分为两部分:一是位于应用程序服务器中的CORBA对象,另一个是应用使用的客户程序。这些客户程序通过CORBA技术使用CORBA对象提供的服务来完成其工作。CORBA规范定义了客户程序与服务程序中的对象如何进行通信的机制

    对象请求代理(ORB)负责处理它们之间的通信。ORB提供了支持分布式处理的机制:为请求查找具体的对象实现,让对象实现作好接收请求的准备,传送构成请求的数据等。客户所看到的接口完全独立于对象所在的物理位置,实现对象的编程语言,以及在对象的接口中没有反映出来的其他特性。ORB通过IDL程序框架或动态程序框架来定位相应的实现代码、传送参数,以及对对象实现的传送控制。

    处理通信的对象分别称为存根和构架。客户端为存根(Stub),服务器端为构架(Skeleton)。在客户端,存根对象担当CORBA对象的代理,当客户程序调用CORBA对象的方法时,存根把调用传递给ORB,ORB使用Smart Agent程序定位CORBA服务器。在CORBA服务器上,ORB应用程序把调用传递给构架,构架ORB的通信需要经过BOA(Basic Object Adaptor ,基本对象适配器),CORBA服务器运行指定的过程,然后由相反的路径返回结果。

    Smart Agent用来定位CORBA服务器。启动程序时,自动访问Smart Agent。如果要支持CORBA,应该在局域网的某台机器上运行Smart Agent,当然也可以启动多个Smart Agent,以提高系统的可靠性。当客户机或服务器启动时,它们通过广播消息寻找Smart Agent,因此无需事先知道Smart Agent的位置。ORB实际上是一组放在动态库orb-r.dll中的函数,用户很少直接调用该DLL中的函数,系统在必要的时候调用他们。当服务器启动时,ORB向Smart Agent注册CORBA服务器。

    5. Java IDL简介

    5.1. 认识IDL

    上述的代码就是一个简单的IDL。在IDL中,接口定义以分号结尾。

    IDL只能用来表示接口而无法用来编程。IDL描述的CORBA对象必须要被实现,例如用C++或Java来实现。

    将IDL翻译为Java编程语言的规则统称为Java编程语言的绑定(Java programming language binding)。语言绑定由OMG负责标准化,所有的CORBA提供商都必须使用相同的规则,将IDL的产品映射到特定的编程语言。


    5.2. IDL中的几个重要概念

    5.2.1. 异常

    异常包含如下环节:
      (1)定义异常处理;
      (2)引发异常;
      (3)捕捉异常,异常处理。

    其中环节(1)应该在OMG IDL中进行,(2)(3)环节应该在客户端对象实现中进行。

    定义异常用exception关键字,抛出异常用raises关键字。下面是简单的IDL异常代码:

    注:
    IDL编译器会将异常类型翻译为一个类。


    5.2.2. 继承

    用OMG IDL可定义继承、多重继承以及跨模块继承。使用冒号“:”表示继承。下面是简单的IDL继承代码:


    5.2.3. 变量、常量与属性

    在CORBA接口中不能使用变量。但可以包含常量,如:

    接口还可以包含属性。属性看起来就像实例变量,但它们其实是一对访问器(accessor)与改写器(mutator)方法的简化。相当于Java中的setXXX、getXXX。但如果属性声明为readonly,就不生成改写器方法。


    5.2.4. in、out和inout参数的使用

    定义一个方法时,对于参数传递,除了Java编程语言提供的选择之外,还有其他选择。每个参数都可以声明为in、out或者inout。

    一个in参数仅仅是传递给方法,与Java中的参数传递机制相同。但是Java中没有与out参数类似的东西。方法在返回前,会在每一个out参数中保存一个值,而方法调用者可以取得保存在out参数中的值。

    如果参数只是声明为out,那么方法就不应该指望该参数被初始化。如果参数声明为inout,那么调用者需要为方法提供参数的初始值,然后,该方法可以修改这个值,而调用者能够获取修改后的值。

    在Java中,这些参数可由特殊的持有者类(holder class)来模拟,持有者类由Java IDL编译器生成。IDL编译器为每个接口生成一个后缀为Holder的类。每个持有者类都有一个被称为value的公共实例变量。

    Holder结尾的类主要用于out类型的参数传递,其中通过xxxHolder.value值能得到返回的值。示例IDL代码如下所示:

    注:
    IDL不支持方法重载,因此必须为每个方法采用不同的名称。
    对于一些基础类型,已经预定义了它们的持有者,如IntHolder、DoubleHolder等。


    5.3. IDL元素与Java元素的映射关系

    图4 IDL中的元素以及与Java中元素的映射关系

    图4 IDL中的元素以及与Java中元素的映射关系

    上图为IDL中的元素以及与Java中元素的映射关系。

    OMG IDL中的基本数据类型包括:Long、Short、unsigned long、unsigned short、float、double、char、boolean、Octet、any(其中any可以用来和任何一种数据类型匹配,包括构造数据类型以及数组)。

    构造数据类型包括:struct、union、enum、sequence、String。

    在IDL中,可以用sequence定义大小可变的数组(相当于Java中的数组)。如果希望限定一维序列的上限,可采用sequence<数据类型,上限>的方式来定义,还可以嵌套定义如sequence<sequence<数据类型>>在声明sequence的参数或返回值之前,必须先定义一个类型。例如,下面定义了一个”产品序列”的类型:

    然后就可以在方法声明中使用该类型了:

    6. CORBA接口开发入门

    6.1. Borland VisiBroker Edition简介

    Borland VisiBroker是市场上最流行的CORBA环境之一,目前最新版是6.5。它支持C++和JAVA语言来开发CORBA应用程序,提供跨网络、跨硬件的应用和服务的交互功能。

    并且由于它可以轻松地与包括CORBA2.6在内的所有ORB版本互操作,使得用Java编写CORBA应用时,不必学习IDL和其他CORBA特性,减少了开发者的学习负担;可以把已有的RMI应用移植到CORBA运行环境上,有效地利用了已有的开发资源。


    6.2. 开发环境的安装

    6.2.1. 安装JDK

    由于Borland VisiBroker 6.5只支持JDK1.3.1或JDK1.4.1,更高的JDK版本不能够运行VisiBroker。

    以下以JDK1.3.1_15为例,安装目录为C:\Program Files\Java\jdk1.3.1_15。在“我的电脑 — 属性 — 高级 — 环境变量 — 系统变量”中设置以下三个变量值(若没有则创建):

     
    JAVA_HOME:C:\Program Files\Java\jdk1.3.1_15
    Path:%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin
    CLASSPATH:.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar

    若本已安装并配置好高版本的JDK,可以再安装JDK1.3.1或JDK1.4.1,然后把环境变量JAVA_HOME的值修改为JDK1.3或1.4的目录即可。

    如下图所示,在DOS下执行命令“java -version”可查看当前JDK版本:

    图5 查看当前JDK版本

    图5 查看当前JDK版本


    6.2.2. 安装Borland VisiBroker Edition

    下载Borland.Enterprise.Server.v6.5.AppServer.Edition-ZWTiSO.zip,解压得到zkbsea65.bin,用虚拟光驱(假设盘符为G)打开该文件,以下为安装过程:
      (1)执行G:\Windows\installer.exe;
      (2)选择BES VisiBroker Edition;
      (3)安装目录选择D:\software\work\VisiBroker;
      (4)一直点击“下一步”直到出现“安装”按钮,点击开始安装;
      (5)安装完成后会弹出一个界面要求注册License,点击“Cancle”按钮取消,安装成功;
      (6)运行cmd,执行G:\crack>java -jar LicenseMaker_For_BES65.jar,然后选择D:\software\work\VisiBroker\var文件夹,点击确定后注册License成功;
      (7)备份D:\software\work\VisiBroker\var目录下的borland.lic文件(因为重启电脑可能会重写borland.lic,导致出现“Borland Enterprise Server License error”错误,无法执行start vbj Server)。
      (8)检查和设置windows的系统环境变量(可选,在DOS下执行或直接写入“我的电脑 — 属性 — 高级 — 环境变量 — 系统变量”中):

     
    set VBROKERDIR=D:\software\work\VisiBroker
    set VBROKER_ADM=%VBROKERDIR%\adm
    set BES_LIC_DIR=%VBROKERDIR%\var
    set BES_LIC_DEFAULT_DIR=%VBROKERDIR%\license
    set OSAGENT_PORT=14000
    set Path=%VBROKERDIR%\bin


    6.2.3. 开发环境测试

    可利用VisiBroker为用户提供的一些例子进行测试环境,测试步骤如下:
      (1)“开始 — 运行 — cmd”,进入DOS系统,然后使用cd命令进入目录D:\software\work\VisiBroker\examples\vbe\basic\bank_agent,执行vbmake.bat;
      (2)执行osagent命令,运行智能代理服务,右下角会出现Smart Agent的运行图标;
      (3)执行start vbj Server,运行CORBA服务器程序,若执行成功则会出现如图 2-8所示的DOS框;
      (4)执行vbj Client duyh,运行CORBA客户端程序,若执行成功则会出现如图 2-9所示的DOS框;
      (5)测试完成。

    图6 执行vbmake.bat

    图6 执行vbmake.bat

    图7 运行Smart Agent智能代理服务

    图7 运行Smart Agent智能代理服务

    图8 运行CORBA服务器程序

    图8 运行CORBA服务器程序

    图9 运行CORBA客户端程序

    图9 运行CORBA客户端程序


    6.3. CORBA版Hello World应用开发实例

    6.3.1. 应用需求

    客户端通过CORBA接口调用的方式,请求执行服务端中对象的方法,服务器返回相应的字串符给客户端。


    6.3.2. 定义IDL

    根据需求只定义一个接口和相应该的返回字串的方法,hello.idl文件的内容如下:

    把hello.idl文件保存到D:\corba_test目录下。


    6.3.3. IDL的编译与解释

    “开始 — 运行 — cmd”,进入DOS系统,然后使用cd命令进入目录D:\corba_test,执行命令“idl2java hello.idl”对hello.idl文件进行编译,编译成功后会在D:\corba_test目录出现一个文件夹hello.idl,并且其中有7个java文件。

    图10 编译hello.idl

    图10 编译hello.idl

    对生成的各个文件说明如下:

    • _HelloStub.java:客户Hello对象的存根代码。
    • Hello.java:Hello接口声明。
    • HelloHelper.java:声明HelloHelper类,定义有用的实用工具方法。
    • HelloHolder.java:声明HelloHolder类,这为传递Hello对象提供容器。
    • HelloOperation.java:本接口说明hello.idl文件中的Hello接口中所定义的方法签名。
    • HelloPOA.java:服务器端Hello对象实央的POA服务参象代码。
    • HelloPOATie.java:通过使用tie机制,在服务器端用以实现Hello对象的类。

    6.3.4. 编写Server端的接口实现代码

    6.3.4.1. HelloImpl.java

    引入了POA概念后,Server方的实现对象称为Servant,编写实现代码实际上就是对IDL定义的每个interface,都编写一个Servant,其中要实现interface中定义的每个方法。.

    这里我们将Servant类定义为HelloImpl.java,代码如下:


    6.3.4.2. HelloServer.java

    Servant仅是实现代码,而Server是包含main()函数的可执行的代码。Server的主要任务就是创建所需的Servant,同时通知POA已准备就绪,可以接受客户方的请求。

    新建一个文件HelloServer.java,代码如下:


    6.3.5. 编写Client端的接口实现代码

    6.3.5.1. HelloClient

    Client程序就是客户方的可执行程序,它需要使用到Server方的服务。

    新建一个文件HelloClient.java,代码如下:


    6.3.6. 程序的编译和运行

    编译客户端和服务端程序并运行,步骤如下:
      (1)把3个java文件HelloImpl.java、HelloServer.java和HelloClient放到目录D:\corba_test下;
      (2)“开始 — 运行 — cmd”,进入DOS系统,然后使用cd命令进入目录D:\corba_test,然后执行命令“vbjc HelloClient.java”编译客户端代码,执行命令“vbjc HelloServer.java”编译服务端代码;
      (3)执行osagent命令,运行智能代理服务;
      (4)执行start vbj HelloServer,运行CORBA服务器程序;
      (5)执行vbj HelloClient,运行CORBA客户端程序,结果输出“Hello World!!”。

    图11 编译客户端和服务端代码

    图11 编译客户端和服务端代码

    图12 运行程序成功

    图12 运行程序成功

    7. CORBA常用的四种服务简介

    7.1. 名字服务

    名字服务允许将一个或多个逻辑名与一个对象引用联结起来,并将名称存储在一个命名空间(namespace)中,也允许客户应用使用命名服务,以通过使用分配给对象的逻辑名称来取得该对象的引用。

    相对于智能代理使用的平面型命名空间,命名服务则使用层次型的命名空间,结构类似于java中的包中的结构,如(com.xyz.corba)。

    使用VisiBroker的命名服务中,对象实现使用NamingContext对象以将名称限制到它们所提供的对象。客户应用使用NamingContext来解析限制到对象引用的名称。

    NamingContext通过rosolve方法取得逻辑Name中的对象引用,因为一个Name可以包含一个或多个NameComponent对象,所以解析能在NameComponent结构中来回移动,只要取其中的一个节点的对象引用,即可获得整个NameComponent结构的所有对象引用。

    字串化的名称用于对应字串和CosNaming::Name,用“/”来分隔名称组件;用“.”来分隔id和kind属性;用“\”来作转义字符。如:

    com/xyz/corba


    7.2. 事件服务

    核心ORB支持的通信模型是实现客户端与服务器的一对一同步通信,通知服务则可支持更丰富的需求,包括:

    • 支持分布/预订应用程序,如多对多
    • 支持单向、异步和缓冲事件分布,其吞吐量远大于同步通信
    • 对持服务质量,如事件/连接可靠性
    • 支持事件筛选

    TCP/IP用于实现接收者、提供者和事件通道之间的通信。分为“拉”和“推”的两种通信模型。事件通道使提供者和接收者不需确定对方的通信模型。

    推式模型更为普遍。推型接收者将大多时间花费在事件的回路中,等待从ProxyPushSupplier而来的数据。

    拉式模型是事件通道定期从提供者拉出数据,拉型提供者将大多时间花费在网络事件回路中,等待接收从ProxyPullConsumer发出的数据请求。


    7.3. 通知服务

    通知服务的实现模式跟事件服务类似。


    7.4. 交易服务

    “一手交钱,一手交货”是交易的基本原则,它的等价命题是既不能提了货却不给钱,又不能给了钱却没提货,在现实生活中,有许多对象之间的操作存在这种类似关系:要么这些操作全都进行,要么这些操作全都不进行,这种关系的操作就构成了事务(Transaction)。


    转载请注明:EXP 技术分享博客 » Corba接口学习笔记

    喜欢 (1) 分享 (0)
    发表我的评论
    取消评论

    表情

    Hi,您需要填写昵称和邮箱!

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址