C#总结(六)EventBus事件总线的使用-自己实现事件总线

在C#中,我们可以在一个类中定义自己的事件,而其他的类可以订阅该事件,当某些事情发生时,可以通知到该类。这对于桌面应用或者独立的windows服务来说是非常有用的。但对于一个web应用来说是有点问题的,因为对象都是在web请求中创建的,而且这些对象生命周期都很短,因而注册某些类的事件是很困难的。此外,注册其他类的事件会使得类紧耦合。事件总线便可以用来解耦并重复利用应用中的逻辑。

 

事件总线带来的好处和引入的问题

好处比较明显,就是独立出一个发布订阅模块,调用者可以通过使用这个模块,屏蔽一些线程切换问题,简单地实现发布订阅功能。

坏处可能比较隐晦,但这些需要足够引起我们的重视

  • 大量的滥用,将导致逻辑的分散,出现问题后很难定位。
  • 没办法实现强类型,在编译的时候就发现问题。
  • 代码可读性有些问题,IDE无法识别这些协议,对IDE不友好。
总得来说,如果项目里面有大量的事件交互,那么还是可以通过EventBus来实现,否则还是推荐自己在模块内部实现观察者模式。

示例代码

所以今天介绍一个简单的事件总线,它是事件发布订阅模式的实现,让我们能在领域驱动设计(DDD)中以事件的弱引用本质对我们的模块和领域边界很好的解耦设计。

目前,所有的源代码已经提交到github 上,地址:https://github.com/weizhong1988/Weiz.EventBus

程序目录结构如下:

事件总线

事件总线是被所有触发并处理事件的其他类共享的单例对象。要使用事件总线,首先应该获得它的一个引用。下面有两种方法来处理:

订阅事件

触发事件之前,应该先要定义该事件。EventBus为我们提供了Subscribe 方法来订阅事件:

复制代码
        public void Subscribe<TEvent>(IEventHandler<TEvent> eventHandler) where TEvent : IEvent
        {
            //同步锁
            lock (_syncObject)
            {
                //获取领域模型的类型
                var eventType = typeof(TEvent);
                //如果此领域类型在事件总线中已注册过
                if (_dicEventHandler.ContainsKey(eventType))
                {
                    var handlers = _dicEventHandler[eventType];
                    if (handlers != null)
                    {
                        handlers.Add(eventHandler);
                    }
                    else
                    {
                        handlers = new List<object>
                        {
                            eventHandler
                        };
                    }
                }
                else
                {
                    _dicEventHandler.Add(eventType, new List<object> { eventHandler });
                }
            }
        }
复制代码

 

所以的事件都集成自IEvent,该类包含了类处理事件需要的属性。

复制代码
 var sendEmailHandler = new UserAddedEventHandlerSendEmail();
 var sendMessageHandler = new UserAddedEventHandlerSendMessage();
 var sendRedbagsHandler = new UserAddedEventHandlerSendRedbags();
 Weiz.EventBus.Core.EventBus.Instance.Subscribe(sendEmailHandler);
 Weiz.EventBus.Core.EventBus.Instance.Subscribe(sendMessageHandler);
 //Weiz.EventBus.Core.EventBus.Instance.Subscribe<UserGeneratorEvent>(sendRedbagsHandler);
 Weiz.EventBus.Core.EventBus.Instance.Subscribe<OrderGeneratorEvent>(sendRedbagsHandler);
复制代码

发布事件

对于事件源,则可以通过Publish 方法发布事件。触发一个事件很简单,如下所示:

复制代码
     public void Publish<TEvent>(TEvent tEvent, Action<TEvent, bool, Exception> callback) where TEvent : IEvent
        {
            var eventType = typeof(TEvent);
            if (_dicEventHandler.ContainsKey(eventType) && _dicEventHandler[eventType] != null &&
                _dicEventHandler[eventType].Count > 0)
            {
                var handlers = _dicEventHandler[eventType];
                try
                {
                    foreach (var handler in handlers)
                    {
                        var eventHandler = handler as IEventHandler<TEvent>;
                        eventHandler.Handle(tEvent);
                        callback(tEvent, true, null);
                    }
                }
                catch (Exception ex)
                {
                    callback(tEvent, false, ex);
                }
            }
            else
            {
                callback(tEvent, false, null);
            }
        }
复制代码

 

