Archive forC/C++

spserver代码阅读笔记

从这里可以下载doc/pdf版:spserver代码阅读.pdf

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

引用:http://iunknown.javaeye.com/blog/59804
spserver 是一个实现了半同步/半异步(Half-Sync/Half-Async)和领导者/追随者(Leader/Follower) 模式的服务器框架,能够简化 TCP server 的开发工作。
spserver 使用 c++ 实现,目前实现了以下功能:

  • 封装了 TCP server 中接受连接的功能
  • 使用非阻塞型I/O和事件驱动模型,由主线程负责处理所有 TCP 连接上的数据读取和发送,因此连接数不受线程数的限制
  • 主线程读取到的数据放入队列,由一个线程池处理实际的业务
  • 一个 http 服务器框架,即嵌入式 web 服务器(请参考: SPWebServer:一个基于 SPServer 的 web 服务器框架)

目录

  1. Leader/Follower 模型
  2. Dispatcher模型
  3. Hahs半同步半异步模型
  4. 代码编写

一、Leader/Follower 模型

一切从类SP_LFServer的构造函数开始。如果SP_LFServer是在主线程中实例化的,那就从主线程讲起。

以[M]代表主线程[MainThread],
[M]建立的线程池中的线程[ChildThread][C]

  1. [M]SP_IOUtils::tcpListen初始化监听socket
    • [M]创建socket listenFD
    • [M]bind listenFD到本地某端口
    • [M]listen在listenFD上
  2. [M]初始化libevent事件基
  3. [M]把listenFD的读事件绑在事件基上,回调函数是SP_EventCallback::onAccept
  4. [M]建立线程池,每个线程都跑去执行SP_LFServer ::lfHandler [C]阻塞在这里
    • [C]一旦有线连接进来,某个[C]在SP_LFServer ::handleOneEvent抢到全局锁,跑去执行一次event_base_loop,也就是执行了SP_EventCallback::onAccept把新连接接进门来。
      • [C]SP_EventCallback::onAccept中,主要是把当前连接放入会话管理器,并为新连接创建SP_Session对象,然后setHandler及setIOChannel,注册连接的读写到SP_LFServer的事件基,并宣告”此连接已经可以读写,某个[C]来处理读写事件吧”。
      • 再就是调用事务的初始化函数(由具体的Handler实现):doStart
    • [C]或者是有数据要收发,那些阻塞在SP_LFServer :: lfHandler -> SP_LFServer :: handleOneEvent中event_base_loop的[C],会被激活并调用相应的读写回调,接收数据或者向连接写数据。
    • [C]或者是有任务要处理,那些阻塞在SP_LFServer :: lfHandler -> SP_LFServer :: handleOneEvent中InputResultQueue->pop的[C],会被激活并取得这个任务。
  5. [M]做完这些事情,主线程自己就睡觉去了(以后再也不干事了) [M]阻塞在这里

此种模式中,实际的数据收发,是由某个最早抢到全局锁mMutex的[C]来做的,它不仅把新连接通过SP_EventCallback::onAccept接进来,也同时在event_base_loop中把其它事件(如果有)也一同处理了,直到某个事件向任务队列InputResultQueue塞了任务(这任务通常是接收了足够多的数据或者其它事件导致的)。

一旦这个[C]接到任务,它就跑去干正经活去,数据收发的事情交由下一个苦力接手,这就要看线程池中哪个线程抢到了,应该是不确定的。干完正经活的线程回到线程池里,跟其它人抢mMutex。

反正几乎每个[C]都会经历:抢mMutext,处理libevent事件(收发数据当然在这里)或者接到任务。

二、Dispatcher模型

以[M]代表主线程[MainThread],以[D]代表[M]创建的[DispatcherThread]线程

  1. [M]SP_IOUtils::tcpListen初始化监听socket
    • [M]创建socket listenFD
    • [M]bind listenFD到本地某端口
    • [M]listen在listenFD上
  2. [M]SP_Dispatcher:: SP_Dispatcher 初始化
    • [M]创建libevent事件基
    • [M]创建阻塞队列,存放连接
    • [M]创建新线程[DispatcherThread]
    • [D]创建两个线程池
      • [D]workerExecutor池中有较多线程,用于执行实际事务
      • [D]actExecutor池中线程较小,用于处理收尾事件
    • [D]进入event_base_loop (DispatcherThread阻塞在这里)
      • [D]有新事件发生,workerExecutor执行,actExecutor收尾
  3. [M]在循环中accept ([M]阻塞在这里)
    • 有新连接进来,激活了[M],空闲,推入阻塞连接队列
      • SP_IOUtils::setNonblock设置连接为非阻塞型
      • 触发onPush事件,激活了[D]
      • [D]为连接设置事务处理对象setHandler
      • [D]为连接初始化数据渠道(ssl/raw/ encrypted)setIOChannel
      • [D]将连接之读写事件加入Dispatcher的事件基中进行监听
        • SP_EventCallback::onRead处理读数据
        • SP_EventCallback::onWrite处理写数据
      • [D]开始调用事务处理的doStart或仅激发可读写可写消息
    • 系统忙,超过连接数,往连接里写” System busy, try again later”,断开连接

