开发自己的Web服务处理程序(以支持Ajax框架异步调用Web服务方法)

当你添加Asp.net
AJAX功能到你的Web程序的时候,你需要在Web.config中做一些改变,需要你显式地移除默认的ASMX处理程序并且添加asp.net
ajax框架自己的脚本处理器来作为ASMX处理程序。在上一篇异步调用Web服务方法中,我们谈论过,ajax框架的asmx(ScriptHandler)是不支持异步调用Web服务方法的,所以为了让asp.netajax支持异步Web方法调用,我们需要避开该处理器,以提供自定义的处理器来取代它。

Asp.netAJAX框架的ASMX处理器——ScriptHandler

ScriptHandler是一个常规的HTTP处理程序,它能够通过解析URL找出调用了哪个Web服务及Web方法。然后对应于Web服务类型通过反射来执行Web方法。调用Web方法所涉及的步骤如下:

1、
通过检查Content-Type来确认这是一个基于Ajax的Web方法调用,并查看它是否有application/json的属性。如果没有,则激活一个异常。

2、 通过解析请求的URL找出哪个.asmx被调用,返回.asmx文件类型且被编译过的程序集。

3、 反射程序集并找出Web服务和被调用的Web方法

4、 反序列化输入参数到适当的数据类型中,以防Http POST反序列化JSON图表

5、 查看方法中的参数并映射每个参数到JSON反序列化的对象上

6、 初始化缓存策略

7、 通过反射调用方法并且传递与JSON匹配的参数值

8、 获取返回值。序列化返回值到JSON/XML

9、 发射JSON/XML作为响应

基本的异步Web服务处理程序

首先你需要一个Http处理程序,它将拦截所有对Web服务的调用。在Web.config的<httphandlers>节点下你需要映射该处理程序到*.asmx来进行扩展。默认情况下,asp.net
ajax将映射到ScriptHandler来处理*.asmx扩展名类型的文件,因此需要用你自己的Http处理程序来取代它。

在提供的代码中,ASMXHttpHandler.cs是主要的HTTP处理程序类。ASMXHttpHandler类实现了IhttpAsyncHandler接口。调用Web服务期间,当该处理程序被调用的时候,asp.net框架会首先调用BeginProcessRequest方法。在该方法中,处理程序会解析锁请求的URL并找出调用了哪个Web服务的Web方法。

