.Net 如何模拟会话级此外信号量,对http接口调用频率进行限制(有demo)必发365乐趣网投手机版

by admin on 2018年12月25日

现在,因为各样因素,你不可能不对一个呼吁或者措施开展频率上的访问限制。
比如说,
你对外提供了一个API接口,注册用户每分钟最多可以调用100次,非注册用户每分钟最多可以调用10次。
诸如,
有一个不行吃服务器资源的主意,在平等时刻不可以超越10私家调用这几个形式,否则服务器满载。
譬如, 有一部分卓殊的页面,访客并无法反复的造访或发言。
比如说, 秒杀活动等展开。
比如
,防范DDOS,当达到一定频率后调用脚本iis服务器ip黑名单,防火墙黑名单。
如上各种的比喻,也就是说,怎么着从一个断面的角度对调用的艺术开展频率上的范围。而对效率限制,服务器层面都有最直接的缓解措施,现在自我说的则是代码层面上的频率管控。

作者:Jack47

正文给出三个示范,一个是据悉单机环境的落实,第二个则是依据分布式的Redis实现

转载请保留作者和原文出处


迎接关注自己的微信公众账号程序员杰克(杰克),两边的著作会同步,也得以添加我的RSS订阅源

以率先个API接口需求为例,先说下单机环境下的落实。
遵从惯性思维,我们自然会想到缓存的晚点策略这种方法,可是严谨来讲就HttpRuntime.Cache而言,通过缓存的逾期策略来对请求举办频率的出现控制是不合适的。
  HttpRuntime.Cache
是应用程序级另外Asp.Net的缓存技术,通过这一个技术可以注明七个缓存对象,能够为每个对象设置过期时间,当过期时光到达后该缓存对象就会不复存在(也就是当您拜访该对象的时候为Null)

本文是Storm体系之一,重要介绍Storm的架构设计,推荐读者在翻阅Storm介绍(一)的功底之上,阅读这一篇。本文只是作者的读书笔记,偏重于浅层次的架构介绍,假若想的确清楚其中设计时候的衡量,还亟需更多的去阅读Storm源码。

  为啥那样说吧?比如对某个方法(方法名:GetUserList)我们要开展1分钟最多10次的范围,现在我们就新建一个int型的Cache对象,然后设置1分钟后过期消失。那么每当访问GetUserList方法前,我们就先判断那一个Cache对象的值是否高于10,假如超越10就不举办GetUserList方法,假使低于10则允许实施。每当访问该对象的时候如若不存在或者逾期就新建,这样循环,则该对象永远不容许跨越10。

接头Storm的架构,有助于协理我们知道大型分布式系统设计中需要缓解的题目,以及解决问题的思绪,帮助我们更好的展开Storm性能调优化。

1   if ((int)HttpRuntime.Cache["GetUserListNum"] > 10) //大于10请求失败
2   {
3      Console.WriteLine("禁止请求");
4   }
5   else
6   {
7      HttpRuntime.Cache["GetUserListNum"] = (int)HttpRuntime.Cache["GetUserListNum"] + 1; //否则该缓存对象的值+1
8      Console.WriteLine("允许请求");
9   }

架构

先上一张Storm的架构图,要是熟谙GFS和Hadoop的架构,会发觉那么些类别的架构图都很接近。
必发365乐趣网投手机版 1

Storm架构图

这样的思辨及贯彻相对来说非凡简单,但是依照这样的一个模型设定,那么就会见世这种场馆:

各节点的法力

设若你熟稔Hadoop的话,可以这样做一下类比:

Hadoop Storm
JobTracker Nimbus(只有一个)
TaskTracker Supervisor(有很多个)
MapReduce任务 Topology

可以看来Nimbus是调度器,WorkerTask的容器,Task是天职的真正实施者。

 必发365乐趣网投手机版 2

起步拓扑

为了在集群上启动一个拓扑,需要首先把代码打包成一个“胖jar包”–必须带有所有的倚重代码,除了Storm它本身,因为Storm集群会提供。然后在一台设置了storm命令行的机械上经过storm jar一声令下来交付拓扑:

storm jar my-topology-version-with-dependency.jar com.corp.MyTopology arg1 arg2

其一命令会连到Nimbus,上传jar包。接下来Nimbus会把拓扑的代码运送到多台不同的机械或者JVM上。唯有当拓扑在机器上布置成功了同时在JVM中开头化了今后,才能确实开首拍卖音信。

 

