从零开始学WCF(5)生成客户端

1. 获取服务终结点的服务协定、绑定以及地址信息

通常我们可以通过使用Service Model Metadata Utility Tool(Svcutil.exe)完成此操作。

从服务中下载元数据,并使用所选择的语言将其转换到托管源代码文件中。

创建一个可用于配置WCF客户端对象的客户端应用程序配置文件:

             svcutil /language:vb /out:ClientCode.vb /config:app.config http://computerName/MyCalculatorService/Service.svc?wsdl

相对应的:编译使用的语言    输出为什么代理类文件 输出的配置文件   服务协定公开的地址

    1. 找到如下地址“C:\Windows\System32\cmd.exe”命令行工具,右键以管理员身份运行(WIN7系统下)。

    2. 输入如下命令:“cd C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin”进入到svcuitl.exe工具所在的文件夹下。然后再输入一下代码生成代理类:

        svcutil.exe /out:c:/ClientCode.cs /config:c:/app.config http://localhost/ServiceModelSamples/Service.svc

    3.按回车进行生成客户端代理类,如果出现一下提示,则证明代理类生成成功:

利用svcutil这个命令行工具(该工具在:"C:\Program Files\Microsoft SDKs\Windows\v7.0A\bin\SvcUtil.exe"里(WIN7系统))。

2. 创建一个WCF客户端对象

WCF客户端是表示某个WCF服务的一个本地对象,客户端可以使用这种表示形式与远程服务进行通信。
WCF客户端类型可实现目标服务协定,因此在创建一个服务协定并配置它之后,就可以直接使用该客户端对象调用服务操作。
WCF运行时将方法调用转换为消息,然后将这些消息发送到服务端,侦听回复,并将这些值作为返回值或out参数(或ref参数)返回到WCF客户端对象中。

3. 调用操作

创建并配置了客户端对象后,请创建一个try/catch块,如果该对象是本地对象,则以相同的方式调用操作,然后关闭WCF客户端对象。
当客户端应用程序调用第一个操作时,WCF将自动打开基础通道,并在回收对象时关闭基础通道。
不要用using块来调用WCF方法,因为using有一个对象释放的过程,有边界的差异,释放的机制是不一样的。

            CalculatorServiceClient client = new CalculatorServiceClient();
            try
            {
                Console.WriteLine(client.Add(4, 6));
                client.Close(); //关闭WCF服务通道
            }                
            catch (TimeoutException timeout) //捕获WCF服务超时的异常
            {
                client.Abort(); //停止WCF服务
            }
            catch (CommunicationException commException) //捕获WCF服务故障
            {
                client.Abort(); //停止异常
            }


4. 错误处理

由于WCF服务是跨网络的,所以所有的消息都是需要序列化的,有操作返回的SOAP(简单对象访问协定)错误导致引发的任何System.ServiceModel.FaultException对象。

至少将应用程序设置为能够处理可能的System.TimeoutException和System.ServiceModel.CommunicationException异常。


Demo:

1) 新建一个“WCF Service Application”项目,然后删除默认自带的“IService1.cs”和“Service1.svc”文件,在添加一个新的“WCF Service”——”CalculatorService“。

2) 在自动新成成的“ICalculatorService.cs”WCF服务接口文件里输入,这里要注意的是在服务协定里定义异常错误协定的方法是使用[FaultContract(typeof(FaultClass))]这样的属性标记。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace Video5.Demo1.Faults
{
    // 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://Video5.Demo1.Faults")]
    public interface ICalculatorService
    {
        [OperationContract]
        int Add(int n1, int n2);

        [OperationContract]
        int Substract(int n1, int n2);

        [OperationContract]
        int Multiply(int n1, int n2);

        [OperationContract]
        [FaultContract(typeof(MathFault))] //当Divide方法出现异常或者错误的时候会自动调用MathFault类来处理异常或错误
        int Divide(int n1, int n2);
    }
}

3) 由于定义了新的错误类型MathFault,这里我们还需要定义这个类,这个类为数据协定DataContract类型。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization;

namespace Video5.Demo1.Faults
{
    //定义错误处理数据协定
    [DataContract(Namespace = "http://Video5.Demo1.Faults")]
    public class MathFault
    {
        private string operation;
        private string problemType;

        [DataMember]
        public string Operation
        {
            set
            {
                operation = value;
            }
            get
            {
                return operation;
            }
        }