下面是发布事件的调用:

            var orderGeneratorEvent = new OrderGeneratorEvent { OrderId = Guid.NewGuid() };

            System.Console.WriteLine("{0}下单成功", orderGeneratorEvent.OrderId);
          
            Weiz.EventBus.Core.EventBus.Instance.Publish(orderGeneratorEvent, CallBack);

定义处理事件

要处理一个事件,应该要实现IEventHandler接口,如下所示:

复制代码
    /// <summary>
    /// send email
    /// </summary>
    public class UserAddedEventHandlerSendEmail : IEventHandler<UserGeneratorEvent>
    {

        public void Handle(UserGeneratorEvent tEvent)
        {
            System.Console.WriteLine(string.Format("{0}的邮件已发送", tEvent.UserId));
        }
    }
复制代码

处理多事件

在一个单一的处理句柄中,可以处理多个事件。这时,你应该为每个事件实现IEventHandler。比如:

复制代码
    /// <summary>
    /// red bags.
    /// </summary>
    public class UserAddedEventHandlerSendRedbags : IEventHandler<UserGeneratorEvent>,IEventHandler<OrderGeneratorEvent>
    {
        public void Handle(OrderGeneratorEvent tEvent)
        {
            System.Console.WriteLine(string.Format("{0}的下单红包已发送", tEvent.OrderId));
        }

        public void Handle(UserGeneratorEvent tEvent)
        {
            System.Console.WriteLine(string.Format("{0}的注册红包已发送", tEvent.UserId));
        }
    }
复制代码

 

最后

以上,就把事件总线介绍完了,完整的代码,请到github 上下载,这个只是EventBus 的简单实现,各位可以根据自己的实际场景和需求,优化修改。

Nginx 和 IIS 实现动静分离

  前段时间,搞Nginx+IIS的负载均衡,想了解的朋友,可以看这篇文章:《nginx 和 IIS 实现负载均衡》,然后也就顺便研究了Nginx + IIS 实现动静分离。所以,一起总结出来,与大家共同探讨。

  动静分离,说白了,就是将网站静态资源(HTML,JavaScript,CSS,img等文件)与后台应用分开部署,提高用户访问静态代码的速度,降低对后台应用服务器的请求。后台应用服务器只负责动态数据请求。

    优势:分担负载,减轻web服务器的压力,适用于大负载。

       静态资源放置cdn,同时还可以通过配置缓存到客户浏览器中,这样极大减轻web服务器的压力。

    劣势:网络环境不佳时,ajax回应很慢,导致页面出现空白,出错处理会不好看。

       不利于网站SEO(搜索引擎优化) ,

       增加了开发复杂度。

  实现方案:动静分离的一种做法是将静态资源部署在nginx上,后台项目部署到Web应用服务器上,根据一定规则静态资源的请求全部请求nginx服务器,达到动静分离的目的。

       

 

  配置

    1. 在location / {}  上方添加 , nginx 的其他配置,请参考前一篇文章《nginx 和 IIS 实现负载均衡》

        #静态资源缓存设置
        location ~ .*\.(gif|jpg|jpeg|png|bmp|swf|ioc|rar|zip|txt|flv|mid|doc|ppt|pdf|xls|mp3|wma)$  {     
            root static;    #static文件夹在Nginx目录下没有,需创建,和conf文件夹同级  
            expires      30d;  
        }          
        location ~ .*\.(js|css)?$ {    
            root static;  
            expires      30d;    
        }  
        

    

    效果如下:

     

 

    2. 在Nginx 下 创建 static 目录,将图片,js, css 等文件 拷贝到该目录下

      注意:最好,网站的原来静态文件目录最好还是保留,我的测试网站是asp.net mvc 删掉相关目录,网站启动会提示找不到相关目录,

  保存,重启Nginx,访问http://localhost:10089,

 

 

聊一聊PV和并发

  最近和几个朋友,聊到并发和服务器的压力问题。很多朋友,不知道该怎么去计算并发?部署多少台服务器才合适? 所以,今天就来聊一聊PV和并发,还有计算web服务器的数量 的等方法。这些都是自己的想法加上一些网上的总结,如有不对,欢迎拍砖。 

  几个概念

    网站流量是指网站的访问量,用来描述访问网站的用户数量以及用户所浏览的网页数量等指标,常用的统计指标包括网站的独立用户数量、总用户数量(含重复访问者)、网页浏览数量、每个用户的页面浏览数量、用户在网站的平均停留时间等。

    网站访问量的常用衡量标准:独立访客(UV) 和 综合浏览量(PV),一般以日为单位来衡量和计算。

    独立访客(UV):指一定时间范围内相同访客多次访问网站,只计算为1个独立访客。

    综合浏览量(PV):指一定时间范围内页面浏览量或点击量,用户每次刷新即被计算一次。

  PV计算带宽



