• ASP.NET Web API 框架研究 Self Host模式下的消息处理管道


      Self Host模式下的ASP.NET Web API与WCF非常相似,都可以寄宿在任意类型的托管应用程序中,宿主可以是Windows Form 、WPF、控制台应用以及Windows Service,寄宿的请求的监听、接收 和响应功能,是通过一个类型为HttpBinding的Binding对象创建的ChannelListener管道来完成的。

    一、Binding绑定模型

      Binding用来创建处理和传输消息的信道栈,信道栈有一组Channel组成,Binding也由一组BindingElement组成,每个BindingElement会创建一个ChannelListener,ChannelListener再创建对应的Channel,而每个Channel负责处理消息的单独一块功能,Binding启动时候会创建多个Channel组成一个消息处理管道,对请求依次进行处理,对相应按相反方向进行处理,有点类似ASP.NET Web API的消息处理管道,而这是宿主内部的功能,要注意区分。

      对于这种由Binding创建的多个Channel组成的消息处理管道的消息处理能力,是由其包含的Channel决定的,有两种Channel是必不可少的,TransportChannel和MessageEncodingChanneI:

    • TransportChannel  面向传输层用于发送和接收消息
    • MessageEncodingChanneI 负责对接收(请求)的消息实施解码 ,并对发送(响应)的消息实施编码

    二、涉及的类及源码分析

      类主要在程序集System.Web.Http SeIfHost.dII中。

       

      

    1、HttpBinding

      继承自Binding,其只由两个BindingElement组成,Http(s)TransportBindingElement和HttpMessageEncodingBindingElement

      Http(s)TransportBindingElement  决定最终采用的传输协议,Http或Https

      HttpMessageEncodingBindingElement  创 建一个MessageEncoder对象完成针对消息的编码/解码工作 。

      public class  HttpBinding : Binding, IBindingRuntimePreferences
      {
        internal const string CollectionElementName = "httpBinding";

        //默认传输模式为Buffered
        internal const  TransferMode DefaultTransferMode = System.ServiceModel.TransferMode.Buffered;
        //传输BindingElement 
        private HttpsTransportBindingElement _httpsTransportBindingElement;
        private HttpTransportBindingElement _httpTransportBindingElement;
        private HttpBindingSecurity _security;

        //编码/解码BindingElement
        private HttpMessageEncodingBindingElement _httpMessageEncodingBindingElement;
        private Action<HttpTransportBindingElement> _configureTransportBindingElement;

        //初始化HttpBinding 
        public HttpBinding()
        {
          Initialize();
        }

        public HttpBinding(HttpBindingSecurityMode securityMode)
        : this()
        {
          _security.Mode = securityMode;
        }

        //从URL中确定主机名的比较模式

        public  HostNameComparisonMode HostNameComparisonMode

        {
          get { return _httpTransportBindingElement.HostNameComparisonMode; }

          set
          {
            _httpTransportBindingElement.HostNameComparisonMode = value;
            _httpsTransportBindingElement.HostNameComparisonMode = value;
         }
        }

     
        public long MaxBufferPoolSize
        {
          get { return _httpTransportBindingElement.MaxBufferPoolSize; }

          set
          {
            _httpTransportBindingElement.MaxBufferPoolSize = value;
            _httpsTransportBindingElement.MaxBufferPoolSize = value;
          }
        }

        //Buffered模式下的消息的最大缓冲区大小,Buffered模式,即消息先会保存于内存缓冲区后一并传输
        [DefaultValue(TransportDefaults.MaxBufferSize)]
        public int MaxBufferSize
        {
          get { return _httpTransportBindingElement.MaxBufferSize; }

          set
          {
            _httpTransportBindingElement.MaxBufferSize = value;
            _httpsTransportBindingElement.MaxBufferSize = value;
          }
        }

        //请求消息的最大尺寸,默认为65536
        [DefaultValue(TransportDefaults.MaxReceivedMessageSize)]
        public long MaxReceivedMessageSize
        {
          get { return _httpTransportBindingElement.MaxReceivedMessageSize; }

          set
          {
            _httpTransportBindingElement.MaxReceivedMessageSize = value;
            _httpsTransportBindingElement.MaxReceivedMessageSize = value;
          }
        }


        public Action<HttpTransportBindingElement> ConfigureTransportBindingElement
        {
          get { return _configureTransportBindingElement; }

          set
          {
            if (value == null)
            {
              throw Error.PropertyNull();
            }

            _configureTransportBindingElement = value;
          }
        }

        //传输模式

        [DefaultValue(HttpTransportDefaults.TransferMode)]
        public TransferMode TransferMode
        {
          get { return _httpTransportBindingElement.TransferMode; }

          set
          {
            _httpTransportBindingElement.TransferMode = value;
            _httpsTransportBindingElement.TransferMode = value;
          }
        }

        //创建BindingElements,只包含两种
        public override BindingElementCollection CreateBindingElements()
        {
          BindingElementCollection bindingElements = new BindingElementCollection();

          bindingElements.Add(_httpMessageEncodingBindingElement);
          bindingElements.Add(GetTransport());

          return bindingElements.Clone();
        }

        private TransportBindingElement GetTransport()
        {
          HttpTransportBindingElement result = null;

          if (_security.Mode == HttpBindingSecurityMode.Transport)
          {
            _security.Transport.ConfigureTransportProtectionAndAuthentication(_httpsTransportBindingElement);
            result = _httpsTransportBindingElement;
          }
          else if (_security.Mode == HttpBindingSecurityMode.TransportCredentialOnly)
          {
            _security.Transport.ConfigureTransportAuthentication(_httpTransportBindingElement);
            result = _httpTransportBindingElement;
          }
          else
          {
            _security.Transport.DisableTransportAuthentication(_httpTransportBindingElement);
            result = _httpTransportBindingElement;
          }

          if (_configureTransportBindingElement != null)
          {
            _configureTransportBindingElement(result);
          }

          return result;
        }

        //初始化各种对象

        private void Initialize()
        {
          _security = new  HttpBindingSecurity();

          _httpTransportBindingElement = new HttpTransportBindingElement();
          _httpTransportBindingElement.ManualAddressing = true;

          _httpsTransportBindingElement = new HttpsTransportBindingElement();
          _httpsTransportBindingElement.ManualAddressing = true;

          _httpMessageEncodingBindingElement = new HttpMessageEncodingBindingElement();
        }
      }

    2、HttpMessage

      Binding处理管道中处理的消息是HttpMessage,其继承自Message,它是对ASP.NET Web API处理的消息HttpRequestMessage和HttpResponseMessage的封装;HttpMessageEncoder对请求消息解码后得到HttpMessage对象,其会转成—个HttpRequestMessage对象并传入ASP.NET WebAPI消息处理管道进行处理,处理完后返回HttpResponseMessage对象,其被封装成HttpMessage对象,在通过传输层将响应返回给客户端之前,需要利用HttpMessageEncoder对其进行编码,然后进行传输。

      internal sealed class  HttpMessage : Message
      {
        private HttpRequestMessage _request;
        private HttpResponseMessage _response;
        private MessageHeaders _headers;
        private MessageProperties _properties;

        //请求对象为参数,进行封装

        public HttpMessage(HttpRequestMessage request)
        {
          Contract.Assert(request != null, "The 'request' parameter should not be null.");
          _request = request;
          Headers.To = request.RequestUri;
          IsRequest = true;
        }

        //响应对象为参数,进行封装  

        public HttpMessage(HttpResponseMessage response)
        {
          Contract.Assert(response != null, "The 'response' parameter should not be null.");
          _response = response;
          IsRequest = false;
        }

        public override MessageVersion Version
        {
          get
          {
            EnsureNotDisposed();
            return MessageVersion.None;
          }
        }

        public override MessageHeaders Headers
        {
          get
          {
            EnsureNotDisposed();
            if (_headers == null)
            {
              _headers = new MessageHeaders(MessageVersion.None);
            }

            return _headers;
          }
        }

        public override MessageProperties Properties
        {
          get
          {
            EnsureNotDisposed();
            if (_properties == null)
            {
              _properties = new MessageProperties();
              _properties.AllowOutputBatching = false;
            }

            return _properties;
          }
        }

        public override bool IsEmpty
        {
          get
          {
            long? contentLength = GetHttpContentLength();
            return contentLength.HasValue && contentLength.Value == 0;
          }
        }

        public override bool IsFault
        {
          get { return false; }
        }

        public bool IsRequest { get; private set; }

        //从HttpMessage中获取HttpRequestMessage,参数extract是否是抽取,抽取即访问一次后第二次访问会返回null

        public HttpRequestMessage GetHttpRequestMessage(bool extract)
        {
          EnsureNotDisposed();
          Contract.Assert(IsRequest, "This method should only be called when IsRequest is true.");
          if (extract)
          {
            HttpRequestMessage req = _request;

            //设置为null,第二次访问为null
            _request = null;
            return req;
          }

          return _request;
        }

        //从HttpMessage中获取HttpResponseMessage ,参数extract是否是抽取,抽取即访问一次后第二次访问会返回null

        public HttpResponseMessage GetHttpResponseMessage(bool extract)
        {
          EnsureNotDisposed();
          Contract.Assert(!IsRequest, "This method should only be called when IsRequest is false.");
          if (extract)
          {
            HttpResponseMessage res = _response;

            //设置为null,第二次访问为null
            _response = null;
            return res;
          }

          return _response;
        }

        protected override void OnClose()
        {
          base.OnClose();
          if (_request != null)
          {
            _request.DisposeRequestResources();
            _request.Dispose();
            _request = null;
          }

          if (_response != null)
          {
            _response.Dispose();
            _response = null;
          }
        }

        private static string GetNotSupportedMessage()
        {
          return Error.Format(
          SRResources.MessageReadWriteCopyNotSupported,
          HttpMessageExtensions.ToHttpRequestMessageMethodName,
          HttpMessageExtensions.ToHttpResponseMessageMethodName,
          typeof(HttpMessage).Name);
        }

        private void EnsureNotDisposed()
        {
          if (IsDisposed)
          {
            throw Error.ObjectDisposed(SRResources.MessageClosed, typeof(Message).Name);
          }
        }

        private long? GetHttpContentLength()
        {
          HttpContent content = IsRequest
          ? GetHttpRequestMessage(false).Content
          : GetHttpResponseMessage(false).Content;

          if (content == null)
          {
            return 0;
          }

          return content.Headers.ContentLength;
        }
      }

    3、HttpSelfHostServer

      继承自HttpServer,是WebAPI 消息处理管道的第一个处理器,类似Web Host模式,管道的配置是通过HttpConfiguration完成,其对应由HttpSelfHostConfiguration来完成,也是在构造函数里指定。

       重要逻辑都在代码注释里,代码太多,只是拿出重要的代码,以下是注意点:

    • 虽然继承HttpServer,但是没有重写SendAsync方法,只是重用了HttpServer的SendAsync,所以这部分逻辑是一致的
    • HttpSelfHostServer本身会打开和开启channel来监听,监听到消息后,会创建HttpRequestMessage,并调用基类HttpServer的SendAsync,进行后续的消息处理,返回响应HttpResponseMessage后,转换成Message返回给客户端。

      public sealed class  HttpSelfHostServer : HttpServer
      {
        private ConcurrentBag<IReplyChannel> _channels = new ConcurrentBag<IReplyChannel>();
        private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();

        private bool _disposed;
        private HttpSelfHostConfiguration _configuration;
        private IChannelListener<IReplyChannel> _listener;

        private readonly object _windowSizeLock = new object();

        //调用基类HttpServer构造函数,所以最有一个处理器默认也是HttpRoutingDispatcher
        public HttpSelfHostServer(HttpSelfHostConfiguration configuration)
        : base(configuration)
        {
          if (configuration == null)
          {
            throw Error.ArgumentNull("configuration");
          }

          _configuration = configuration;
          InitializeCallbacks();
        }

        //初始化回调,接受到消息后,通过回调触发
        private void InitializeCallbacks()
        {
          //...
          _onReceiveRequestContextComplete = new AsyncCallback(OnReceiveRequestContextComplete);
          _onReplyComplete = new AsyncCallback(OnReplyComplete);

        }

        //打开服务
        public Task OpenAsync()
        {
          if (Interlocked.CompareExchange(ref _state, 1, 0) == 1)
          {
            throw Error.InvalidOperation(SRResources.HttpServerAlreadyRunning, typeof(HttpSelfHostServer).Name);
          }

          _openTaskCompletionSource = new TaskCompletionSource<bool>();
          BeginOpenListener(this);
          return _openTaskCompletionSource.Task;
        }

        private void BeginOpenListener(HttpSelfHostServer server)
        {
          Contract.Assert(server != null);

          try
          {
            // 创建 WCF HTTP transport channel
            HttpBinding binding = new HttpBinding();

            // 获取配置(从HttpSelfHostConfiguration ),并设置到HttpBinding 
            BindingParameterCollection bindingParameters = server._configuration.ConfigureBinding(binding);
            if (bindingParameters == null)
            {
              bindingParameters = new BindingParameterCollection();
            }

            // 创建channel listener
            server._listener = binding.BuildChannelListener<IReplyChannel>(server._configuration.BaseAddress, bindingParameters);
            if (server._listener == null)
            {
              throw Error.InvalidOperation(SRResources.InvalidChannelListener, typeof(IChannelListener).Name, typeof(IReplyChannel).Name);
            }

            //开始监听

            IAsyncResult result = server._listener.BeginOpen(_onOpenListenerComplete, server);
            if (result.CompletedSynchronously)
            {

              //监听到请求消息,触发回调函数
              OpenListenerComplete(result);
            }
          }
          catch (Exception e)
          {
            FaultTask(server._openTaskCompletionSource, e);
          }
        }

        //..省略各种回调

        //Channel接收到消息后,通过回调最后会调用此方法
        private async void ProcessRequestContext(ChannelContext channelContext, RequestContext requestContext)
        {
          Contract.Assert(channelContext != null);
          Contract.Assert(requestContext != null);

          //调用下边的核心方法SendAsync

          HttpResponseMessage response = await SendAsync(channelContext, requestContext);

          //异步调用处理完后返回响应消息,把响应消息转换成Message 
          Message reply = response.ToMessage();

          //传回给客户端
          BeginReply(new ReplyContext(channelContext, requestContext, reply));
        }

        //核心方法,注意没有重写(override)基类SendAsync

        private async  Task<HttpResponseMessage> SendAsync(ChannelContext channelContext, RequestContext requestContext)
        {
          HttpRequestMessage request = null;
          try
          {
            request = CreateHttpRequestMessage(requestContext);
          }
          catch
          {
            return new HttpResponseMessage(HttpStatusCode.BadRequest);
          }

          try
          {

            //SendAsync方法是HttpServer的SendAsync,因为本类HttpSelfHostServer没有override该方法,

            //所以其他逻辑,比如建立Web API消息处理管道等,都和HttpServer一样
            HttpResponseMessage response = await channelContext.Server.SendAsync(request, channelContext.Server._cancellationTokenSource.Token);

            if (response == null)
            {
              response = request.CreateResponse(HttpStatusCode.InternalServerError);
            }

            return response;
          }
          catch (OperationCanceledException operationCanceledException)
          {
            return request.CreateErrorResponse(HttpStatusCode.ServiceUnavailable, SRResources.RequestCancelled, operationCanceledException);
          }
        }

        //构建HttpRequestMessage 

        private  HttpRequestMessage CreateHttpRequestMessage(RequestContext requestContext)
        {
          // 从HTTP请求中获取 WCF Message
          HttpRequestMessage request = requestContext.RequestMessage.ToHttpRequestMessage();
          if (request == null)
          {
            throw Error.InvalidOperation(SRResources.HttpMessageHandlerInvalidMessage, requestContext.RequestMessage.GetType());
          }

          // 创建windows授权的 principal 信息并添加进请求HttpRequestMessage 
          SetCurrentPrincipal(request);

          HttpRequestContext httpRequestContext = new SelfHostHttpRequestContext(requestContext, _configuration,
          request);
          request.SetRequestContext(httpRequestContext);

          // 添加查询客户端证书委托到属性字典中,以便以后可以查询
          request.Properties.Add(HttpPropertyKeys.RetrieveClientCertificateDelegateKey, _retrieveClientCertificate);

          // 添加表示是否是本地请求信息到请求的属性字典中
          request.Properties.Add(HttpPropertyKeys.IsLocalKey, new Lazy<bool>(() => IsLocal(requestContext.RequestMessage)));
          return request;
        }

        protected override void Dispose(bool disposing)
        {
          if (!_disposed)
          {
            _disposed = true;
            if (_cancellationTokenSource != null)
            {
              _cancellationTokenSource.Dispose();
              _cancellationTokenSource = null;
            }
          }

          base.Dispose(disposing);
        }

        private static void SetCurrentPrincipal(HttpRequestMessage request)
        {
          SecurityMessageProperty property = request.GetSecurityMessageProperty();
          if (property != null)
          {
            ServiceSecurityContext context = property.ServiceSecurityContext;
            if (context != null && context.PrimaryIdentity != null)
            {
              WindowsIdentity windowsIdentity = context.PrimaryIdentity as WindowsIdentity;

              if (windowsIdentity != null)
              {

                //设置Thread.CurrentPrincipal 为WindowsPrincipal
                Thread.CurrentPrincipal = new WindowsPrincipal(windowsIdentity);
              }
            }
          }

        }
      }

    4、HttpSelfHostConfiguration

      继承自HttpConfiguration,构造函数中指定一个Uri作为监听基地址,Self Host模式下,请求的监听、接收、响应基本都是通过HttpBinding完成的,HttpSelfHostConfiguration的大部分属性都是用于对创建HttpBinding进行配置,所以它们的属性基本相同。

      public class  HttpSelfHostConfiguration : HttpConfiguration
      {
        public HttpSelfHostConfiguration(string baseAddress)
        : this(CreateBaseAddress(baseAddress))
        {
        }

        //基地址Uri
        public Uri BaseAddress
        {
          get { return _baseAddress; }
        }

        //最大请求并发量,默认值是100,若是多处理器,要cheny乘以处理器个数
        public int MaxConcurrentRequests
        {
          get { return _maxConcurrentRequests; }

          set
          {
            if (value < MinConcurrentRequests)
            {
              throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinConcurrentRequests);
            }
            _maxConcurrentRequests = value;
          }
        }

        //消息传输模式,分Streamed和Buffered(默认)
        public TransferMode TransferMode
        {
          get { return _transferMode; }

          set
          {
            TransferModeHelper.Validate(value, "value");
            _transferMode = value;
          }
        }

        //从URI中获取主机名的匹配比较模式
        public HostNameComparisonMode HostNameComparisonMode
        {
          get { return _hostNameComparisonMode; }

          set
          {
            HostNameComparisonModeHelper.Validate(value, "value");
            _hostNameComparisonMode = value;
          }
        }

        //Buffered模式的最大缓冲池大小,默认值65536

        public int MaxBufferSize
        {
          get
          {
            if (_maxBufferSizeIsInitialized || TransferMode != TransferMode.Buffered)
            {
              return _maxBufferSize;
            }

            long maxReceivedMessageSize = MaxReceivedMessageSize;
            if (maxReceivedMessageSize > Int32.MaxValue)
            {
              return Int32.MaxValue;
            }
            return (int)maxReceivedMessageSize;
          }

          set
          {
            if (value < MinBufferSize)
            {
              throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinBufferSize);
            }
            _maxBufferSizeIsInitialized = true;
            _maxBufferSize = value;
          }
        }

        //允许请求消息的最大大小,默认为65536

        public long MaxReceivedMessageSize
        {
          get { return _maxReceivedMessageSize; }

          set
          {
            if (value < MinReceivedMessageSize)
            {
              throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, MinReceivedMessageSize);
            }
            _maxReceivedMessageSize = value;
          }
        }

        //接收请求消息的超时时间,默认为10分钟  

        public TimeSpan ReceiveTimeout
        {
          get { return _receiveTimeout; }

          set
          {
            if (value < TimeSpan.Zero)
            {
              throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, TimeSpan.Zero);
            }

            _receiveTimeout = value;
          }
        }

        //发送响应消息的超时时间,默认为1分钟  

        public TimeSpan SendTimeout
        {
          get { return _sendTimeout; }

          set
          {
            if (value < TimeSpan.Zero)
            {
              throw Error.ArgumentMustBeGreaterThanOrEqualTo("value", value, TimeSpan.Zero);
            }

            _sendTimeout = value;
          }
        }

        //客户端采用的用户凭证类型
        public HttpClientCredentialType ClientCredentialType
        {
          get { return _clientCredentialType; }
          set { _clientCredentialType = value; }
        }

        //将配置应用到httpBinding
        internal BindingParameterCollection ConfigureBinding(HttpBinding httpBinding)
        {
          return OnConfigureBinding(httpBinding);
        }

        
        protected virtual BindingParameterCollection OnConfigureBinding(HttpBinding httpBinding)
        {
          if (httpBinding == null)
          {
            throw Error.ArgumentNull("httpBinding");
          }

          if (_clientCredentialType != HttpClientCredentialType.Basic && _credentials.UserNameAuthentication.CustomUserNamePasswordValidator != null)
          {
            throw Error.InvalidOperation(SRResources.CannotUseOtherClientCredentialTypeWithUserNamePasswordValidator);
          }

          if (_clientCredentialType != HttpClientCredentialType.Certificate && _credentials.ClientCertificate.Authentication.CustomCertificateValidator != null)
          {
            throw Error.InvalidOperation(SRResources.CannotUseOtherClientCredentialTypeWithX509CertificateValidator);
          }

          httpBinding.MaxBufferSize = MaxBufferSize;
          httpBinding.MaxReceivedMessageSize = MaxReceivedMessageSize;
          httpBinding.TransferMode = TransferMode;
          httpBinding.HostNameComparisonMode = HostNameComparisonMode;
          httpBinding.ReceiveTimeout = ReceiveTimeout;
          httpBinding.SendTimeout = SendTimeout;

          if (_baseAddress.Scheme == Uri.UriSchemeHttps)
          {

            httpBinding.Security = new HttpBindingSecurity()
            {
              Mode = HttpBindingSecurityMode.Transport,
            };
          }

          if (_clientCredentialType != HttpClientCredentialType.None)
          {
            if (httpBinding.Security == null || httpBinding.Security.Mode == HttpBindingSecurityMode.None)
            {
              // Basic over HTTP case
              httpBinding.Security = new HttpBindingSecurity()
              {
                Mode = HttpBindingSecurityMode.TransportCredentialOnly,
              };
            }

            httpBinding.Security.Transport.ClientCredentialType = _clientCredentialType;
          }

          if (UserNamePasswordValidator != null || X509CertificateValidator != null)
          {
            // those are the only two things that affect service credentials
            return AddCredentialsToBindingParameters();
          }
          else
          {
            return null;
          }
        }

        private BindingParameterCollection AddCredentialsToBindingParameters()
        {
          BindingParameterCollection bindingParameters = new BindingParameterCollection();
          bindingParameters.Add(_credentials);
          return bindingParameters;
        }

        //根据基地址创建对应Uri

        private static Uri CreateBaseAddress(string baseAddress)
        {
          if (baseAddress == null)
          {
            throw Error.ArgumentNull("baseAddress");
          }

          return new Uri(baseAddress, UriKind.RelativeOrAbsolute);
        }
      }

    三、HttpBiding、HttpSelfHostServer和消息处理管道的衔接

      先根据指定的监听基地址创建一个HttpSelftHostConfiguration对象,然后,根据它创建HttpSelfHostServer,调用OpenAsync方法开启时候,HttpSelfHostServer会创建一个HttpBinding,并用指定的HttpSelfHostConfiguration对HttpBinding进行配置,然后,HttpBinding会根据监听基地址创建一个ChannelListener管道,对请求进行监听,请求到达时候,接收的二进制数据会经过解码后生成HttpMessage,其是对HttpRequestMessage的封装,然后,HttpSelfHostServer会从该HttpMessage提取出HttpRequestMessage,传递给WebAPI消息处理管道的其他处理器依次处理,处理完返回一个HttpResponseMessage对象,把其封装成HttpMessage,接着对其进行编码,通过传输层进行传输,返回给客户端。

      另外,特别注意的是,Web API消息处理管道的最后一个消息处理器还是HttpRoutingDispatcher,其在HttpSelfHostServer创建时候,调用基类HttpServer的构造函数时候指定,而且在HttpRoutingDispatcher路由时候,由于路由数据没在HttpRequestMessage的属性字典中,所以要直接进行路由解析,获得的路由数据也会放在HttpRequestMessage的属性字典中,所以,后续的Controller创建等操作需要的路由数据都是从HttpRequestMessage的属性字典中获取。

      

  • 相关阅读:
    程序员外包网站
    网络测试
    数据库系统在线网课
    字体
    正则表达式测试工具
    豆瓣Top250数据可视化
    前端模板
    豆瓣Top250电影爬取
    PyCharm激活码
    爬虫禁止访问解决方法(403)
  • 原文地址:https://www.cnblogs.com/shawnhu/p/8046242.html
Copyright © 2020-2023  润新知