        [DataMember]
        public string ProblemType
        {
            set
            {
                problemType = value;
            }
            get
            {
                return problemType;
            }
        }
    }

}

4) 然后我们来实现WCF服务CalculatorService,并且当在除数为零的时候,抛出MathFault异常。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace Video5.Demo1.Faults
{
    // 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
    {
        //实现定义的WCF服务接口类
        public int Add(int n1, int n2)
        {
            return n1 + n2;
        }

        public int Substract(int n1, int n2)
        {
            return n1 - n2;
        }

        public int Multiply(int n1, int n2)
        {
            return n1 * n2;
        }

        public int Divide(int n1, int n2)
        {
            try
            {
                return n1 / n2;
            }
            catch (DivideByZeroException) //捕获分母为零的情况下的异常
            {
                MathFault mf = new MathFault(); //生成一个错误类型对象 也就是前面定义的数据类型
                mf.Operation = "division";   //赋值
                mf.ProblemType = "division by zero";  //赋值
                throw new FaultException<MathFault>(mf);  //抛出FaulException泛型把mf异常传递出去
            }
        }
    }
}

5) 然后定义Web.config WCF服务参数:

<?xml version="1.0"?>
<configuration>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.serviceModel>
    <services>
      <service name="Video5.Demo1.Faults.CalculatorService" behaviorConfiguration="CalculatorServiceBehavior">
        <endpoint address="" binding="wsHttpBinding" contract="Video5.Demo1.Faults.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" />
  </system.serviceModel>
 <system.webServer>
    <modules runAllManagedModulesForAllRequests="true"/>
  </system.webServer>
  
</configuration>

6) 编译一下当前项目,找到所生成的bin文件夹、CalculatorService.svc和Web.Config三个文件后,拷贝到C:\inetpub\wwwroot\Video5.Demo1.Faults 这个IIS应用程序文件家里。

7) 打开IIS管理器后,在默认网站上右键后,选择“添加应用程序”,然后再打开的窗体里,浏览到C:\inetpub\wwwroot\Video5.Demo1.Faults这个物理路径,别名为"FaultsService",然后点击确定。


8) 在IE上输入 “http://localhost/FaultsService/CalculatorService.svc” 就可以打开该WCF服务的说明页了。注意svcutil.exe后面的公开服务传输协议为HTTP协议。



9) 这样就把WCF服务创建并且部署到了IIS里了。下一步就是创建一个客户端程序来调用这个WCF服务。

10) 在Video5.Demo1.Faults这个解决方案里,在创建一个客户端要使用的windows控制台程序“Client”。

11) 在“Client”项目里,引用上面所部署到IIS里的WCF服务,这里有两种方法来引用这个服务,一种是使用SVCUTIL.EXE这个命令来手动生成代理类和配置文件,另外一种是利用VS2010自带的工具来自动生成,第一种方法在最上面的"获取服务终结点的服务协定、绑定以及地址信息"里已经介绍过了,这里我们使用第二种方法。

12) 首先在Client项目里的References文件夹上右键点击后,选择“Add Service Reference”,在打开的窗体里的Address里输入,上面我们在IE上打开的地址“http://localhost/FaultsService/CalculatorService.svc”这也就是在IIS上部署好的WCF服务的地址,然后点击OK即在该客户端项目里添加上了WCF服务了。


13) 下面我们来调用一下这个服务是否添加成功,在Main方法中添加如下代码,这个程序会进入到catch里执行,获取我们自定义的数据协定类型MathFault的值。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            ServiceReference1.CalculatorServiceClient sc = new ServiceReference1.CalculatorServiceClient();
            try
            {
                int value1 = 15;
                int value2 = 0;
                int result = sc.Divide(value1, value2);  //除数为0,引发异常

                sc.Close(); //关闭与WCF服务的连接
            }
            //这里捕获到的异常就是在CalculatorService里抛出的异常类型,要与抛出的异常类型一致。
            catch (FaultException<ServiceReference1.MathFault> e) 
            {
                //通过使用e.Deail...来获取我们在WCF接口中定义的DataContract数据协议类型的值
                Console.WriteLine("发生异常的运算为:" + e.Detail.Operation + "异常类型为:" + e.Detail.ProblemType);
                sc.Abort(); //停止当前WCF服务操作。
            }
        }
    }
}

源代码:  http://download.csdn.net/detail/eric_k1m/6342575


14)  我们来做一个简单的修改,来测试一下Timeout连接超时的异常。修改client的Main方法如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;

