.net,sessionState的Session共享问题解决方案

时间:2022-12-26 20:25:26

最近项目因为要负载均衡所以就使用了sessionState的Session共享,但是却发现多台服务器中有个别服务器的Session没有共享,于是就有了这篇文章,下面开始说说。

  这个基本上就分两种情况:一种就是在多台服务器上,在IIS中建立的网站的标识符一样(要想都一样,需要在建网站的时候输入同样的描述符,就我知道的方 法,只有在第一次建这个网站的时候,输入的描述符要一样,建好后再改描述符是没用的,它会保留以前的标识符,另外这种情况有一个特例,就是默认网站,虽然 在iis中可以看到默认网站的标识符都是1,但是它不属于第一种情况,它属于第二情况)。
另外一种情况是,网站的标识符不一样,比如在两台服务器上不同名的网站,或者在同一台服务器上的两个或多个网站(在同一IIS下这种情况根本就不可能会有相同标识符的网站),还有就是默认网站的情况。

这个我在网上有看到别人说,说到要起相同网站名的文章有一篇,但那个说的不对,它只是强调起相同的名字,其实真正是要IIS中网站的标识符一样,不是它们的网站名(也就是描述符)一样,同一IIS下也能有两个网站拥有相同的标识符。这点一定要注意。

另外要说的是,如果使用cookie来存储sessionid的话,这个一定要确保这些共享session的网站在网页浏览器中可以使用相同的域名来访问 到,比如a.test.com,b.test.com,c.test.com等等的.test.com域的网站。这些网站所在的计算机或者说服务器不非得 真的处在这样的一个域或者网站有这样的一个域名什么的,只要能让浏览器所在的计算机通过这些域名访问到这些服务器就行,我自己的测试环境就是家里的两台计 算机的一个小内网,也没有域,只是修改了使用浏览器做测试的机器上的C:\WINDOWS\system32\drivers\etc\host,让浏览 器可以使用那些域名访问到另外的服务器即可。这个也说明在客户端的浏览器使用cookie的一个机制,只要是地址栏里输入的*域一样,比如访问 a.test.com,b.test.com什么的,只要都是test.com的,它就会使用那些cookie了。所以说要确保这些服务器都在同一个域名 下面,这样这些服务器才能得到一样的sessionid,因为asp.net会把sessionid存储在叫ASP.NET_SessionId的 cookie中,只有访问同样的域名的网站才会发送这个COOKIE,不然访问其他域名的网站,比如a.test1.com,浏览器不会发送这个 cookie,即使使用同样的stateserver,不能得到相同的sessionid,也无法共享session。

先说一下stateserver的web.config等的配置,这些不论是上面说的哪两种情况都是一样的,在web.config中需要添加以下两个节点:

1234 <!--web.config中sessionState节点的配置方案--><sessionState cookieName="DotNesession" mode="StateServer" stateConnectionString="tcpip=127.0.0.1:42424" cookieless="false" timeout="30" /><httpRuntime targetFramework="4.5" /><machineKey validationKey="D4033EDA0C4490B94331CAD18C43A72146BC0083FBB0D5ABE876A43EE271904EB2DAE181096561A451F7ADF61ED9EDBE40C0B920ED45682F96A8EA2788B96913" decryptionKey="DDE7A8EF5E6B8C31DA73F8D942FF28B0790BF0F2446202BBA0F80F39A5375BAA" validation="SHA1" decryption="AES" />

  这个machineKey的值可以是随意的,但一定要配成一样的,因为需要用它来给session进行解密。另外要说的就是如果stateserver配为远程的服务器的话,则需要修改stateserver服务器的注册表的[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\aspnet_state\Parameters]中的AllowRemoteConnection,把它改成1,默认是不允许远程连接的,值为0,另外在这里也可以改Port 端口号,默认的stateserver需要用的asp.net状态服务需要监听42424端口。然后在stateserver上启动asp.net状态服务,另外要是先启动了这个服务,再修改的注册表,则需要再重启一下这个服务。

最后说一下以上两种情况分别需要写不同的处理程序,其实处理第二种情况的程序也能处理第一种情况,不过第一种情况的程序比第二种的简单。在这里先说一下分 类的标准,这是根据,在如果没有写处理程序的情况下,stateserver会把这些要共享session的网站产生的session放置到 appdomin的情况来区分的。

第一种情况,因为网站的标识ID是相同的,当然除了默认网站那个特例,因为aps.net状态服务应该是根据网站的标识id来决定放置session的 appdomain的id, 所以放置这些网站的appdomain的id也是相同的,因此这些网站产生的session会被放在同一个appdomain中,所以处理程序很简单,只 要确保ASP.NET_SessionId这个cookie能在这些网站*享就行了。网上给出来的通用做 法,就是在自定义实现IHttpModule的自定义类中,在request结束的事件中,改写ASP.NET_SessionId的cookie的 domain为这些网站的主域名即可,按上例来说,即.test.com。代码如下:

