我们还是简单的来复习一下Session吧:Session的数据时保存在服务器端,并且每个客户端对应不同Session。那么Session究竟是如何保存,如何区分客服端的了?我们还是沿用以前的方法来讲吧,以一个demo开始:

    protected void Page_Load(object sender, EventArgs e)  
           {  
               string name = this.Request["Name"];  
               object sessionName = Session["Name"];  
               if (string.IsNullOrEmpty(name) && sessionName==null)  
               {  
                   Response.Write("Please Enter your name!");  
               }  
               else   
               {  
                   if (sessionName == null)  
                   {  
                       Session.Add("Name", name);  
                       Response.Write("Set Session name and Session ID:"+Session.SessionID);  
                   }  
                   else  
                   {  
                       Response.Write("Get Session Name and Session ID:"+ Session.SessionID);  
      
                   }  
                   Response.Write(" Name:" + name);  
               }  
           }  

 假设我们的请求路径为http://localhost:18385/WebForm1.aspx?name=majiang

第一次请求数据如下:

Asp.net Session认识加强-Session究竟是如何存储你知道吗?

第二次请求数据了:

Asp.net Session认识加强-Session究竟是如何存储你知道吗?

这里我们看见在第一次请求的返回头里面有一个ASP.NET_SessionId,在第二次请求过程中这个请求头里面也含有ASP.NET_SessionId,并且它的值刚好是Session.SessionID(我这里用的是asp.net4.5),我们可以猜测这个ASP.NET_SessionId就是用来区分不同的客户端请求。那么这个值是什么时候生成的又是什么时候输出的了?

首先我们需要知道我们用到的那个Session具体在什么地方定义的,其实它是定义于HttpContext的Session属性中,我们一般的page也只是调用这个属性而已。

public HttpSessionState Session
{
    get
    {
        if (this.HasWebSocketRequestTransitionCompleted)
        {
            return null;
        }
        if (this._sessionStateModule != null)
        {
            lock (this)
            {
                if (this._sessionStateModule != null)
                {
                    this._sessionStateModule.InitStateStoreItem(true);
                    this._sessionStateModule = null;
                }
            }
        }
        return (HttpSessionState) this.Items["AspSession"];
    }
}
 
这里用到一个_sessionStateModule的变量,那么究竟在什么地方操作它们的了?在HttpContext中有两个操作sessionStateModule方法如下:
  internal void AddDelayedHttpSessionState(SessionStateModule module)
    {
        if (this._sessionStateModule != null)
        {
            throw new HttpException(SR.GetString("Cant_have_multiple_session_module"));
        }
        this._sessionStateModule = module;
    }

    internal void RemoveDelayedHttpSessionState()
    {
        this._sessionStateModule = null;
    }

这两个方法干什么的我就不说了,它们是在什么地方调用的了。如果你开发过asp.net,那么你应该知道在SessionStateModule 类,它是一个IHttpModule的实现者专门用来管理Session的,在这个类中有一个InitModuleFromConfig方法,该方法主要 是在该类的Init中调用,如丧我们来看看它的具体实现吧:

    private void InitModuleFromConfig(HttpApplication app, SessionStateSection config)  
       {  
           if (config.Mode != SessionStateMode.Off)  
           {  
               app.AddOnAcquireRequestStateAsync(new BeginEventHandler(this.BeginAcquireState), new EndEventHandler(this.EndAcquireState));  
               app.ReleaseRequestState += new EventHandler(this.OnReleaseState);  
               app.EndRequest += new EventHandler(this.OnEndRequest);  
               this._partitionResolver = this.InitPartitionResolver(config);  
               switch (config.Mode)  
               {  
                   case SessionStateMode.InProc:  
                       if (HttpRuntime.UseIntegratedPipeline)  
                       {  
                           s_canSkipEndRequestCall = true;  
                       }  
                       this._store = new InProcSessionStateStore();  
                       this._store.Initialize(null, null);  
                       break;  
      
                   case SessionStateMode.StateServer:  
                       if (HttpRuntime.UseIntegratedPipeline)  
                       {  
                           s_canSkipEndRequestCall = true;  
                       }  
                       this._store = new OutOfProcSessionStateStore();  
                       ((OutOfProcSessionStateStore) this._store).Initialize(null, null, this._partitionResolver);  
                       break;  
      
                   case SessionStateMode.SQLServer:  
                       this._store = new SqlSessionStateStore();  
                       ((SqlSessionStateStore) this._store).Initialize(null, null, this._partitionResolver);  
                       break;  
      
                   case SessionStateMode.Custom:  
                       this._store = this.InitCustomStore(config);  
                       break;  
               }  
               this._idManager = this.InitSessionIDManager(config);  
               if (((config.Mode == SessionStateMode.InProc) || (config.Mode == SessionStateMode.StateServer)) && this._usingAspnetSessionIdManager)  
               {  
                   this._ignoreImpersonation = true;  
               }  
           }  
       }  

