从零开始学WCF(6)数据协定
来源:互联网 发布:网络传送带使用教程 编辑:程序博客网 时间:2024/05/16 14:27
1 数据协定概述
数据协定是在客户端与服务端之间进行数据传递的一种包装方式。数据协定也是一个基本的class,只不过在类上添加了[DataContract],并且在类中的元素使用[DataMember]来修饰。
默认情况下,WCF使用成为数据协定序列化程序的序列化引擎对数据进行序列化和反序列化(与XML进行相互转换)。
所有.NET Framework基元类型(如整形和字符串型)以及某些被视为济源的类型(如DateTime和XmlElement)无需做其他任何准备工作就可序列化并被视为拥有默认数据协定。
必须为所创建的新复杂类型定义数据协定以便可以进行序列化。
正常情况下可以通过将DataContractAttribute属性应用到该类型来完成该任务,可以将此属性应用到类、结构和枚举。
然后必须将DataMemberAttribute属性应用到数据协定类型的每个成员,以指示这些成员为数据成员,序列化引擎就会对这些元素进行序列化。
数据协定是使用“选择性加入”编程模型设计的;未用DataMemberAttribute属性显示标记的任何内容均不会被序列化。
可以将DataMemberAttribute属性应用与字段、属性和事件。
成员可访问级别(internal、private、protected或public)对数据协定无任何影响。
如果将DataMemberAttribute属性应用与静态成员,则将忽略该属性,不能把DataMemberAttribute放在静态成员上使用。
在序列化期间,为属性数据成员调用property-get代码来获取要序列化的属性的值。在发序列化期间,首先创建一个未初始化的对象,而不再该类型上调用任何构造函数,然后反序列化所有数据成员。在发序列化期间,为属性数据成员调用property-set代码,将属性设置为要反序列化的值。
对于将要生效的数据协定,它必须能序列化其所有数据成员。泛型类型的处理方式与非泛型类型完全相同。泛型参数无特殊要求。
无论用于泛型类型参数(T)的类型能否序列化,此类型都可序列化。因为它必须能序列化所有数据成员,所以下面的类型仅在泛型类型参数也可序列化时才可序列化。也就是说Class<T>里的T类能够序列化的话,该Class泛型也就可以序列化。
Demo讲解
1) 新建一个WCF Service Application——Video6.Demo1.GettingStarted,删掉自带的Service1.svc和IService1.cs文件后添加一个WCF Service——CalculatorService。在新建一个数据协定类MyData,注意这里的构造函数,在客户端程序中是无法调用的,只能使用由[DataMember]标记的对象。
using System;using System.Collections.Generic;using System.Linq;using System.Web;using System.Runtime.Serialization;namespace Video6.Demo1.GettingStarted{ //标记为数据协定类,并且定义命名空间,这会在序列化的时候,包装在XML文件里的元素命名空间的名字。 [DataContract(Namespace="http://Video6.Demo1.GettingStarted")] public class MyData { //构造函数 public MyData(string _firstname, string _lastName) { this.firstName = _firstname; this.lastName = _lastName; } string firstName; //DataMember修饰字段 //DataMemberAttribute.IsRequired 属性:获取或设置一个值,该值指示序列化引擎该成员在读取或反序列化时必须存在。 [DataMember(IsRequired = true)] public string lastName; //DataMember修饰属性 [DataMember] public string FirstName { get { return firstName; } set { firstName = value; } } }}
2) 在ICalculatorService里定义服务接口:
using System;using System.Collections.Generic;using System.Linq;using System.Runtime.Serialization;using System.ServiceModel;using System.Text;namespace Video6.Demo1.GettingStarted{ // NOTE: You can use the "Rename" command on the "Refactor" menu to change the interface name "ICalculatorService" in both code and config file together. [ServiceContract(Namespace="http://Video6.Demo1.GettingStarted")] public interface ICalculatorService { [OperationContract] MyData TestMethod(MyData mydata); }}
3) 在CalculatorService里实现ICalculatorService服务接口:
using System;using System.Collections.Generic;using System.Linq;using System.Runtime.Serialization;using System.ServiceModel;using System.Text;namespace Video6.Demo1.GettingStarted{ // NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "CalculatorService" in code, svc and config file together. public class CalculatorService : ICalculatorService { public MyData TestMethod(MyData mydata) { return new MyData(mydata.FirstName + "+111", mydata.lastName + "+222"); } }}
4) 修改配置文件Web.config:
<?xml version="1.0"?><configuration> <system.web> <compilation debug="true" targetFramework="4.0" /> </system.web> <system.serviceModel> <services> <service name="Video6.Demo1.GettingStarted.CalculatorService" behaviorConfiguration="CalculatorServiceBehavior"> <endpoint address="" binding="wsHttpBinding" contract="Video6.Demo1.GettingStarted.ICalculatorService"/> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> </service> </services> <behaviors> <serviceBehaviors> <behavior name="CalculatorServiceBehavior"> <!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment --> <serviceMetadata httpGetEnabled="true" /> <!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information --> <serviceDebug includeExceptionDetailInFaults="false" /> </behavior> </serviceBehaviors> </behaviors> <serviceHostingEnvironment multipleSiteBindingsEnabled="true" aspNetCompatibilityEnabled="true" /> </system.serviceModel> <system.webServer> <modules runAllManagedModulesForAllRequests="true" /> </system.webServer></configuration>
6) 在该项目上添加一个新的Windows控制台程序项目”Client“,在Main方法中测试在客户端调用该服务。首先要添加上面我们部署的WCF服务,服务地址就是”http://localhost/GettingStartedService/CalculatorService.svc“。添加好该服务后,在Main方法中测试是否调用成功。
using System;using System.Collections.Generic;using System.Linq;using System.Text;namespace Client{ class Program { static void Main(string[] args) { ServiceReference1.CalculatorServiceClient client = new ServiceReference1.CalculatorServiceClient(); ServiceReference1.MyData mydata = new ServiceReference1.MyData(); mydata.FirstName = "wang"; mydata.lastName = "xiaoming"; ServiceReference1.MyData newMD = client.TestMethod(mydata); Console.WriteLine("FirstName:{0};lastName:{1}", newMD.FirstName, newMD.lastName); Console.ReadKey(); } }}
源代码: http://download.csdn.net/detail/eric_k1m/6371325
2 数据协定名称
有时,客户端和服务端不共享相同的类型。但只要两端的数据协定是等同的,客户端和服务端仍然可以互相传递数据。也就是说在客户端里生成的代理数据类的类名属性名等都不重要,重要的是DataContractAttribute和DataMemberAttribute里的namespace和name要和服务端一样,才可以调用服务端的数据协定。
完全限定的数据协定名称有命名空间和名称组成。
数据成员只有名称,而没有命名空间。
处理数据协定时,WCF基础结构对于命名空间以及数据协定和数据成员的名称区分大小写。
数据协定命名空间:
数据协定命名空间采用统一资源标示符(URI)的形式。设置DataContractAttribute的Namespace属性。
数据协定名称:
给定类型的默认数据协定名称是该类型的名称(类名)。若要重写默认值,请将DataContractAttribute的Name属性设置为其他名称。
数据成员名称:
给定字段或属性的默认数据成员名称是该字段或属性的名称。若要重写默认值,请将DataMemberAttribute的Name属性设置为其他值。
3 数据协定等效性
要使用数据协定等效,其命名空间和名称必须相同。此外,某一端上的每个数据成员还必须在另一端上具有等效的数据成员。
要使用数据成员等效,其命名控件必须相同。此外,他们还必须表示同一类型的数据,也就是说,其数据协定必须等效。
如果同一端(发送方或接收方)存在两种类型,而其数据协定又不等效(例如,他们的数据成员不同),则不应为他们指定相同的名称和命名空间。否则,可能会引起异常。
由于Person类的数据协定Name是Customer,所以他和没有指定数据协定名称的Customer是等效的,并且他里面的成员标记了Name是一样的所以他们之间也是等效的,
数据成员顺序和数据协定等效性
使用DataMemberAttribute类的Order属性可能会影响数据协定等效性。其成员必须以相同顺序出现,数据协定才能等效。默认顺序是按字母顺序。
数据排序的基本规则包括:
1) 如果数据协定类型是继承层次结构的一部分,则其基类型的数据成员始终排在第一位。
2) 排在下一位的是当前类型的数据成员(按字母顺序排列),这些成员未设置DataMemberAttribute属性(attribute)的Order属性(property)。
3) 在下面的设置了DataMemberAttribute属性(attribute)的Order属性(property)的任何数据成员。这些成员首先按Order属性的值排序,如果多个成员具有特定的Order值,则按字母顺序排列。可以跳过Order值。
DEMO2
1) 我们以我们上面的DEMO为基础进行修改来测试一下等效性的问题,服务端的数据协定并不做任何修改,修改client客户端里引用的服务代理类Reference.cs,把代理类MyData的类名字改成MyData1,但是他上面的属性修饰不做任何修改。
[System.Runtime.Serialization.DataContractAttribute(Name="MyData", Namespace="http://Video6.Demo1.GettingStarted")]
2) 把其他文件中使用到的MyData类名全都改成MyData1后,运行该项目,可以发现该客户端项目可以正常调用WCF服务,这就表明客户端与服务端的数据协定是等效的,也就表明等效性并不是客户端数据协定类名与服务端数据协定的类名一致即可,需要保证的是:DataContractAttribute的Name和Namespace要一致。
3) 把Reference.cs里的数据协定属性改为:
[System.Runtime.Serialization.DataContractAttribute(Name="MyData", Namespace="http://Video6.Demo1.GettingStarted1")]这时候在运行该程序就会报错,这是因为客户端与服务端的数据协定不等效,NameSpace不一致导致的。
4) 然后我们在指定一下客户端代理类数据协定成员的Order顺序,默认顺序为FirstName,然后是lastName;我们现在改为顺序为lastName,然后是FirstName。
[System.Runtime.Serialization.DataMemberAttribute(Order=2)] public string FirstName { get { return this.FirstNameField; } set { if ((object.ReferenceEquals(this.FirstNameField, value) != true)) { this.FirstNameField = value; this.RaisePropertyChanged("FirstName"); } } } [System.Runtime.Serialization.DataMemberAttribute(IsRequired=true,Order=1)] public string lastName { get { return this.lastNameField; } set { if ((object.ReferenceEquals(this.lastNameField, value) != true)) { this.lastNameField = value; this.RaisePropertyChanged("lastName"); } } }改完之后我们在运行一下程序,发现是会报错的,这是由于服务端的数据协定成员的顺序是默认的,也就是按照字母排序的(先是FirstName,然后是lastName)。然后客户端代理类中的数据协定成员的数据手动修改为了先是lastName,然后是FirstName,所以这就导致客户端与服务器端的没有等效性,也就无法调用服务器端的成员。
4 数据协定已知类型
数据协定已知类型:
1) 发送的数据协定源自预期的数据协定,客户端发送的数据协定并不是服务器定义的真正的服务协定,而是派生自服务器的数据协定类型。
2) 要传输的信息的声明类型是接口,而非类、结构或枚举。
3) 要传输的信息的声明类型是Object。
4) 有些类型(包括.NET Framework类型)具有属于上述三种类别之一的成员。例如,Hashtable使用Object在哈希表中存储实际对象。
knownTypeAttribute类
通过首先检查传入消息选择为反序列化而实例化的类型,以确定消息内容遵循的数据协定。然后反序列化引擎尝试查找实现与消息内容兼容的数据协定的CLR类型。反序列化引擎在此过程中允许的候选类型集成为反序列化的“已知类型”集。也就是说当在服务端定义了一个类A为[DataContract]数据协定,并且该类有一个派生类为B,B也是一个DataContract,那么这时候就可以在A类声明的[DataContract]下一行添加一个[KnownType(typeof(B))]的标示,这样的话当客户端调用该服务的时候,就可以同时认识这两个类,并且可以使用这两个类所公开的数据成员。
让反序列化引擎了解某个类型的一种方法是使用KnownTypeAttribute。
不能将属性应用于单个数据成员,只能将它应用与整个数据协定类型。
将属性应用于可能为类或结构的“外部类型”。在其最基本的用法中,应用属性会将类型指定为“已知类型”。只要反序列化外部类型的对象或通过其成员引用的任何对象,这就会导致已知类型成为已知类型集的一部分。
可以将多个KnownTypeAttribute属性应用于同一类型。
DEMO
1) 新建一个WCF Service Application,然后删掉默认生成的svc及.cs文件,添加新的WCF服务——“DataContractCalculatorService”,在生成的IDataContractCalculatorService里定义WCF服务接口:
using System;using System.Collections.Generic;using System.Linq;using System.Runtime.Serialization;using System.ServiceModel;using System.Text;namespace Video6.Demo3.KnownTypes{ // NOTE: You can use the "Rename" command on the "Refactor" menu to change the interface name "IDataContractCalculatorService" in both code and config file together. [ServiceContract(Namespace = "http://Video6.Demo3.KnownTypes")] public interface IDataContractCalculatorService { [OperationContract] ComplexNumber Add(ComplexNumber n1, ComplexNumber n2); [OperationContract] ComplexNumber Substract(ComplexNumber n1, ComplexNumber n2); [OperationContract] ComplexNumber Multiply(ComplexNumber n1, ComplexNumber n2); [OperationContract] ComplexNumber Divide(ComplexNumber n1, ComplexNumber n2); }}
2) 我们在定义WCF服务接口时,参数用到的是类ComplexNumber类,所以我们要创建ComplexNumber类为数据协定类:
这里的KnowType的意思就是在客户端使用该服务的时候,将服务端的ComplexNumber类反序列化为ComplexNumberWithMagnitude类型。
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Runtime.Serialization;namespace Video6.Demo3.KnownTypes{ //定义数据协定 [DataContract(Namespace = "http://Video6.Demo3.KnownTypes")] //在反序列化过程中,将ComplexNumber类型KnowType为ComplexNumberWithMagnitude类型 [KnownType(typeof(ComplexNumberWithMagnitude))] public class ComplexNumber { [DataMember] private double real; [DataMember] private double imaginary; public ComplexNumber(double r1, double i1) { this.Real = r1; this.Imaginary = i1; } public double Real { get { return real; } set { real = value; } } public double Imaginary { get { return imaginary; } set { imaginary = value; } } }}
3) ComplexNumberWithMagnitude类为ComplexNumber类的继承类。
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.Runtime.Serialization;namespace Video6.Demo3.KnownTypes{ [DataContract(Namespace="http://Video6.Demo3.KnownTypes")] public class ComplexNumberWithMagnitude:ComplexNumber { //在该类的构造函数里调用父类的构造函数方法 public ComplexNumberWithMagnitude(double real, double imaginary):base(real,imaginary) { } [DataMember] public double Magnitude { get { return Math.Sqrt(Imaginary * Imaginary + Real * Real); } set { throw new NotImplementedException(); } } }}
4) 实现该WCF服务接口:
using System;using System.Collections.Generic;using System.Linq;using System.Runtime.Serialization;using System.ServiceModel;using System.Text;namespace Video6.Demo3.KnownTypes{ // NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "DataContractCalculatorService" in code, svc and config file together. public class DataContractCalculatorService : IDataContractCalculatorService { public ComplexNumber Add(ComplexNumber n1, ComplexNumber n2) { //返回派生类类型的返回值 return new ComplexNumberWithMagnitude(n1.Real + n2.Real, n1.Imaginary + n2.Imaginary); } public ComplexNumber Substract(ComplexNumber n1, ComplexNumber n2) { //返回派生类类型的返回值 return new ComplexNumberWithMagnitude(n1.Real - n2.Real, n1.Imaginary - n2.Imaginary); } public ComplexNumber Multiply(ComplexNumber n1, ComplexNumber n2) { double real1 = n1.Real * n2.Real; double imaginary1 = n1.Real * n2.Imaginary; double imaginary2 = n2.Real * n1.Imaginary; double real2 = n1.Imaginary * n2.Imaginary * -1; //返回基类类型的返回值 return new ComplexNumber(real1 + real2, imaginary1 + imaginary2); } public ComplexNumber Divide(ComplexNumber n1, ComplexNumber n2) { ComplexNumber conjugate = new ComplexNumber(n2.Real, -1 * n2.Imaginary); ComplexNumber numerator = Multiply(n1, conjugate); ComplexNumber denominator = Multiply(n2, conjugate); //返回基类类型的返回值 return new ComplexNumber(numerator.Real / denominator.Real, numerator.Imaginary); } }}
5) 修改配置文件Web.Config:
<?xml version="1.0"?><configuration> <system.web> <compilation debug="true" targetFramework="4.0" /> </system.web> <system.serviceModel> <services> <service name="Video6.Demo3.KnownTypes.DataContractCalculatorService" behaviorConfiguration="CalculatorServiceBehavior"> <endpoint address="" binding="wsHttpBinding" contract="Video6.Demo3.KnownTypes.IDataContractCalculatorService"/> <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/> </service> </services> <behaviors> <serviceBehaviors> <behavior name="CalculatorServiceBehavior"> <!-- To avoid disclosing metadata information, set the value below to false and remove the metadata endpoint above before deployment --> <serviceMetadata httpGetEnabled="true"/> <!-- To receive exception details in faults for debugging purposes, set the value below to true. Set to false before deployment to avoid disclosing exception information --> <serviceDebug includeExceptionDetailInFaults="false"/> </behavior> </serviceBehaviors> </behaviors> <serviceHostingEnvironment multipleSiteBindingsEnabled="true" /> </system.serviceModel> <system.webServer> <modules runAllManagedModulesForAllRequests="true"/> </system.webServer> </configuration>
6) 编译当前项目,部署到IIS里,并且在IE上使用“http://localhost/KnownTypesService/DataContractCalculatorService.svc”来打开WCF服务页面,确认是否正常。
7) 添加一个客户端项目Client,添加部署到IIS上的WCF服务,测试一下KnowType的功能:
using System;using System.Collections.Generic;using System.Linq;using System.Text;using System.ServiceModel;using Client.ServiceReference1;namespace Client{ class Program { static void Main(string[] args) { DataContractCalculatorServiceClient client = new DataContractCalculatorServiceClient(); ComplexNumber value1 = new ComplexNumber(); value1.real = 1; value1.imaginary = 2; ComplexNumber value2 = new ComplexNumber(); value2.real = 3; value2.imaginary = 4; ComplexNumber result = client.Add(value1, value2); Console.WriteLine("Add({0} + {1}i, {2} + {3}i) = {4} + {5}i", value1.real, value1.imaginary, value2.real, value2.imaginary, result.real, result.imaginary); //由于在服务端实现服务接口的Add方法的时候,返回值类型是派生类ComplexNumberWithMagnitude类型的,所以这里会认为result是ComplexNumberWithMagnitude类型的, //并且可以获得派生类标记了[DataMember]的成员的值。 if (result is ComplexNumberWithMagnitude) { Console.WriteLine("Manitude: {0}", ((ComplexNumberWithMagnitude)result).Magnitude); } else { Console.WriteLine("No magnitude was sent from the service"); } result = client.Multiply(value1, value2); Console.WriteLine("Multiply({0} + {1}i, {2} + {3}i) = {4} + {5}i", value1.real, value1.imaginary, value2.real, value2.imaginary, result.real, result.imaginary); //由于在服务端实现的服务接口的multiply方法的返回值是基类型ComplexNumber的类型,所以这里就不会认为result是ComplexNumberWithMagnitude类型的, //也就无法获取该派生类中所标记了[DataMember]的成员的值。 if (result is ComplexNumberWithMagnitude) { Console.WriteLine("Magnitude: {0}", ((ComplexNumberWithMagnitude)result).Magnitude); } else { Console.WriteLine("No magnitude was sent from the service"); } Console.ReadKey(); } }}
源代码: http://download.csdn.net/detail/eric_k1m/6378145
5 数据协定版本管理
重大更改与非重大更改
对数据协定的更改可能是重大更改,也可能是非重大更改。对数据协定进行非重大更改时,使用较早版本协定的应用程序和使用较新版本协定的应用程序可以互相通信。另一方面,如果进行重大更改,则会阻止单向或双向通信。
下面的更改始终是重大更改:
1) 更改数据协定的Name或Namespace值。
2) 通过DataMemberAttribute的Order属性来更改数据成员的顺序。
3) 重命名数据成员。
4) 更改数据成员的数据协定。
添加或移除数据成员
将具有额外字段的类型反序列化为具有确实字段的类型时,将忽略额外的信息。
具有缺失字段的类型反序列化为具有额外字段的类型时,额外字段将保留其默认值,通常为零或null。
必须的数据成员
1) 通过将DataMemberAttribute的IsRequired属性设置为true,可以将数据成员标记为必须的数据成员。如果反序列化时缺少必须的数据,则会引发异常,而不是将数据成员设置为其默认值。
2) 添加必需的数据成员是重大更改。
3) 移除在任何早期版本中标记为必需成员的数据成员也是重大更改。
4) 将IsRequired属性值从true更改为false则不是重大更改;如果类型的任何早期版本都没有相应数据成员,将该属性值从false更改为true就可能是重大更改。
DEMO
1) 我们打开之前我们使用的GettingStarted来测试一下,先把客户端里的代理类Reference.cs里的FirstName属性注释掉,这里由于在服务端DataContract里的DataMember的FirstName属性并没有标记IsRequired=true,所以这个数据协定成员并不是一个必需的数据成员,这里我们在客户端注释掉后,在Main方法中注释掉调用的参数后,不会影响程序的运行,不会产生错误信息。
//[System.Runtime.Serialization.DataMemberAttribute(Order=1)] //public string FirstName { // get { // return this.FirstNameField; // } // set { // if ((object.ReferenceEquals(this.FirstNameField, value) != true)) { // this.FirstNameField = value; // this.RaisePropertyChanged("FirstName"); // } // } //}
2) 然后再注释掉代理类的lastName属性,删掉调用的参数,然后再运行一下客户端,编译成功,但是在运行的时候会出现错误信息,这是因为在服务端定义的数据协定lastName使用了IsRequired=true的属性标记,这就导致这个数据成员是必需的,如果在客户端代理类里没有声明这个成员的话,就会导致出现错误信息。
//[System.Runtime.Serialization.DataMemberAttribute(IsRequired=true,Order=2)] //public string lastName { // get { // return this.lastNameField; // } // set { // if ((object.ReferenceEquals(this.lastNameField, value) != true)) { // this.lastNameField = value; // this.RaisePropertyChanged("lastName"); // } // } //}
3) 我们把主调掉的lastName属性恢复后,在客户端代理类的lastName标记属性的IsRequired=true改为=false(也即是默认值),这时候我们在运行程序,程序运行成功,这是由于修改代理类的IsReqired属性属于非重大更改,不会影响服务的调用。
[System.Runtime.Serialization.DataMemberAttribute(IsRequired = false, Order = 2)] public string lastName { get { return this.lastNameField; } set { if ((object.ReferenceEquals(this.lastNameField, value) != true)) { this.lastNameField = value; this.RaisePropertyChanged("lastName"); } } }
- 从零开始学WCF(6)数据协定
- 从零开始学WCF(7)消息协定
- 从零开始学WCF(2)设计和实现服务协定
- 传说中的WCF(6):数据协定(b)
- 传说中的WCF(6):数据协定(b)
- WCF学习心得----(六)数据协定
- WCF数据协定
- WCF数据协定
- 从零开始学WCF(11)大型数据和流
- 从零开始学WCF(1)WCF概述
- WCF笔记(2)数据协定
- 传说中的WCF(5):数据协定(a)
- 传说中的WCF(5):数据协定(a)
- 从零开始学WCF(3)配置服务
- 从零开始学WCF(4)承载服务
- 从零开始学WCF(5)生成客户端
- 从零开始学WCF(8)Message类
- 从零开始学WCF(10)序列化
- Python3.3 安装 MySQLdb(Ubuntu13.04)
- 为什么有些文件不能从svn服务器上更新下来
- 一个挺有用的获取tomcat项目路径的工具类
- Setting the Qt Application Icon
- Ubuntu 10.04 修改开机boot
- 从零开始学WCF(6)数据协定
- Flash&Flex大全
- [jbpm]一个完整的spring+jbpm+mysql整合例子
- Linux下安装Python-3.3.2
- C常见问题之字符串的数组形式与指针形式的区别
- SAE 搭建 Discuz
- java实现栈(链表方式)
- maven 配置文件说明
- 中国App如何挑战全球?