Master结点(Master node)

在分布式系统中,调度服务很是重大,它的计划,会平昔关乎到系统的周转效用,错误復苏(fail
over),故障检测(error detection)和水平扩充(scale)的力量。

集群上职责(task)的调度由一个Master节点来顶住。这台机器上运行的Nimbus过程负责任务的调度。此外一个进程是Storm
UI,可以界面上查看集群和装有的拓扑的运作状态。

如上图,每个点代表两回访问请求,我在0秒的时候
新建了一个名字为GetUserListNum的缓存对象。
在0~0.5秒之内
我访问了3次,在0.5~1秒之内,我们访问了7次。此时,该对象消失,然后我们跟着访问,该目标重置为0.
              
 在第1~1.5秒之内,仍旧访问了7次,在第1.5秒~2秒之内做客了3次。

从节点(Slave node)

Storm集群上有多个从节点,他们从Nimbus上下载拓扑的代码,然后去真正进行。Slave上的Supervisor进程是用来监督和治本实际上运作工作代码的过程。在Storm
0.9后头,又多了一个过程Logviewer,可以用Storm
UI来查看Slave节点上的log文件。
在配置文件storm.yaml中,决定了一台机械上运行多少个worker:

supervisor.slots.ports:
- 6700
- 6701
- 6702

依照这种概括缓存过期策略的模型,在这2秒钟内,我们即使平均每分钟都访问了10次,满意这多少个确定,不过倘诺我们从中取一个期间段,0.5秒~1.5秒之内,也是1分钟,可是却的确的拜会了14次!远远超越了俺们设置的
1分钟最多访问10次的 限制。

ZooKeeper的作用

ZooKeeper在Storm上不是用来做新闻传输用的,而是用来提供协调服务(coordination
service),同时储存拓扑的情状和总计数据。

  • ZooKeeper相当于一块黑板,SupervisorNimbus和worker都在地方留下约定好的信息。例如Supervisor启动时,会在ZooKeeper上注册,Nimbus就可以发现SupervisorSupervisor在ZooKeeper上预留心跳音信,Nimbus透过这些心跳音讯来对Supervisor展开常规检测,检测出坏节点
  • 是因为Storm组件(component)的图景信息囤积在ZooKeeper上,所以Storm组件就可以无状态,可以kill -9来杀死
    • 诸如:Supervisors/Nimbus的重启不影响正在运转中的拓扑,因为状态都在ZooKeeper上,从ZooKeeper上重复加载一下就好了
  • 用来做心跳
    • Worker通过ZooKeeper把孩子executor的场合以心跳的形式汇报给Nimbus
    • Supervisor进程经过ZK把团结的景色也以心跳的款型报告给Nimbua
  • 仓储最近任务的错误意况(拓扑结束时会删除)

 

Storm的容错(Fault Tolerance)机制

正如“搭建一个Storm集群”一文介绍的等同,必须用工具如daemontools或者monit来监督Nimbus和Supervisor的后台进程。这样一旦Nimbus或者Supervisor经过挂掉,会被daemontools检测到,并展开重启。

NimbusSupervisor过程被规划成很快战败(fail
fast)的(当遭受特其余事态,进程就会挂掉)并且是无状态的(状态都封存在Zookeeper或者在磁盘上)。

最根本的是,worker进程不会因为Nimbus或者Supervisor挂掉而受影响。这跟Hadoop是不一样的,当JobTracker挂掉,所有的天职都会没了。

  1. 当Nimbus挂掉会怎么着?

    万一Nimbus是以引进的法门处于进程监管(例如通过supervisord)之下,这它会被重启,不会有此外影响

    否则当Nimbus挂掉后:

    • 曾经存在的拓扑可以继续健康运行,不过无法交到新拓扑
    • 正在运作的worker进程如故可以继续工作。而且当worker挂掉,supervisor会一贯重启worker。
    • 败北的职责不会被分配到另外机器(是Nimbus的天职)上了
  2. 当一个Supervisor(slave节点)挂掉会咋样?

    假若Supervisor是以引进的方法处于进程监管(例如通过(supervisord)[supervisord.org/])之下,这它会被重启,不会有任何影响

    不然当Supervisor挂掉:
    分配到这台机械的享有任务(task)会晚点,Nimbus会把这多少个任务(task)重新分配给另外机器。

  3. 当一个worker挂掉会什么?

    当一个worker挂掉,supervisor会重启它。假若开行一向失败那么此时worker也就无法和Nimbus保持心跳了,Nimbus会重新分配worker到另外机器

  4. Nimbus算是一个单点故障吗?
    假设Nimbus节点挂掉,worker进程仍旧可以延续工作。而且当worker挂掉,supervisor会平素重启worker。不过,没有了Nimbus,当需要的时候(如若worker机器挂掉了)worker就不可以被重新分配到此外机器了。
    为此答案是,Nimbus在“某种程度”上属于单点故障的。在实际中,这种意况没什么大不断的,因为当Nimbus进程挂掉,不会有悲惨的事体发生