这里主要是设置 this._store和  this._idManager 它们两个变量,其中 this._store的设置根据Session的存储类型不同设置为不同的实例,这里的存储方式有以下四种

public enum SessionStateMode
{
    Off,
    InProc,
    StateServer,
    SQLServer,
    Custom
}
默认的是SessionStateMode.InProc,所以默认的this._store是一个InProcSessionStateStore实 例,而this._idManager默认是一个SessionIDManager实例。这个方法结束后我们的 this._store和  this._idManager这两个变量就已经有值了。在SessionStateModule类中还有一个很重要的方法 BeginAcquireState:

    private IAsyncResult BeginAcquireState(object source, EventArgs e, AsyncCallback cb, object extraData)  
       {  
           IAsyncResult result;  
           bool sessionStateItem = true;  
           bool flag3 = false;  
           this._acquireCalled = true;  
           this._releaseCalled = false;  
           this.ResetPerRequestFields();  
           this._rqContext = ((HttpApplication) source).Context;  
           this._rqAr = new HttpAsyncResult(cb, extraData);  
           this.ChangeImpersonation(this._rqContext, false);  
           try  
           {  
               if (EtwTrace.IsTraceEnabled(4, 8))  
               {  
                   EtwTrace.Trace(EtwTraceType.ETW_TYPE_SESSION_DATA_BEGIN, this._rqContext.WorkerRequest);  
               }  
               this._store.InitializeRequest(this._rqContext);  
               bool requiresSessionState = this._rqContext.RequiresSessionState;  
               if (this._idManager.InitializeRequest(this._rqContext, false, out this._rqSupportSessionIdReissue))  
               {  
                   this._rqAr.Complete(true, null, null);  
                   if (EtwTrace.IsTraceEnabled(4, 8))  
                   {  
                       EtwTrace.Trace(EtwTraceType.ETW_TYPE_SESSION_DATA_END, this._rqContext.WorkerRequest);  
                   }  
                   return this._rqAr;  
               }  
               if ((s_allowInProcOptimization && !s_sessionEverSet) && (!requiresSessionState || !((SessionIDManager) this._idManager).UseCookieless(this._rqContext)))  
               {  
                   flag3 = true;  
               }  
               else  
               {  
                   this._rqId = this._idManager.GetSessionID(this._rqContext);  
               }  
               if (!requiresSessionState)  
               {  
                   if (this._rqId != null)  
                   {  
                       this._store.ResetItemTimeout(this._rqContext, this._rqId);  
                   }  
                   this._rqAr.Complete(true, null, null);  
                   if (EtwTrace.IsTraceEnabled(4, 8))  
                   {  
                       EtwTrace.Trace(EtwTraceType.ETW_TYPE_SESSION_DATA_END, this._rqContext.WorkerRequest);  
                   }  
                   return this._rqAr;  
               }  
               this._rqExecutionTimeout = this._rqContext.Timeout;  
               if (this._rqExecutionTimeout == DEFAULT_DBG_EXECUTION_TIMEOUT)  
               {  
                   this._rqExecutionTimeout = s_configExecutionTimeout;  
               }  
               this._rqReadonly = this._rqContext.ReadOnlySessionState;  
               if (this._rqId != null)  
               {  
                   sessionStateItem = this.GetSessionStateItem();  
               }  
               else if (!flag3)  
               {  
                   bool flag4 = this.CreateSessionId();  
                   this._rqIdNew = true;  
                   if (flag4)  
                   {  
                       if (s_configRegenerateExpiredSessionId)  
                       {  
                           this.CreateUninitializedSessionState();  
                       }  
                       this._rqAr.Complete(true, null, null);  
                       if (EtwTrace.IsTraceEnabled(4, 8))  
                       {  
                           EtwTrace.Trace(EtwTraceType.ETW_TYPE_SESSION_DATA_END, this._rqContext.WorkerRequest);  
                       }  
                       return this._rqAr;  
                   }  
               }  
               if (sessionStateItem)  
               {  
                   this.CompleteAcquireState();  
                   this._rqAr.Complete(true, null, null);  
               }  
               result = this._rqAr;  
           }  
           finally  
           {  
               this.RestoreImpersonation();  
           }  
           return result;  
       }  