所有进来的连接,都是先经[M]Accept的,因为Accept后直接抛入连接队列,所以Accept可以很快进行,不管当前有多少连接都会非常及时响应。

待[D]开始监听新连接的读写事件后,数据的收发都是由[D]自已负责,这是在2.e步骤行的。

一旦读写完(或者处理完其它信号或者事件),就去看看任务队列InputResultQueue中有无需要处理的事件(比如在读写阶段,读完一段数据如果符合条件,就会被组成一个任务扔到任务任务队列中)。当然,[D]不会事必躬亲,而是把任务取出来扔给workerExecutor线程池。

然后,[D]会去看任务执行结果队列OutputResultQueue,这里的元素是由workerExecutor中的某线程在执行事务的时候产生的,即执行完一个任务就把任务打包放到结果队列。[D]从结果队列取出来,扔给actExecutor线程池,由它去收尾。其实一般情况下只是做些释放内存之类的工作,或者统计一下成功执行了多少任务(请求)。

三、Hahs半同步半异步模型

以[M]代表主线程[MainThread]

一切从类SP_Server::start讲起

  1. [M]SP_IOUtils::tcpListen初始化监听socket
    • [M]创建socket listenFD
    • [M]bind listenFD到本地某端口
    • [M]listen在listenFD上
  2. [M]通过SP_EventArg建立libevent事件基
  3. [M]把listenFD的读事件绑在事件基上,回调函数是SP_EventCallback::onAccept
  4. [M]建立大线程池workerExecutor和小线程池actExecutor,这些池中的线程都是阻塞在判断队列中是否有元素的函数上
  5. [M]进入libevent事件处理循环:event_base_loop [M]阻塞在这里
    • 直到有事件发生,比如新连接进来,[M]调用onAccept去迎接新连接,并将其读写事件注册到事件基上
    • 处理完事件后,去任务队列中看看有无要处理的任务(InputResultQueue),有的话提出来扔给大线程池workerExecutor,由池中的线程去干实际的活
    • 然后看看有无处理完的任务(OutputResultQueue),有的话提出来扔给小线程池actExecutor

这个模型里,所有数据读写及其它libevent事件处理都由实例化SP_Serverr 的线程来做的,如果是文件传送这样的应用,此线程会很忙。

我猜想,名字中的同步是否是指实际的事务处理是同步的,比如流程一直都是接收数据,处理数据(比如处理的时候可以很长时间),返回数据这么个固定的顺序;而实际数据的收发,在事务处理时不需要关心,只要认为数据已经发出去就好了,至于真正是在什么时候发给客户端的,事务处理函数管不着,这种不等待数据真正地外发做法是否就是异步呢?

四、代码编写

每个连接开始都是可读又可写的,根据要实现的功能的不同,可以由Client/Server任一方先出招。所有的交流总是会有某方主动的,像http服务,总是会由客户端先行发个请求,然后服务器回应。

在代码编写的时候,一般只需要实现这么几个地方:

  1. SP_MsgDecoder子类的decode
  2. SP_Handler子类的handle/start/error/timeout

SP_MsgDecoder::decode在每次接收完数据的时候被调用,返回值代表了是不是已经收到一个完整的数据包了,如果是则必须返回eOK让接受数据的函数组个任务放到任务队列,让线程池中的某位[C]来调用SP_Handler::
handle。如果还只是不完整的数据包,那就返回eMoreData,告诉接收函数我还没吃够,继续喂我吧。

这其实会有个小问题,如果我们的逻辑数据包太大,内存会占用较多,效率不高。并且这样的处理方式在需要连接接收大量数据的地方不太实用,比如像接收客户端传送的大文件。有人会说,像传文件的时候,可以设置让decode一直返回eOK嘛,这确实是可行的,但是decode函数还是会被调用N次,这个调用还是挺耗资源的。每接收一小片数据,都会去调用一下decode+handle,还有个深层次的问题是这里的decode+handle都是父类虚函数的实现,效率就更低了。

每个东西都会有它适合的应用场合,像以上的模式更适应像http或者telnet这种交互性较多,但每次传递数据较少的应用。如果是做ftp文件传送之类,还是考虑其它模型吧。

不过我倒还真是用它来实现过一个文件服务器,文件内容传送是被分片打包进自定义的数据包的,和其它命令混合着传送到服务器,服务端一片片地将文件内容写入目标文件。由于文件碎片数据包中包含了足够的信息,所以这样断断续续地写也是可以传文件的。传输速度由于只是测试过单个客户端连接的情况,所以也就没什么参考意义。

评论

DB2的sqC文件名前八位不能重复的解决办法

我们的项目中使用到了IBM的DB2数据库,系统开发语言是C++,操作数据库是直接在C++中编写ESQL(嵌入式SQL)语句。在使用中发现,只要有文件名前8位相同,就造成部分文件中的代码失效。