namespace Client
{
    class Program
    {
        static void Main(string[] args)
        {
            ServiceReference1.CalculatorServiceClient sc = new ServiceReference1.CalculatorServiceClient();
            try
            {
                Console.WriteLine("设置timeout为很短的时间。。。");
                //设置当前使用的WCF服务的通道Timeout时间未0.001毫秒,这样就导致超时异常的产生
                sc.InnerChannel.OperationTimeout = TimeSpan.FromMilliseconds(0.001);

                int value1 = 1;
                int value2 = 3;
                int result = sc.Add(value1, value2);
                Console.WriteLine("加法的结果为:" + result);

                //最后关闭wcf服务
                sc.Close();
            }
            catch (TimeoutException e)
            {
                Console.WriteLine("捕获到Timeout超时连接异常" + e.GetType());
                sc.Abort();  //中断连接
            }
        }
    }
}


5. 配置和保护客户端

服务协定的安全要求已经在服务协定接口中声明,并且如果Svcutil.exe已经创建了一个配置文件,则该文件通常会包含一个能够支持服务安全要求的绑定。但是在某些情况中,可能需要更多的安全配置,例如配置客户端凭据。


6. 为双工服务创建回调对象

双工服务指定一个回调协定,客户端应用程序必须实现该协定以便提供一个该服务能够根据协定要求调用的回调对象。

1) 实现一个回调协定类。

2) 创建回调协定实现类的一个实例,并使用该实例创建传递给WCF客户端构造函数的System.ServiceModel.InstanceContext对象。

3) 调用操作并处理操作回调。


双工DEMO演示:

1. 新建一个WCF Service Application类型的项目Video5.Demo3.Duplex。然后删除默认的IService1.cs和Service1.svc服务文件。接着添加一个新的WCF Service——CalculatorService,这样就会自动生成服务接口类ICalculatorService以及服务CalculatorService.svc文件。

2. 在ICalculatorService里输入如下代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace Video5.Demo3.Duplex
{
    // NOTE: You can use the "Rename" command on the "Refactor" menu to change the interface name "ICalculatorService" in both code and config file together.
    //注意这里的会话模式(SessionMode)是SessionMode.Required,会在后面的课程中详细讲解。
    //CallbackContract是回调方法的接口是哪个类
    [ServiceContract(Namespace="http://Video5.Demo3.Duplex",SessionMode=SessionMode.Required,CallbackContract=typeof(ICalculatorDuplexCallback))]
    public interface ICalculatorService
    {
        [OperationContract(IsOneWay = true)]
        void Clear();
        [OperationContract(IsOneWay = true)]
        void AddTo(double n);
        [OperationContract(IsOneWay = true)]
        void SubstractFrom(double n);
        [OperationContract(IsOneWay = true)]
        void MultiplyBy(double n);
        [OperationContract(IsOneWay = true)]
        void DivideBy(double n);
    }
}

3. 生成回调函数调用的接口类ICalculatorDuplexCallback:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;

namespace Video5.Demo3.Duplex
{
    //注意回调协定没有服务协定的声明(ServiceContract),这事因为该接口并不是用来让客户端调用的服务
    //而且这里只在方法上标记是操作协定,这事由于该回调协定是在服务端定义,在客户端来实现,并且是在服务端来调用该回调方法。
    public interface ICalculatorDuplexCallback
    {
        [OperationContract(IsOneWay = true)]
        void Result(double result);
        [OperationContract(IsOneWay = true)]
        void Equation(string eqn);
    }
}


4. 实现服务接口,并且在实现中调用callback回调方法类来调用客户端实现的方法:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace Video5.Demo3.Duplex
{
    // NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "CalculatorService" in code, svc and config file together.
    //服务行为来修改当前的对象,他的实例化模式是每个会话创建一个实例化对象,会在后面的课程详细介绍。
    [ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession)]
    public class CalculatorService : ICalculatorService
    {
        double result = 0.0D;
        string equation;

        public CalculatorService()
        {
            equation = result.ToString();
        }

        //属性用来获取回调函数的回调对象的实例 Callback是客户端的对象
        ICalculatorDuplexCallback Callback
        {
            get
            {
                return OperationContext.Current.GetCallbackChannel<ICalculatorDuplexCallback>();
            }
        }

        public void Clear()
        {
            Callback.Equation(equation + "=" + result.ToString());
            equation = result.ToString();
        }

        public void AddTo(double n)
        {
            result += n;
            equation += "+" + n.ToString();
            Callback.Result(result);
        }

        public void SubstractFrom(double n)
        {
            result -= n;
            equation += "-" + n.ToString();
            Callback.Result(result);
        }

        public void MultiplyBy(double n)
        {
            result *= n;
            equation += "*" + n.ToString();
            Callback.Result(result);
        }

        public void DivideBy(double n)
        {
            result /= n;
            equation += "/" + n.ToString();
            Callback.Result(result);
        }
    }
}

