http://blog.csdn.net/u011011917/article/details/17203539

传统的、教科书里的I/O复用等待函数select/poll在处理数以万计的客户端连接时,往往出现效率低下甚至完全瘫痪,,这被称为C10K 问题。

本文尝试着以一个最简单的单线程epoll程序为基础,轻松应对收发数据不频繁的过万客户端并发连接。并以此回顾C10K问题,介绍应对C10K问题的本质方法,线程模式的选择,介绍服务器编程流行的Reactor模式。  顺带介绍怎么应对socket句柄耗尽。

以下是一段使用Epoll的代码。用它可以轻松应对不繁忙的过万客户端连接。

为了让它可以工作,需要修改 sudovi /etc/security/limits.conf 增加如下行:

* soft nofile 65535

* hard nofile 65535

  1. int main(int argc,char** argv)
  2. {
  3. //Socket初始化
  4. socket(AF_INET,SOCK_STREAM,0);
  5. bind( listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))
  6. listen(listenfd,5);
  7. make_socket_non_blocking(listenfd)
  8. //Create epoll
  9. epollfd = epoll_create1(EPOLL_CLOEXEC);
  10. event.data.fd = listenfd;
  11. event.events = EPOLLIN | EPOLLET;
  12. val = epoll_ctl(epollfd,EPOLL_CTL_ADD,listenfd,&event
  13. //应对句柄耗尽问题,实现开无用一个句柄候着
  14. idleFd = open("/dev/null", O_RDONLY | O_CLOEXEC);
  15. for(;;)
  16. {
  17. //阻塞等待epoll事件
  18. int nfds = epoll_wait(epollfd,events,MAXEVENTS,-1);
  19. //有nfds个句柄待处理
  20. for(i = 0; i < nfds; i++)
  21. {
  22. if((events[i].events & EPOLLERR)
  23. ||(events[i].events & EPOLLHUP)
  24. ||(!(events[i].events & EPOLLIN))
  25. )
  26. {//出错处理
  27. close(events[i].data.fd);
  28. continue;
  29. }
  30. else if(listenfd == events[i].data.fd)
  31. {//accept,一定要accept 到errno ==EAGAIN 为止
  32. while(1)
  33. {
  34. infd = accept(events[i].data.fd,(struct sockaddr*)&in_addr,&in_len);
  35. if(infd < 0)
  36. {
  37. switch (errno)
  38. {
  39. case EAGAIN:
  40. case ECONNABORTED:
  41. case EINTR:
  42. case EPROTO:
  43. case EPERM:
  44. {
  45. //忽略
  46. }
  47. break;
  48. case EMFILE: // 句柄耗尽
  49. // 先关闭空句柄,再将本次连接的句柄accept进来关掉它,再打开空句柄候着。
  50. close(idleFd);
  51. idleFd= accept(events[i].data.fd,(struct sockaddr*)&in_addr,&in_len);
  52. close(idleFd);
  53. idleFd = open("/dev/null",O_RDONLY | O_CLOEXEC);
  54. break;
  55. default:
  56. //错误处理,有可能是accept期间对方断开了连接等。
  57. break;
  58. }
  59. }
  60. val = make_socket_non_blocking(infd);
  61. event.data.fd = infd;
  62. event.events = EPOLLIN|EPOLLET;
  63. val = epoll_ctl(epollfd,EPOLL_CTL_ADD,infd,&event);
  64. }//while(1)
  65. continue;
  66. }//Accept
  67. else if(events[i].events & EPOLLIN)
  68. {//Read data
  69. //读数据,一定要读到(errno == EAGAIN)为止
  70. }
  71. else if(events[i].events & EPOLLOUT)
  72. {//发数据,一定要发完,或者发到(errno == EAGAIN)为止                ;
  73. }
  74. }
  75. }
  76. }

这就是epoll的程序框架。很重要的一点是,它将编程的思维转变过来。由主动accept连接,主动调用send/receive改为当相应句柄数据准备好时,由操作系统通知你来处理事件,处理数据。这个就是Reactor模式的基本要点

网上有很多关于epoll的例子,因此我不打算深入展开解释。简单提一下ET模式和LT模式,

