<i id="yupd0"></i>
  • <i id="yupd0"></i>
  • <thead id="yupd0"></thead>
    <blockquote id="yupd0"></blockquote>
    <i id="yupd0"></i>
  • 碼迷,www.greeshyz.com
    首頁 > Web開發 > 詳細

    .NET Core Session源碼探究

    時間:2020-06-05 01:05:59      閱讀:37      評論:0      收藏:0      [點我收藏+]

    標簽:tca   inter   ntc   edm   time   offset   聯網   好的   rom   

    前言

    ????隨著互聯網的興起,技術的整體架構設計思路有了質的提升,曾經Web開發必不可少的內置對象Session已經被慢慢的遺棄。主要原因有兩點,一是Session依賴Cookie存放SessionID,即使不通過Cookie傳遞,也要依賴在請求參數或路徑上攜帶Session標識,對于目前前后端分離項目來說操作起來限制很大,比如跨域問題。二是Session數據跨服務器同步問題,現在基本上項目都使用負載均衡技術,Session同步存在一定的弊端,雖然可以借助Redis或者其他存儲系統實現中心化存儲,但是略顯雞肋。雖然存在一定的弊端,但是在.NET Core也并沒有拋棄它,而且借助了更好的實現方式提升了它的設計思路。接下來我們通過分析源碼的方式,大致了解下新的工作方式。

    Session如何使用

    ????.NET Core的Session使用方式和傳統的使用方式有很大的差別,首先它依賴存儲系統IDistributedCache來存儲數據,其次它依賴SessionMiddleware為每一次請求提供具體的實例。所以使用Session之前需要配置一些操作,相信介紹情參閱微軟官方文檔會話狀態。簡單來說大致配置如下

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
        public IConfiguration Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDistributedMemoryCache();
            services.AddSession(options =>
            {
                options.IdleTimeout = TimeSpan.FromSeconds(10);
                options.Cookie.HttpOnly = true;
                options.Cookie.IsEssential = true;
            });
        }
    
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app.UseSession();
        }
    }
    

    Session注入代碼分析

    注冊的地方設計到了兩個擴展方法AddDistributedMemoryCache和AddSession.其中AddDistributedMemoryCache這是借助IDistributedCache為Session數據提供存儲,AddSession是Session實現的核心的注冊操作。

    IDistributedCache提供存儲

    上面的示例中示例中使用的是基于本地內存存儲的方式,也可以使用IDistributedCache針對Redis和數據庫存儲的擴展方法。實現也非常簡單就是給IDistributedCache注冊存儲操作實例

    public static IServiceCollection AddDistributedMemoryCache(this IServiceCollection services)
    {
        if (services == null)
        {
            throw new ArgumentNullException(nameof(services));
        }
        services.AddOptions();
        services.TryAdd(ServiceDescriptor.Singleton<IDistributedCache, MemoryDistributedCache>());
        return services;
    }
    

    ????關于IDistributedCache的其他使用方式請參閱官方文檔的分布式緩存篇,關于分布式緩存源碼實現可以通過Cache的Github地址自行查閱。

    AddSession核心操作

    AddSession是Session實現的核心的注冊操作,具體實現代碼來自擴展類SessionServiceCollectionExtensions,AddSession擴展方法大致實現如下

    public static IServiceCollection AddSession(this IServiceCollection services)
    {
        if (services == null)
        {
            throw new ArgumentNullException(nameof(services));
        }
        services.TryAddTransient<ISessionStore, DistributedSessionStore>();
        services.AddDataProtection();
        return services;
    }
    

    這個方法就做了兩件事,一個是注冊了Session的具體操作,另一個是添加了數據保護保護條例支持。和Session真正相關的其實只有ISessionStore,話不多說,繼續向下看DistributedSessionStore實現

    public class DistributedSessionStore : ISessionStore
    {
        private readonly IDistributedCache _cache;
        private readonly ILoggerFactory _loggerFactory;
    
        public DistributedSessionStore(IDistributedCache cache, ILoggerFactory loggerFactory)
        {
            if (cache == null)
            {
                throw new ArgumentNullException(nameof(cache));
            }
            if (loggerFactory == null)
            {
                throw new ArgumentNullException(nameof(loggerFactory));
            }
            _cache = cache;
            _loggerFactory = loggerFactory;
        }
        public ISession Create(string sessionKey, TimeSpan idleTimeout, TimeSpan ioTimeout, Func<bool> tryEstablishSession, bool isNewSessionKey)
        {
            if (string.IsNullOrEmpty(sessionKey))
            {
                throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(sessionKey));
            }
            if (tryEstablishSession == null)
            {
                throw new ArgumentNullException(nameof(tryEstablishSession));
            }
            return new DistributedSession(_cache, sessionKey, idleTimeout, ioTimeout, tryEstablishSession, _loggerFactory, isNewSessionKey);
        }
    }
    

    這里的實現也非常簡單就是創建Session實例DistributedSession,在這里我們就可以看出創建Session是依賴IDistributedCache的,這里的sessionKey其實是SessionID,當前會話唯一標識。繼續向下找到DistributedSession實現,這里的代碼比較多,因為這是封裝Session操作的實現類。老規矩先找到我們最容易下手的Get方法

    public bool TryGetValue(string key, out byte[] value)
    {
        Load();
        return _store.TryGetValue(new EncodedKey(key), out value);
    }
    

    我們看到調用TryGetValue之前先調用了Load方法,這是內部的私有方法

    private void Load()
    {
        //判斷當前會話中有沒有加載過數據
        if (!_loaded)
        {
            try
            {
                //根據會話唯一標識在IDistributedCache中獲取數據
                var data = _cache.Get(_sessionKey);
                if (data != null)
                {
                    //由于存儲的是按照特定的規則得到的二進制數據,所以獲取的時候要將數據反序列化
                    Deserialize(new MemoryStream(data));
                }
                else if (!_isNewSessionKey)
                {
                    _logger.AccessingExpiredSession(_sessionKey);
                }
                //是否可用標識
                _isAvailable = true;
            }
            catch (Exception exception)
            {
                _logger.SessionCacheReadException(_sessionKey, exception);
                _isAvailable = false;
                _sessionId = string.Empty;
                _sessionIdBytes = null;
                _store = new NoOpSessionStore();
            }
            finally
            {
               //將數據標識設置為已加載狀態
                _loaded = true;
            }
        }
    }
    
    private void Deserialize(Stream content)
    {
        if (content == null || content.ReadByte() != SerializationRevision)
        {
            // Replace the un-readable format.
            _isModified = true;
            return;
        }
    
        int expectedEntries = DeserializeNumFrom3Bytes(content);
        _sessionIdBytes = ReadBytes(content, IdByteCount);
    
        for (int i = 0; i < expectedEntries; i++)
        {
            int keyLength = DeserializeNumFrom2Bytes(content);
            //在存儲的數據中按照規則獲取存儲設置的具體key
            var key = new EncodedKey(ReadBytes(content, keyLength));
            int dataLength = DeserializeNumFrom4Bytes(content);
            //將反序列化之后的數據存儲到_store
            _store[key] = ReadBytes(content, dataLength);
        }
    
        if (_logger.IsEnabled(LogLevel.Debug))
        {
            _sessionId = new Guid(_sessionIdBytes).ToString();
            _logger.SessionLoaded(_sessionKey, _sessionId, expectedEntries);
        }
    }
    

    通過上面的代碼我們可以得知Get數據之前之前先Load數據,Load其實就是在IDistributedCache中獲取數據然后存儲到了_store中,通過當前類源碼可知_store是本地字典,也就是說Session直接獲取的其實是本地字典里的數據。

    private IDictionary<EncodedKey, byte[]> _store;
    
    這里其實產生兩點疑問:
    1.針對每個會話存儲到IDistributedCache的其實都在一個Key里,就是以當前會話唯一標識為key的value里,為什么沒有采取組合會話key單獨存儲。
    2.每次請求第一次操作Session,都會把IDistributedCache里針對當前會話的數據全部加載到本地字典里,一般來說每次會話操作Session的次數并不會很多,感覺并不會節約性能。

    接下來我們在再來查看另一個我們比較熟悉的方法Set方法

    public void Set(string key, byte[] value)
    {
        if (value == null)
        {
            throw new ArgumentNullException(nameof(value));
        }
        if (IsAvailable)
        {
            //存儲的key是被編碼過的
            var encodedKey = new EncodedKey(key);
            if (encodedKey.KeyBytes.Length > KeyLengthLimit)
            {
                throw new ArgumentOutOfRangeException(nameof(key),
                    Resources.FormatException_KeyLengthIsExceeded(KeyLengthLimit));
            }
            if (!_tryEstablishSession())
            {
                throw new InvalidOperationException(Resources.Exception_InvalidSessionEstablishment);
            }
            //是否修改過標識
            _isModified = true;
            //將原始內容轉換為byte數組
            byte[] copy = new byte[value.Length];
            Buffer.BlockCopy(src: value, srcOffset: 0, dst: copy, dstOffset: 0, count: value.Length);
            //將數據存儲到本地字典_store
            _store[encodedKey] = copy;
        }
    }
    

    這里我們可以看到Set方法并沒有將數據放入到存儲系統,只是放入了本地字典里。我們再來看其他方法

    public void Remove(string key)
    {
        Load();
        _isModified |= _store.Remove(new EncodedKey(key));
    }
    
    public void Clear()
    {
        Load();
        _isModified |= _store.Count > 0;
        _store.Clear();
    }
    

    這些方法都沒有對存儲系統DistributedCache里的數據進行操作,都只是操作從存儲系統Load到本地的字典數據。那什么地方進行的存儲呢,也就是說我們要找到調用_cache.Set方法的地方,最后在這個地方找到了Set方法,而且看這個方法名就知道是提交Session數據的地方

    public async Task CommitAsync(CancellationToken cancellationToken = default)
    {
        //超過_ioTimeout CancellationToken將自動取消
        using (var timeout = new CancellationTokenSource(_ioTimeout))
        {
            var cts = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, cancellationToken);
            //數據被修改過
            if (_isModified)
            {
                if (_logger.IsEnabled(LogLevel.Information))
                {
                    try
                    {
                        cts.Token.ThrowIfCancellationRequested();
                        var data = await _cache.GetAsync(_sessionKey, cts.Token);
                        if (data == null)
                        {
                            _logger.SessionStarted(_sessionKey, Id);
                        }
                    }
                    catch (OperationCanceledException)
                    {
                    }
                    catch (Exception exception)
                    {
                        _logger.SessionCacheReadException(_sessionKey, exception);
                    }
                }
                var stream = new MemoryStream();
                //將_store字典里的數據寫到stream里
                Serialize(stream);
                try
                {
                    cts.Token.ThrowIfCancellationRequested();
                    //將讀取_store的流寫入到DistributedCache存儲里
                    await _cache.SetAsync(
                        _sessionKey,
                        stream.ToArray(),
                        new DistributedCacheEntryOptions().SetSlidingExpiration(_idleTimeout),
                        cts.Token);
                    _isModified = false;
                    _logger.SessionStored(_sessionKey, Id, _store.Count);
                }
                catch (OperationCanceledException oex)
                {
                    if (timeout.Token.IsCancellationRequested)
                    {
                        _logger.SessionCommitTimeout();
                        throw new OperationCanceledException("Timed out committing the session.", oex, timeout.Token);
                    }
                    throw;
                }
            }
            else
            {
                try
                {
                    await _cache.RefreshAsync(_sessionKey, cts.Token);
                }
                catch (OperationCanceledException oex)
                {
                    if (timeout.Token.IsCancellationRequested)
                    {
                        _logger.SessionRefreshTimeout();
                        throw new OperationCanceledException("Timed out refreshing the session.", oex, timeout.Token);
                    }
                    throw;
                }
            }
        }
    }
    
    private void Serialize(Stream output)
    {
        output.WriteByte(SerializationRevision);
        SerializeNumAs3Bytes(output, _store.Count);
        output.Write(IdBytes, 0, IdByteCount);
        //將_store字典里的數據寫到Stream里
        foreach (var entry in _store)
        {
            var keyBytes = entry.Key.KeyBytes;
            SerializeNumAs2Bytes(output, keyBytes.Length);
            output.Write(keyBytes, 0, keyBytes.Length);
            SerializeNumAs4Bytes(output, entry.Value.Length);
            output.Write(entry.Value, 0, entry.Value.Length);
        }
    }
    

    那么問題來了當前類里并沒有地方調用CommitAsync,那么到底是在什么地方調用的該方法呢?姑且別著急,我們之前說過使用Session的三要素,現在才說了兩個,還有一個UseSession的中間件沒有提及到呢。

    UseSession中間件

    通過上面注冊的相關方法我們大概了解到了Session的工作原理。接下來我們查看UseSession中間件里的代碼,探究這里究竟做了什么操作。我們找到UseSession方法所在的地方SessionMiddlewareExtensions找到第一個方法

    public static IApplicationBuilder UseSession(this IApplicationBuilder app)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }
        return app.UseMiddleware<SessionMiddleware>();
    }
    

    SessionMiddleware的源碼

    public class SessionMiddleware
    {
      private static readonly RandomNumberGenerator CryptoRandom = RandomNumberGenerator.Create();
      private const int SessionKeyLength = 36; // "382c74c3-721d-4f34-80e5-57657b6cbc27"
      private static readonly Func<bool> ReturnTrue = () => true;
      private readonly RequestDelegate _next;
      private readonly SessionOptions _options;
      private readonly ILogger _logger;
      private readonly ISessionStore _sessionStore;
      private readonly IDataProtector _dataProtector;
    
      public SessionMiddleware(
          RequestDelegate next,
          ILoggerFactory loggerFactory,
          IDataProtectionProvider dataProtectionProvider,
          ISessionStore sessionStore,
          IOptions<SessionOptions> options)
      {
          if (next == null)
          {
              throw new ArgumentNullException(nameof(next));
          }
          if (loggerFactory == null)
          {
              throw new ArgumentNullException(nameof(loggerFactory));
          }
          if (dataProtectionProvider == null)
          {
              throw new ArgumentNullException(nameof(dataProtectionProvider));
          }
          if (sessionStore == null)
          {
              throw new ArgumentNullException(nameof(sessionStore));
          }
          if (options == null)
          {
              throw new ArgumentNullException(nameof(options));
          }
          _next = next;
          _logger = loggerFactory.CreateLogger<SessionMiddleware>();
          _dataProtector = dataProtectionProvider.CreateProtector(nameof(SessionMiddleware));
          _options = options.Value;
         //Session操作類在這里被注入的
          _sessionStore = sessionStore;
      }
    
      public async Task Invoke(HttpContext context)
      {
          var isNewSessionKey = false;
          Func<bool> tryEstablishSession = ReturnTrue;
          var cookieValue = context.Request.Cookies[_options.Cookie.Name];
          var sessionKey = CookieProtection.Unprotect(_dataProtector, cookieValue, _logger);
          //會話首次建立
          if (string.IsNullOrWhiteSpace(sessionKey) || sessionKey.Length != SessionKeyLength)
          {
              //將會話唯一標識通過Cookie返回到客戶端
              var guidBytes = new byte[16];
              CryptoRandom.GetBytes(guidBytes);
              sessionKey = new Guid(guidBytes).ToString();
              cookieValue = CookieProtection.Protect(_dataProtector, sessionKey);
              var establisher = new SessionEstablisher(context, cookieValue, _options);
              tryEstablishSession = establisher.TryEstablishSession;
              isNewSessionKey = true;
          }
          var feature = new SessionFeature();
          //創建Session
          feature.Session = _sessionStore.Create(sessionKey, _options.IdleTimeout, _options.IOTimeout, tryEstablishSession, isNewSessionKey);
          //放入到ISessionFeature,給HttpContext中的Session數據提供具體實例
          context.Features.Set<ISessionFeature>(feature);
          try
          {
              await _next(context);
          }
          finally
          {
              //置空為了在請求結束后可以回收掉Session
              context.Features.Set<ISessionFeature>(null);
              if (feature.Session != null)
              {
                  try
                  {
                      //請求完成后提交保存Session字典里的數據到DistributedCache存儲里
                      await feature.Session.CommitAsync();
                  }
                  catch (OperationCanceledException)
                  {
                      _logger.SessionCommitCanceled();
                  }
                  catch (Exception ex)
                  {
                      _logger.ErrorClosingTheSession(ex);
                  }
              }
          }
      }
    
      private class SessionEstablisher
      {
          private readonly HttpContext _context;
          private readonly string _cookieValue;
          private readonly SessionOptions _options;
          private bool _shouldEstablishSession;
    
          public SessionEstablisher(HttpContext context, string cookieValue, SessionOptions options)
          {
              _context = context;
              _cookieValue = cookieValue;
              _options = options;
              context.Response.OnStarting(OnStartingCallback, state: this);
          }
    
          private static Task OnStartingCallback(object state)
          {
              var establisher = (SessionEstablisher)state;
              if (establisher._shouldEstablishSession)
              {
                  establisher.SetCookie();
              }
              return Task.FromResult(0);
          }
    
          private void SetCookie()
          {
              //會話標識寫入到Cookie操作
              var cookieOptions = _options.Cookie.Build(_context);
              var response = _context.Response;
              response.Cookies.Append(_options.Cookie.Name, _cookieValue, cookieOptions);
              var responseHeaders = response.Headers;
              responseHeaders[HeaderNames.CacheControl] = "no-cache";
              responseHeaders[HeaderNames.Pragma] = "no-cache";
              responseHeaders[HeaderNames.Expires] = "-1";
          }
    
          internal bool TryEstablishSession()
          {
              return (_shouldEstablishSession |= !_context.Response.HasStarted);
          }
      }
    }
    

    ????通過SessionMiddleware中間件里的代碼我們了解到了每次請求Session的創建,以及Session里的數據保存到DistributedCache都是在這里進行的。不過這里仍存在一個疑問由于調用CommitAsync是在中間件執行完成后統一進行存儲的,也就是說中途對Session進行的Set Remove Clear的操作都是在Session方法的本地字典里進行的,并沒有同步到DistributedCache里,如果中途出現程序異常結束的情況下,保存到Session里的數據,并沒有真正的存儲下來,會出現丟失的情況,不知道在設計這部分邏輯的時候是出于什么樣的考慮。

    總結

    ????通過閱讀Session相關的部分源碼大致了解了Session的原理,工作三要素,IDistributedCache存儲Session里的數據,SessionStore是Session的實現類,UseSession是Session被創建到當前請求的地方。同時也留下了幾點疑問

    • 針對每個會話存儲到IDistributedCache的其實都在一個Key里,就是以當前會話唯一標識為key的value里,為什么沒有采取組合會話key單獨存儲。
    • 每次請求第一次操作Session,都會把IDistributedCache里針對當前會話的數據全部加載到本地字典里,一般來說每次會話操作Session的次數并不會很多,感覺并不會節約性能。
    • 調用CommitAsync是在中間件執行完成后統一進行存儲的,也就是說中途對Session進行的Set Remove Clear的操作都是在Session方法的本地字典里進行的,并沒有同步到DistributedCache里,如果中途出現程序異常結束的情況下,保存到Session里的數據,并沒有真正的存儲下來,會出現丟失的情況。
    對于以上疑問,不知道是個人理解不足,還是在設計的時候出于別的考慮。歡迎在評論區多多溝通交流,希望能從大家那里得到更好的解釋和答案。

    .NET Core Session源碼探究

    標簽:tca   inter   ntc   edm   time   offset   聯網   好的   rom   

    原文地址:https://www.cnblogs.com/wucy/p/13044467.html

    (0)
    (0)
       
    舉報
    評論 一句話評論(0
    登錄后才能評論!
    迷上了代碼!
    91色国产论坛,久草99福利资源,青青草在线华人,99热在线视频观看免费,青青草a片免费看,青青草av视频导航,香蕉网站伊人大香蕉 青青碰人青青草免费 国产自拍偷拍在线播放 青青草华人在线av 伊人影院在线大香 无码大香蕉网伊人色 久久大香蕉视频网 青草精品资源在线 伊人大香蕉在线精品 97资源总站久久爱视频 自拍中文字幕 青青草手机在线视频 青青草久久爱大香蕉 国产青青草自拍 伊人大香蕉在线精品 97资源总站久久爱视频 自拍中文字幕 av图片在线看 久草视频福利免费资源站 自拍国产视频在线 www.琪琪色 爱色影爱色搞搞 97资源库 大香萑a久草视频 久草在线福利资站 青青草成人在线免费视频 久久精品国can视频在热 99热国产情侣偷拍 国产自拍 在线 凹凸分类视频97免费 av啪啪中文网站 青青草91自拍视频 奇米影视第四色 国产无限制自拍 大香蕉伊人精品在线 久久手机看片国产免费 无码大香蕉网伊人色 亚洲激情色 久草99福利资源 狼人色终合网站 91色国产色去色 2019奇米777奇米网 久久草妹妹色 米奇影院888奇米色 青青青草成人免费现看 超碰自拍 在线99热这里精品 色琪琪永久无码 伊人大香蕉成人视频综合 国产自拍视频在线 大香蕉伊人久操在线 青青草久草热久久草 偷拍自拍第四色 奇米网电影网 超碰99久久天天拍日日操 天天拍天天拍久草片 激情图片,激情小说 超碰碰av大香蕉伊人 久草97大香蕉伊人 美女伊人色情香蕉网站 青青草华人免费视频在线 大香蕉伊人久草视频 奇米网在线手机在线 青草七次郞视频观看 青青草公开在线观看 狠狠夜夜干大香蕉伊人 777奇米网 日逼视频网站 欧美人和动物XXX 大香蕉色 欧美 国产 奇米影院首页 大香蕉伊人久久爱在线 青青青草免费手机播放 国产青青草 欧美一级黑寡妇夜夜干 啪啪 国产精品 东方aⅴ在线看 手机看A片 777米奇色狠狠俺去啦 香蕉网站伊人中文字幕 久操在线新免费视频 伊人影院在线大香 大香蕉成人伊人在线视频 青青草成人在线视频观看 91国产自拍偷拍视频 青青草色爱久久 精品国产自拍 小色狗 琪琪热热色无码 影音先锋大香蕉久草资源 小明视频看看成人免费 俺去了色网婷婷色 久久色情片 超碰在线青青草 超碰视频起碰视频 国产偷拍自拍影音先锋 小明视频看看成人免费 俺去了色网婷婷色 久久色情片 超碰在线青青草 超碰视频起碰视频 国产偷拍自拍影音先锋 老鸭窝在线视频 www.奇米在线四色 老版本日本怡春院 强奸乱伦在线观看 青青草在观视频 色琪琪在线视频原网 欧美色色大香焦 欧美色热图 亚洲香蕉手机在线观看视频 偷拍偷窥自拍网站 超91在线观看 最新奇米奇色777在线 婷婷网站 微拍秒拍福利视频 自拍在线 精品视频 欧美情色无码在线 久草激情视频 午夜高清影院在线观看 第四色青娱乐奇米影视 青青草在线综合 99热这里有精品 自拍偷拍影音先锋 国产自拍 先锋影音 成人在线视频97 久草在线免费观看大香蕉 99er久久国产精品在线 精品国产在线偷拍 啪啪青青草视频 国内偷拍 亚洲 大香蕉成人手机在线 在线伊人大香蕉手机版 久草伊人久草视频 伊人久久青青草综合网 青青草手机在线视频 青青草久久爱大香蕉 国产青青草自拍 伊人大香蕉在线精品 97资源总站久久爱视频 自拍中文字幕 av图片在线看 久草视频福利免费资源站 自拍国产视频在线 www.琪琪色 爱色影爱色搞搞 97资源库 大香蕉在线视频免费观看 a片直播妹子在线视频 a.v在线视频 开心五月色婷婷免费 香蕉福利视频在线观 青青草成人+激情偷拍 青青草视频大香蕉伊人网 av啪啪中文网站 青青草91自拍视频 奇米影视第四色 国产无限制自拍 大香蕉伊人精品在线 久久手机看片国产免费 无码大香蕉网伊人色 亚洲激情色 久草99福利资源 狼人色终合网站 91色国产色去色 2019奇米777奇米网 久久草妹妹色 米奇影院888奇米色 青青青草成人免费现看 超碰自拍 在线99热这里精品 色琪琪永久无码 伊人大香蕉成人视频综合 国产自拍视频在线 大香蕉伊人久操在线 青青草久草热久久草 偷拍自拍第四色 奇米网电影网 超碰99久久天天拍日日操 天天拍天天拍久草片 激情图片,激情小说 青青草久草热久久草 偷拍自拍第四色 奇米网电影网 超碰99久久天天拍日日操 天天拍天天拍久草片 操好屌色 米奇先锋 久草 偷拍 亚洲,偷拍,自拍,精品 伊人大香蕉综合色 大香蕉久久久 成人色啪啪 大香蕉色看片 欧美自拍在线 久久色综合网站xoxo 久热草大香蕉在线视频 久久大香蕉视频网 肏逼免费视频在线 网友自拍偷拍 欧美 国产 奇米影院首页 大香蕉伊人久久爱在线 青青青草免费手机播放 国产青青草 欧美一级黑寡妇夜夜干 啪啪 国产精品 东方aⅴ在线看 手机看A片 777米奇色狠狠俺去啦 香蕉网站伊人中文字幕 久操在线新免费视频 伊人影院在线大香 大香蕉成人伊人在线视频 青青草成人在线视频观看 91国产自拍偷拍视频 青青草色爱久久 精品国产自拍 小色狗 琪琪热热色无码 影音先锋大香蕉久草资源 小明视频看看成人免费 俺去了色网婷婷色 久久色情片 超碰在线青青草 欧美激情图片 自拍超碰 久草视频在线 天天 综合色伊人网 大香蕉视频成人中文网 大香蕉伊人欧美色 久热在线播放中文字幕 青青草久草福利 欧美色色大香焦 欧美色热图 亚洲香蕉手机在线观看视频 偷拍偷窥自拍网站 超91在线观看 最新奇米奇色777在线 婷婷网站 微拍秒拍福利视频 自拍在线 精品视频 欧美情色无码在线 久草激情视频 午夜高清影院在线观看 第四色青娱乐奇米影视 青青草在线综合 99热这里有精品 自拍偷拍影音先锋 国产自拍 先锋影音 成人在线视频97 久草在线免费观看大香蕉 99er久久国产精品在线
    久久草2019在线观看 99这里有精品 大香蕉免费公开青青草 狠狠色丁香婷婷综合 国产久草福利手机在线 青青草黄色三级网站 大香蕉他也啪 青青草视频大香蕉伊人网 久久爱大香焦在线视频 日本有码视频 成人视屏 国产精品91在线 91国产自拍小视频 伊人狼人香蕉网小说 青青草香蕉草久在线 av青青操 亚洲自拍欧美 91 国产 在线 97超碰资源共享 青青草伊人大香蕉 奇米四色网 国産偷拍91 精品国产在线自拍 伊人大香蕉色琪琪影院 caoprom超碰 青青视频在线视频 开心五月婷婷色婷在线 第四色色 Caoprom超碰视频 亚洲图片欧美图色姐 天天干情色网 操妹妹干妹妹 丁香五月婷婷伊人大香蕉 国产精品自拍偷拍在线 青青青成人社区 成人av在线 碰碰视频在线免费播放 影音先锋 小说色 久久草大香蕉 亚洲情色狼人网 思思久久re热线播放 久久草超碰 播播影院私人影院 大香蕉他也啪 米奇影视 成人国产老鸭窝 伊人大香蕉久久天天啪 男人的天堂 国产精品自偷拍 很很鲁在线97