在这个方法中有以下3句比较重要

    this._rqId = this._idManager.GetSessionID(this._rqContext);
   sessionStateItem = this.GetSessionStateItem();

    this.CompleteAcquireState();

第一句获取SessionID,第二句货物SessionStateItem,第三句主要是调用一个CompleteAcquireState方法,而这个方法里面有一句  SessionStateUtility.AddDelayedHttpSessionStateToContext(this._rqContext, this);或则this.InitStateStoreItem(true); 这个方法主要对应一句

 SessionStateUtility.AddHttpSessionStateToContext(this._rqContext, this._rqSessionState);,在这个类中还有一个方法OnReleaseState里面有这么一句

 SessionStateUtility.RemoveHttpSessionStateFromContext(this._rqContext, delayed);

我们首先来可看看SessionStateUtility的AddHttpSessionStateToContext、RemoveHttpSessionStateFromContext方法的实现吧。

internal static void AddDelayedHttpSessionStateToContext(HttpContext context, SessionStateModule module)
{
    context.AddDelayedHttpSessionState(module);
}
internal void AddDelayedHttpSessionState(SessionStateModule module)
{
    if (this._sessionStateModule != null)
    {
        throw new HttpException(SR.GetString("Cant_have_multiple_session_module"));
    }
    this._sessionStateModule = module;
}

public static void AddHttpSessionStateToContext(HttpContext context, IHttpSessionState container)
    {
        HttpSessionState state = new HttpSessionState(container);
        try
        {
            context.Items.Add("AspSession", state);
        }
        catch (ArgumentException)
        {
            throw new HttpException(SR.GetString("Cant_have_multiple_session_module"));
        }
    }

  internal static void RemoveHttpSessionStateFromContext(HttpContext context, bool delayed)
    {
        if (delayed)
        {
            context.RemoveDelayedHttpSessionState();
        }
        else
        {
            context.Items.Remove("AspSession");
        }
    }

其中HttpContext的RemoveDelayedHttpSessionState就一句    this._sessionStateModule = null;我想对于SessionStateUtility里面的这几个方法我就不多说吧,很简单。

我们还是回头看看前面那2句吧,

public string GetSessionID(HttpContext context)
{
    string id = null;
    this.CheckInitializeRequestCalled(context);
    if (this.UseCookieless(context))
    {
        return (string) context.Items["AspCookielessSession"];
    }
    HttpCookie cookie = context.Request.Cookies[Config.CookieName];
    if ((cookie != null) && (cookie.Value != null))
    {
        id = this.Decode(cookie.Value);
        if ((id != null) && !this.ValidateInternal(id, false))
        {
            id = null;
        }
    }
    return id;
}

默认情况下我们的cookie是可用的,这里的Config.CookieName实际上就是SessionStateSection的CookieName属性

服务器保存的id就是cookie value过后的Decode,其实现code 如下:

 public virtual String Decode(String id) {
            // Need to do UrlDecode if the session id could be custom created.
            if (_isInherited) {
                Debug.Trace("SessionIDManager", "Decode is doing UrlDecode ");
                return HttpUtility.UrlDecode(id);
            }
            else {
                Debug.Trace("SessionIDManager", "Decode is doing nothing");
                return id.ToLower(CultureInfo.InvariantCulture);
            }
        }

  其中  _isInherited = !(this.GetType() == typeof(SessionIDManager));的取值。SessionIDManager的实例代码如下

  ISessionIDManager InitSessionIDManager(SessionStateSection config) {
            string  sessionIDManagerType = config.SessionIDManagerType;
            ISessionIDManager  iManager;

            if (String.IsNullOrEmpty(sessionIDManagerType)) {
                iManager = new SessionIDManager();
                _usingAspnetSessionIdManager = true;
            }
            else {
                Type    managerType;

                managerType = ConfigUtil.GetType(sessionIDManagerType, "sessionIDManagerType", config);
                ConfigUtil.CheckAssignableType(typeof(ISessionIDManager), managerType, config, "sessionIDManagerType");

                iManager = (ISessionIDManager)HttpRuntime.CreatePublicInstance(managerType);
            }

            iManager.Initialize();

            return iManager;
        }

 

