下载库SDK接口重设计

下载库 接口 设计 P2SP SDK

在做新版的下载库sdk,p2s的基本功能已经完成。近期要做sdk一个核心的需求——会员增值服务。为了满足这个需求,sdk需要提供新的接口。

看了下目前sdk对外暴露接口的方式,实现简介高效。但是也有个问题,扩展性不高。所以需要修改暴露接口的方式。

会员需求

sdk的接口对外暴露下载资源的功能。调用者可以调用接口创建下载任务,获取任务状态信息,停止任务等。

分析需求,需要SDK对外暴露的接口有下面几个要求:

  1. 接口调用的安全性,对于非法调用不能出现未定义错误
    1. 参数的合法性检测
    2. 接口是有状态的,调用顺序的合法性检测
  2. 除初始化反初始化接口,其他接口需要保证线程安全

接口函数申明

一如既往,sdk的接口大致如下:

int init();
int uninit();

int create_start_task(param p, int* p_task_id);
int query_task(int task_id, task_info * p_task_info);
int stop_release_task(int task_id);

SDK 工作线程

SDK 的工作(主)线程是sdk初始化时创建的。 也思考过,为什么不在调用者的线程上工作呢? 可能问题比较傻,但还是对比下好处吧:

  1. 实现逻辑独立,可控性强
  2. 接口设计简单,不依赖与调用者的实现通用性强
  3. 反之,如果调用者是多线程调用,则会傻眼

解决方法

参数合法性检测

以上接口的设计已经体现了参数的合法性检测了。创建任务的接口简单的实现方式:

int create_task(param p, task **pTask)
{
    *pTask = new Task(p);
    return 0;
}

对外直接暴露任务指针也就意味着,后面的接口会使用外部传入的指针。如果传入的指针是非法的,最终sdk会访问到非法地址。

而目前的接口设计,对外暴露的仅有任务句柄。内部保存了task_idtask对象的映射关系,如果传入的task_id未出现在映射关系表中,即认为调用非法。

映射关系需要维护的——创建任务时增加映射关系,释放任务时移除映射关系。之后多线程调用接口就必须给操作映射关系的相关代码加锁了。

调用顺序的合法性

接口的调用最终都需要到主线程中执行,即需要线程间通信。线程间通信的实质是数据共享。 一般线程间通信都是采用消息队列的方式实现的。原有的实现并没有采用这种做法。使用了最淳朴,最实在的数据共享。

每种任务状态有一个队列。如:

  • 需要开始的任务——waiting_start_list
  • 正在运行的任务——running_list
  • 需要释放的任务——waiting_stop_list

接口线程调用start_task时,在接口线程将任务存于waiting_start_list之中。待主线程的循环执行到响应接口线程的时候,便开始任务,并将任务移到running_list之中。其他操作同理。

这样的设计,可以很好的满足创建开始查询停止释放的接口需求。但是在需要增加新接口就不方便了。比如现在要增加一个修改任务属性的接口,就不知道怎么添加了。如果是增加一个类似的队列,那么不同操作之前的顺序怎么保证?

备注:使用多个队列来保存任务操作可以解决一个问题,合并任务操作。在测试程序的时候,经常会频繁的点击开始和暂停按钮。假如创建一个任务之后,用户在主线程循环这个时间内调用了4次开始、停止任务。主线程一次性获取到这些操作后,可以现在不同的队列对任务进行切换,最终的操作依赖与队列中的内容。

为了程序的扩展性,这里最终使用消息队列的方式实现。

class Command
{
  public:
    virtual void Excute() = 0;    //主线程中执行
  private:
    bool m_bIsSynaCommand;
    Event m_event;
};

class CommandList
{
  
  public:
  
    bool SendCommand(Command * pCommand);
    bool PostCommand(Command * pCommand);
  private:
    std::list< Command* > m_listCommands;
};

这样所有的操作都放在了同一个队列之中,先后顺序可以在队列中体现出来。另外,扩展性也较好,增加新的接口仅需继承Command即可。

锁的使用

前面提到多线程通信的本质是数据共享。数据共享必然要解决读写冲突的问题,这里使用锁来保证读和写操作的唯一。

接口总的说来有3类锁:

  1. 任务id和任务对象映射的锁,接口线程存在多线程
  2. 命令队列的锁。多个接口线程,接口线程和主线程的读写
  3. 任务的状态的锁。

目前query_task的接口实现较为特殊,操作没有添加到命令 队列中。这样做出于两点考虑:

  1. query_task调用较频繁
  2. query_task是唯一一个获取任务数据的接口,即有返回值

所以,每个任务的任务信息都需要单独的一个锁。

吕飞

锲而舍之,朽木不折;锲而不舍,金石可镂