Asp.net 跨Web应用程序共享SQL Server会话状态

Asp.net 跨Web应用程序共享SQL Server会话状态,asp.net,sql-server,iis-7.5,session-variables,session-state,Asp.net,Sql Server,Iis 7.5,Session Variables,Session State,我正在设置一个非常基本的SQL Server会话状态演示,但在使其正常工作时遇到了一些问题。我正在尝试在本地运行Windows7和IIS7.5以及SQLServer2008R2来测试这一点 最终,我需要一种方法来跟踪登录到几个不同web服务器之间负载平衡的系统的用户数量。因此,每次用户登录或注销时,我都需要更新会话变量(存储在SQL中)。因此会话ID可能总是不同的。这可能吗? 以下是我迄今为止所做的工作: 在IIS中创建了两个新站点(Site1和Site2) 在VS 2010中创建了两个新的We

我正在设置一个非常基本的SQL Server会话状态演示,但在使其正常工作时遇到了一些问题。我正在尝试在本地运行Windows7和IIS7.5以及SQLServer2008R2来测试这一点

最终,我需要一种方法来跟踪登录到几个不同web服务器之间负载平衡的系统的用户数量。因此,每次用户登录或注销时,我都需要更新会话变量(存储在SQL中)。因此会话ID可能总是不同的。这可能吗?