[ConfigurationProperty("cookieName", DefaultValue="ASP.NET_SessionId")]
public string CookieName
{
    get
    {
        return (string) base[_propCookieName];
    }
    set
    {
        base[_propCookieName] = value;
    }
}

到这里大家应该知道为什么Http请求和返回关于Session对应Cookie的id是ASP.NET_SessionId了吧。不过大家要注意一点这里的SessionIDManager 在操作cookie做了一些数据验证处理,如果在特殊情况需要自定义验证规则我们可以自己来实现ISessionIDManager接口。这里我们可以看到第一次请求是没有sessionid的,所以sessionStateItem = this.GetSessionStateItem();这句代码不会执行,sessionStateItem默认为true,但是第二次请求时有sessionid这句代码就会执行。GetSessionStateItem()的实现这里我们就忽略了吧,这个方法设置一个SessionStateStoreData的实例 this._rqItem ,如果 this._rqItem为null则返回false。一般我们的Session都是可读写的。GetSessionStateItem方法主要是调用  this._rqItem = this._store.GetItemExclusive(this._rqContext, this._rqId, out flag2, out span, out this._rqLockId, out this._rqActionFlags);

现在我们回到CompleteAcquireState方法中来:

  if (flag)
            {
                SessionStateUtility.AddDelayedHttpSessionStateToContext(this._rqContext, this);
                this._rqSessionState = s_delayedSessionState;
            }
            else
            {
                this.InitStateStoreItem(true); //SessionStateUtility.AddHttpSessionStateToContext(this._rqContext, this._rqSessionState);
            }

这里是flag默认是false,里面具体判断就不说,InitStateStoreItem方法主要代码:

if (this._rqItem == null)
            {
                this._rqItem = this._store.CreateNewStoreData(this._rqContext, s_timeout);
            }

this._rqSessionItems = this._rqItem.Items;

   this._rqSessionState = new HttpSessionStateContainer(this, this._rqId, this._rqSessionItems, this._rqStaticObjects, this._rqItem.Timeout, this._rqIsNewSession, s_configCookieless, s_configMode, this._rqReadonly);
            SessionStateUtility.AddHttpSessionStateToContext(this._rqContext, this._rqSessionState);

这里InProcSessionStateStore 的CreateNewStoreData方法实际就是调用SessionStateUtility.CreateLegitStoreData:

 internal static SessionStateStoreData CreateLegitStoreData(HttpContext context, ISessionStateItemCollection sessionItems, HttpStaticObjectsCollection staticObjects, int timeout)
{
    if (sessionItems == null)
    {
        sessionItems = new SessionStateItemCollection();
    }
    if ((staticObjects == null) && (context != null))
    {
        staticObjects = GetSessionStaticObjects(context);
    }
    return new SessionStateStoreData(sessionItems, staticObjects, timeout);
}

其中SessionStateItemCollection的定义如下:

public sealed class SessionStateItemCollection : NameObjectCollectionBase, ISessionStateItemCollection, ICollection, IEnumerable

这里创建了一个  HttpSessionStateContainer实例。我想大家到这里就应该明白我们的Session实际上就是一个HttpSessionStateContainer实例

好现在我来看 Session.SessionID这个是怎么实现的
public string SessionID
{
    get
    {
        if (this._id == null)
        {
            this._id = this._stateModule.DelayedGetSessionId();
        }
        return this._id;
    }
}

而SessionStateModule的DelayedGetSessionId方法实现如下:

internal string DelayedGetSessionId()
{
    this.ChangeImpersonation(this._rqContext, false);
    try
    {
        this._rqId = this._idManager.GetSessionID(this._rqContext);
        if (this._rqId == null)
        {
            this.CreateSessionId();
        }

    }
    finally
    {
        this.RestoreImpersonation();
    }
    return this._rqId;
}
这里的CreateSessionId具体是怎么创建我就不说了吧,知道它是真正创建sessionid的就可以。而session的实际操作都是在ISessionStateItemCollection里面如HttpSessionStateContainer的Add方法:

