现在的位置: 首页 > 综合 > 正文

在WCF中实现迭代访问

2011年10月26日 ⁄ 综合 ⁄ 共 3680字 ⁄ 字号 评论关闭

一般来说,在如下场景中我们需要用到迭代器的方式访问序列而不是一次性返回所有序列:

  1. 序列内容较多,一次性全部返回需要占用大量内存
  2. 返回所有序列所需时间较长,需要快速响应
  3. 无需读取所有序列,只需要找到关键的数据后即可终止遍历

这个规则也适用于网络编程中,如果出现上述场景,则往往需要我们适用迭代器模式获取数据。一个非常典型的例子就是SNMP中的遍历,SNMP协议甚至都定义了get-next标准操作来实现迭代访问。另外一个大家比较熟悉的例子就是数据库中的select操作中通过游标读取所有数据。

简单的尝试

那么,在WCF中我们如何实现迭代访问呢?假如我们要提供一个迭代访问的接口实现数据的遍历,一个比较直观的想法就是这么实现:

    [ServiceContract]
    public
interface
IService1
    {
        [OperationContract]
        IEnumerable<int> GetValue();
    }

但是,当我们生成客户端代码时就会发现,客户端生成的接口是:

    public
int[] GetValue()

返回值是int[]而不是IEnumerable<int>,也就是说,服务器端是等到遍及完成后才返回的,客户端是一次性读取的,相当于服务器端隐式调用了一个ToArray函数,无法实现迭代访问的。

因此,我们必须自己实现迭代访问,常用的方法有两种:1. 基于索引的访问,2. 基于游标的访问,下面就来简单的介绍下这两种方法。

索引式迭代访问

索引式访问的典型例子是snmp的get-next操作,基本原理如下:

  1. 通过空索引获取第一个值和第一个值的索引
  2. 根据第一个值的索引获取第二个值和第二个值的索引
  3. 重复第2步,直至获取所有的值

方法比较简单,一个函数就能实现。

服务器端:

    [ServiceContract]
    public
interface
IService1
    {
        [OperationContract]
        bool GetNextValue(ref
int? index, out
int value);
    }

    public
class
Service1 : IService1
    {
        int[] valueList = new
int[] { 2, 3, 5, 8, 4 };

        public
bool GetNextValue(ref
int? index, out
int value)
        {
            index = (index ?? -1) + 1;

            if (index == valueList.Length)    //最后一个点,无法getnext
            {
                value = 0;
                return
false;
            }

            value = valueList[(int)index];
            return
true;
        }
    }

客户端:

    int? index = null;
    int value = 0;
    while (client.GetNextValue(ref index, out value))
    {
        Console.WriteLine(value);
    }

虽然这个方法看起来比较简单,但实现过mib的朋友知道,这种访问方式对数据结构约束比较大:

  • 数据源必须维护一个可以迭代的key,这样才能根据key找到next-key
  • 数据源必须是可查询的,这样才能根据next-key找到next-value
  • 每一次获取数据需要经历完整的查询,性能较差,需要数据源是一个高效的查询数据结构。

由于有这几个约束,往往需要把它存储成一个树形的数据结构(数组式结构通常不能满足增删性能需求),但这种结构又不一定能适用于本身的业务。因此很多时候就存储了两套数据数据结构,一个实现mib的遍历,一个用于实现业务,当业务数据发生增删时,还需要同步更新树,非常麻烦。

基于索引式的迭代和我们传统的yield生成的迭代器方式的一个很大的区别是:索引式迭代没有保存迭代状态,因此需要靠索引来推导迭代,每一次迭代都是一次全新的查询,效率低,实现限制大。

游标式迭代访问

有鉴于此,我们可以按照类似于Sql-select的游标的方式,在服务器端保存迭代状态,当进行迭代访问时,只需要将游标下移即可。在C#中,我们可以通过yield语法糖非常容易的在服务器端生成游标式迭代,一个主要的问题就是:如何把游标—IEnumerator传递到客户端

由于前面已经尝试过,IEnumerator和IEnumerable是无法直接返回给客户端的(无法序列化),因此我们需要在做一个映射,通过返回客户端一个Key来查询迭代器。

基本实现如下:

服务器端:

    [ServiceContract]
    public
interface
IService1
    {
        [OperationContract]
        Guid GetIterator();

        [OperationContract]
        bool GetNextValue(Guid iteratorKey, out
int value);
    }

    public
class
Service1 : IService1
    {
        int[] valueList = new
int[] { 2, 3, 5, 8, 4 };
        static
Dictionary<Guid, IEnumerator<int>> iterators = new
Dictionary<Guid, IEnumerator<int>>();

        public
Guid GetIterator()
        {
            var iterator = valueList.Select(i => i).GetEnumerator();
            var key = Guid.NewGuid();

            iterators[key] = iterator;
            return key;
        }

        public
bool GetNextValue(Guid iteratorKey, out
int value)
        {
            var iterator = iterators[iteratorKey];

            if (!iterator.MoveNext())
            {
                iterator.Dispose();
                iterators.Remove(iteratorKey);

                value = 0;
                return
false;
            }
            else
            {
                value = iterator.Current;
                return
true;
            }
        }
    }

客户端:

    var iteratorKey = client.GetIterator();
    int value = 0;
    while (client.GetNextValue(iteratorKey, out value))
    {
        Console.WriteLine(value);
    }

虽然看上去这种方式比前面的索引的更为复杂,但这个过程中基本上都是为了实现了迭代器和Key的映射,这一部分代码是通用的,把它提成一个泛型的Helper类的话可以直接复用到任何需要使用这种模式的地方。而迭代的实现过程只用了valueList.Select(i => i).GetEnumerator()就实现了,比前面的方式要简单得多(实际项目中的索引式迭代比我前面的例子要复杂很多)。

一个隐藏的BUG:

看了前面的代码后,有经验的朋友会发现,该实现有一个Bug:当客户端只访问了部分数据就退出遍历时,服务器端保存的迭代器无法释放。这个问题通常有两种解决方案:

  1. 服务器端提供Dispose接口,让客户端遍历后主动通知释放迭代器
  2. 服务器按照一定机制对迭代器进行超时处理。例如:如果1分钟内没有迭代访问则自动删除迭代器。

这两种方式各有利弊,也可以组合使用。不过需要注意的是,客户端主动调用Dispose接口是不靠谱的(可能访问几个数据后客户端进程被意外杀掉了),服务器端仍是需要维持超时策略的,或者是把服务设置成会话模式,当感知到会话结束时(客户端退出)释放该会话的所有迭代器,代码我就不实现了。

抱歉!评论已关闭.