ET模式被称为高速模式,是当句柄可用时epoll只通知你一次,因此在代码里我们需要用循环抱住所有的事件处理,一直处理到数据为空为止。否则会收不到接下来的事件。在这个模式下,意味着如果你有大数据需要接收,或者大数据需要发送,不可避免地会对本线程的其它连接造成影响。它的好处是通过减少epoll相关的系统调用(epoll_wait,epoll_ctl)来增加效率。坏处是编程复杂,容易出错。

LT模式是epoll的缺省处理模式,只要句柄就绪了,epoll就会不停地通知你。一直到你处理完毕为止。用它编程简单,可以均衡线程中的各个连接处理。但是用它关注EPOLLOUT时会有点小麻烦。每当socket可写时,不管你有没有数据要发送,它都会不停地通知你。于是腾讯有这么一道面试题:epoll水平触发模式(Level-Triggered);当socket可写时,会不停的触发socket可写的事件,如何处理?解决办法是只有数据要发送时才关注EPOLLOUT,一旦数据发送完毕,立刻调用epoll_ctl停止观察。

ET模式和LT模式谁的效率更高则有待时间检验,目前缺乏相应的基准测试数据。

回头来再来看看C10K问题。没错,我们已经解决它了。C10K的原文在这里,英文不好的人可以看看翻译版的。C10K最大的特点是提升机器性能并不能相应提升程序的吞吐量

应对C10K主要有两方面的策略:

1) 应用程序以何种方式和操作系统合作,获取I/O事件并调度多个Socket上的I/O操作?

2) 应用程序以何种方式处理任务和线程/进程的关系?

我们先来看策略1:

传统的select/poll函数返回后需要对传入的句柄列表做一次扫描来确定引发事件的相应句柄,其单个任务消耗的资源和当前连接的关系是O(n),CPU占用率和并发数近似成O(n2)。

Epoll则用一个数组只返回就绪的句柄。单个任务和连接的关系是O(1)。在有大量空闲的情况下无疑效率要高出一大截来。

我们再来看策略2:

本质问题时你要create几个线程,每个线程用来干什么?废话不多说,我只谈我自己的理解:再多CPU多核的环境下,效率最高的线程数量是和CPU个数*核数相等。这样可以避开操作系统线程调度的开销。

简单的处理方式是,一个线程负责accept,其它线程负责send/receive。线程的总数固定。好处是编程简单,计算任务可以直接在send/receive线程中完成,当出现某个计算任务过大时不会把系统跑死。一般用这个方案就可以了。

复杂方案是按千兆比特每秒来配置send/receive线程,也就是一个千兆以太网卡一个线程,其它的放入线程池,用来计算用。

再复杂的线程配置方案则可以开一个专题来讲,半同步半异步模式、领导追随者模式等。