原因:
——————
db2默认情况下使用*.sqC的名字生成*.bnd,又以*.bnd的文件名部分在数据库中做绑定,建一个同名的package。而db2的package的名字是有八字节长度限制的,造成前八位文件名相同的C++代码,总会后面的覆盖前面的。

知道原因就好办事了。

解决:
——————
取原始文件名的md5摘要前N位,拼成临时sqC文件的名字,再以此文件去预编译,继而用同名bnd文件到目标数据库做绑定,这样package就不会前后覆盖了。

makefile中实际的做法:
————————

%.C : %.sqC
  db2 connect to $(dbName) user $(dbUser) using $(dbPassword);\
  cp $*.sqC P${shell echo $* |md5sum -|cut -c1-8}_$*.sqC;\
  db2 P${shell echo $* |md5sum -|cut -c1-8}_$*.sqC bindfile;\
  db2 bind P${shell echo $* |md5sum -|cut -c1-8}_$*.bnd;\
  db2 connect reset; \
  db2 terminate;

2010.12.08 更新

  • 要改名的不是bnd文件,而是sqC,所以要在bindfile的时候已经改好名了。
  • md5散列可能出现前字母是数字,所以文件名加固定前缀。

评论

Google Javascript脚本引擎v8 学习笔记

本来11月初就要写的日志,断断续续居然写了近半个月,晚上没心情加班,终于狠狠心把它写完了。

1. google V8学习笔记.doc
2. google V8学习笔记.pdf
3. google V8学习笔记 – 源码

目录
-------------------------
一、Google v8 整体印象
 1.1 Google v8是什么,能做什么?
 1.2 Google v8的获取及编译
  1.2.1 获取源码
  1.2.2 编译类库及示例

二、Google v8入门
 2.1 v8的基本概念
 2.2 从HelloWorld开始
 2.3 在C++中访问Javascript的变量及函数 
 2.4 在Javascript中访问C++全局函数及变量
 2.5 在C++中“声明”“Javascript类”, 供Javascript实例化
 2.6 封装完整的C++类到Javascript中,供Javascript实例化

三、Google v8的开源应用
 3.1 Node.js
 3.2 v8cgi

四、参考资料
五、本文源码
 

仅仅是学习笔记,不是教程。

Comments (5)

[zz]在可执行程序中嵌入资源文件(linux环境)

gcc似乎没有”资源文件”(windows中的.res)一说,如果要在可执行程序中放入体积比较大的文件,一般是定义一个非常大的数组,将文件的每字节填充数组,这种做法的好处是无平台依赖性。下面的做法更优雅,但似乎只有gcc支持。

转载自:http://hi.baidu.com/yeyingxian/blog/item/fe0322d1c8e75e359a5027d8.html

1、首先利用objcopy工具把二进制文件装到一个.o文件中
objcopy -I binary -O elf32-i386 –binary-architecture i386 data.txt data.o

可以看到这个.o之中包含了符号

$ nm data.o
0000001c D _binary_data_txt_end
0000001c A _binary_data_txt_size
00000000 D _binary_data_txt_start

_binary_data_txt_start是存放data.txt的起始位置,_binary_data_txt_size是data.txt文件大小

2、写一个c程序,在里面使用这些二进制资源

/* main.c */
#include 
extern int _binary_data_txt_end;
extern int _binary_data_txt_size;
extern int _binary_data_txt_start;

int main(void)
{
   int size = (int)&binary_data_txt_size;
   char *data = (char *)&binary_data_txt_start;
   char *end = (char *)&binary_data_txt_end;
   printf("taille: %d\n", size);
   printf("buffer: %s", data);
   printf("start: 0x%p ; end: 0x%p\n",data,end);
   return 0;
}

3、编译、链接
gcc -o test main.c data.o

参考

http://www.finiderire.com/post/2009/06/02/Et-un-fichier-exe-pour-les-gouverner-tous

http://www.linuxjournal.com/content/embedding-file-executable-aka-hello-world-version-5967

Comments (1)

C++与脚本语言的交互

为了让报文处理脚本化,准备研究一下C++中嵌入脚本语言,这比自己写蹩脚的脚本语言要好多了。

以下是待选项:

  • V8 Google的Javascript解析引擎,性能应该不错,再是js毕竟好写,大众化。
  • CINT C/C++脚本解析引擎,没错,是把c/c++当脚本
  • lua 听说很强大,在网易的游戏系统中大量使用。

2010-11-04
 学习使用V8的过程中,发现有人用v8开发了一款用javascript写服务器软件的node.js,阅读它的源码是学习V8比较好的方式。研究中。。。
 另外还有IBM soliddb的研究…

2010-11-08
 和google v8磨合了一个星期,终于心里有点底了。上周几乎所有业余时间都花在阅读代码上:node.js和v8cgi,两者代码非常的相似。我有时会高估自己,之前根本没想到学习如何使用一个c++类库竟然要花上一星期的时间。学习心得这周补上来。

Comments (2)

·