`
iunknown
  • 浏览: 409543 次
社区版块
存档分类
最新评论

SPServer : 一个基于线程池(包括HAHS和LF)的高并发 server 框架

阅读更多
spserver 是一个实现了半同步/半异步(Half-Sync/Half-Async)和领导者/追随者(Leader/Follower) 模式的服务器框架,能够简化 TCP server 的开发工作。
spserver 使用 c++ 实现,目前实现了以下功能:
1.封装了 TCP server 中接受连接的功能;
2.使用非阻塞型I/O和事件驱动模型,由主线程负责处理所有 TCP 连接上的数据读取和发送,因此连接数不受线程数的限制;
3.主线程读取到的数据放入队列,由一个线程池处理实际的业务。
4.一个 http 服务器框架,即嵌入式 web 服务器(请参考: SPWebServer:一个基于 SPServer 的 web 服务器框架


0.6 版本之前只包含 Half-Sync/Half-Async 模式的实现,0.6 版本开始包含 Leader/Follower 模式的实现
0.7 版本开始支持 ssl 。把 socket 相关的操作抽象为一个 IOChannel 层,关于 openssl 的部分单独实现为一个 plugin 的形式,对于不使用 ssl 功能的用户,不需要引入 ssl 相关的头文件和库。
0.7.5 增加了一个 sptunnel 程序,是一个通用的 ssl proxy 。类似 stunnel 。
0.9.0 移植 spserver 到 windows 平台,需要在 windows 下编译 libevent 和 pthread 。
0.9.1 在 windows 平台,去掉了对 libevent 和 pthread 依赖,完全使用 iocp 和 windows 的线程机制实现了半同步半异步的框架。
0.9.2 移植了所有的功能到 windows 平台,同时新增加了 xyssl 的插件。

主页:
http://code.google.com/p/spserver/

源代码下载:
http://spserver.googlecode.com/files/spserver-0.6.src.tar.gz
http://code.google.com/p/spserver/downloads/list


在实现并发处理多事件的应用程序方面,有如下两种常见的编程模型:
ThreadPerConnection的多线程模型和事件驱动的单线程模型。

ThreadPerConnection的多线程模型
优点:简单易用,效率也不错。在这种模型中,开发者使用同步操作来编写程序,比如使用阻塞型I/O。使用同步操作的程序能够隐式地在线程的运行堆栈中维护应用程序的状态信息和执行历史,方便程序的开发。
缺点:没有足够的扩展性。如果应用程序只需处理少量的并发连接,那么对应地创建相应数量的线程,一般的机器都还能胜任;但如果应用程序需要处理成千上万个连接,那么为每个连接创建一个线程也许是不可行的。

事件驱动的单线程模型
优点:扩展性高,通常性能也比较好。在这种模型中,把会导致阻塞的操作转化为一个异步操作,主线程负责发起这个异步操作,并处理这个异步操作的结果。由于所有阻塞的操作都转化为异步操作,理论上主线程的大部分时间都是在处理实际的计算任务,少了多线程的调度时间,所以这种模型的性能通常会比较好。
缺点:要把所有会导致阻塞的操作转化为异步操作。一个是带来编程上的复杂度,异步操作需要由开发者来显示地管理应用程序的状态信息和执行历史。第二个是目前很多广泛使用的函数库都很难转为用异步操作来实现,即是可以用异步操作来实现,也将进一步增加编程的复杂度。

并发系统通常既包含异步处理服务,又包含同步处理服务。系统程序员有充分的理由使用异步特性改善性能。相反,应用程序员也有充分的理由使用同步处理简化他们的编程强度。

针对这种情况,ACE 的作者提出了 半同步/半异步 (Half-Sync/Half-Async) 模式。
引用

《POSA2》上对这个模式的描述如下:
半同步/半异步 体系结构模式将并发系统中的异步和同步服务处理分离,简化了编程,同时又没有降低性能。该模式介绍了两个通信层,一个用于异步服务处理,另一个用于同步服务处理。

目标:
需要同步处理的简易性的应用程序开发者无需考虑异步的复杂性。同时,必须将性能最大化的系统开发者不需要考虑同步处理的低效性。让同步和异步处理服务能够相互通信,而不会使它们的编程模型复杂化或者过度地降低它们的性能。

解决方案:
将系统中的服务分解成两层,同步和异步,并且在它们之间增加一个排队层协调异步和同步层中的服务之间的通信。在独立线程或进程中同步地处理高层服务(如耗时长的数据库查询或文件传输),从而简化并发编程。相反,异步地处理底层服务(如从网络连接上读取数据),以增强性能。如果驻留在相互独立的同步和异步层中的服务必须相互通信或同步它们的处理,则应允许它们通过一个排队层向对方传递消息。

模式原文:Half-Sync/Half-Async: An Architectural Pattern for Efficient and Well-structured Concurrent I/O
http://www.cs.wustl.edu/~schmidt/PDF/HS-HA.pdf
中文翻译:http://blog.chinaunix.net/u/31756/showart_245841.html

如果上面关于 半同步/半异步 的说明过于抽象,那么可以看一个《POSA2》中提到的例子:
许多餐厅使用 半同步/半异步 模式的变体。例如,餐厅常常雇佣一个领班负责迎接顾客,并在餐厅繁忙时留意给顾客安排桌位,为等待就餐的顾客按序排队是必要的。领班由所有顾客“共享”,不能被任何特定顾客占用太多时间。当顾客在一张桌子入坐后,有一个侍应生专门为这张桌子服务。




下面来看一个使用 spserver 实现的简单的 line echo server 。

class SP_EchoHandler : public SP_Handler {
public:
  SP_EchoHandler(){}
  virtual ~SP_EchoHandler(){}

  // return -1 : terminate session, 0 : continue
  virtual int start( SP_Request * request, SP_Response * response ) {
    request->setMsgDecoder( new SP_LineMsgDecoder() );
    response->getReply()->getMsg()->append(
      "Welcome to line echo server, enter 'quit' to quit.\r\n" );

    return 0;   
  }     

  // return -1 : terminate session, 0 : continue
  virtual int handle( SP_Request * request, SP_Response * response ) {
    SP_LineMsgDecoder * decoder = (SP_LineMsgDecoder*)request->getMsgDecoder();

    if( 0 != strcasecmp( (char*)decoder->getMsg(), "quit" ) ) {
      response->getReply()->getMsg()->append( (char*)decoder->getMsg() );
      response->getReply()->getMsg()->append( "\r\n" );
      return 0;         
    } else {    
      response->getReply()->getMsg()->append( "Byebye\r\n" );
      return -1;        
    }           
  }     
  virtual void error( SP_Response * response ) {}

  virtual void timeout( SP_Response * response ) {}

  virtual void close() {}
};

class SP_EchoHandlerFactory : public SP_HandlerFactory {
public:
  SP_EchoHandlerFactory() {}
  virtual ~SP_EchoHandlerFactory() {}

  virtual SP_Handler * create() const {
    return new SP_EchoHandler();
  }
};

int main( int argc, char * argv[] )
{
  int port = 3333;

  SP_Server server( "", port, new SP_EchoHandlerFactory() );
  server.runForever();

  return 0;
}


在最简单的情况下,使用 spserver 实现一个 TCP server 需要实现两个类:SP_Handler 的子类 和 SP_HandlerFactory 的子类。
SP_Handler 的子类负责处理具体业务。
SP_HandlerFactory 的子类协助 spserver 为每一个连接创建一个 SP_Handler 子类实例。

1.SP_Handler 生命周期
SP_Handler 和 TCP 连接一对一,SP_Handler 的生存周期和 TCP 连接一样。
当 TCP 连接被接受之后,SP_Handler 被创建,当 TCP 连接断开之后,SP_Handler将被 destroy。

2.SP_Handler 函数说明
SP_Handler 有 5 个纯虚方法需要由子类来重载。这 5 个方法分别是:
start:当一个连接成功接受之后,将首先被调用。返回 0 表示继续,-1 表示结束连接。
handle:当一个请求包接收完之后,将被调用。返回 0 表示继续,-1 表示结束连接。
error:当在一个连接上读取或者发送数据出错时,将被调用。error 之后,连接将被关闭。
timeout:当一个连接在约定的时间内没有发生可读或者可写事件,将被调用。timeout 之后,连接将被关闭。
close:当一个 TCP 连接被关闭时,无论是正常关闭,还是因为 error/timeout 而关闭。

3.SP_Handler 函数调用时机
当需要调用 SP_Handler 的 start/handle/error/timeout 方法时,相关的参数将被放入队列,然后由线程池来负责执行 SP_Handler 对应的方法。因此在 start/handle/error/timeout 中可以使用同步操作来编程,可以直接使用阻塞型 I/O 。
在发生 error 和 timeout 事件之后,close 紧跟着这两个方法之后被调用。
如果是程序正常指示结束连接,那么在主线程中直接调用 close 方法。

4.高级功能--MsgDecoder
这个 line echo server 比起常见的 echo server 有一点不同:只有在读到一行时才进行 echo。
这个功能是通过一个称为 MsgDecoder 的接口来实现的。不同的 TCP server 在应用层的传输格式上各不相同。
比如在 SMTP/POP 这一类的协议中,大部分命令是使用 CRLF 作为分隔符的。而在 HTTP 中是使用 Header + Body 的形式。
为了适应不同的 TCP server,在 spserver 中有一个 MsgDecoder 接口,用来处理这些应用层的协议。
比如在这个 line echo server 中,把传输协议定义为:只有读到一行时将进行 echo 。
那么相应地就要实现一个 SP_LineMsgDecoder ,这个 LineMsgDecoder 负责判断目前的输入缓冲区中是否已经有完整的一行。

MsgDecoder 的接口如下:

class SP_MsgDecoder {
public:
  virtual ~SP_MsgDecoder();

  enum { eOK, eMoreData };
  virtual int decode( SP_Buffer * inBuffer ) = 0;
};


decode 方法对 inBuffer 里面的数据进行检查,看是否符合特定的要求。如果已经符合要求,那么返回 eOK ;如果还不满足要求,那么返回 eMoreData。比如 LineMsgDecoder 的 decode 方法的实现为:

int SP_LineMsgDecoder :: decode( SP_Buffer * inBuffer )
{               
  if( NULL != mLine ) free( mLine );
  mLine = inBuffer->getLine();
        
  return NULL == mLine ? eMoreData : eOK;
}   


spserver 默认提供了几个 MsgDecoder 的实现:
SP_DefaultMsgDecoder :它的 decode 总是返回 eOK ,即只要有输入就当作是符合要求了。
    如果应用不设置 SP_Request->setMsgDecoder 的话,默认使用这个。
SP_LineMsgDecoder : 检查到有一行的时候,返回 eOK ,按行读取输入。
SP_DotTermMsgDecoder :检查到输入中包含了特定的 <CRLF>.<CRLF> 时,返回 eOK。

具体的使用例子可以参考示例:testsmtp 。

5.高级功能--实现聊天室
spserver 还提供了一个广播消息的功能。使用消息广播功能可以方便地实现类似聊天室的功能。具体的实现可以参考示例:testchat 。

6.libevent
spserver 使用 c++ 实现,使用了一个第三方库--libevent,以便在不同的平台上都能够使用最有效的事件驱动机制(Currently, libevent supports /dev/poll, kqueue(2), select(2), poll(2) and epoll(4). )。
分享到:
评论
36 楼 qiezi 2007-06-28  
我把原来那个单线程的reactor增加了线程,接口和前面描述的基本一样:
class EchoProtocol : public Protocol
{
protected:
    virtual void connectionMade()
    {
        requestRead(this, 4, readPackageLength, true, false);
    }

    static void readPackageLength(void* pthis, const char* ptr, size_t len, bool finished)
    {
        assert(finished);
        int packlen = strtol(...) // 解出长度
        requestRead(pthis, packlen, readPackage, true, true);
    }
};

对于HTTP这种没有长度前缀的,最好还是用reactor方式,当然用上面的proactor方式也是可以的,因为有上readall参数可以设置为false。好像和原来的reactor又兼容得不好了,我想是不是可以在收到数据后判断是否有读请求,如果没有则直接调用dataReceived。不过感觉和requestRead这种请求/回调的思想不一致。也不想再维护一个单线程的reactor、一个多线程reactor和一个多线程的proactor。
35 楼 iunknown 2007-06-25  
iunknown 写道
引用
可以看出来这个Proactor完全是在reactor上模拟的一个小玩意。


呵呵,这两天重新看 POSA2 关于 reactor/proactor 的论述,理解的更多了一点。对于 spserver ,我也有这种看法。

reactor 和 proactor 的主要区别:
proactor 是由于异步操作的完成事件触发 handler 的执行,reactor 是由于产生了可以进行非阻塞操作的事件触发 handler 的执行。

libevent 可以说是一个 reactor 的实现。这一点通过对比 reactor 的 Event_Handler 和 libevent 的 callback ,可以和清楚地看到是非常类似的。两者在接口上,体现的是 IO 句柄是否可读或可写。

spserver 也就是相当于基于 libevent 的 reactor 封装了一个 proactor 出来。也是从对比 proactor 和的 Complete_Handler 和 SP_Handler 可以看出来。两者在接口上,体现的是异步操作的结果:proactor 是 Async_Result ,spserver 是 SP_Request->getMsgDecoder() 。

PS:上次提到的实现 connector 的想法,看来可以通过重构 spserver 为一个 proactor 而实现。


找到了一个用模拟 async IO 来实现 Proactor 的文章。
Comparing Two High-Performance I/O Design Patterns

下面粗体字部分描述的做法,也正是 spserver 的做法。

引用
Proposed solution

In this section, we will propose a solution to the challenge of designing a portable framework for the Proactor and Reactor I/O patterns. To demonstrate this solution, we will transform a Reactor demultiplexor I/O solution to an emulated async I/O by moving read/write operations from event handlers inside the demultiplexor (this is "emulated async" approach). The following example illustrates that conversion for a read operation:

        * An event handler declares interest in I/O events (readiness for read) and provides the demultiplexor with information such as the address of a data buffer, or the number of bytes to read.
        * Dispatcher waits for events (for example, on select());
        * When an event arrives, it awakes up the dispatcher. The dispatcher performs a non- blocking read operation (it has all necessary information to perform this operation) and on completion calls the appropriate handler.
        * The event handler handles data from the user-defined buffer, declares new interest, along with information about where to put the data buffer and the number bytes to read in I/O events. The event handler then returns control to the dispatcher.

As we can see, by adding functionality to the demultiplexor I/O pattern, we were able to convert the Reactor pattern to a Proactor pattern. In terms of the amount of work performed, this approach is exactly the same as the Reactor pattern. We simply shifted responsibilities between different actors. There is no performance degradation because the amount of work performed is still the same. The work was simply performed by different actors. The following lists of steps demonstrate that each approach performs an equal amount of work:

Standard/classic Reactor:

    * Step 1) wait for event (Reactor job)
    * Step 2) dispatch "Ready-to-Read" event to user handler ( Reactor job)
    * Step 3) read data (user handler job)
    * Step 4) process data ( user handler job)

Proposed emulated Proactor:

    * Step 1) wait for event (Proactor job)
    * Step 2) read data (now Proactor job)
    * Step 3) dispatch "Read-Completed" event to user handler (Proactor job)
    * Step 4) process data (user handler job)

With an operating system that does not provide an async I/O API, this approach allows us to hide the reactive nature of available socket APIs and to expose a fully proactive async interface. This allows us to create a fully portable platform-independent solution with a common external interface.


34 楼 qiezi 2007-06-23  
iunknown 写道

第一次看到 SCTP, qiezi 能做多一些介绍吗?
从上面的描述来看,SCTP 看来比 TCP 功能强大,但是目前很多的应用需要用到 SCTP 的加强功能吗?

SCTP好像是从linux-kernel-2.6.11才开始加入,具体也不是很清楚,在ubuntu里面安装libsctp就可以了。

IBM developerWorks上面有些介绍:
http://www-128.ibm.com/developerworks/cn/linux/l-sctp/index.html

这个协议提出是满早的,实现出来还是有点晚,目前windows上好像只有商业实现,vista上就不知道有没有,这里有一些实现方面的信息:

http://www.sctp.org/implementations.html

相对于TCP它的好处还是比较多的:
1、多流。TCP是单流的,掉包会阻塞整个流。SCTP是多流的,掉包只阻塞一个流。
2、数据报模式。有点像是UDP,但SCTP是可靠连接,它提供的数据报模式很适合做网络游戏数据包,结合多流,真是个不错的选择。
3、心跳探测。目前长连接网络应用程序都会自己发心跳包探测连接情况,SCTP在协议上实现了。
4、多宿(host)。一个连接可以在多条线路上进行,比如某个服务器有2块网卡,分别连接内网、外网,另一服务器也有这样2个网卡,可以在一个连接上绑定多个IP,它会在其中一条路径失效时,选择另一条路径。
5、防止DDOS攻击。
6、最后一个小的好处。没有TIME_WAIT,对于大量频繁的短连接还是非常有用的。
33 楼 iunknown 2007-06-23  
qiezi 写道
再补一个。

现在TCP是主流,以后如果SCTP成主流了,现有的各种应用层协议不知道会不会修改,我是比较看好SCTP,比起TCP来说省事太多了。


引用

http://www.ptsn.net.cn/xueyuan/technic/switch/comment.php3?id=387

不同于TCP的是,SCTP提供了许多对于信令传输很重要的功能,同时,对于其他一些对性能和可靠性有额外需要的应用,它能提供传输优势来满足这些需要。SCTP和TCP最大的区别在于SCTP对多宿(multihoming)和部分有序(partial ordering)的支持。SCTP的多宿使得每个端点可被多个传输地址访问到,选择不同传输地址会导致两个端点间不同的数据路径,理想情况是在每一条路径都建立一条独立的拥塞控制。所以,SCTP的多主机拥塞控制仍需改进。



第一次看到 SCTP, qiezi 能做多一些介绍吗?
从上面的描述来看,SCTP 看来比 TCP 功能强大,但是目前很多的应用需要用到 SCTP 的加强功能吗?

qiezi 写道

我对于模板也是又爱又恨,看到模板的灵活和产生代码的强大,就会赞叹不已,不过一编译起来那个恨啊,真是咬牙切齿。ACE在以前的台PC上编译一次要30分钟。


我对于模板的记忆来一是自于无法使用 purify 进行测试,当时搞得好痛苦。
第二也是编译 ACE,再一台比较旧的开发机上,编译了两个小时。
从那以后,了解到模板在某些工具的支持上还是有点不足的,也就对使用模板不是很有兴趣了。

32 楼 iunknown 2007-06-23  
关于实现 connector 的计划,今天尝试了一下。
实现的结果不是一个 connector ,而是一个 dispatcher 。

增加了一个 SP_Dispatcher 类,有相应的使用 SP_Dispatcher 来实现 Echo Server 的例子。
testdispatcher.cpp

最新的代码
http://spserver.googlecode.com/files/spserver-0.5.src.tar.gza
31 楼 qiezi 2007-06-23  
再补一个。

现在TCP是主流,以后如果SCTP成主流了,现有的各种应用层协议不知道会不会修改,我是比较看好SCTP,比起TCP来说省事太多了。
30 楼 qiezi 2007-06-23  
ACE是自己写了一套基础库,因为实现比较早,另外模板应用也非常广泛,模板实现通常又不能写到CPP文件里,所以它使用了一些技巧,看起来也显得乱,这跟C++语言本身也有很大关系,基本上可以说C++是成也模板(有点夸大),败也模板(不过分)。
ACE的基础库也很有可取之处,至少它没有回避多线程问题,这在STL里面可是得处处小心。<<Modern C++ Design>>里提到的一些模板策略模式,ACE也有用到,特别是把锁类型作为模板参数。我对于模板也是又爱又恨,看到模板的灵活和产生代码的强大,就会赞叹不已,不过一编译起来那个恨啊,真是咬牙切齿。ACE在以前的台PC上编译一次要30分钟。

ACE对于网络应用架构模式的应用通常都被称为典范,它支持多种网络模型、协议、线程模型、IO模型、操作系统平台,这点来说其它网络框架无人能及,不过正是大而全,作为应用框架来说还是显得原始,使用它要解决的问题比自己从头实现通常不会少。

POSA2这本书还没看过,看目录讲得很系统啊,一定要看看。以前只是看过ACE方面的书,实例为主。模式这东西,通常写过几年代码差不多都自然用到了(除非总在Ctrl+C,Ctrl+V),再看这方面的书一点就通,只是人家总结出来了想想的确是这么回事,可以作为架构和开发人员之间交流用的通用语言。

关于这个话题我没什么多的建议了,下面先抽时间看看POSA2再说
29 楼 iunknown 2007-06-22  
引用
ACE这个原始却笨重的框架


从这两天读 reactor/proactor 的描述来看, ACE 的实现有一个比较大的问题就是不符合目前流行的 DI 要求。各个类之间存在循环依赖,即使在 POSA2 的书上是少量的代码和完整的文字描述,依然看得云里雾里。

引用
twisted的架构是非常先进的,我感觉作为reactor实现,它已经超过ACE这个原始却笨重的框架了,另外它还提供了很多延迟回调(异步)方法,这和它的语法上的优势有很大关系。


使用 python 也有几年的时间了,不过都只是用来做一些比 shell 脚本稍微复杂的工作。还没用 python 做过服务器开发。twisted 也久闻大名了,有时间要认真看看。
28 楼 qiezi 2007-06-22  
connector最主要的挑战在于异步,另外还有个容易忽略的东西就是host解析,所以实际上connector有个2阶段异步操作。在twisted里面,它先发起一个解析host的异步操作,解析完成时再发起connect异步操作。

twisted的架构是非常先进的,我感觉作为reactor实现,它已经超过ACE这个原始却笨重的框架了,另外它还提供了很多延迟回调(异步)方法,这和它的语法上的优势有很大关系。
27 楼 iunknown 2007-06-22  
引用
可以看出来这个Proactor完全是在reactor上模拟的一个小玩意。


呵呵,这两天重新看 POSA2 关于 reactor/proactor 的论述,理解的更多了一点。对于 spserver ,我也有这种看法。

reactor 和 proactor 的主要区别:
proactor 是由于异步操作的完成事件触发 handler 的执行,reactor 是由于产生了可以进行非阻塞操作的事件触发 handler 的执行。

libevent 可以说是一个 reactor 的实现。这一点通过对比 reactor 的 Event_Handler 和 libevent 的 callback ,可以和清楚地看到是非常类似的。两者在接口上,体现的是 IO 句柄是否可读或可写。

spserver 也就是相当于基于 libevent 的 reactor 封装了一个 proactor 出来。也是从对比 proactor 和的 Complete_Handler 和 SP_Handler 可以看出来。两者在接口上,体现的是异步操作的结果:proactor 是 Async_Result ,spserver 是 SP_Request->getMsgDecoder() 。

PS:上次提到的实现 connector 的想法,看来可以通过重构 spserver 为一个 proactor 而实现。
26 楼 qiezi 2007-06-21  
iunknown 写道

当时考虑到要使用一个对象来实现 requestRead 的功能,主要是考虑到实际的环境中,有很多情况是无法用长度来判断的(比如 XMPP,LDAP等协议)。所以就使用一个 MsgDecoder 对象,在这个对象中可以实现任意复杂的协议解码。当然,使用对象来实现,也相应增加了复杂度。这个复杂度可以通过提供一些常用的 MsgDecoder 来简化。

目前我打算第一个基本实现的handle类(对应于twisted的Protocol)是样的:
class Protocol
{
    void dataReceived(void* p, size_t len);
    void requestThread();
}

如果是无法用长度来判断还接收多少数据的,就在Protocol上派生类处理就行了,想让线程处理时调用requestThread就行。

对于有长度前缀的,可以使用另一个基类:
class Proactor : public Protocol
{
    void dataReceived(void* p, size_t len)
    {
        取request列表,回调相应接口,根据要求调用requestThread
    }

    void requestRead(...);
}

可以看出来这个Proactor完全是在reactor上模拟的一个小玩意。
25 楼 iunknown 2007-06-20  
qiezi 写道
libevent把线程给阻隔了,通常操作系统提供的接口比如epoll/select还有各个socket操作函数都支持多个线程的,libevent把这个优点给掩盖了,当然使用它的理由也很充分,自己做超时处理还是比较麻烦,如果不做超时处理,我宁愿自己使用epoll了。多线程实现起来是点麻烦,ACE的线程池Reactor在处理每一个有事件的handle时,都会暂时清除掉该handle上的事件,所以它可以用多个线程跑事件循环,不过估计效率会比单线程低,事件部分还是单线程比较好。


libevent 只是把《Unix Network Programming》中提到的“迭代服务器程序”做了一个封装。
http://iunknown.iteye.com/blog/41077
也就是说,如果使用 libevent 来实现“迭代服务器程序”,那么是最自然,最方便的。
在 UNP 中还提到了其他的服务器程序架构,这些不同的服务器程序架构,对于 I/O 的处理流程都有所不同。
如果使用 libevent 来实现其他的服务器程序架构,那么就需要使用一些迂回的方法。比如:可以使用 libevent 来实现 ThreadPerConnection 的模型,每个线程一个独立的 event_base,而且这个 event_base 也可能就只有一个 IO 句柄。当然,如果迂回的太多,就会使得 libevent 带来的坏处超过好处,这个时候就不如放弃 libevent ,另寻解决方法。

qiezi 写道
spserver目前是自己处理事件循环的,使用event_dispatch应该也可以吧?自己处理有什么优点吗?当然其中一个好处是其它线程也可以调用shutdown。使用event_dispatch就会一直循环,这正是想要的。当需要退出时,只需要先关闭侦听端口,由于spserver使用了msgqueue,所以总是会有事件,等到各个线程处理完毕并且也没有其它WRITE事件了,就可以调用event_loopexit了。


spserver 可以使用 event_base_dispatch 。目前使用 event_base_loop( eventArg.mEventBase, EVLOOP_ONCE ) 主要就是为了能够控制程序自行退出。其实 event_base_dispatch 的实现也就一行代码:

int
event_base_dispatch(struct event_base *event_base)
{
  return (event_base_loop(event_base, 0));
}


qiezi 写道
持久存储是一个方案,不过通常服务器结束时都希望它把已经接收到的连接处理完毕,并不希望下次还要重启才能处理完成,重启后socket连接已经失效,就算把数据包给处理了也没办法回复。


通常持久存储记录的信息是一些比较高层次上的状态。比如对于 smtp 来说,记录的可能是“服务器是否已经把邮件完整接收了”这样的状态,而不是某个数据是否发送成功了。如果已经接收了,但是在正式投递的时候,smtp 服务器异常退出,那么在 smtp 重启之后,就能够从持久存储中获得相应的信息,然后重试之前未完成的任务。当然,smtp 可以这样做的原因,也在于 smtp 协议在协议层的规定:

引用
RFC2821
In sending a positive  completion reply to the end of data indication, the receiver takes full responsibility for the message (see section 6.1).


所以,对于服务器可靠性的保证,还在于协议的设计。
24 楼 qiezi 2007-06-20  
libevent把线程给阻隔了,通常操作系统提供的接口比如epoll/select还有各个socket操作函数都支持多个线程的,libevent把这个优点给掩盖了,当然使用它的理由也很充分,自己做超时处理还是比较麻烦,如果不做超时处理,我宁愿自己使用epoll了。多线程实现起来是点麻烦,ACE的线程池Reactor在处理每一个有事件的handle时,都会暂时清除掉该handle上的事件,所以它可以用多个线程跑事件循环,不过估计效率会比单线程低,事件部分还是单线程比较好。

spserver目前是自己处理事件循环的,使用event_dispatch应该也可以吧?自己处理有什么优点吗?当然其中一个好处是其它线程也可以调用shutdown。使用event_dispatch就会一直循环,这正是想要的。当需要退出时,只需要先关闭侦听端口,由于spserver使用了msgqueue,所以总是会有事件,等到各个线程处理完毕并且也没有其它WRITE事件了,就可以调用event_loopexit了。

持久存储是一个方案,不过通常服务器结束时都希望它把已经接收到的连接处理完毕,并不希望下次还要重启才能处理完成,重启后socket连接已经失效,就算把数据包给处理了也没办法回复。
23 楼 iunknown 2007-06-20  
引用
目前看到的很多网络服务器框架都有一个共同的问题,退出比较草率,直接结束掉事件循环。。。。。。

在检测到退出标志后,关闭侦听端口,再等待队列处理空,然后结束掉线程池


的确是这样。这两天正在看 apache 的代码,apache 在这方面应该是做的比较好的,通过发送信号,让 apache 优雅地结束掉。

spserver 在这方面的实现的确不够。
遇到的主要问题是:SP_Handler 的子类需要参与到这件事情上去。因为对于有些长连接的协议,如果 SP_Handler 不在处理过程中指示要结束连接,那么会导致队列总是不为空。上面提到 apache 实现了“优雅地结束”,其中有一个原因就在于 http 协议比较简单,一个 request 过来之后,返回一个 response 就可以了。而对于像 smtp 这类协议,如果在发出结束信号的时候,协议交互才刚刚开始,那么就需要等待这个连接进行完一次完整的 smtp 协议交互。这样等待的时间就不可预测了。
所以最终 spserver 对于这一点,采取了不作为的态度。这个也是一直在考虑改进的功能点。曾经考虑的解决方法是在 shutdown 方法中增加一个 timeout 时间,如果能在 timeout 时间内等到队列为空,那么优雅地结束。如果超过 timeout 时间,队列都不为空,那么就强行退出。不过还是不够完整。

要实现“优雅地关闭”,主要的考虑应该是想保证服务的可靠性?之前看过关于 XMPP 协议在可靠性方面的讨论,提到只在网络接口这一层保证可靠还不足于保证可靠性,而需要在协议的定义上来保证。可以参考这个帖子中引用的URL:http://iunknown.iteye.com/blog/57430

另外,如果需要最大限度地保证服务器的可靠性,那么最彻底的解决方法其实是服务器端把每个连接对应的处理状态保存到持久存储中。在这种情况下,即使服务器异常退出(core dump,或者直接被 kill -9 ),也能从持久存储中恢复处理的状态。就目前了解的情况来看,有些 SMTP 服务器是这样做的。曾经也考虑过 XMPP 的实现,如果要保证每条消息的可靠传输,那么需要在接收到消息之后,需要把消息保存起来,然后再进行转发,转发成功之后,删除消息。这种策略在 《POSA2》 关于 ACT (异步完成标记)的讨论中,也提到过。
22 楼 qiezi 2007-06-19  
我大致看了一下代码,还有一点疑问,也是我比较关心的。

目前看到的很多网络服务器框架都有一个共同的问题,退出比较草率,直接结束掉事件循环,实际上有些正在接收、正在处理或正在发送中的操作都没有优雅地结束掉,所以实际在编写服务器程序时,我宁愿自己写这部分,使用每连接一线程方式做这种还是比较简单的,当然可能性能问题比较严重,不过最近做的工作都是TCP短连接的,同时忙的线程也不会太多,问题还不大。

通常在结束时先关闭侦听端口,再等待处理中的操作结束,这个结束当然包括它处理完了并且发送完回复,使用libevent的event_loopexit,或者是spserver的shutdown,我都没有看到这样一个优雅的关闭过程。我自己也使用了libevent,发现还是比较难操作,特别交给线程池处理的操作,使用event_loopexit可能不行,像spserver一样自己处理循环应该还是可以做到的,只需要在检测到退出标志后,关闭侦听端口,再等待队列处理空,然后结束掉线程池。
21 楼 iunknown 2007-06-18  
关于 MsgDecoder 再作一些说明。

class Protocol  
{  
    /** 
     * bytes: 要读取的字节数 
     * callback: 读取完成时的回调委托函数 
     * recvall: 是否要读取所有请求的字节数才回调 
     * threading: 是否交由线程池 
     * 
     * void delegate(char[] data, bool finished) 参数: 
     * data: 数据 
     * finished: 是否已经接收完成。对于recvall为true的,回调时这个参数肯定为true,否则可能为true或false 
     */  
    void requestRead(size_t bytes, void delegate(char[], bool) callback, bool recvall, bool threading);  
}


在 qiezi 的实现中,使用 requestRead 来指定需要读取的内容。这个函数的用意和 MsgDecoder 是一样的。
只不过 requestRead 里有指定是否采用 threading 的参数,MsgDecoder 没有。

当时考虑到要使用一个对象来实现 requestRead 的功能,主要是考虑到实际的环境中,有很多情况是无法用长度来判断的(比如 XMPP,LDAP等协议)。所以就使用一个 MsgDecoder 对象,在这个对象中可以实现任意复杂的协议解码。当然,使用对象来实现,也相应增加了复杂度。这个复杂度可以通过提供一些常用的 MsgDecoder 来简化。

同时,在具体的使用过程中,很多时候是否采用 threading ,不一定能够在读取内容之前就决定,而是需要在读取内容之后才能决定。曾经考虑在 MsgDecoder 中加入指定是否需要 threading 的接口。主要是担心在应用程序不断修改演进的过程中,可能原来不使用 block system call 的功能,后来又使用了,如果实现者忘记修改关于 threading 的设定,会导致整个程序出现问题。所以目前还是没有在 MsgDecoder 中加入这个功能。

20 楼 qiezi 2007-06-17  
我说的是我上面的想法,是reactor作底层;处理部分是半同步/半异步;requestRead这种算是proactor的做法,请求/回调,当然我那个想法里面回调可以是反应器所在的线程,也可以是用来处理的线程池,原理上和proactor是相近的。
19 楼 iunknown 2007-06-17  
引用
应该算是reactor + proactor + 你这个半同步/半异步。

实现方式是reactor,反应式的;用户接口则是以proactor方式来调用,请求/回调;处理部分算是半同步/半异步。


关于 proactor ,按 <POSA2> 的说法,“主动器模式是同步反应器模式的异步变体”。在这本书的描述中,主动器专门指封装了 windows 的 IOCP 和 POSIX 的 aio_* API 系列异步 I/O 机制的框架。而在我们这里讨论的上下文中,都没有提到使用 IOCP 或者 aio_* ,严格来说,这里体现的不是 proactor 模式,仍然就是 reactor 模式。

不过楼上的说法是“用户接口是proactor方式”。如果从这个角度来说,可以认为是 proactor 。<POSA2>中提到,“如果操作系统不支持异步操作,可以在实现中使用多线程模拟主动器模式的语意”。

在 spserver 的结构中, event loop 线程可以认为就是用来模拟异步操作的。线程池就是异步操作的发起者,一方面,msgdecoder 可以看成是线程池向 event loop 发起的一个读操作,msgdecoder 判断到读操作完成时就回调线程池;另一方面,response 就是由线程池向 event loop 发起的写操作,写操作完成之后,通过 completionhandler 来回调线程池。
18 楼 iunknown 2007-06-16  
qiezi 写道
你目前的实现方式是收到数据后,由msgdecoder来判断是否完成(是这样没错吧?),然后放入线程池我觉得欠点灵活性。

比如我有这样一个应用A,它非常简单,不需要放入线程池,收到后只需要根据长度就可以判断是否收完了,而且不耗CPU,所以不需要线程池,这时msgdecoder也是多余的了。

另一个应用B收到数据后要计算,所以交由线程池处理。



对于不需要线程池的应用,直接使用 libevent 就是了。
spserver 是基于 libevent 来实现的,对于单线程高并发的应用,直接使用 libevent 就非常好了。
spserver 的主要作用是在 libevent 的基础上,增加一个线程池,使得能够方便地实现需要线程来处理的应用。

前几天和一个朋友讨论到这个问题,他也是强调这一点,认为 spserver 的结构不够灵活,不能很好地优化不需要线程池的应用。但如果了解 spserver 和 libevent 各自的侧重点,那么对于不需要线程池的应用,直接使用 libevent 就是行了。只有需要线程池的应用才使用 spserver 。
17 楼 qiezi 2007-06-14  
应该算是reactor + proactor + 你这个半同步/半异步。

实现方式是reactor,反应式的;用户接口则是以proactor方式来调用,请求/回调;处理部分算是半同步/半异步。

相关推荐

Global site tag (gtag.js) - Google Analytics