这就是说咋样正确的来缓解地点的题材呢?我们可以透过模拟对话级此外信号量这一招数,这也就是我们前几日的核心了。
   什么是信号量?仅就以代码而言,  static
SemaphoreSlim semaphoreSlim = new SemaphoreSlim(5); 
它的情致就象征在多线程情状下,在另外一每日,只好同时5个线程去做客。

硬件要求

 

ZooKeeper

  1. 推介精心设计过的机器,因为ZooKeeper是Storm的瓶颈
    • 每个机器使用一个ZK的实例
    • 只顾因为同样台机器上的任何进程或者虚拟机他们是共享这台机器的,所以可能会潜移默化ZK的性质(来源)
  2. I/O是ZooKeeper的瓶颈
  • 把ZooKeeper的存储放到自己的磁盘上
  • 应用SSD会显著升级性能
  • 健康状态下,Zookeeper的每一回写操作都会联手到磁盘,这就导致了三次磁盘寻址操作(一遍是数码,一遍是数额的日志)。当有着的worker都发心跳给ZooKeeper时,可能会精通影响属性(来源)。
    • 急需监控ZooKeeper节点的I/O负载
  1. 引进在生产条件上运行的ZooKooper集群有至少3个节点,那样即使有一个ZooKeeper服务器挂掉了(例如举办维护),也是足以的。

4容器4线程模型

今昔,在实现代码的前边我们先规划一个模型。

必发365乐趣网投手机版 3

  倘若我们有一个用户A的管道,那个管道里装着用户A的乞请,比如用户A在一分钟发出了10次呼吁,那么每一个请求过来,管道里的要素都会多一个。不过大家设定这多少个管道最六只好容纳10个要素,而且每个元素的存活期为1秒,1秒后则该因素消失。那么这么设计的话,无论是速率仍旧多少的突进,都会有管道长度的范围。这样一来,无论从哪一个刻钟节点依旧时间距离出发,这个管道都能满足我们的频率限制需求。

而这边的管道,就必须和会话Id来对号入座了。每当有新会话进来的时候就生成一个新管道。这多少个会话id依据自己场景所定,能够是sessionId,可以是ip,也可以是token。

这就是说既然这一个管道是会话级其它,我们终将得需要一个器皿,来装这些管道。现在,我们以IP来命名会话管道,并把富有的管道都装载在一个器皿中,如图

必发365乐趣网投手机版 4

而据悉刚才的设定,我们还需要对容器内的每条管道的要素举办拍卖,把过期的给删除掉,为此,还索要独自为该容器开辟出一个线程来为每条管道举办元素的清理。而当管道的要素为0时,大家就清掉该管道,以便节省容器空间。

 必发365乐趣网投手机版 5

理所当然,由于用户量多,一个器皿内或者存在上万个管道,这么些时候偏偏用一个器皿来装载来清理,在功效上肯定是不够的。这些时候,我们就得对容器进行横向扩大了。

  比如,我们得以按照Cpu核心数自动生成对应的数目的容器,然后遵照一个算法,对IP来拓展导流。我当下cpu是4个逻辑主题,就生成了4个容器,每当用户访问的时候,都会初阶经过一个算法,这些算法会对IP进行拍卖,如192.168.1.11~192.168.1.13那一个Ip段进第一个容器,xxx~xxx进第二个容器,依次类推,相应的,也就有了4个线程去分别处理4个容器中的管道。

必发365乐趣网投手机版 6

 

这就是说,最后就形成了我们的4容器4线程模型了。