IAsyncResult IHttpAsyncHandler.BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)
        {
            // Proper Content-Type header must be present in order to make a REST call
            if (!IsRestMethodCall(context.Request))
            {
                return GenerateErrorResponse(context, "Not a valid REST call", extraData);
            }

            string methodName = context.Request.PathInfo.Substring(1);

            WebServiceDef wsDef = WebServiceHelper.GetWebServiceType(context, context.Request.FilePath);
            WebMethodDef methodDef = wsDef.Methods[methodName];

            if (null == methodDef) return GenerateErrorResponse(context, "Web method not supported: " + methodName, extraData);

            // GET request will only be allowed if the method says so
            if (context.Request.HttpMethod == "GET" && !methodDef.IsGetAllowed)
                return GenerateErrorResponse(context, "Http Get method not supported", extraData);

            context.Response.Filter = new ResponseFilter(context.Response);

            // If the method does not have a BeginXXX and EndXXX pair, execute it synchronously
            if (!methodDef.HasAsyncMethods)
            {

WebServiceDef类对Type类进行了包装,同时还包含Web服务类型的一些信息。它维护了WebMethodDef类的集合项,其中每项都包含对某个Web方法的定义。WebMethodDef类为每个方法,与方法相关的特性、是否支持Http
GET方式以及如果可能,对Begin和End方法对的引用都提供了一个名称。如果没有Begin和End方法对,该方法会如下例代码所示,以同步方式执行。所有的这些类都作为Web服务和Web方法进行缓存处理,因此不需要重复使用反射查看元数据。

// If the method does not have a BeginXXX and EndXXX pair, execute it synchronously
            if (!methodDef.HasAsyncMethods)
            {
                // Do synchronous call
                ExecuteMethod(context, methodDef, wsDef);

                // Return a result that says method was executed synchronously
                return new AsmxHandlerSyncResult(extraData);
            }

当方法同步执行的时候,BeginProcessRequest将立即返回。它将返回一个AsmxHandlerSyncRequest实例,表示请求以按同步方式执行并且不需要出发EndProcessRequest方法。AsmxHandlerSyncRequest类实现了IasyncRequest接口。它会从CompletedSynchronously属性返回真值。

public class AsmxHandlerSyncResult : IAsyncResult
    {
        #region Fields

        private WaitHandle handle = new ManualResetEvent(true);
        private object state;

        #endregion Fields

        #region Constructors

        public AsmxHandlerSyncResult(object state)
        {
            this.state = state;
        }

        #endregion Constructors

        #region Properties

        object IAsyncResult.AsyncState
        {
            get { return this.state; }
        }

        WaitHandle IAsyncResult.AsyncWaitHandle
        {
            get { return this.handle; }
        }

        bool IAsyncResult.CompletedSynchronously
        {
            get { return true; }
        }

        bool IAsyncResult.IsCompleted
        {
            get { return true; }
        }

        #endregion Properties
    }

回到BeginProcessRequest方法上,当存在Begin和End方法对的时候,它会调用Web方法的BeginXXX方法并从该方法返回。执行到Asp.net框架并返回线程到线程池。

下面的代码展示了调用Web服务上BeginXXX方法的准备步骤。首先,除了最后两个参数(一个是AsyncCallback,另一个是某个对象状态),来自请求的所有参数都需要进行适当的映射。

else
            {
                // Call the Begin method of web service
                IDisposable target = Activator.CreateInstance(wsDef.WSType) as IDisposable;

                WebMethodDef beginMethod = methodDef.BeginMethod;
                int allParameterCount = beginMethod.InputParametersWithAsyc.Count;

                IDictionary<string, object> inputValues = GetRawParams(context, beginMethod.InputParameters, wsDef.Serializer);
                object[] parameterValues = StrongTypeParameters(inputValues, beginMethod.InputParameters);

                // Prepare the list of parameter values which will also include the AsyncCallback and the state
                object[] parameterValuesWithAsync = new object[allParameterCount];
                Array.Copy(parameterValues, parameterValuesWithAsync, parameterValues.Length);

                // Populate last two parameters with async callback and state
                parameterValuesWithAsync[allParameterCount - 2] = cb;

                AsyncWebMethodState webMethodState = new AsyncWebMethodState(methodName, target,
                    wsDef, methodDef, context, extraData);
                parameterValuesWithAsync[allParameterCount - 1] = webMethodState;

注:Web服务从System.Web.Services.WebService命名空间继承,并实现了Idisposable接口。Activator.CreateInstance是.net框架类的一个实例,它能从自己的类型中动态实例化任何类并返回一个对象的引用。在上例代码中,Web服务类实例被创建,并且使用了对Idisposable接口的引用。使用Idisposable接口是因为当我们调用结束后需要释放占用的资源。

一旦准备工作完成,BeginXXX方法会被调用。现在BeginXXX方法能同步执行并立即返回。在这种情况下,你必须完整地产生BeginXXX方法的响应并完成该请求的执行。但是,如果BeginXXX方法需要更多的时间来异步执行的话,你需要为Asp.net框架返回该执行以便它把线程返回给线程池。当异步操作完成后,EndProcessRequest方法将执行回调并继续处理该请求。

try
                {
                    // Invoke the BeginXXX method and ensure the return result has AsyncWebMethodState. This state
                    // contains context and other information which we need in oreder to call the EndXXX
                    IAsyncResult result = beginMethod.MethodType.Invoke(target, parameterValuesWithAsync) as IAsyncResult;

                    // If execution has completed synchronously within the BeginXXX function, then generate response
                    // immediately. There's no need to call EndXXX
                    if (result.CompletedSynchronously)
                    {
                        object returnValue = result.AsyncState;
                        GenerateResponse(returnValue, context, methodDef, wsDef);

                        target.Dispose();
                        return new AsmxHandlerSyncResult(extraData);
                    }
                    else
                    {
                        if (result.AsyncState is AsyncWebMethodState) return result;
                        else throw new InvalidAsynchronousStateException("The state passed in the " + beginMethod.MethodName + " must inherit from " + typeof(AsyncWebMethodState).FullName);
                    }
                }
                catch (Exception x)
                {
                    target.Dispose();
                    WebServiceHelper.WriteExceptionJsonString(context, x, wsDef.Serializer);
                    return new AsmxHandlerSyncResult(extraData);
                }

当异步操作完成并且回调被激活的时候,EndProcessRequest方法也被激活。例如,如果你异步调用某个外部Web服务其里面包含了命名为BeginXXX的Web方法,则你需要传递一个对AsyncCallback的引用。这会为你接受到的BeginProcessRequest执行同样的回调。Asp.net框架会给你创建一个回调的引用,你可以在HTTP处理程序中用Web服务的EndXXX方法来获得响应和产生输出。

void IHttpAsyncHandler.EndProcessRequest(IAsyncResult result)
        {
            if (result.CompletedSynchronously) return;

            AsyncWebMethodState state = result.AsyncState as AsyncWebMethodState;

            if (result.IsCompleted)
            {
                MethodInfo endMethod = state.MethodDef.EndMethod.MethodType;

                try
                {
                    object returnValue = endMethod.Invoke(state.Target, new object[] { result });
                    GenerateResponse(returnValue, state.Context, state.MethodDef, state.ServiceDef);
                }
                catch (Exception x)
                {
                    WebServiceHelper.WriteExceptionJsonString(state.Context, x, state.ServiceDef.Serializer);
                }
                finally
                {
                    state.Target.Dispose();
                }

                state.Dispose();
            }
        }

当EndXXX类的Web方法调用完成后,如果该方法不是一个返回空类型的方法,你将得到一个返回值,在这种情况下,你需要把该返回值转换到某个JSON格式的字符串中并返回到浏览器端。然而,该方法也可以返回某个XML格式的字符串而取代JSON格式。因此,仅需要把这些字符串写入到HttpResponse对象即可。

private void GenerateResponse(object returnValue, HttpContext context, WebMethodDef methodDef, WebServiceDef serviceDef)
        {
            if (context.Response.Filter.Length > 0)
            {
                // Response has already been transmitted by the WebMethod.
                // So, do nothing
                return;
            }
            string responseString = null;
            string contentType = "application/json";

            if (methodDef.ResponseFormat == System.Web.Script.Services.ResponseFormat.Json)
            {
                responseString = "{ d : (" + serviceDef.Serializer.Serialize(returnValue) + ")}";
                contentType = "application/json";
            }
            else if (methodDef.ResponseFormat == System.Web.Script.Services.ResponseFormat.Xml)
            {
                responseString = returnValue as string;
                contentType = "text/xml";
            }

            context.Response.ContentType = contentType;

            // If we have response and no redirection happening and client still connected, send response
            if (responseString != null
                && !context.Response.IsRequestBeingRedirected
                && context.Response.IsClientConnected)
            {
// Convert the response to response encoding, e.g. utf8
                byte[] unicodeBytes = Encoding.Unicode.GetBytes(responseString);
                byte[] utf8Bytes = Encoding.Convert(Encoding.Unicode, context.Response.ContentEncoding, unicodeBytes);

                // Emit content length in UTF8 encoding string
                context.Response.AppendHeader("Content-Length", utf8Bytes.Length.ToString());

                // Instead of Response.Write which will convert the output to UTF8, use the internal stream
                // to directly write the utf8 bytes
                context.Response.OutputStream.Write(utf8Bytes, 0, utf8Bytes.Length);
            }
            else
            {
                // Send no body as response and we will just abort it
                context.Response.AppendHeader("Content-Length", "0");
                context.Response.ClearContent();
                context.Response.StatusCode = 204; // No Content
            }
        }


为Web方法添加事务化的能力

到此为止,Web方法的执行并没有支持事务。可以使用[TransactionalMethod]特性来界定事务包括的范围、代码的隔离级别和超时时段。

带有TransactionalMethod特性的Web方法将在事务块中自动执行。这里我们将使用.net 2.0事务。事务管理完全在HTTP处理程序中执行并且Web方法并不需要做过多事情。当Web方法遇到异常的时候,事务会自动回滚;否则,事务将自动提交。

ASMXHttpHandler的ExecuteMethod方法同步调用Web方法并提供了事务支持。目前,由于从一个线程到另一个线程的不断转换,针对异步方法的事务支持还没有实现。所以TransactionScope从本地线程存储中的特性丢失了。

private void ExecuteMethod(HttpContext context, WebMethodDef methodDef, WebServiceDef serviceDef)
        {
            IDictionary<string, object> inputValues = GetRawParams(context, methodDef.InputParameters, serviceDef.Serializer);
            object[] parameters = StrongTypeParameters(inputValues, methodDef.InputParameters);

            object returnValue = null;
            using (IDisposable target = Activator.CreateInstance(serviceDef.WSType) as IDisposable)
            {
                TransactionScope ts = null;
                try
                {
                    // If the method has a transaction attribute, then call the method within a transaction scope
                    if (methodDef.TransactionAtt != null)
                    {
                        TransactionOptions options = new TransactionOptions();
                        options.IsolationLevel = methodDef.TransactionAtt.IsolationLevel;
                        options.Timeout = TimeSpan.FromSeconds( methodDef.TransactionAtt.Timeout );

                        ts = new TransactionScope(methodDef.TransactionAtt.TransactionOption, options);
                    }

                    returnValue = methodDef.MethodType.Invoke(target, parameters);

                    // If transaction was used, then complete the transaction because no exception was
                    // generated
                    if( null != ts ) ts.Complete();

                    GenerateResponse(returnValue, context, methodDef, serviceDef);
                }
                catch (Exception x)
                {
                    WebServiceHelper.WriteExceptionJsonString(context, x, serviceDef.Serializer);
                }
                finally
                {
                    // If transaction was started for the method, dispose the transaction. This will
                    // rollback if not committed
                    if( null != ts) ts.Dispose();

                    // Dispose the web service
                    target.Dispose();
                }
            }
        }

上例代码展示了一个执行得当的Web方法并产生一个响应。该Web方法在一个定义为TransactionalMethod特性的事务范围块内执行。但是当Web方法抛出一个异常的时候,它会定位到出现异常消息块的地方。最终,TransactionScope被释放并会检查本次操作是否已经提交。如果没有提交,TransactionScope会回滚该事务。

catch (Exception x)
                {
                    WebServiceHelper.WriteExceptionJsonString(context, x, serviceDef.Serializer);
                }
                finally
                {
                    // If transaction was started for the method, dispose the transaction. This will
                    // rollback if not committed
                    if( null != ts) ts.Dispose();

                    // Dispose the web service
                    target.Dispose();
                }

整个事务化管理都位于HTTP处理程序中,因此,不需要担心Web服务中的事务。仅需要添加一个特性,然后该Web方法就具有了事务特性。

添加缓存头

Asp.net Ajax框架在调用Web方法之前会进行缓存策略初始化操作。如果你没有在[WebMethod]属性中设置缓存持续时间,那么它将把MaxAge设置为零。一旦MaxAge被设置为零,就不能再对它增加。因此,为了从浏览器端获取缓存响应,你不能动态地从你的Web方法代码中增加MaxAge的值。而由于HttpCachePolicy的限制,一旦设定MaxAge的值,就不能再对它进行增加。此外,如果你使用Http检测工具来查看从Web服务调用返回的响应,你将会看到该响应丢失了Content-Length特性。没有该特性,浏览器不能使用Http管道,这将很大滴提升Http响应的下载时间。

为了处理缓存策略,在GenerateResponse方法中作了一些补充。我们的想法是该Web方法在HttpResponse对象中已经设置了一些缓存策略,因此它将不会改变任何缓存设置。否则,它会进行缓存设置检查是否应用了WebMethod特性,然后设置缓存头。

// If we have response and no redirection happening and client still connected, send response
            if (responseString != null
                && !context.Response.IsRequestBeingRedirected
                && context.Response.IsClientConnected)
            {
                // Produce proper cache. If no cache information specified on method and there's been no cache related
                // changes done within the web method code, then default cache will be private, no cache.
                if (IsCacheSet(context.Response) || methodDef.IsETagEnabled)
                {
                    // Cache has been modified within the code. So, do not change any cache policy
                }
                else
                {
                    // Cache is still private. Check if there's any CacheDuration set in WebMethod
                    int cacheDuration = methodDef.WebMethodAtt.CacheDuration;
                    if (cacheDuration > 0)
                    {
                        // If CacheDuration attribute is set, use server side caching
                        context.Response.Cache.SetCacheability(HttpCacheability.Server);
                        context.Response.Cache.SetExpires(DateTime.Now.AddSeconds(cacheDuration));
                        context.Response.Cache.SetSlidingExpiration(false);
                        context.Response.Cache.SetValidUntilExpires(true);

                        if (methodDef.InputParameters.Count > 0)
                        {
                            context.Response.Cache.VaryByParams["*"] = true;
                        }
                        else
                        {
                            context.Response.Cache.VaryByParams.IgnoreParams = true;
                        }
                    }
                    else
                    {
                        context.Response.Cache.SetNoServerCaching();
                        context.Response.Cache.SetMaxAge(TimeSpan.Zero);
                    }
                }

                // Check if there's any need to do ETag match. If ETag matches, produce HTTP 304, otherwise
                // render the content along with the ETag
                if (methodDef.IsETagEnabled)
                {
                    string etag = context.Request.Headers["If-None-Match"];
                    string hash = GetMd5Hash(responseString);

                    if (!string.IsNullOrEmpty(etag))
                    {
                        if (string.Compare(hash, etag, true) == 0)
                        {
                            // Send no body as response and we will just abort it
                            context.Response.ClearContent();
                            context.Response.AppendHeader("Content-Length", "0");
                            context.Response.SuppressContent = true;
                            context.Response.StatusCode = 304;

                            // No need to produce output response body
                            return;
                        }
                    }

                    // ETag comparison did not happen or comparison did not match. So, we need to produce new ETag
                    HttpContext.Current.Response.Cache.SetCacheability(HttpCacheability.Public);
                    HttpContext.Current.Response.Cache.AppendCacheExtension("must-revalidate, proxy-revalidate");
                    HttpContext.Current.Response.Cache.SetETag(hash);
                    HttpContext.Current.Response.Cache.SetLastModified(DateTime.Now);

                    int cacheDuration = methodDef.WebMethodAtt.CacheDuration;
                    if (cacheDuration > 0)
                    {
                        context.Response.Cache.SetExpires(DateTime.Now.AddMinutes(cacheDuration));
                        context.Response.Cache.SetMaxAge(TimeSpan.FromMinutes(cacheDuration));
                    }
                    else
                    {
                        context.Response.Cache.SetMaxAge(TimeSpan.FromSeconds(10));
                    }

                }

                // Convert the response to response encoding, e.g. utf8
                byte[] unicodeBytes = Encoding.Unicode.GetBytes(responseString);
                byte[] utf8Bytes = Encoding.Convert(Encoding.Unicode, context.Response.ContentEncoding, unicodeBytes);

                // Emit content length in UTF8 encoding string
                context.Response.AppendHeader("Content-Length", utf8Bytes.Length.ToString());

                // Instead of Response.Write which will convert the output to UTF8, use the internal stream
                // to directly write the utf8 bytes
                context.Response.OutputStream.Write(utf8Bytes, 0, utf8Bytes.Length);
            }

IsCacheSet方法检查在一些通用的缓存设置中是否有任何变化。如果发生变化,Web方法本身需要对缓存进行处理(由框架完成),并且GenerateResponse方法对于缓存策略并没有发生任何改变。

 private bool IsCacheSet(HttpResponse response)
        {
            if (response.CacheControl == "public") return true;

            FieldInfo maxAgeField = response.Cache.GetType().GetField("_maxAge", BindingFlags.GetField | BindingFlags.Instance | BindingFlags.NonPublic);
            TimeSpan maxAgeValue = (TimeSpan)maxAgeField.GetValue(response.Cache);

            if (maxAgeValue != TimeSpan.Zero) return true;

            return false;
        }

使用

只需在Web.config中将ScriptHandler处理器用该处理器替代即可。

源代码下载

原文发布时间为:2011-12-04

本文作者:vinoYang

本文来自合作伙伴CSDN博客,了解相关信息可以关注CSDN博客。

时间: 2017-11-22

开发自己的Web服务处理程序(以支持Ajax框架异步调用Web服务方法)的相关文章

异步调用Web服务方法

基于Ajax技术构建的门户是web 2.0这一代中最为成功的Web应用程序.而这块市场上iGoogle和Pageflakes这两大站点已经走在了时代的前列. 当你打开Pageflakes,将会看到如下的界面: 接下来就是界面上的各个"部件"去向服务器请求各种web服务,而服务器作为代理,则代为向外部web服务发出请求.(这是因为ajax调用无法跨越,所以常通过代理来请求数据) 问题场景:某个很受用户欢迎的"部件"很长时间不能执行,导致很对请求无法及时执行,引起请求失

异步调用Web服务

web|web服务|异步 //////////////////////////////////////////////////////////////////////////////////Author: stardicky ////E-mail: stardicky@hotmail.com ////QQNumber: 9531511 ////CompanyName: Ezone International ////Class: HBS-0308 ////title: 异步调用Web服务 ///

序列化和反序列化,异步调用web/wcf/函数

//xml序列化 public static string Seria(DataSet ds) { XmlSerializer serializer = new XmlSerializer(typeof(DataSet)); StringBuilder sb = new StringBuilder(); XmlWriter writer = XmlWriter.Create(sb); serializer.Serialize(writer, ds); return sb.ToString();

使用AJAX Extensions客户端进行Web服务调用

从根本上讲,ASP.NET 自始至终都是一项服务器端技术.当然,在某些情况下 ASP.NET 会生成客户端 JavaScript,特别是在验证控件中以及在新推出的 Web 部件基础结构中,但它通常只是简单地将客户端 属性转换成客户端行为.作为开发人员,在收到下一个 POST 请求之前不必考虑与客户端进行交互.对于 需要使用客户端 JavaScript 和 DHTML 构建更具交互性的页面的开发人员而言,则需要在 ASP.NET 2.0 脚本回调功能提供的一些帮助下自己编写代码.这一情况在去年得到

WCF中的REST架构二 (支持AJAX的WCF服务

我在昨天的文章WCF中的REST架构一(REST 概述)谈了REST的基本概要,并提出了从HI REST (高REST)到 LO REST (低REST) 的RESTFULness(REST度)的概念.在今天的文章中,我将详细介绍大家可能最为熟悉的REST风格的WCF 服务:支持AJAX的服务.此类服务应属于LO REST的范畴.现在很多人直觉地将"好"等同于"高大全",因而低估了这种LO REST实现的价值.本篇将告诉你这决非事实,支持AJAX的WCF服务是足够强

《Web测试囧事》——1.3 测试Web Service能否正常提供JSON数据

1.3 测试Web Service能否正常提供JSON数据 某一天,小蔡所在的项目组刚开发完成一个Web Service,服务的功能是,通过在客户端调用时指定的一个ID,可以从后台数据库中读取对应的房产信息,还有与这个房产关联的一到多个房东信息.一到多个图片信息,以及地址信息等.Web Service最终把这些信息组合成JSON格式的数据返回给调用方,调用方可以通过界面来展示相关信息,也可以通过其他方式去使用这些信息.但是,调用方具体如何使用这些信息与Web Service服务本身的测试关系不大

异步调用Restful的WCF服务

上周在pedramr blog上看到有人问是否能够异步调用Restful的WCF服务,下面便是具体实现异步调用Restful的WCF实现细节.通过本文的学习,有助于如下知识的掌握: 如何设定WCF的Restful支持 如何异步调用Restful的WCF服务 第一步:创建一个解决方案:AsyCallRestfulWcf,该解决方案包含下面四个项目: 项目名称 备注 AsyCallRestfulWcf.Contracts WCF服务的契约项目,包含服务契约和数据契约的定义 AsyCallRestfu

安卓调用WEb service实现增删改查功能

问题描述 安卓调用WEb service实现增删改查功能 安卓调用WEb service的String ServerUrl=""http://10.0.2.2:4124/Service1.asmx"",输入这个地址ServerUrl为什么不能实现功能,请问要输入什么地址啊

在SOA开发中使用WBSF动态调用业务服务,第1部分

引言 在面向服务的体系结构(SOA)中,业务服务(Business Service)代表了一组业务功能,其业务行为能够根据创建的策略和运行时上下文自适应调整,以更好地满足服务使用者的需求. 业务服务具有以下特点: 1.从业务层面对业务功能进行设计 2.通过业务策略和元数据来实现灵活,自适应的业务行为 3.通过参照行业模型以简化各个系统之间的互操作 4.基于web service 和行业标准构建 因此,通过业务服务构建IT 应用使得 IT 实现与业务需求吻合,非常好地体现了如下SOA理念:低成本,