以下是我迄今为止所做的工作:

  • 在IIS中创建了两个新站点(Site1和Site2)
  • 在VS 2010中创建了两个新的Web应用程序项目(Site1和Site2)
  • 在这两个站点的Default.aspx页面上,我创建了一个按钮,单击该按钮时,将“Site1”或“Site2”(取决于站点)保存到名为“LastSiteUsed”的会话变量中。还有另一个按钮,单击该按钮时,将从“LastSiteUsed”会话变量中读取值,并将其显示在标签中
  • 在SQL中,我创建了一个名为dbSessionTest的新数据库。然后我按照上面的说明去做
  • 在我的两个Web应用程序的Web.config文件中,我在
    下添加了以下内容:
  • 两个站点都正常运行,但它们似乎没有共享会话。例如,当我单击我的按钮从Site1保存会话变量时,我希望能够从Site2读取该值,但它不起作用。Site2将只返回“Site2”,Site1将只返回“Site1”

    有人知道我做错了什么吗?我认为应该能够从Site2读取Site1设置的值,这是错误的吗

    更新:

    我可以看到会话数据存储在SQL Management Studio的
    ASPStateTempSessions
    表中,但每个站点仍然只能看到它编写的值。两个站点都将会话变量设置为:

    Session("LastSiteUsed") = "Site2"
    
    两个站点都在检索值,如:

    lblValue.Text = "Value from Session: " & Session("LastSiteUsed").ToString
    
    对于访问存储在SQL Server中的会话变量,是否需要以不同的方式执行此操作

    更新2:

    我已尝试使用默认数据库ASPState,该数据库是通过运行以下命令创建的:

    aspnet_regsql.exe -S MyServerName -E -ssadd -sstype p
    
    然后将我的每个Web.config文件简化为:

    <sessionState mode="SQLServer" sqlConnectionString="Data Source=.\sqlexpress;User ID=myUsername;Password=myPassword" cookieless="false" timeout="20"/>
    
    我还在两个Web.config文件中包含了相同的机器密钥:

    <machineKey validationKey="59C42C5AB0988049AB0555E3F2128061AE9B75E3A522F25B34A21A46B51F188A5C0C1C74F918DFB44C33406B6E874A54167DFA06AC3BE1C3FEE8506E710015DB" decryptionKey="3C98C7E33D6BC8A8C4B7D966F42F2818D33AAB84A81C594AF8F4A533ADB23B97" validation="SHA1" decryption="AES" />
    
    
    

    现在我可以(使用IE)打开我的两个站点,从Site1设置一个值,然后从Site2读取它(反之亦然)。当我检查该表时,只存在一个SessionID(两个站点正确使用同一个会话)。我之前的想法是错误的,打开一个新的浏览器(例如,使用Chrome),将使用相同的会话-一个新的浏览器将启动它自己的会话。不过,在负载平衡的情况下,这应该不会导致任何问题。

    您遇到的问题是跨ASP.NET应用程序共享会话状态。SQL Server会话提供程序不支持开箱即用。看这个


    我不会使用会话来管理此共享状态(即,
    LastSiteUsed
    )。此信息应保存在用户配置文件存储(、自定义数据库等)中。由于会话可能会过期—跨应用程序使用共享会话不是跟踪持久用户特定状态的可靠机制。如果持久性并不比使用内存中的跨应用共享状态更重要。

    < P>对于任何想要或需要解决这个问题而不修改数据库存储过程的人来说,请考虑这种方法(而不是针对那些懦弱的人)。 基本思想是,
    System.Web.SessionState.SqlSessionStateStore.s_singlePartitionInfo
    中有一个
    SqlSessionStateStore.SqlPartitionInfo
    的静态实例,我通过调用
    InitSqlInfo
    初始化该实例。初始化此实例后,我设置内部
    \u appSuffix
    字段的值。在设置
    \u appSuffix
    字段之前,需要初始化实例,否则在初始化过程中会覆盖我的自定义值。我使用与ASP.NET状态数据库中相同的哈希函数从提供的应用程序名称计算哈希代码

    请注意,这是非常糟糕的,完全取决于内部实现细节,这些细节可能会在任何时间点发生变化。然而,我不知道有什么切实可行的选择。自行决定使用,并承担风险

    现在我已经给出了我的免责声明,如果.NET BCL中的这种实现确实发生了变化,我会感到惊讶,因为这种身份验证方法正在被取代,我不认为微软有任何理由在其中进行修补

    /*
     * This code is a workaround for the fact that session state in ASP.NET SqlServer mode is isolated by application. The AppDomain's appId is used to
     * segregate applications, and there is no official way exposed to modify this behaviour.
     * 
     * Some workarounds tackle the problem by modifying the ASP.NET state database. This workaround approaches the problem from the application code
     * and will be appropriate for those who do not want to alter the database. We are using it during a migration process from old to new technology stacks, 
     * where we want the transition between the two sites to be seamless.
     * 
     * As always, when relying on implementation details, the reflection based approach used here may break in future / past versions of the .NET framework.
     * Test thoroughly.
     * 
     * Usage: add this to your Global.asax:
     *       protected void Application_BeginRequest()
     *       {
     *           SessionStateCrossApplicationHacker.SetSessionStateApplicationName("an application");
     *       }
     */
    
    using System;
    using System.Data.SqlClient;
    using System.Globalization;
    using System.Reflection;
    using System.Web.SessionState;
    
    public static class SessionStateCrossApplicationHacker
    {
        static string _appName;
        static readonly object _appNameLock = new object();
    
        public static void SetSessionStateApplicationName(string appName)
        {
            if (_appName != appName)
            {
                lock (_appNameLock)
                {
                    if (_appName != appName)
                    {
                        SetSessionStateApplicationNameOnceOnly(appName);
    
                        _appName = appName;
                    }
                }
            }
        }
    
        static void SetSessionStateApplicationNameOnceOnly(string appName)
        {
            //get the static instance of SqlSessionStateStore.SqlPartitionInfo from System.Web.SessionState.SqlSessionStateStore.s_singlePartitionInfo
            var sqlSessionStateStoreType = typeof (SessionStateMode).Assembly.GetType("System.Web.SessionState.SqlSessionStateStore");
            var sqlSessionStatePartitionInfoInstance = GetStaticFieldValue(sqlSessionStateStoreType, "s_singlePartitionInfo");
            if (sqlSessionStatePartitionInfoInstance == null)
                throw new InvalidOperationException("You'll need to call this method later in the pipeline - the session state mechanism (SessionStateModule, which is an IHttpModule) has not been initialised yet. Try calling this method in Global.asax's Application_BeginRequest. Also make sure that you do not specify a partitionResolverType in your web.config.");
    
            //ensure that the session has not been used prior to this with an incorrect app ID
            var isStaticSqlPartitionInfoInitialised = GetFieldValue<bool>(sqlSessionStatePartitionInfoInstance, "_sqlInfoInited");
            if (isStaticSqlPartitionInfoInitialised)
                throw new InvalidOperationException("You'll need to call this method earlier in the pipeline - before any sessions have been loaded.");
    
            //force initialisation of the static SqlSessionStateStore.SqlPartitionInfo instance - otherwise this will happen later and overwrite our change
            var connectionString = GetFieldValue<string>(sqlSessionStatePartitionInfoInstance, "_sqlConnectionString");
            using (var connection = new SqlConnection(connectionString))
            {
                connection.Open();
                CallInstanceMethod(sqlSessionStatePartitionInfoInstance, "InitSqlInfo", connection);
            }
    
            //calculate and set the application hash code
            string applicationNameHashCode = GetHashCode(appName).ToString("x8", CultureInfo.InvariantCulture);
            GetField(sqlSessionStatePartitionInfoInstance, "_appSuffix").SetValue(sqlSessionStatePartitionInfoInstance, applicationNameHashCode);
        }
    
        static int GetHashCode(string appName)
        {
            string s = appName.ToLower();
            int hash = 5381;
            int len = s.Length;
    
            for (int i = 0; i < len; i++)
            {
                int c = Convert.ToInt32(s[i]);
                hash = ((hash << 5) + hash) ^ c;
            }
    
            return hash;
        }
    
        static void CallInstanceMethod(object instance, string methodName, params object[] parameters)
        {
            var methodInfo = instance.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
            methodInfo.Invoke(instance, parameters);
        }
    
        static object GetStaticFieldValue(Type typeWithStaticField, string staticFieldName)
        {
            return typeWithStaticField.GetField(staticFieldName, BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
        }
    
        static FieldInfo GetField(object instance, string name)
        {
            return instance.GetType().GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
        }
    
        static T GetFieldValue<T>(object instance, string name)
        {
            return (T)GetField(instance, name).GetValue(instance);
        }
    }
    
    /*
    *对于ASP.NET SqlServer模式下的会话状态是由应用程序隔离的这一事实,此代码是一种变通方法。AppDomain的appId用于
    *隔离应用程序,并且没有公开的官方方法来修改此行为。
    * 
    *一些变通方法通过修改ASP.NET状态数据库来解决此问题。此解决方法从应用程序代码中解决问题
    *并且适用于那些不想更改数据库的用户。我们在从旧技术堆栈向新技术堆栈的迁移过程中使用它,
    *我们希望这两个站点之间的过渡是无缝的。
    * 
    *与往常一样,当依赖于实现细节时,此处使用的基于反射的方法可能会在.NET framework的未来/过去版本中中断。
    *彻底测试。
    * 
    *用法:将其添加到您的Global.asax:
    *受保护的无效应用程序_BeginRequest()
    *       {
    *SessionStateCrossApplicationHacker.SetSessionStateApplicationName(“应用程序”);
    *       }
    */
    使用制度;
    使用System.Data.SqlClient;
    利用制度全球化;
    运用系统反思;
    使用System.Web.SessionState;
    公共静态类SessionStateCrossApplicationHacker
    {
    静纹
    
    /*
     * This code is a workaround for the fact that session state in ASP.NET SqlServer mode is isolated by application. The AppDomain's appId is used to
     * segregate applications, and there is no official way exposed to modify this behaviour.
     * 
     * Some workarounds tackle the problem by modifying the ASP.NET state database. This workaround approaches the problem from the application code
     * and will be appropriate for those who do not want to alter the database. We are using it during a migration process from old to new technology stacks, 
     * where we want the transition between the two sites to be seamless.
     * 
     * As always, when relying on implementation details, the reflection based approach used here may break in future / past versions of the .NET framework.
     * Test thoroughly.
     * 
     * Usage: add this to your Global.asax:
     *       protected void Application_BeginRequest()
     *       {
     *           SessionStateCrossApplicationHacker.SetSessionStateApplicationName("an application");
     *       }
     */
    
    using System;
    using System.Data.SqlClient;
    using System.Globalization;
    using System.Reflection;
    using System.Web.SessionState;
    
    public static class SessionStateCrossApplicationHacker
    {
        static string _appName;
        static readonly object _appNameLock = new object();
    
        public static void SetSessionStateApplicationName(string appName)
        {
            if (_appName != appName)
            {
                lock (_appNameLock)
                {
                    if (_appName != appName)
                    {
                        SetSessionStateApplicationNameOnceOnly(appName);
    
                        _appName = appName;
                    }
                }
            }
        }
    
        static void SetSessionStateApplicationNameOnceOnly(string appName)
        {
            //get the static instance of SqlSessionStateStore.SqlPartitionInfo from System.Web.SessionState.SqlSessionStateStore.s_singlePartitionInfo
            var sqlSessionStateStoreType = typeof (SessionStateMode).Assembly.GetType("System.Web.SessionState.SqlSessionStateStore");
            var sqlSessionStatePartitionInfoInstance = GetStaticFieldValue(sqlSessionStateStoreType, "s_singlePartitionInfo");
            if (sqlSessionStatePartitionInfoInstance == null)
                throw new InvalidOperationException("You'll need to call this method later in the pipeline - the session state mechanism (SessionStateModule, which is an IHttpModule) has not been initialised yet. Try calling this method in Global.asax's Application_BeginRequest. Also make sure that you do not specify a partitionResolverType in your web.config.");
    
            //ensure that the session has not been used prior to this with an incorrect app ID
            var isStaticSqlPartitionInfoInitialised = GetFieldValue<bool>(sqlSessionStatePartitionInfoInstance, "_sqlInfoInited");
            if (isStaticSqlPartitionInfoInitialised)
                throw new InvalidOperationException("You'll need to call this method earlier in the pipeline - before any sessions have been loaded.");
    
            //force initialisation of the static SqlSessionStateStore.SqlPartitionInfo instance - otherwise this will happen later and overwrite our change
            var connectionString = GetFieldValue<string>(sqlSessionStatePartitionInfoInstance, "_sqlConnectionString");
            using (var connection = new SqlConnection(connectionString))
            {
                connection.Open();
                CallInstanceMethod(sqlSessionStatePartitionInfoInstance, "InitSqlInfo", connection);
            }
    
            //calculate and set the application hash code
            string applicationNameHashCode = GetHashCode(appName).ToString("x8", CultureInfo.InvariantCulture);
            GetField(sqlSessionStatePartitionInfoInstance, "_appSuffix").SetValue(sqlSessionStatePartitionInfoInstance, applicationNameHashCode);
        }
    
        static int GetHashCode(string appName)
        {
            string s = appName.ToLower();
            int hash = 5381;
            int len = s.Length;
    
            for (int i = 0; i < len; i++)
            {
                int c = Convert.ToInt32(s[i]);
                hash = ((hash << 5) + hash) ^ c;
            }
    
            return hash;
        }
    
        static void CallInstanceMethod(object instance, string methodName, params object[] parameters)
        {
            var methodInfo = instance.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance);
            methodInfo.Invoke(instance, parameters);
        }
    
        static object GetStaticFieldValue(Type typeWithStaticField, string staticFieldName)
        {
            return typeWithStaticField.GetField(staticFieldName, BindingFlags.NonPublic | BindingFlags.Static).GetValue(null);
        }
    
        static FieldInfo GetField(object instance, string name)
        {
            return instance.GetType().GetField(name, BindingFlags.NonPublic | BindingFlags.Instance);
        }
    
        static T GetFieldValue<T>(object instance, string name)
        {
            return (T)GetField(instance, name).GetValue(instance);
        }
    }