当今,着眼于编码实现:

  首先大家需要一个能承载这些器皿的载体,这一个载体类似于连接池的定义,可以遵照部分索要自动生成适应数量的容器,倘使有特殊要求的话,还足以在容器上切出一个器皿管理的面,在线程上切出一个线程管理的面以便于实时监控和调度。如若真要做这么一个连串,那么
容器的调度 和 线程的调度功能是必不可少的,而本Demo则是完结了重在职能,像容器和线程在代码中自己也没剥离开来,算法也是一贯写死的,实际设计中,对算法的计划仍旧很重点的,还有多线程模型中,怎么着上锁才能让效能最大化也是主要的。

而这里为了案例的直观就一直写死成4个容器。

public static List<Container> ContainerList = new List<Container>(); //容器载体
static Factory()
{
     for (int i = 0; i < 4; i++)
     {
        ContainerList.Add(new Container(i));  //遍历4次  生成4个容器
     }
     foreach (var item in ContainerList)
     {
        item.Run();    //开启线程
     }
}

目前,我们只要 有编号为 0 到 40 这样的 41个用户。那么这么些导流算法
我也就一贯写死,编号0至9的用户
将他们的乞请给抛转到第一个容器,编号10~19的用户
放到第二个容器,编号20~29放到第两个容器,编号30~40的用户放到第六个容器。

那么这一个代码就是这般的:

 static Container GetContainer(int userId, out int i) //获取容器的算法
 {
     if (0 <= userId && userId < 10)    //编号0至9的用户  返回第一个容器  依次类推
     {
          i = 0;
          return ContainerList[0];
     }
     if (10 <= userId && userId < 20)
     {
          i = 1;
          return ContainerList[1];
     }
     if (20 <= userId && userId < 30)
     {
          i = 2;
          return ContainerList[2];
      }
      i = 3;
      return ContainerList[3];
  }

当大家的对话请求经过算法的导流之后,都无法不调用一个格局,用于辨别管道数量。假诺管道数量一度不止10,则请求败北,否则成功

  public static void Add(int userId)
  {
       if (GetContainer(userId, out int i).Add(userId))
            Console.WriteLine("容器" + i + " 用户" + userId + "  发起请求");
       else
            Console.WriteLine("容器" + i + " 用户" + userId + "  被拦截");
  }

接下去就是容器Container的代码了。

那里,对容器的选型用线程安全的ConcurrentDictionary类。
  线程安全:当六个线程同时读写同一个共享元素的时候,就会油可是生数量错乱,迭代报错等安全问提
  ConcurrentDictionary:除了GetOrAdd方法要慎用外,是.Net4.0专为解决Dictionary线程安全而出的新品类
  ReaderWriterLockSlim:较ReaderWriterLock优化的读写锁,六个线程同时做客读锁
或  一个线程访问写锁

private ReaderWriterLockSlim obj = new ReaderWriterLockSlim();  //在每个容器中申明一个读写锁
public ConcurrentDictionary<string, ConcurrentList<DateTime>> dic = new ConcurrentDictionary<string, ConcurrentList<DateTime>>(); //创建该容器 dic

接下来当您向容器添加一条管道中的数据是透过那一个情势:

 public bool Add(int userId)
 {
     obj.EnterReadLock();//挂读锁,允许多个线程同时写入该方法
     try
     {
         ConcurrentList<DateTime> dtList = dic.GetOrAdd(userId.ToString(),t=>{ new ConcurrentList<DateTime>()}); //如果不存在就新建 ConcurrentList
         return dtList.CounterAdd(10, DateTime.Now); //管道容量10,当临界管道容量后 返回false
     }
     finally
     {
         obj.ExitReadLock();
     }
 }

 这里,为了在末端的线程遍历删除ConcurrentList的管道的时候保证ConcurrentList的安全性,所以这边要加读锁。

 而ConcurrentList,因为.Net没有生产List集合类的线程安全(此间自己表达下:之所以不用ConcurrentBag是因为要确保count和add的一致性,这里补充一下),所以自己新建了一个接续于List<T>的安全项目,在此地
封装了3个需要采取的章程。

public class ConcurrentList<T> : List<T>
{
    private object obj = new object();

    public bool CounterAdd(int num, T value)
    {
        lock (obj)
        {
            if (base.Count >= num)
                return false;
            else
                base.Add(value);
            return true;
        }
    }
    public new bool Remove(T value)
    {
        lock (obj)
        {
            base.Remove(value);
            return true;
        }
    }
    public new T[] ToArray() 
    {
        lock (obj)
        {
            return base.ToArray();
        }
    }
}