5. 配置该服务的配置文件:

<?xml version="1.0"?>
<configuration>

  <system.web>
    <compilation debug="true" targetFramework="4.0" />
  </system.web>
  <system.serviceModel>

    <services>
      <service name="Video5.Demo3.Duplex.CalculatorService" behaviorConfiguration="CalculatorServiceBehavior">
        <endpoint address="" binding="wsDualHttpBinding" contract="Video5.Demo3.Duplex.ICalculatorService"/>
        <endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange"/>
      </service>               
    </services>
    
    <behaviors>
      <serviceBehaviors>
        <behavior name="CalculatorServiceBehavior">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="false"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>

  </system.serviceModel>
  
</configuration>

6) 编译一下当前项目,找到所生成的bin文件夹、CalculatorService.svc和Web.Config三个文件后,拷贝到C:\inetpub\wwwroot\Video5.Demo3.Duplex 这个IIS应用程序文件夹里。

7) 打开IIS管理器后,在默认网站上右键后,选择“添加应用程序”,然后再打开的窗体里,浏览到C:\inetpub\wwwroot\Video5.Demo3.Duplex这个物理路径,别名为"DuplexService",然后点击确定。


8) 在IE上输入 “http://localhost/DuplexService/CalculatorService.svc” 就可以打开该WCF服务的说明页了。注意svcutil.exe后面的公开服务传输协议为HTTP协议。



9) 这样就把WCF服务创建并且部署到了IIS里了。下一步就是创建一个客户端程序来调用这个WCF服务。

10) 在Video5.Demo3.Duplex这个解决方案里,在创建一个客户端要使用的windows控制台程序“Client”。

11) 在“Client”项目里,引用上面所部署到IIS里的WCF服务,这里有两种方法来引用这个服务,一种是使用SVCUTIL.EXE这个命令来手动生成代理类和配置文件,另外一种是利用VS2010自带的工具来自动生成,第一种方法在最上面的"获取服务终结点的服务协定、绑定以及地址信息"里已经介绍过了,这里我们使用第二种方法。

12) 首先在Client项目里的References文件夹上右键点击后,选择“Add Service Reference”,在打开的窗体里的Address里输入,上面我们在IE上打开的地址“http://localhost/DuplexService/CalculatorService.svc”这也就是在IIS上部署好的WCF服务的地址,然后点击OK即在该客户端项目里添加上了WCF服务了。



14) 我们需要在客户端来创建一个回调协定的实现类CallbackHandler,用这个类来实现服务端定义的接口类ICalculatorDuplexCallback。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using Client.ServiceReference1;

namespace Client
{
    //定义一个类来实现回调协定的接口
    public class CallbackHandler:ICalculatorServiceCallback
    {
        public void Result(double result1)
        {
            Console.WriteLine("Result( {0} )", result1);
        }

        public void Equation(string eqn)
        {
            Console.WriteLine("Equation( {0} )", eqn);
        }
    }
}


15) 下面我们来调用一下这个服务是否添加成功,在Main方法中添加如下代码。

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)
        {
            //new一个客户端的回调类,在把这个客户端回调类传递给实例化上下文InstanceContext的实例instanceContext中去。
            InstanceContext instanceContext = new InstanceContext(new CallbackHandler());

            //创建客户端实例,在把实例化上下文传递给服务对象的构造函数,让服务能够知道客户端的回调的对象类型是什么
            CalculatorServiceClient client = new CalculatorServiceClient(instanceContext);

            double value = 100.00D;
            client.AddTo(value);

            client.Clear();

            Console.ReadLine();

            //关闭客户端实例
            client.Close();
        }
    }
}

运行该客户端项目,即可看到结果。
源代码:  http://download.csdn.net/detail/eric_k1m/6367029


7. 异步调用服务

如何调用操作完全取决于客户端开发人员。这是因为当在托管代码中表示组成操作的消息时,这些消息可以映射到同步或异步方法中。因此,如果想要生成异步调用操作的客户端,则可以使用SVCUTIL.EXE通过/async选项生成异步客户端代码。