.net,sessionState的Session共享问题解决方案
public class Test:IHttpModule
{
#region IHttpModule 成员

void IHttpModule.Dispose()
{
//throw new Exception("The method or operation is not implemented.");
}

void IHttpModule.Init(HttpApplication context)
{
context.EndRequest += new EventHandler(this.EndRequest);
}

#endregion

private void EndRequest(object sender, EventArgs args)
{
HttpApplication application = sender as HttpApplication;
for (int i = 0; i < application.Response.Cookies.Count; i++ )
{
if( application.Response.Cookies[i].Name == "ASP.NET_SessionId")
application.Response.Cookies[i].Domain = ".test.com";
}
}
}
.net,sessionState的Session共享问题解决方案

在EndRequest当中,或在此类中的其他相应方法中,切记不要没有for循环而使用以下代码来赋值 
application.Response.Cookies["ASP.NET_SessionId"].domain = ".test.com"
甚至是不用循环而直接在方法中直接使用像string str = application.Response.Cookies["ASP.NET_SessionId"].value这样的代码。不然这样会产生一种这样 的效果,每刷新两次浏览器,session就会发生更新,这样就导致程序出现问题。内部的原因我也不甚了解,但从表面来看,是因为 application.Response.Cookies这个集合只有在网站最开始打开的时候,可以通过循环访问到ASP.NET_SessionId 这个cookie,然后再刷新网页,这个循环就不会访问到ASP.NET_SessionId这个cookie了,但是使用上面的代码,则仍可以直接访问 到ASP.NET_SessionId这个cookie。应该是就因为在这种情况下修改了这个cookie,导致cookie或什么发生了变化等原因,最 终让下一个提交请求被认为是新的请求,而产生了新的session。

第二种情况,根据第一种情况中的描述,现在因为网站的标识id不同(默认网站情况除外),所以每个网站放置session的appdomain的id也不 同,因此这些session被放在了不同的appdomain中,所以像上面那样的代码,虽然可以确保将想共享的sessionid传至服务器,但是由于 session被放在了不同的appdomain中,所以实际上是产生了两个相同sessionid的session被分别放在了属于不同网站的两个 appdomain中, 这样肯定也是不能共享session的了,这个解决起来就麻烦点,需要通过反射来调用一个asp.net没有公开的类 OutOfProcSessionStateStore,看名字也知道它代表的是进程外session存储了,来修改它一个静态成员s_uribase, 此成员代表state外部存储需要访问的appdomain的一个内部id,只要在创建session前,设置一个相同的appdomain的id(当然 看程序这个id其实好像可以随意设了,应该不局限于只使用网站的根域名),这样就能确保取session和放置session都到同一个 appdomain中。大致的代码如下:

.net,sessionState的Session共享问题解决方案
public class CookieTest:IHttpModule
{
#region IHttpModule 成员

void IHttpModule.Dispose()
{
//throw new Exception("The method or operation is not implemented.");
}

void IHttpModule.Init(HttpApplication context)
{
//throw new Exception("The method or operation is not implemented.");
Type stateServerSessionProvider = typeof(HttpSessionState).Assembly.GetType("System.Web.SessionState.OutOfProcSessionStateStore");
FieldInfo uriField = stateServerSessionProvider.GetField("s_uribase", BindingFlags.Static | BindingFlags.NonPublic);

if (uriField == null)
throw new ArgumentException("UriField was not found");

uriField.SetValue(null, ".test.com");
context.EndRequest += new EventHandler(this.EndRequest);

}


private void EndRequest(object sender, EventArgs args)
{
HttpApplication application = sender as HttpApplication;

for (int i = 0; i < application.Response.Cookies.Count; i++)
{

application.Response.Cookies[i].Domain = ".test.com";
}
}
}
.net,sessionState的Session共享问题解决方案

最后在web.config中注册这个http模块:
<httpModules>
<add name="test" type="WebApplication5.Test,WebApplication5"/>
</httpModules>

这样就可以在相同*域名的网站间共享session状态了.

还可以在Global.asax.cs中加入下面代码

public override void Init()
{
base.Init();
foreach (string moduleName in this.Modules)
{
string appName = "APPNAME";
IHttpModule module
= this.Modules[moduleName];
SessionStateModule ssm
= module as SessionStateModule;
if (ssm != null)
{
FieldInfo storeInfo
= typeof(SessionStateModule).GetField("_store", BindingFlags.Instance | BindingFlags.NonPublic);
SessionStateStoreProviderBase store
= (SessionStateStoreProviderBase)storeInfo.GetValue(ssm);
if (store == null)//In IIS7 Integrated mode, module.Init() is called later
{
FieldInfo runtimeInfo
= typeof(HttpRuntime).GetField("_theRuntime", BindingFlags.Static | BindingFlags.NonPublic);
HttpRuntime theRuntime
= (HttpRuntime)runtimeInfo.GetValue(null);
FieldInfo appNameInfo
= typeof(HttpRuntime).GetField("_appDomainAppId", BindingFlags.Instance | BindingFlags.NonPublic);
appNameInfo.SetValue(theRuntime, appName);
}
else
{
Type storeType
= store.GetType();
if (storeType.Name.Equals("OutOfProcSessionStateStore"))
{
FieldInfo uribaseInfo
= storeType.GetField("s_uribase", BindingFlags.Static | BindingFlags.NonPublic);
uribaseInfo.SetValue(storeType, appName);
}
}
}
}
}