最终就是线程的运行模式:

 public void Run()
 {
     ThreadPool.QueueUserWorkItem(c =>
     {
         while (true)
         {
             if (dic.Count > 0)
             {
                 foreach (var item in dic.ToArray())
                 {
                     ConcurrentList<DateTime> list = item.Value;
                     foreach (DateTime dt in list.ToArray())   
                     {
                         if (DateTime.Now.AddSeconds(-3) > dt)
                         {
                             list.Remove(dt);
                             Console.WriteLine("容器" + seat + " 已删除用户" + item.Key + "管道中的一条数据");
                         }
                     }
                     if (list.Count == 0)
                     {
                         obj.EnterWriteLock();
                         try
                         {
                             if (list.Count == 0)
                             {
                                 if (dic.TryRemove(item.Key, out ConcurrentList<DateTime> i))
                                 { Console.WriteLine("容器" + seat + " 已清除用户" + item.Key + "的List管道"); }
                             }
                         }
                         finally
                         {
                             obj.ExitWriteLock();
                         }
                     }
                 }

             }
             else
             {
                 Thread.Sleep(100);
             }
         }
     }
   );
 }

终极,是效能图,一个是依照控制台的,还一个是遵照Signalr的。

 必发365乐趣网投手机版 7必发365乐趣网投手机版 8

Storm安全性

原本设计Storm时,完全没有把安全性考虑在内
现行平安性能相关的意义在一步步加进去
Storm 0.9.x版本上的河池问题:

  1. 从未有过认证机制(authentication),没有授权机制(authorization)
  2. 传输的数码(例如worker之间)没有加密
  3. ZooKeeper上囤积的多寡没有访问限制
  4. 若果Nimbus的Thrift端口没有锁住,任意的用户代码都足以在节点上实施

更多Storm安全性方面的提出见这里

题外话:
在触及Storm之后,有个问题在自身的脑英里升腾,国内的大公司,比如Baidu,Ali,腾讯,都是有出生Storm这类实时总结框架的土壤的,可是为何一贯不做出来呢?

Apache Storm Basic
Training

Fault
tolerance

Storm in pictures

Storm 0.9 Basic
Training


尽管您看了本篇博客,觉得对你有所收获,请点击右下角的“推荐”,让更五个人见到!

援助杰克(Jack)47写作,打赏一个鸡蛋灌饼钱吗

必发365乐趣网投手机版 9

微信打赏

必发365乐趣网投手机版 10

支付宝打赏

分布式下Redis

地方介绍了一种频率限制的模型,分布式与单机相比,无非就是载体不同,大家要是把这么些容器的载体从程序上移植出来,来弄成一个独自的劳动还是间接借用Redis也是实用的。

这里就介绍分布式情状下,Redis的实现。

不同于Asp.Net的多线程模型,大概因为Redis的各个类型的因素非凡粒度的操作导致各类加锁的复杂性,所以在网络请求处理那块Redis是单线程的,基于Redis的落实则因为单线程的来由在编码角度不用太多着想到与逻辑无关的题材。

  简单介绍下,Redis是一个内存数据库,这多少个数据库属于非关系型数据库,它的定义不同于一般的大家体会的Mysql
Oracle
SqlServer关系型数据库,它没有Sql没有字段名尚未表名这一个概念,它和HttpRun提姆e.Cache的定义差不多一样,首先从操作上属于键值对形式,就如
Cache[“键名”]
这样就能博取到值类似,而且能够对各样Key设置过期策略,而Redis中的Key所对应的值并不是想存啥就存啥的,它扶助五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及sorted
set(有序聚集)。

前几天要说的是Sorted
set有序聚集,有序聚集相相比较其他的会合类型的特殊点在于,使用有序聚集的时候仍可以给插入的因素指定一个
积分score,我们把这么些积分score明白为排体系,它其中会对积分举行排序,积分允许再一次,而一成不变聚集中的元素则是绝无仅有。

  依然一如既往的思绪,每当有用户访问的时候,都对该用户的
管道(有序聚集)中添加一个要素,然后设置该因素的积分为眼前岁月。接着在程序中开个线程,来对管道中积分小于约定时间的元素举行清理。因为规定有序聚集中的元素只好是唯一值,所以在赋值方面只假若满足uuid即可。

 必发365乐趣网投手机版 11