public void Add(string name, object value)
{
    this._sessionItems[name] = value;
}

而这里的_sessionItems实际上是this._rqItem.Items,本来想忽略_rqItem的创建,看来这个实例比较强啊。

 this._rqItem = this._store.GetItemExclusive(this._rqContext, this._rqId, out flag2, out span, out this._rqLockId, out this._rqActionFlags);
        if ((((this._rqItem == null) && !flag2) && (this._rqId != null)) && ((s_configCookieless != HttpCookieMode.UseUri) || !s_configRegenerateExpiredSessionId))
        {
            this.CreateUninitializedSessionState();
            this._rqItem = this._store.GetItemExclusive(this._rqContext, this._rqId, out flag2, out span, out this._rqLockId, out this._rqActionFlags);
        }

这里的CreateUninitializedSessionState方法实际就是调用this._store.CreateUninitializedItem(this._rqContext, this._rqId, s_timeout);

我们前面知道this._store这个可以取很多实例的,是SessionStateStoreProviderBase类型,这里我们也已默认的 InProcSessionStateStore(继承SessionStateStoreProviderBase)来说说吧,相关方法:

private SessionStateStoreData DoGet(HttpContext context, string id, bool exclusive, out bool locked, out TimeSpan lockAge, out object lockId, out SessionStateActions actionFlags)
{
    bool flag;
    string key = this.CreateSessionStateCacheKey(id);
    InProcSessionState state = (InProcSessionState) HttpRuntime.CacheInternal.Get(key);
    if (state == null)
    {
        return null;
    }
  ......
    return SessionStateUtility.CreateLegitStoreData(context, state._sessionItems, state._staticObjects, state._timeout);
}

public override void CreateUninitializedItem(HttpContext context, string id, int timeout)
{
    string key = this.CreateSessionStateCacheKey(id);
    SessionIDManager.CheckIdLength(id, true);
    InProcSessionState state = new InProcSessionState(null, null, timeout, false, DateTime.MinValue, NewLockCookie, 1);
    try
    {
    }
    finally
    {
        if (HttpRuntime.CacheInternal.UtcAdd(key, state, null, Cache.NoAbsoluteExpiration, new TimeSpan(0, timeout, 0), CacheItemPriority.NotRemovable, this._callback) == null)
        {
            PerfCounters.IncrementCounter(AppPerfCounter.SESSIONS_TOTAL);
            PerfCounters.IncrementCounter(AppPerfCounter.SESSIONS_ACTIVE);
        }
    }
}

现在我们终于明白一个Sessionid对应一个SessionStateStoreData,所以它能区分不同的用户请求,这里的id就是我们前面的this._rqId了。

现在我们也总结一下吧,我们的HttpContext的Session属性实际上是一个HttpSessionStateContainer实例(HttpSessionStateContainer继承IHttpSessionState),而它数据成员都是保存在ISessionStateItemCollection实例中,每一次http请求我们都会去获取它的Sessionid,第一次请求sessionid问null,我们没有对应的SessionStateStoreData数据,这时我们在SessionStateModule的 InitStateStoreItem方法调用SessionStateStoreProviderBase的CreateNewStoreData方法来创建一个SessionStateStoreData实例,其中该实例有一个成员变量类型是ISessionStateItemCollection用来保存用户session的数据。同一个用户第二次请求我们能获取到它的sessionid,默认也能获取到SessionStateStoreData实例(session过期则取不到)。一个用户对应一个SessionStateStoreData,每个SessionStateStoreData里面有一个ISessionStateItemCollection实例用来保存用户数据,至于sessionid也就是用户身份的区别依赖于ISessionIDManager的实现。


前几天有人问我session过期处理流程是怎么样的。这里以InProcSessionStateStore为列来简单说明一下:

InProcSessionStateStore中有CreateUninitializedItem方法和SetAndReleaseItemExclusive方法,分别有HttpRuntime.CacheInternal.UtcAdd(key, state, null, Cache.NoAbsoluteExpiration, new TimeSpan(0, timeout, 0), CacheItemPriority.NotRemovable, this._callback)
和 cacheInternal.UtcInsert(key, state2, null, Cache.NoAbsoluteExpiration, new TimeSpan(0, state2._timeout, 0), CacheItemPriority.NotRemovable, this._callback);的方法调用