计算带宽大小需要关注两个指标:峰值流量和页面的平均大小。

    举个例子:


假设网站的平均日PV:10w 的访问量,页面平均大小0.4 M 。


网站带宽 = 10w / (24 *60 * 60)* 0.4M * 8 =3.7 Mbps


具体的计算公式是:网站带宽= PV / 统计时间(换算到S*平均页面大小(单位KB* 8

    在实际的网站运行过程中,我们的网站必须要在峰值流量时保持正常的访问,假设,峰值流量是平均流量的5倍,按照这个计算,实际需要的带宽大约在 3.7 Mbps * 5=18.5 Mbps 。

    PS:1. 字节的单位是Byte,而带宽的单位是bit,1Byte=8bit,所以转换为带宽的时候,要乘以 8。

       2. 在实际运行中,由于缓存、CDN、白天夜里访问量不同等原因,这个是绝对情况下的算法。


PV与并发

    具体的计算公式是:并发连接数 = PV / 统计时间 * 页面衍生连接次数 * http响应时间 * 因数 / web服务器数量;


解释:


页面衍生连接次数: 一个页面请求,会有好几次http连接,如外部的css, js,图片等,这个根据实际情况而定。


http响应时间: 平均一个http请求的响应时间,可以使用1秒或更少。


因数: 峰值流量 和平均流量的倍数,一般使用5 ,最好根据实际情况计算后得出。


例子:

    10PV的并发连接数: (100000PV / 86400秒 * 50个派生连接数 * 1秒内响应 * 5倍峰值) / 1台Web服务器 = 289 并发连接数

  所以,如果我们能够测试出单机的并发连接数,和 日pv 数,那么我们同样也能估算出需要web的服务器数量。

  还有一套通过单机 QPS计算 pv 和 需要的web服务器数量的方法,目前一些公司采用这种计算方法,但是其实计算的原理都是差不多的。

  QPS、PV和需要部署机器数量计算公式(转)

  术语说明: 

QPS = req/sec = 请求数/秒 


【QPS计算PV和机器的方式】 


QPS统计方式 [一般使用 http_load 进行统计] 


QPS = 总请求数 / ( 进程总数 *   请求时间 ) 


QPS: 单个进程每秒请求服务器的成功次数 


单台服务器每天PV计算 


公式1:每天总PV = QPS * 3600 * 6 


公式2:每天总PV = QPS * 3600 * 8 


服务器计算 


服务器数量 =  ( 每天总PV / 单台服务器每天总PV ) 


【峰值QPS和机器计算公式】 


原理:每天80%的访问集中在20%的时间里,这20%时间叫做峰值时间 


公式:( 总PV数 * 80% ) / ( 每天秒数 * 20% ) = 峰值时间每秒请求数(QPS) 


机器:峰值时间每秒QPS / 单台机器的QPS   = 需要的机器 


例子:每天300w PV 的在单台机器上,这台机器需要多少QPS? 

       ( 3000000 * 0.8 ) / (86400 *
0.2 ) = 139 (QPS) 


例子:如果一台机器的QPS是58,需要几台机器来支持? 

       139 / 58 = 3 

 

服务架构-域名泛解析设置

接前一篇《MVC实现动态二级域名》,前面我们说道MVC如何实现动态二级域名,其中也涉及到DNS服务器,也要做相应的泛域名解析设置。所以我在这里,就来说道说道泛域名解析是怎么回事。

  1、什么是泛域名解析

泛域名解析是指将*.域名解析到同一IP。在域名前添加任何子域名,均可访问到所指向的WEB地址。也就是将 *.xxx.com(*代表所有合法二级域名头,如:bbs www news)指向同一IP,服务器就同时绑定了所有.xxx.com的二级域名,不需要一个个绑定。例如:博客,电商平台的店铺,qq空间等。都是通过动态的二级域名的泛域名解析来实现的。

  2、和域名解析的区别

泛域名解析是:*.域名解析到同一IP。

域名解析是:子域名.域名解析到同一IP。

注意:只有你的空间是独立IP的时候泛域名才有意义。虚拟主机不支持泛域名解析,虚拟主机是通过绑定域名的主机头来访问网站,虚拟主机只可以绑定有限的域名,也就是只能保定固定死域名。

  3、泛域名有什么作用
相信大家都发现类似58同城这样的网站,成都的网址是cd.58.com 上海的是sh.58.com类似的上千个网站,其实没有那么多个网站,域名前面那部分就是泛域名解析,相当于是传递一个参数,所有的域名实际上访问的都是一个网站,仅仅是传递了不一样的参数显示不一样的内容。

  4、怎样设置域名泛解析
1. 在域名管理里面,增加一个*.xxx.com的次级域名A记录指向你的IP,如下图所示

 

2. 如果你有单独的DNS服务器,可以直接在服务器上设置域名泛解析

原文地址如下:让windows自带的DNS服务支持泛解析

1、添加好test.com,如下图

2、在test下添加一个名称为 * 的域 (右键,添加域),添加完如下图

3、在*的域下,添加一个主机(右键,新建主机,主机名称为空,IP则填写为您要将域名泛解析的对应IP),添加完如下图。

测试一下吧。ping abc.test.com ,看是不是解析到相应的IP 下。

 

需要注意:如果你的服务器上有多个站点,则主站不要绑定主机头。其他二级域名的子系统,需要绑定主机头。

   

架构设计-MVC实现动态二级域名

  前段时间,一个朋友问我ASP.NET MVC下实现动态二级域名的问题。跟他聊了一些解决方案,这里也总结一下,以供参考。

相信大家都发现类似58同城这样的网站,成都的网址是cd.58.com 上海的是sh.58.com类似的上千个网站,其实没有那么多个网站,域名前面那部分就是泛域名解析,相当于是传递一个参数,所有的域名实际上访问的都是一个网站,仅仅是传递了不一样的参数显示不一样的内容。

比如网站主域名入口为:www.58.com

当成都的用户登录时,解析到:cd.58.com

当上海的用户登录时,则解析到:sh.58.com

首先想到的是对Url的重写:(这在ASP.NET中也是常用的手法。网上有关于UrlRewrite的实现,这里不再重复。)

还有就是MVC 应用程序中的典型URL模式,这里只讨论MVC应用程序URL模式下的动态二级域名实现,测试实例下载

 1.定义DomainData、DomainRoute类

  public class DomainRoute : Route
  {
  private Regex domainRegex;
  private Regex pathRegex;

  public string Domain { get; set; }

  public DomainRoute(string domain, string url, RouteValueDictionary defaults): base(url, defaults, new MvcRouteHandler())
  {
    Domain = domain;
  }

  public DomainRoute(string domain, string url, RouteValueDictionary defaults, IRouteHandler routeHandler): base(url, defaults, routeHandler)

  {
    Domain = domain;
  }

  public DomainRoute(string domain, string url, object defaults): base(url, new RouteValueDictionary(defaults), new MvcRouteHandler())
  {
    Domain = domain;
  }

  public DomainRoute(string domain, string url, object defaults, IRouteHandler routeHandler): base(url, new RouteValueDictionary(defaults), routeHandler)
  {
    Domain = domain;
  }

  public override RouteData GetRouteData(HttpContextBase httpContext)
  {
    // 构造 regex
    domainRegex = CreateRegex(Domain);
    pathRegex = CreateRegex(Url);
    // 请求信息
    string requestDomain = httpContext.Request.Headers["host"];
    if (!string.IsNullOrEmpty(requestDomain))
    {
      if (requestDomain.IndexOf(":") > 0)
      {
        requestDomain = requestDomain.Substring(0, requestDomain.IndexOf(":"));
      }
    }
    else
    {
      requestDomain = httpContext.Request.Url.Host;
    }
    string requestPath = httpContext.Request.AppRelativeCurrentExecutionFilePath.Substring(2) + httpContext.Request.PathInfo;

    // 匹配域名和路由
    Match domainMatch = domainRegex.Match(requestDomain);
    Match pathMatch = pathRegex.Match(requestPath);

    // 路由数据
    RouteData data = null;
    if (domainMatch.Success && pathMatch.Success)
    {
      data = new RouteData(this, RouteHandler);
      // 添加默认选项
      if (Defaults != null)
      {
        foreach (KeyValuePair<string, object> item in Defaults)
        {
          data.Values[item.Key] = item.Value;
        }
      }

      // 匹配域名路由
      for (int i = 1; i < domainMatch.Groups.Count; i++)
      {
        Group group = domainMatch.Groups[i];
        if (group.Success)
        {
          string key = domainRegex.GroupNameFromNumber(i);
          if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
          {
            if (!string.IsNullOrEmpty(group.Value))
            {
              data.Values[key] = group.Value;
            }
          }
        }
      }

      // 匹配域名路径
      for (int i = 1; i < pathMatch.Groups.Count; i++)
      {
        Group group = pathMatch.Groups[i];
        if (group.Success)
        {
          string key = pathRegex.GroupNameFromNumber(i);

          if (!string.IsNullOrEmpty(key) && !char.IsNumber(key, 0))
          {
            if (!string.IsNullOrEmpty(group.Value))
            {
              data.Values[key] = group.Value;
            }
          }
        }
      }
    }

    return data;
  }

  public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
  {
    return base.GetVirtualPath(requestContext, RemoveDomainTokens(values));
  }

  public DomainData GetDomainData(RequestContext requestContext, RouteValueDictionary values)
  {
    // 获得主机名
    string hostname = Domain;
    foreach (KeyValuePair<string, object> pair in values)
    {
      hostname = hostname.Replace("{" + pair.Key + "}", pair.Value.ToString());
    }

    // Return 域名数据
    return new DomainData
    {
      Protocol = "http",
      HostName = hostname,
      Fragment = ""
    };
  }

    private Regex CreateRegex(string source)
  {
    // 替换
    source = source.Replace("/", @"\/?");
    source = source.Replace(".", @"\.?");
    source = source.Replace("-", @"\-?");
    source = source.Replace("{", @"(?<");
    source = source.Replace("}", @">([a-zA-Z0-9_]*))");

    return new Regex("^" + source + "$", RegexOptions.IgnoreCase);
  }

  private RouteValueDictionary RemoveDomainTokens(RouteValueDictionary values)
  {
    Regex tokenRegex = new Regex(@"({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?({[a-zA-Z0-9_]*})*-?\.?\/?");
    Match tokenMatch = tokenRegex.Match(Domain);
    for (int i = 0; i < tokenMatch.Groups.Count; i++)
    {
      Group group = tokenMatch.Groups[i];
      if (group.Success)
      {
        string key = group.Value.Replace("{", "").Replace("}", "");
        if (values.ContainsKey(key))
          values.Remove(key);
      }
    }

    return values;
   }
  }
  public class DomainData
  {
    public string Protocol { get; set; }
    public string HostName { get; set; }
    public string Fragment { get; set; }
  }

 

2.修改RouteConfig,增加如下代码

    routes.Add(
      "DomainRoute", new DomainRoute(
      "{CityNameUrl}.weiz.com",
      "{controller}/{action}/{id}",
      new { CityNameUrl = "", controller = "City", action = "Index", id = "" }
    ));

3.增加CityController控制类

public class CityController : Controller
  {
    public ActionResult Index()
    {
      var cityName = RouteData.Values["CityNameUrl"];
      ViewBag.CityName = cityName;
      return View();
    }
  }

 

4.发布网站,并修改相关配置
方式一:修改host,我们通过修改host文件,来实现对二级域名的,只能通过一个一个增加解析如:
#host文件
127.0.0.1 www.weiz.com
127.0.0.1 a.weiz.com
127.0.0.1 b.weiz.com
127.0.0.1 c.weiz.com

方式二:增加泛域名解析,配置DNS服务,也就是让你的域名支持泛解析 (Windows Server 才会有,其他的Windows系统只能修改尝试修改Host文件,便于测试) 请看我的另一篇文章《域名泛解析设置

5. 效果

  

需要注意:如果你的服务器上有多个站点,则主站不要绑定主机头。其他二级域名的子系统,需要绑定主机头。

 

架构设计-nginx 和 IIS 实现负载均衡

  Nginx的作用和优点,这里不必多说,今天主要是nginx负载均衡实验,把做的步骤记录下来,作为一个学习笔记吧,也可以给大家做下参考。

  1.Nginx安装
    1.下载地址:http://nginx.org/en/download.html

    2.解压到后在window的cmd窗口,输入如下图所示的命令,进入到nginx目录,使用“start nginx.exe ”进行nginx的安装,如下图所示:

            
    安装成功后,在“任务管理器”中会看到“nginx.exe”进程。

    3.在浏览器地址栏输入:127.0.0.1,会看到nginx欢迎界面。说明Nginx已经安装成功。

  2.站点搭建及配置

    1.搭建两个iis站点
      新建一个站点下只有一个简单的index页面,将两个站点都部署到本机了,分别绑定了8097和8098两个端口。

    2.修改nginx配置信息,nginx的配置信息,都在nginx.conf ,这个文件中配置。

     a.修改nginx监听端口,修改http server下的listen节点值
      listen 8096;

     b.在http节点下添加upstream(服务器集群),server设置的是集群服务器的信息,我这里搭建了两个站点,配置了两条信息。

      #服务器集群名称为test.com
      upstream test.com {
        server 127.0.0.1: 8097;
        server 127.0.0.1: 8098;
      }

     c.在http节点下找到location节点修改

      location / {
        root html;
        index index.aspx index.html index.htm; #修改主页为index.aspx
        #其中test.com 对应着upstream设置的集群名称
        proxy_pass http:// test.com;
        #设置主机头和客户端真实地址,以便服务器获取客户端真实IP
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      }

      修改完成配置文件之后,重启nginx服务,

  3.运行结果
    访问http://localhost:8096/index.aspx
    
    可以看到,我们的请求被分发到了8097站点和8098站点,说明负载均衡搭建成功了。

    停掉8098站点,刷新页面,则请求会分发给8097 站点, 说明其中一个站点挂了,只要还有一个站点是好的,系统仍然能够继续提供服务。

  4.session共享

    a.使用数据库保存session信息,可以查看本人前一篇文章:数据库实现多站点共享Session

    b.使用nginx将同一ip的请求分配到固定服务器,修改如下。ip_hash会计算ip对应hash值,然后分配到固定服务器,(这个还没试验过)

      upstream test.com {
        server 127.0.0.1: 8097; 
        server 127.0.0.1: 8098; 
        ip_hash;
      }

    c.搭建一台Redis服务器,对session的读取都从该Redis服务器上读取。

 

  注意:nginx作为负载均衡服务器时候,无法正常加载css和js这些文件而出现这样的问题,通过一番搜索和查找,修改nginx下的nginx.conf配置文件才得以正常显示,修改的配置如下:

    

架构设计-分布式系统实现多站点共享Session数据库

数据库实现多站点共享Session

多站点共享Session有很多方法,多站点共享Session常见的做法有:

  • 使用.net自动的状态服务(Asp.net State Service);
  • 使用.net的Session数据库;
  • 使用Redis等缓存。
  • 使用Cookie方式实现多个站点间的共享,但是这种方式只限于几个站点都在同一域名的情况下;

这里主要介绍数据库的形式存储Session,来实现多站点共享Session。

1.新建web站点,添加setSession.aspx 等页面,如下图:

2.修改web.config 配置,增加 sessionState配置是让 Session 保存在数据库中

具体配置如下:

 

网站部分这样就好了,发布成两个不同的网站,http://localhost:8097和http://localhost:8098。

 

3.配置session的sqlserver 模式

网站创建好之后,下面就是要配置据库,具体配置方法,参考前一篇博客:Session的SqlServer模式的配置

 

4.共享SessionID

ASPStateTempSessions 表中的SessionID ,包括两个部分:网站生成的24位SessionID及8位AppId组成,对于不同的站点,其AppId和AppName也不同,在能够在不同站点下Session共享,就得保证这个32位的SessionID 一致,所以可以通过修改存储过程TempGetAppID,使其得到的SessionID与AppName无关,修改TempGetAppID如下:

 

修改完之后,重启一下各站点。再在浏览一下网站,两个网站能获取到同一个session了:

 

Session的SqlServer模式的配置

  很多时候,由于各种莫名其妙的原因,会导致session丢失。不过ASP.NET还允许将会话数据存储到一个数据库服务器中,方法是将mode属性变成SqlServer。 在这种情况下,ASP.NET尝试将会话数据存储到由sqlConnectionString属性(其中包含数据源以及登录服务器所需的安全凭证)指定的SQL Server中,这样能够保证session丢失的问题。

1、  配置ASPState session 数据库

在命令行下运行如下命令:aspnet_regsql.exe  -ssadd -sstype p -S -U -P

 

该命令对此应用进行了持久化操作。这时会看到多一个ASPState数据库,里面两张表,ASPStateTempSessions就可以用来保存Session。

注:为数据库实例名,为sa(或与sa同等权限的), 为 sa用户名的密码

2、应用时,需要在webconfig中添加如下配置:

配置节点如下:
 

虽然timeout设置的是20分钟但是 过期以后仍然可以获取到session的值。

3. 项目应用

1. 新建项目 web项目,在加入如下代码:

Session[“SessionID”] =
DateTime.Now.ToString(“yyyy-MM-dd HH:mm:ss”);
Response.Write(Session[“SessionID”]);

2. web.config 中增加sessionState 配置

3. 运行该网站,之后查看数据库ASPState中的数据表ASPStateTempSessions增加了一条数据

 

注意:只有在写session 的时候,才会在ASPStateTempSessions表里增加了一条session记录。

1. SessionId包括两个部分:网站生成的24位SessionID及8位AppID,AppName对于不同的站点,其AppName不同,在能够在不同站点下使24位SessionID相同的情况下。

2. Created和Expires是这个Session的创建日期和有效期 这个有效期是根据配置文件中TimeOut算出来的,虽然时间达到有效期了 但是还能获取到session(不知道这个有效期有什么用,但是用SqlServer的Session的模式 就是为了不掉线,这点也符合了这个初衷)

3. LockData与LockDataLocal都是最后一次更新Session的时间 (每次操作该Session的时候,它的有效期都会改变)。

4. TimeOut是配置文件中配置的超时分钟数(这里的1是我测试超时时间的时候临时在配置文件中改的,默认是20).

5. SessionItemShort是真正的Session的内容,数据类型是varbinary(7000),存的内容可以是int string 等常用类型,如果是集合或是对象,则必须是可序列化的([Serializable]可序列化的属性)。

 

注意:要设置Session过期删除,启动SQL server 代理中的作业完成。

 

架构设计-单点登录(SSO)系统的总结

前些天一位其他开发部门的同事找到我们了解一些关于SSO单点登录的事,他们要做单点登录,同时也需要和我们这边的系统做集成,要我帮忙做一单点登录,了解关于单点登录的解决方案和资料,虽然做单点登录已经很久了,自以为对SSO系统也算比较了解。但是被他这么一问,反倒是一下讲不清楚,所以总结一下目前正在使用的SSO 解决方案的实现原理,也算是真正的再一次学习SSO 吧。

 

首先,单点登陆(SSO) 是为了一次登陆,就能在其他各子系统获得访问权限,无需用户再输入用户名和密码,所以一般会使用集中验证方式,多个站点集中SSO验证。如下图所示:

所以,通过上图,可以看出,当访问主站是,会请求SSO 进行身份验证,SSO系统验证成功后,会给主站返回一个令牌,这样在主站(OA)访问其他子系统的时候,带上令牌,这样就实现了单点登录,无需再验证用户名和密码。

 

下面说说SSO 系统单点登录的验证过程:

当用户访问应用系统是,会验证session 是否存在,如果session存在,则直接进入系统,如果session 不存在,说明用户未登陆该系统,然后验证用户的主系统是否登陆,是否有令牌,如果有令牌则验证令牌是否有效,如果令牌合法,那么进入该应用系统,反之则需要重新登陆,生成令牌。

 

注:这里的令牌,是通过加密的cookie传输的,由SSO系统颁发可在各分站中流通的标识。

令牌是由SSO系统颁发,系统接收到令牌之后,生成会话(Session)。 令牌通过Cookies的方式在各跨域分站中进行流通,所以SSO生成的令牌放在Cookie中返回给各个系统,并指定Cookie.Domain=”oa.com”。

由于令牌是通过cookie 流通,所以各业务子系统都需要在oa.com这个域下,否者会接收不到SSO产生的令牌。其次是需要增加一个SysAdapter.aspx 中间页,用于令牌的获取和验证。

 

由错误处理引发的联想-防御式编程

  前两天和一同学谈到程序出错应该如何处理的问题,他讲到错误处理的两个原则,

  第一,应该在错误发生时立即将它抛出,而且得抛的很明显,有些人采用静默出错的原则,尝试修复错误并继续运行,这回导致代码调试起来很困难,所以他认为,当程序逻辑出错时,应该立刻崩溃,并生成一段有意义的错误消息,立即崩溃是为了不让事情变得更糟,错误消息应担被写入永久的错误日志,以便过后查明是哪里出错。

  第二,就是抛错要快,也要文明,文明抛错,就是只有程序猿才能看到程序崩溃时产生的详细错误消息,程序的用户绝对不能看到这些消息,另一方面,用户也应该得到一些警告,让他们知道有错误发生这一情况,以及可以采取措施来补救。

  以上这两点,我觉得还是蛮有道理的,重要的业务逻辑出错了,程序应该停止执行,避免错误蔓延扩散,产生一些列不可预知的问题,到最后都不知道哪出错了。

  但是,我又在想,这些都是事后的处理,那么如何提高程序的健壮性呢?联想到了《代码大全》 里面的一章,防御式编程。还有《注重实效的程序员》 这本书里面也有相关防御式编程的介绍。主要的防御式编程手段有断言、错误处理技术、异常、隔离。

  断言

  断言是常用的软件设计技术,其实很简单,就是一个判断一个布尔表达式的语句,如果这个布尔表达式为真,不会有任何效果,但是如果为假,就会告诉程序员,这里有一个断言,去看一下。

  需要注意一下几点:

   (1) 用断言来处理绝不应该发生的状况,用断言去检查一些理论上不可能发生的情况,因为如果发生了就说明内部逻辑有问题,也就是有bug了。

   (2) 避免把需要执行的代码放到断言中

   (3) 用断言来注解并验证前条件和后条件,

   (4) 断言是用来检测程序内部逻辑的,如果是和外部有数据交流,就不是断言的范畴。

  2 错误处理

  程序员在编写软件的时候,应该尽可能的预测到可能发生的错误,并对这些错误进行处理,另外,为使程序结构更加清晰,可以将错误的处理交给其它或专门的处理程序,而本身只是报告发生的错误,然后返回相应的错误码。

  当错误发生时,我们应该如何处理:

  a) 遇到错误数据的最佳做法就是继续执行操操作并简单地返回一个没有危害的数值,数值计算可以返回0,系统记录下这个错误。

  b) 把警告信息记录到日志文件中。

  c) 返回一个错误码,其实就是把错误向上抛出,由上游会有子程序处理该错误。

  d) 当程序出错时,就立即在出错的地方处理。

  e) 关闭程序,安全或性命攸关的程序,遇到错误,关闭是最好的选择。

  还有一个重要的原则:对错误进行分类,

  1.重大错误,程序崩溃,内存溢出等。这类错误一般不可恢复,通常的做法都是报告后直接退出,

  2.无关用户的一般性错误,这类错误一般情况下不会导致程序退出,而且和用户没有直接的联系,这时可以写入日志,以便以后进行排查。

  3.与用户相关的一般性错误,这类错误通常是由于用户输入错误数据引起这个时候,通常需要告诉用户,哪出错了,

  理解错误处理最重要的就是分清楚项目需要处理错误的类型,并对不同的错误采取恰当的处理方式。

  异常

  异常是指程序无法预料到的情况引发的错误,异常不仅在开发中起到很好的防御式作用,它更是一个非常好的调试工具,通过不断地缩小捕获异常代码的范围,准确找出错误的准确位置。

  1. 异常和断言一样,都是来处理那些不仅罕见甚至永远不该发生的情况,但是不可滥用,它弱化了类的“封装”性,可能增加复杂度。

  2. 如果可以在局部处理异常,就不要抛出。

  3. 在异常消息中加入关于导致异常发生的全部信息。

  4. 创建一个集中的异常报告机制,这样做能确保异常处理的一致性。

  5. 把项目中对异常的使用标准化,目的是保持异常处理便于管理。

  4 隔离

  隔离是在设计上简化错误处理的策略,事实上,如果所有的代码都做异常和错误处理,会使代码变得臃肿,可读性下降,我们需要在高层次上面避免这种情况的发生,

  1. 把某些接口选定为“安全”区域的边界,在这些接口里对边界数据进行合法性校验

  2. 将类的公用方法设计为特殊的安全方法,负责检查数据并进行清理,确保安全后,传递给私有方法进行正式操作

  3. 在输入数据时将其转换为恰当的类型

  总结

  软件的质量与其健壮性有很大的联系,作为软件的开发人员,要有足够的重视,不要忽视任何的细节,不能依赖测试去发现bug,而是要不断地思考可能发生的问题并采取进行预防,这才是防御式编程的本质。