异步调用服务的DEMO

1) 新建一个Windows控制台项目Video5.Demo4.Asynchronous,在该项目中添加服务接口类ICalculator,代码如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;

namespace Video5.Demo4.Asynchronous
{
    [ServiceContract(Namespace="http://Video5.Demo4.Asynchronous")]
    public interface ICalculator
    {
        [OperationContract]
        double Add(double n1, double n2);

        [OperationContract]
        double Substract(double n1, double n2);

        //由于该方法需要有IO输出文件,所以定义为异步调用服务
        [OperationContract(AsyncPattern=true)]
        IAsyncResult BeginMultiply(double n1, double n2, AsyncCallback callback, object state);
        double EndMultiply(IAsyncResult ar);

        //由于该方法需要有IO输出文件,所以定义为异步调用服务
        [OperationContract(AsyncPattern = true)]
        IAsyncResult BeginDivide(double n1, double n2, AsyncCallback callback, object state);
        double EndDivide(IAsyncResult ar);
    }
}

2) 创建实现该服务接口的实现类CalculatorService
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.Threading;

namespace Video5.Demo4.Asynchronous
{
    [ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall,ConcurrencyMode=ConcurrencyMode.Multiple)]
    public class CalculatorService:ICalculator
    {
        public double Add(double n1, double n2)
        {
            Console.WriteLine("收到ADD方法的同步调用方法,在线程ID{0}: Sleeping for 3 seconds", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(3000); //线程暂停3秒
            Console.WriteLine("返回ADD的结果线程ID为{0}", Thread.CurrentThread.ManagedThreadId);
            return n1 + n2;
        }

        public double Substract(double n1, double n2)
        {
            Console.WriteLine("收到Substract方法的同步调用方法的ID为{0}: Sleeping for 3 seconds", Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(3000);
            Console.WriteLine("返回Subtract方法的线程ID为{0}", Thread.CurrentThread.ManagedThreadId);
            return n1 - n2;
        }

        public IAsyncResult BeginMultiply(double n1, double n2, AsyncCallback callback, object state)
        {
            Console.WriteLine("异步呼叫:BeginMultiply的线程ID为{0}", Thread.CurrentThread.ManagedThreadId);
            //返回一个异步结果
            return new MathAsyncResult(new MathExpression(n1, n2, "*"), callback, state);
        }

        public double EndMultiply(IAsyncResult ar)
        {
            Console.WriteLine("呼叫EndMultiply方法在线程ID为{0}", Thread.CurrentThread.ManagedThreadId);
            //使用异步来完成该异步操作
            return MathAsyncResult.End(ar);
        }

        public IAsyncResult BeginDivide(double n1, double n2, AsyncCallback callback, object state)
        {
            Console.WriteLine("异步调用:BeginDivide 在线程ID{0}上", Thread.CurrentThread.ManagedThreadId);
            //返回一个异步结果
            return new MathAsyncResult(new MathExpression(n1, n2, "/"), callback, state);
        }

        public double EndDivide(IAsyncResult ar)
        {
            Console.WriteLine("调用EndDivide在线程ID为{0}", Thread.CurrentThread.ManagedThreadId);
            //使用异步结果来完成该异步操作
            return MathAsyncResult.End(ar);
        }
    }
}

3) MathExpression类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;

namespace Video5.Demo4.Asynchronous
{
    public class MathExpression
    {
        private double n1;
        private double n2;
        private string operation;

        public MathExpression(double n1, double n2, string operation)
        {
            this.n1 = n1;
            this.n2 = n2;
            this.operation = operation;
        }

        public double N1
        {
            get { return n1; }
        }

        public double N2
        {
            get { return n2; }
        }

        public string Operation
        {
            get { return operation; }
        }

        public double Result
        {
            get
            {
                switch (operation)
                { 
                    case "+":
                        return N1 + N2;
                    case "-":
                        return N1 - N2;
                    case "*":
                        return N1 * N2;
                    case "/":
                        return N1 / N2;
                    default:
                        throw new InvalidOperationException("could not handle" + Operation + " operation");
                }
            }
        }

        public byte[] ToBytes()
        {
            return Encoding.Unicode.GetBytes(String.Format(CultureInfo.InvariantCulture, "{0} {1} {2} = {3}", N1, Operation, N2, Result));
        }
    }
}

4) MathAsyncResult类:

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值