这就是说用Redis来实现的代码这就是近乎这种:

必发365乐趣网投手机版 12

由此using语法糖实现IDisposable而包装的Redis分布式锁,然后里面正常的逻辑判断。

如此这般的代码固然也能成功效率,但不够自己。Redis是个基于内存的数据库,于性能而言,瓶颈在于网络
IO 上,与Get一遍暴发一回呼吁相比较,能不可以经过一段脚本来实现多数逻辑吗?

有的,Redis支持 Lua脚本:
  Lua
是一种轻量小巧的脚本语言,用标准C语言编写并以源代码格局开放,
其计划目标是为着放置应用程序中,从而为应用程序提供灵活的扩张和定制效能。
  大致意思就是,直接向Redis发送一段脚本或者让它直接本地读取一段脚本从而一向实现所有的逻辑。

/// <summary>
/// 如果 大于10(AccountNum) 就返回1   否则就增加一条集合中的元素 并返回 空
/// </summary>
/// <param name="zcardKey"></param>
/// <param name="score"></param>
/// <param name="zcardValue"></param>
/// <param name="AccountNum"></param>
/// <returns></returns>
public string LuaAddAccoundSorted(string zcardKey, double score, string zcardValue, int AccountNum)
{
    string str = "local uu = redis.call('zcard',@zcardKey) if (uu >=tonumber(@AccountNum)) then return 1 else redis.call('zadd',@zcardKey,@score,@zcardValue)  end";
    var re = _instance.GetDatabase(_num).ScriptEvaluate(LuaScript.Prepare(str), new { zcardKey = zcardKey, score = score, zcardValue = zcardValue, AccountNum=AccountNum });
    return re.ToString();
}

local
uu就是表明一个为名uu的变量的意思,redis.call就是redis命令,这段脚本意思就是倘诺大于10(AccountNum) 就回去1   否则就扩张一条集合中的元素 并赶回 空。

管道内元素处理的章程就是:

 /// <summary>
 /// 遍历当前所有前缀的有序集合,如果数量为0,那么就返回1 否则 就删除 满足最大分值条件区间的元素,如果该集合个数为0则消失
 /// </summary>
 /// <param name="zcardPrefix"></param>
 /// <param name="score"></param>
 /// <returns></returns>
public string LuaForeachRemove(string zcardPrefix, double score)
 {
     StringBuilder str = new StringBuilder();
     str.Append("local uu = redis.call('keys',@zcardPrefix) "); //声明一个变量 去获取 模糊查询的结果集合
     str.Append("if(#uu==0) then");    //如果集合长度=0
     str.Append("   return 1 ");
     str.Append("else ");
     str.Append("   for i=1,#uu do ");   //遍历
     str.Append("       redis.call('ZREMRANGEBYSCORE',uu[i],0,@score) ");  //删除从0 到 该score 积分区间的元素
     str.Append("       if(redis.call('zcard',uu[i])==0) then ");  //如果管道长度=0
     str.Append("           redis.call('del',uu[i]) ");   //删除
     str.Append("       end ");
     str.Append("   end ");
     str.Append("end ");
     var re = _instance.GetDatabase(_num).ScriptEvaluate(LuaScript.Prepare(str.ToString()), new { zcardPrefix = zcardPrefix + "*", score = score });
     return re.ToString();

这2段代码通过发送Lua脚本的款型来形成了方方面面经过,因为Redis的网络模型原因,所以把LuaForeachRemove方法给指出来做个劳务来单独处理即可。至于这种多容器多线程的落实,则统统可以开多少个Redis的实例来实现。最终放上效果图。

必发365乐趣网投手机版 13

最终,我把这一个都给做成了个Demo。但是没有找到适合的上传网盘,所以我们可以留邮箱(留了就发),或者直接加QQ群文件自取,研讨互换:166843154

 

我喜爱和我一样的人交朋友,不被环境影响,自己是上下一心的教职工,欢迎加群
.Net web交换群, QQ群:166843154 欲望与挣扎

 

作者:小曾
出处:http://www.cnblogs.com/1996V/p/8127576.html 欢迎转载,但任何转载必须保留完整文章及博客园出处,在显要地方显示署名以及原文链接。
.Net交流群, QQ群:166843154 欲望与挣扎 

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图