附上自己随手写的的单线程epoll程序,是上面代码的完整版。ET模式下收到数据后返回8K内容。

    1. #include <stdlib.h>
    2. #include <sys/epoll.h>
    3. #include <stdio.h>
    4. #include <sys/socket.h>
    5. #include <netinet/in.h>
    6. #include <fcntl.h>
    7. #include <unistd.h>
    8. #include <string.h>
    9. #include <errno.h>
    10. #include <string.h>
    11. #define MAXEVENTS 64
    12. #define PORT 2981
    13. int make_socket_non_blocking (int sfd)
    14. {
    15. int flags, s;
    16. flags = fcntl (sfd, F_GETFL, 0);
    17. if (flags == -1)
    18. {
    19. perror ("fcntl");
    20. return -1;
    21. }
    22. flags |= O_NONBLOCK;
    23. s = fcntl (sfd, F_SETFL, flags);
    24. if (s == -1)
    25. {
    26. perror ("fcntl");
    27. return -1;
    28. }
    29. return 0;
    30. }
    31. struct StruEventBuf
    32. {
    33. int fd;
    34. char *pszData;
    35. int nSize;
    36. int nStart;
    37. };
    38. int main(int argc,char** argv)
    39. {
    40. int listenfd = 0;
    41. struct sockaddr_in servaddr;
    42. int epollfd = 0;
    43. int val = 0;
    44. struct epoll_event event;
    45. struct epoll_event events[MAXEVENTS];
    46. int i = 0;
    47. int j = 0;
    48. int idleFd;
    49. int nPort = 0;
    50. int flag;
    51. int nSendBuf=10;    // 设置为32K
    52. socklen_t optlen;
    53. idleFd = open("/dev/null", O_RDONLY | O_CLOEXEC);
    54. //Create Socket and bind
    55. listenfd = socket(AF_INET,SOCK_STREAM,0);
    56. if( listenfd < 0)
    57. {
    58. printf("socket error\n");
    59. return 1;
    60. }
    61. bzero(events,sizeof(events));
    62. bzero(&servaddr,sizeof(servaddr));
    63. servaddr.sin_family = AF_INET;
    64. servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    65. servaddr.sin_port = htons(PORT);
    66. flag  = 1;
    67. optlen = sizeof(flag);
    68. setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&flag,optlen);
    69. if( bind( listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
    70. {
    71. printf("bind error:(%d) %s\n", errno, strerror(errno));
    72. }
    73. val = listen(listenfd,1024);
    74. //Listen
    75. if(-1 == val )
    76. {
    77. printf("listen error,errno = %d, %s\n",errno, strerror(errno));
    78. return 1;
    79. }
    80. //Set socket opention to no block
    81. val = fcntl(listenfd,F_GETFL,0);
    82. fcntl(listenfd,F_SETFL, val | O_NONBLOCK);
    83. val = fcntl(STDIN_FILENO,F_GETFL,0);
    84. fcntl(STDIN_FILENO,F_SETFL, val | O_NONBLOCK);
    85. val = fcntl(STDOUT_FILENO,F_GETFL,0);
    86. fcntl(STDOUT_FILENO,F_SETFL, val | O_NONBLOCK);
    87. //Create epoll
    88. epollfd = epoll_create1(EPOLL_CLOEXEC);
    89. printf("epollfd = %d\n",epollfd);
    90. struct StruEventBuf *pStruEventBufListen = (struct StruEventBuf*)malloc(sizeof(struct StruEventBuf));
    91. bzero(pStruEventBufListen,sizeof(struct StruEventBuf));
    92. pStruEventBufListen->fd = listenfd;
    93. event.data.ptr = pStruEventBufListen;
    94. event.events = EPOLLIN | EPOLLET;
    95. val = epoll_ctl(epollfd,EPOLL_CTL_ADD,listenfd,&event);
    96. if(-1 == val)
    97. {
    98. printf("epoll_ctl error\n");
    99. }
    100. for(;;)
    101. {
    102. int nfds = epoll_wait(epollfd,events,MAXEVENTS,-1);
    103. if(-1 == nfds)
    104. {
    105. printf("epoll_pwat error\n");
    106. return 1;
    107. }
    108. for(i = 0; i < nfds; i++)
    109. {
    110. struct StruEventBuf*pStruEventBuf = (struct StruEventBuf*) events[i].data.ptr;
    111. int fd = pStruEventBuf->fd;
    112. if((events[i].events & EPOLLERR)
    113. ||(events[i].events & EPOLLHUP)
    114. ||(!(events[i].events & (EPOLLIN | EPOLLOUT)))
    115. )
    116. {
    117. printf("epoll error fd = %d, events = %0x, errno = %d, %s\n"
    118. , fd,events[i].events,errno
    119. ,strerror(errno));
    120. close(fd);
    121. continue;
    122. }
    123. else if(listenfd == fd)
    124. {//accept
    125. while(1)
    126. {
    127. struct sockaddr_in in_addr;
    128. socklen_t in_len;
    129. int infd;
    130. #define NI_MAXHOST 100
    131. #define NI_MAXSERV 100
    132. char hbuf[NI_MAXHOST],sbuf[NI_MAXSERV];
    133. in_len = sizeof(in_addr);
    134. infd = accept(fd,(struct sockaddr*)&in_addr,&in_len);
    135. if(infd < 0)
    136. {
    137. switch (errno)
    138. {
    139. case EAGAIN:
    140. case ECONNABORTED:
    141. case EINTR:
    142. case EPROTO: // ???
    143. case EPERM:
    144. {
    145. //    printf("accept expected error %d,%s\n", errno,strerror(errno));
    146. }
    147. break;
    148. case EMFILE: // per-process lmit of open file desctiptor ???
    149. // expected errors
    150. close(idleFd);
    151. idleFd= accept(fd,(struct sockaddr*)&in_addr,&in_len);
    152. inet_ntop(AF_INET,&in_addr.sin_addr,hbuf,sizeof(hbuf));
    153. nPort = ntohs(in_addr.sin_port);
    154. printf("Max connection ,will close connection from %s, port %d errno = %d %s\n",hbuf,nPort,errno, strerror(errno));
    155. close(idleFd);
    156. idleFd = open("/dev/null",O_RDONLY | O_CLOEXEC);
    157. break;
    158. case EBADF:
    159. case EFAULT:
    160. case EINVAL:
    161. case ENFILE:
    162. case ENOBUFS:
    163. case ENOMEM:
    164. case ENOTSOCK:
    165. case EOPNOTSUPP:
    166. {
    167. printf("accept unexpected error %d,%s\n", errno,strerror(errno));
    168. }
    169. break;
    170. default:
    171. {
    172. printf("accept unkonw error %d,%s\n", errno,strerror(errno));
    173. } break;
    174. }
    175. if((EAGAIN == errno)
    176. ||(EWOULDBLOCK == errno))
    177. {
    178. //we have processed all incoming connections
    179. break;
    180. }
    181. else
    182. {
    183. printf("accept error %d,%s\n", errno,strerror(errno));
    184. break;
    185. }
    186. }
    187. inet_ntop(AF_INET,&in_addr.sin_addr,hbuf,sizeof(hbuf));
    188. nPort = ntohs(in_addr.sin_port);
    189. printf("connection from %s, port %d \n",hbuf,nPort);
    190. /*val = getnameinfo(&in_addr,in_len,hbuf, sizeof(hbuf),
    191. sbuf,sizeof(sbuf));
    192. if(0 == val)
    193. {
    194. printf("accepted connection on descriptor %d"
    195. "(host = %s, Port= %s)\n",infd,hbuf,sbuf);
    196. }
    197. */
    198. val = make_socket_non_blocking(infd);
    199. if(-1 == val)
    200. {
    201. printf("make socket_non_bolcking error\n");
    202. }
    203. printf("accepted, fd = %d\n",infd);
    204. nSendBuf = 10;
    205. setsockopt(infd,SOL_SOCKET,SO_SNDBUF,(const char*)&nSendBuf,sizeof(int));
    206. struct StruEventBuf *pStruEventBuf = (struct StruEventBuf*)malloc(sizeof(struct StruEventBuf));
    207. bzero(pStruEventBuf,sizeof(struct StruEventBuf));
    208. pStruEventBuf->fd = infd;
    209. //pStruEventBuf->pszData = (char*) malloc(8 * 1024 + 1);
    210. //pStruEventBuf->nSize = 8 * 1024 + 1;
    211. pStruEventBuf->nStart = 0;
    212. event.data.ptr = pStruEventBuf;
    213. event.events = EPOLLIN|EPOLLOUT|EPOLLET;
    214. val = epoll_ctl(epollfd,EPOLL_CTL_ADD,infd,&event);
    215. if(val == -1)
    216. {
    217. printf("epoll_ctrl error\n");
    218. }
    219. }//while(1)
    220. continue;
    221. }//Accept
    222. else if(events[i].events & EPOLLIN)
    223. {//Read data
    224. //We have data on the fd waiting to be read.
    225. //Read and display it. We must read whateveer data
    226. //is available and completly, as we are running in
    227. //edge-triggereed mode and won't get a notification
    228. //again for the same data
    229. int done = 0;
    230. struct StruEventBuf* pStruEventBuf = (struct StruEventBuf*) (events[i].data.ptr);
    231. while(1)
    232. {
    233. ssize_t count;
    234. char buf[512];
    235. int fd = pStruEventBuf->fd;
    236. count = read(fd, buf, sizeof(buf));
    237. if(-1 == count)
    238. {
    239. //if(errno == EAGAIN, that means we have read all data.
    240. //So goback to the main loop.
    241. if(errno != EAGAIN)
    242. {
    243. printf("read error\n");
    244. done = 1;
    245. }
    246. break;
    247. }
    248. else if(count == 0)
    249. {
    250. printf("Remote has close the socket\n");
    251. done = 1;
    252. break;
    253. }
    254. buf[count] = 0;
    255. printf("receive %s\n", buf);
    256. //send(fd,buf,count,0);
    257. pStruEventBuf->nSize = 8 * 1024 + 1;
    258. pStruEventBuf->pszData = (char*)malloc(pStruEventBuf->nSize);
    259. char *p = pStruEventBuf->pszData;
    260. for(i = 0; i < 8; i++)
    261. {
    262. for(j = 0; j < 1023; j ++)
    263. {
    264. *p++ = '0' + i;
    265. }
    266. *p++ = '\n';
    267. }
    268. //*p++ = '\0';
    269. if( p >= pStruEventBuf->pszData + pStruEventBuf->nSize )
    270. {
    271. printf("ERRORRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR!\n");
    272. }
    273. val = send(fd,pStruEventBuf->pszData, pStruEventBuf->nSize - pStruEventBuf->nStart,0);
    274. if(val < 0)
    275. {
    276. if((-1 == val) && (errno != EAGAIN))
    277. {
    278. printf("write error\n");
    279. done = 1;
    280. }
    281. }
    282. else
    283. {
    284. pStruEventBuf->nStart += val;
    285. }
    286. }
    287. if(done)
    288. {
    289. printf("closed connection on descriptor %d \n",
    290. fd);
    291. //Closing the descriptor will make epoll remove it from the
    292. //set of descriptiors which are monitored
    293. //struct StruEventBuf *p = (struct StruEventBuf*) events[i].data.ptr;
    294. free(pStruEventBuf);
    295. close(fd);
    296. }
    297. }
    298. else if(events[i].events & EPOLLOUT)
    299. {
    300. //struct StruEventBuf *pStruEventBuf= (struct StruEventBuf*) events[i].data.ptr;
    301. while( pStruEventBuf->nStart < pStruEventBuf->nSize)
    302. {
    303. int nLen = pStruEventBuf->nSize - pStruEventBuf->nStart;
    304. val = send(fd,pStruEventBuf->pszData + pStruEventBuf->nStart,nLen,0);
    305. if(val < nLen)
    306. {
    307. if(val < 0)
    308. {
    309. if((-1 == val) && (errno != EAGAIN))
    310. {
    311. printf("write error\n");
    312. ;//done = 1;
    313. }
    314. }
    315. else
    316. {
    317. pStruEventBuf->nStart += val;
    318. printf("Send data\n");
    319. }
    320. break;
    321. }
    322. else
    323. {
    324. char *p = pStruEventBuf->pszData;
    325. free(p);
    326. pStruEventBuf->pszData = NULL;
    327. pStruEventBuf->nSize = 0;
    328. pStruEventBuf->nStart= 0;
    329. }
    330. }
    331. }
    332. }
    333. }
    334. close(epollfd);
    335. return 0;
    336. }

轻松应对C10k问题的更多相关文章

  1. 轻松应对IDC机房带宽突然暴涨问题

    轻松应对IDC机房带宽突然暴涨问题! 1[提出问题] [实际案例一] 凌晨3:00点某公司(网站业务)的一个IDC机房带宽流量突然从平时高峰期150M猛增至1000M,如下图: 该故障的影响:直接导致 ...

  2. 云小课 | WAF反爬虫“三板斧”:轻松应对网站恶意爬虫

    描述:反爬虫是一个复杂的过程,针对爬虫常见的行为特征,WAF反爬虫三板斧——Robot检测(识别User-Agent).网站反爬虫(检查浏览器合法性)和CC攻击防护(限制访问频率)可以全方位帮您解决业 ...

  3. 轻松应对并发问题,Newbe.Claptrap 框架中 State 和 Event 应该如何理解?

    Newbe.Claptrap 框架中 State 和 Event 应该如何理解?最近整理了一下项目的术语表.今天就谈谈什么是 Event 和 State. Newbe.Claptrap 是一个用于轻松 ...

  4. 轻松应对并发,Newbe.Claptrap 框架入门,第四步 —— 利用 Minion,商品下单

    接上一篇 Newbe.Claptrap 框架入门,第三步 —— 定义 Claptrap,管理商品库存 ,我们继续要了解一下如何使用 Newbe.Claptrap 框架开发业务.通过本篇阅读,您便可以开 ...

  5. 虚拟节点轻松应对 LOL S11 百万并发流量——腾竞体育的弹性容器实践

    作者 刘如梦,腾竞体育研发工程师,擅长高并发.微服务治理.DevOps,主要负责电竞服务平台架构设计和基础设施建设. 詹雪娇,腾讯云弹性容器服务EKS产品经理,主要负责 EKS 虚拟节点.容器实例相关 ...

  6. 轻松应对并发问题,简易的火车票售票系统,Newbe.Claptrap 框架用例,第一步 —— 业务分析

    Newbe.Claptrap 框架非常适合于解决具有并发问题的业务系统.火车票售票系统,就是一个非常典型的场景用例. 本系列我们将逐步从业务.代码.测试和部署多方面来介绍,如何使用 Newbe.Cla ...

  7. SpreadJS + GcExcel 一出,谁与争锋!全栈表格技术轻松应对复杂公式计算场景(一)

    设计思路篇 Excel是我们日常办公中最常用的电子表格程序,不仅可满足报表数据的计算需求,还可提供绘图.数据透视分析.BI和Visual Basic for Applications (VBA)宏语言 ...

  8. Nginx高并发配置思路(轻松应对1万并发量)

    测试机器为腾讯云服务器1核1G内存,swap分区2G,停用除SSH外的所有服务,仅保留nginx,优化思路主要包括两个层面:系统层面+nginx层面. 一.系统层面 1.调整同时打开文件数量 ulim ...

  9. 云HBase发布全文索引服务,轻松应对复杂查询

    云HBase发布了“全文索引服务”功能,自2019年01月25日后创建的云HBase实例,可以在控制台免费开启此“全文索引服务”功能.使用此功能可以让用户在HBase之上构建功能更丰富的搜索业务,不再 ...

随机推荐

  1. [liu yanling]软件测试技巧

    1.添加.修改功能 (1)是否支持tab键 (2)是否支持enter键 (3)不符合要求的地方是否有错误提示 (4)保存后,是否也插入到数据库中 (5)字段唯一的,是否可以重复添加 (6)对编辑页列表 ...

  2. Jmeter初步使用二--使用jmeter做一个简单的性能测试

    经过上一次的初步使用,我们懂得了Jmeter的安装与初步使用的方法.现在,我们使用Jmeter做一个简单的性能测试.该次测试,提交的参数不做参数化处理,Jmeter各元件使用将在介绍在下一博文开始介绍 ...

  3. 【原】spark-submit提交应用程序的内部流程

    我们经常通过spark-submit来提交spark应用程序,那么让我们一起看一下这里面到底发生了什么吧. 知识点: 1.CLI命令行界面启动Spark应用程序 Unix有两种方式:1)spark-s ...

  4. JavaScript性能优化:度量、监控与可视化1

    HTTP事务所需要的步骤: 接下来,浏览器与远程Web服务器通过TCP三次握手协商来建立一个TCP/IP连接,类似对讲机的Over(完毕) Roger(明白) TCP/IP模型 TCP即传输控制协议( ...

  5. CentOS下恢复Firefox的复制等功能

    在CentOS下使用firefox编辑博客时,我发现无法使用复制粘帖功能,可用如下两种方法恢复(方法一已验证可行): 方法一: 找到user.js所在的目录,Linux下的user.js所在目录为Un ...

  6. android获取屏幕分辨率

    DisplayMetrics dm = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(dm); dm. ...

  7. CM5(Cloudera Manager 5) + CDH5(Cloudera's Distribution Including Apache Hadoop 5)的安装详细文档

    参考 :http://www.aboutyun.com/thread-9219-1-1.html Cloudera Manager5及CDH5在线(cloudera-manager-installer ...

  8. iOS网络编程(三) 异步加载及缓存图片---->SDWebImage

    @SDWebImage提供一个UIImageView的类别以支持加载来自网络的远程图片.具有缓存管理.异步下载.同一个URL下载次数控制和优化等特征. @SDWebImage的导入1.https:// ...

  9. JBPM学习(三):管理流程定义

    概念: ProcessDefinition,流程定义:一个流程的步骤说明,如一个请假流程.报销流程.是一个规则. ProcessDefinition,流程定义对象,是解析.jpdl.xml文件得到流程 ...

  10. Android FM模块学习之二 FM搜索频率流程

    上一篇大概分析了一下FM启动流程,若不了解Fm启动流程的,能够去打开前面的链接先了解FM启动流程,接下来我们简单分析一下FM的搜索频率流程. 在了解源代码之前.我们先看一下流程图: 事实上从图中能够看 ...