其中this._callback的赋值语句在Initialize方法中
public override void Initialize(string name, NameValueCollection config)
    {
        if (string.IsNullOrEmpty(name))
        {
            name = "InProc Session State Provider";
        }
        base.Initialize(name, config);
        this._callback = new CacheItemRemovedCallback(this.OnCacheItemRemoved);
    }
 public void OnCacheItemRemoved(string key, object value, CacheItemRemovedReason reason)
    {
        PerfCounters.DecrementCounter(AppPerfCounter.SESSIONS_ACTIVE);
        InProcSessionState state = (InProcSessionState) value;
        if (((state._flags & 2) == 0) && ((state._flags & 1) == 0))
        {
            switch (reason)
            {
                case CacheItemRemovedReason.Removed:
                    PerfCounters.IncrementCounter(AppPerfCounter.SESSIONS_ABANDONED);
                    break;

                case CacheItemRemovedReason.Expired:
                    PerfCounters.IncrementCounter(AppPerfCounter.SESSIONS_TIMED_OUT);
                    break;
            }
            if (this._expireCallback != null)
            {
                string id = key.Substring(CACHEKEYPREFIXLENGTH);
                this._expireCallback(id, SessionStateUtility.CreateLegitStoreData(null, state._sessionItems, state._staticObjects, state._timeout));
            }
        }
    }
现在我们来看看 this._expireCallback是什么东东

 public override bool SetItemExpireCallback(SessionStateItemExpireCallback expireCallback)
    {
        this._expireCallback = expireCallback;
        return true;
    }

SetItemExpireCallback则在SessionStateModule类中调用

  public event EventHandler End {
            add {
                lock(_onEndTarget) {
                    if (_store != null && _onEndTarget.SessionEndEventHandlerCount == 0) {
                        _supportSessionExpiry = _store.SetItemExpireCallback(
                                new SessionStateItemExpireCallback(_onEndTarget.RaiseSessionOnEnd));
                    }
                    ++_onEndTarget.SessionEndEventHandlerCount;
                }
            }
            remove {
                lock(_onEndTarget) {
                    --_onEndTarget.SessionEndEventHandlerCount;
                    //
                    if (_store != null && _onEndTarget.SessionEndEventHandlerCount == 0) {
                        _store.SetItemExpireCallback(null);
                        _supportSessionExpiry = false;
                    }
                }
            }
        }

其中SessionOnEndTarget的定义如下:

 class SessionOnEndTarget {
        internal int _sessionEndEventHandlerCount;

        internal SessionOnEndTarget() { 
        }
 
        internal int SessionEndEventHandlerCount { 
            get {
                return _sessionEndEventHandlerCount; 
            }
            set {
                _sessionEndEventHandlerCount = value;
            } 
        }
 
        internal void RaiseOnEnd(HttpSessionState sessionState) { 
            Debug.Trace("SessionOnEnd", "Firing OnSessionEnd for " + sessionState.SessionID);
 
            if (_sessionEndEventHandlerCount > 0) {
                HttpApplicationFactory.EndSession(sessionState, this, EventArgs.Empty);
            }
        } 

        internal void RaiseSessionOnEnd(String id, SessionStateStoreData item) { 
            HttpSessionStateContainer sessionStateContainer = new HttpSessionStateContainer( 
                    id,
                    item.Items, 
                    item.StaticObjects,
                    item.Timeout,
                    false,
                    SessionStateModule.s_configCookieless, 
                    SessionStateModule.s_configMode,
                    true); 
 
            HttpSessionState    sessionState = new HttpSessionState(sessionStateContainer);
 
            if (HttpRuntime.ShutdownInProgress) {
                // call directly when shutting down
                RaiseOnEnd(sessionState);
            } 
            else {
                // post via thread pool 
                SessionOnEndTargetWorkItem workItem = new SessionOnEndTargetWorkItem(this, sessionState); 
                WorkItem.PostInternal(new WorkItemCallback(workItem.RaiseOnEndCallback));
            } 
        }

    }
View Code

相关文章:

  • 2022-12-23
  • 2021-07-14
  • 2021-07-05
  • 2022-12-23
猜你喜欢
  • 2021-11-11
  • 2021-12-06
  • 2022-01-10
  • 2021-08-08
  • 2022-12-23
  • 2021-11-21
相关资源
相似解决方案