| 前 的个人资料miss your eye照片日志列表 | 帮助 |
miss your eye |
|||||||
|
|
9月4日 snow leopard升级后的一些问题* iphone sdk for snow leopard强力减肥,从2g+变为400M+,还真不能高兴的太早,虽说下载没那么痛苦了,可这个sdk里除了iphone相关的就没别的了,如果升级时没安装snow leopard带的xcode,/usr/bin下面可没有gcc给你用 *python升级到了2.6.1,就差那么点儿也不给2.6.2 *某些自动运行的app升级后不见了?也不一定,找到运行一下,会再次提示你这玩艺来自不安全的web⋯⋯ *到底用不用64位模式,这个问题经常在睡梦中不断的折磨着我 - 9月5日 *好吧,系统送的python2.6,自己是universal binary,所以也只能加载universal模块 *universal在snow leopard上大多时候是指32_64 fat,也就是x86 + x86_64组合,ppc32,64可以编译(?),但自己已然不支持 *所以自己编译的python模块也得是universal *有时这比较难办,比如boost,单独编译i386或x64都没问题,universal就败了,等更新吧 *macports正在努力升级自己的python26成为universal,现在upgrade会失败,幸好,还是留着10.5的32bit python26吧,脚本语言搞的这么平台依赖怎么行 *非c语言的module很安全 *我做的那个亲爱的python javascript wrapper module for python 3.1算是毁了,没人爱用python3,我自己都回来找2.6了 8月13日 小强有多强早上起来,打开冰箱拿酸奶,听见烤箱里希希邃邃的响,疑似小强潜入,抬头一看,果不其然,有只小强正在里面寻路。顺手拧了一下开关,就洗漱去了,出来一看烤箱里面通红一片,心中不禁一阵狰狞,于是关了开关前去验尸,结果一拉开门,没闻到熟悉的蛋白质焦臭,疑惑,正待低头细看,忽见一只小强飞速从烤箱中逃窜出来,震惊,等到反应过来,小强已经不见踪影…… 7月14日 大爱~ 勇者斗恶龙9红黑树c++代码几乎被这玩意弄成脑死亡。
注意:最好把class T换成typename T,自己实现特化Compare时需要单加一个.h,我对模板一知半解,如果特化Compare放在T类型定义的.h里,编译就出错,error redifinition
enum
{ RB_Red, RB_Black, }; template <class T>
class RBTreeNode { public: RBTreeNode* _link[2]; T* _data; RBTreeNode* _parent; unsigned char _color; }; template <typename T>
int Compare(T* t1, T* t2) { return t1 == t2 ? 0 : (t1 < t2 ? -1 : 1); }; template <class T>
class RBTree { public: RBTreeNode<T>* search(T* it); RBTreeNode<T>* insert(T* it); RBTreeNode<T>* probe(T* item); T* remove(T* item); RBTreeNode<T>* rebalance(RBTreeNode<T>* n); private: RBTreeNode<T>* _root; int _count; int _gen; }; template<class T> inline RBTreeNode<T> *RBTree<T>::search(T* it)
{ int d = 0; RBTreeNode<T> *p = NULL, *n = NULL, *q = NULL; // search
for (q = NULL, p = _root; p != NULL; q = p, p = p->_link[d]) { int cmp = Compare<T>(it, p->_data); if(cmp == 0) return p; d = cmp > 0; } return NULL;
} template<class T> inline RBTreeNode<T> *RBTree<T>::probe(T* it)
{ int d = 0; RBTreeNode<T> *p = NULL, *n = NULL, *q = NULL; // search
for (q = NULL, p = _root; p != NULL; q = p, p = p->_link[d]) { int cmp = Compare<T>(it, p->_data); if(cmp == 0) return p; d = cmp > 0; } // insert n = new RBTreeNode<T>; if(!n) return NULL; _count++;
n->_link[0] = n->_link[1] = NULL; n->_parent = q; n->_data = it; if(q != NULL)
{ q->_link[d] = n; } else _root = n; n->_color = RB_Red;
// rebalance
q = n; while(true) { RBTreeNode<T> *f, *g; if(!q->_parent) break; f = q->_parent;
if(!f->_parent) break; g = f->_parent;
if(g->_link[0] == f)
{ // rebalance left RBTreeNode<T>* h = g->_link[1]; if(h != NULL && h->_color == RB_Red) { // q has a red uncle h->_color = f->_color = RB_Black; g->_color = RB_Red; q = g; } else { RBTreeNode<T>* i = g->_parent; if(i == NULL) i = _root; if(f->_link[1] == q) { // q is right son of f // exchange q & f, let f become q's left son, then f->_link[1] = q->_link[0]; q->_link[0] = f; g->_link[0] = q; f->_parent = q; if(f->_link[1] != NULL) f->_link[1]->_parent = f; f = q; } // q is left son of f
// rotate right at g g->_color = RB_Red; f->_color = RB_Black; g->_link[0] = f->_link[1];
f->_link[1] = g; i->_link[i->_link[1] == g] = f; f->_parent = g->_parent;
g->_parent = f; if(g->_link[0] != NULL) g->_link[0]->_parent = g; break;
} } else { // right RBTreeNode<T>* h = g->_link[0]; if(h != NULL && h->_color = RB_Red) { // case 1 f->_color = h->_color = RB_Black; g->_color = RB_Red; q = g; } else { RBTreeNode<T>* i = g->_parent; if(i == NULL) i = _root; if(f->_link[0] == q)
{ // case 3, transform to case 2 f->_link[0] = q->_link[1]; q->_link[1] = f; g->_link[1] = q; f->_parent = q; if(f->_link[0] != NULL) f->_link[0]->_parent = f; f = q; } // case 2
g->_color = RB_Red; f->_color = RB_Black; g->_link[1] = f->_link[0];
f->_link[0] = g; i->_link[i->_link[0] == g] = f; f->_parent = g->_parent;
g->_parent = f; if(g->_link[1] != NULL) g->_link[1]->_parent = g; break;
} } } _root->_color = RB_Black;
return n;
} // Holy~ brain killer ...
template<class T> inline T *RBTree<T>::remove(T *item)
{ RBTreeNode<T> *p, *q, *f; T* it = NULL; int d; // find target
if(!_root) return NULL; for (q = NULL, p = _root; p != NULL; q = p, p = p->_link[d])
{ int cmp = Compare<T>(it, p->_data); if(cmp == 0) break; d = cmp > 0; } if(!p)
return NULL; it = p->_data;
q = p->_parent; if(q == NULL) { q = (RBTreeNode<T>*)&_root; // c pointer trick: q->_link[0] = root, _link[1] is _count d = 0; } // delete target
if(p->_link[1] == NULL) { // no right son. q->_link[d] = p->_link[0]; if(q->_link[d] != NULL) q->_link[d]->_parent = p->_parent; f = q; } else { unsigned char c; RBTreeNode<T>* r = p->_link[1]; if(r->_link[0] == NULL)
{ // right son doesn't have a left son r->_link[0] = p->_link[0]; q->_link[d] = r; r->_parent = p->_parent; if(r->_link[0] != NULL) r->_link[0]->_parent = r; c = p->_color; p->_color = r->_color; r->_color = c; f = r;
d = 1; } else { // right son has a left son ... RBTreeNode<T>* s = r->_link[0]; while(s->_link[0] != NULL) { s = s->_link[0]; // find the successor } r = s->_parent; r->_link[0] = s->_link[1]; s->_link[0] = p->_link[0]; s->_link[1] = p->_link[1]; q->_link[d] = s; if(s->_link[0] != NULL) { s->_link[0]->_parent = s; } s->_link[1]->_parent = s; s->_parent = p->_parent; if(r->_link[0] != NULL)
r->_link[0]->_parent = r; c = p->_color;
p->_color = s->_color; s->_color = c; f = r;
d = 0; } } // rebalance
if(p->_color = RB_Black) { while(true) { RBTreeNode<T> *x, *g, *y; x = f->_link[d];
if(x != NULL && x->_color == RB_Red) { x->_color = RB_Black; break; } if(f == (RBTreeNode<T>*) &_root)
break; g = f->_parent;
if(g == NULL) g = (RBTreeNode<T>*) &_root; if(d == 0)
{ // left side rebalance RBTreeNode<T>* w = f->_link[1]; if(w->_color == RB_Red) { // w is black w->_color = RB_Black;
f->_color = RB_Red; f->_link[1] = w->_link[0];
w->_link[0] = f; g->_link[g->_link[0] != f] = w; w->_parent = f->_parent;
f->_parent = w; g = w;
w->_parent = f; } if( (w->_link[0] == NULL || w->_link[0]->_color == RB_Black) && (w->_link[1] == NULL || w->_link[1]->_color == RB_Black)) { // left side rebalance w->_color = RB_Red; } else { if(w->_link[1] == NULL || w->_link[1]->_color == RB_Black) { // transform left-side rebalancing RBTreeNode<T>* y = w->_link[0]; y->_color = RB_Black; w->_color = RB_Red; w->_link[0] = y->_link[1]; y->_link[1] = w; if(w->_link[0] != NULL)
w->_link[0]->_parent = w; w = f->_link[1] = y; w->_link[1]->_parent = w; } w->_color = f->_color; f->_color = RB_Black; w->_link[1]->_color = RB_Black; f->_link[1] = w->_link[0];
w->_link[0] = f; g->_link[g->_link[0] != f] = w; w->_parent = f->_parent;
f->_parent = w; if(f->_link[1] != NULL) f->_link[1]->_parent = f; break; } } else { // right side rebalance RBTreeNode<T>* w = f->_link[0]; if(w->_color == RB_Red) { w->_color = RB_Black; f->_color = RB_Red; f->_link[0] = w->_link[1];
w->_link[1] = f; g->_link[g->_link[0] != f] = w; w->_parent = f->_parent;
f->_parent = w; g = w;
w = f->_link[0]; w->_parent = f;
} if( (w->_link[0] == NULL || w->_link[0]->_color = RB_Black)
&& (w->_link[1] == NULL || w->_link[1]->_color == RB_Black)) { w->_color = RB_Red; } else { if(w->_link[0] == NULL || w->_link[0]->_color = RB_Black ) { RBTreeNode<T>* y = w->_link[1]; y->_color = RB_Black; w->_color = RB_Red; w->_link[1] = y->_link[0]; y->_link[0] = w; if(w->_link[1] != NULL) w->_link[1]->_parent = w; w = f->_link[0] = y; w->_link[0]->_parent = w; } w->_color = f->_color;
f->_color = RB_Black; w->_link[0]->_color = RB_Black; f->_link[0] = w->_link[1];
w->_link[1] = f; g->_link[g->_link[0] != f] = w; w->_parent = f->_parent;
f->_parent = w; if(f->_link[0] != NULL) f->_link[0]->_parent = f; break; } } y = f;
f = f->_parent; if(f == NULL) f = (RBTreeNode<T>*) &_root; d = f->_link[0] != y; } } //
return p ? p->_data : NULL; } 5月11日 用double CAS实现lock-free队列参考 Optimised Lock-Free FIFO Queue 实现了一个lock-free的fifo。在ia32上,可以用cmpxchg8b来模拟double cas,只是操作数中,2个需要更新的值必须是连续的内存地址。 定义宏如下: #define CAS2(ret, mem, old1, old2, new1, new2) \ do {\ asm volatile( \ ".align 4 \n" \ "movl $0, %0 \n" \ "LOCK cmpxchg8b (%%esi)\n" \ "setz %0 \n" \ : "=m" (ret) \ : "S" (mem), "a" (old1), "d" (old2), "b" (new1), "c" (new2) \ : "memory", "cc" \ ); \ } while(0); #define DEAD_NODE(q) \ &((q)->dead) #define CAS(ret, mem, old, new) \ do {\ asm volatile( \ ".align 4 \n" \ "movl $0, %0 \n" \ "LOCK cmpxchgl %%edx, (%%esi) \n" \ "setz %0 \n" \ : "=m" (ret) \ : "d" (new), "S" (mem), "a" (old) \ : "memory", "cc" \ ); \ } while(0); struct { node* head; unsigned long tag1; node* tail; unsigned long tag2; node dead; } queue; struct { node* next; void* data; } node; --------------------------------------------------------------------------------------------------------- init void init_queue(queue* q) { memset(q, 0, sizeof(queue)); q->head = q->tail = &q->dead; } --------------------------------------------------------------------------------------------------------- pop node* pop(queue* q) { node* n = NULL; while(1) { int ret; unsigned long tag1 = q->tag1; node* head = q->head; unsigned long tag2 = q->tag2; node* next = head->next; if ( tag1 == q->tag1 ) { if(head == q->tail) { if(next == NULL) return NULL; CAS2(ret, &q->tail, head, tag2, next, tag2 + 1); } else if(next != NULL) { CAS2(ret, &q->head, head, tag1, next, tag1 + 1); if(ret) { n = head; break; } } } } if(n == DEAD_NODE(q)) { push(q, n); n = pop(q); } return n; } --------------------------------------------------------------------------------------------------------- push void push(queue* q, node* n) { n->next = NULL; while(1) { int ret = 0; unsigned long tag2 = q->tag2; node* tail = q->tail; CAS(ret, &tail->next, NULL, n); if(ret) break; CAS2(ret, &q->tail, tail, tag2, tail->next, tag2 + 1); } CAS2(ret, &q->tail, tail, tag2, n, tag2 + 1); } 3月25日 爱表现的阿姨新来了一个阿姨,总是在正干活的时候过来做清扫,抹桌子,拖地,换垃圾袋,一定要让你看在眼里不说,还一定要让你亲身感受到,这不,正敲键盘写代码,一只手抓着一块抹布过来,在桌子上围着你平放的双臂画出一幅拓片,于是你不好意思的收回双手,停下工作,然后抹布就很顺畅的在你的键盘和屏幕上游走一遍,让人哭笑不得。 这还不算完,好容易坐定,一条拖把又悄然无息的伸到脚下,轻轻而又坚决的触碰着你的鞋子,你一定要满含歉意的收回双脚,打断思路,退后三尺,在阿姨豁达宽容的话语中(没事,不用动,你忙你的),频频点头称是。 早上临近11点,迟到的我终于赶到公司,阿姨已经打扫完毕,心中窃喜,赶忙坐下,拿出电脑,顺手摸把桌子……一手黑灰,我操。 2月24日 笔记本出院了 送修一周,拿回来了,换了左侧风扇,不再满腹委屈的怪叫,也不发烧抗议了。 于是难得有了2三天没电脑用的日子,本以为很难熬,却也很平静的度过了。 顺便给满是胡须泥垢的厨房地板擦净了脸,为堪比越冬蔬菜大棚的冷藏室降了温,这个容我藏身5年多的小地方,默默忍受着我对生活毫无关爱的所作所为,宽容的就像那些善待我的亲人朋友们一样。 最后要躺下检讨检讨,我还是热爱生命的,总想象着周围的一切都是活着具有灵性的,一会我闭眼了要憧憬明天能打出个塔格奥腰带或是乔丹之石,不想背可怜的笔记本上班了地铁那安检公司的小p孩们每次都不放过牠,还有老猫那个会倒翻跟头的青蛙很帅,我也想练那招…… 12月9日 neocd emu for iphone11月3日 pcre正则表达式库的使用对
于约定格式的字符串处理,假如需要对应的格式较多,而且随着需求变化成线性增长,那么最好从一开始就使用正则表达式,否则会演变成自虐行为。在
c/c++里可使用的库并不多,posix正则库在*nix上基本已经算是内建了,但是在mac上看man
page,提及仍然是alpha阶段,不但性能不高,而且有一些bug,似乎从2004年开始就没有什么改进,而linux上没有这样的警告,不过在我草
草看了一遍pcre(perl compatible regular expressions)的man
page之后,还是决定麻烦一下自己,给工程增加一个第三方依赖。 pcre吸引我的首要的好处就是支持utf8,我在工程里一直偷懒回避字符集问题,对于所有的字符串都视为utf8,工程所用到的xml,数据库字符集也 都设置为utf8,偷懒的结果就是许多分析串的代码都成了心病,内容无关的场合按字节处理也许不是问题,内容相关的场合,按字符处理就大大的麻烦,用支持 utf8的正则库分解字串正好可以消除一些隐患。 pcre的安装很简单,发行版linux上在线安装就可以,在fedora 8上, # yum install pcre pcre-devel 即可。 mac上我没用port,直接下载的源代码编译安装了。configure时注意utf8支持默认是关闭的。 最基本的正则匹配过程分为两步,模式分析和匹配应用。 compile用于模式分析,exec用于匹配串。 举例:现有类似$MOVE#username#x,y#msg$的消息,其中x和y是整数,msg包含任意字符,那么模式为(^\$MOVE)#(.*)#([0-9]*),([0-9]*)#(.*)\$$,差不多了,来段代码看看 pcre *re; const char *error; char *pattern; char *subject; unsigned char *name_table; int erroffset; int find_all; int namecount; int name_entry_size; int ovector[30]; int subject_length; int rc, i; pattern = (char*) "(^\\$MOVE)#(.*)#([0-9]*),([0-9]*)#(.*)\\$$"; subject = (char*) "$MOVE#robot#100,100#haha\ntest$"; subject_length = (int) strlen(subject); re = pcre_compile(pattern, PCRE_UTF8, &error, &erroffset, NULL); if (re == NULL) { ... } rc = pcre_exec(re, NULL, subject, subject_length, 0, 0, ovector, 30); if (rc < PCRE_ERROR_NOMATCH) { printf("No match\n"); } /* Show substrings stored in the output vector by number. Obviously, in a real application you might want to do things other than print them. */ for (i = 0; rc == 0 && i < 10; i++) { char *substring_start = subject + ovector[2 * i]; int substring_length = ovector[2 * i + 1] - ovector[2 * i]; printf("%2d: %.*s\n", i, substring_length, substring_start); } 运行结果: No Match 哈哈,我不是逗你玩,生活需要调剂嘛。目标串含有一个换行符是个问题,默认来说匹配是执行到行尾,有两个选择,一个是给pcre_compile的第二个 参数合并上PCRE_DOTALL,另一个是在模式前面加上选项:'(?s)(^\$MOVE)#(.*)#(\d+),(\d+)#(.*)\$$' (?s)就是强制匹配到字串结尾。一般默认是多行匹配(?m),行分隔符在不同的系统上有着倔强的不同,windows是CRLF,苹果是CR,linux是LF,根据不同的系统,只好分别设置选项。 (*CR)对应PCRE_NEWLINE_CR (*CRLF) - PCRE_NEWLINE_CRLF (*LF) - PCRE_NEWLINE_LF (*ANYCRLF) - PCRE_NEWLINE_ANYCRLF 或者(*ANY) - PCRE_NEWLINE_ANY 我个人还是倾向于把选项写到模式中,我是把模式都放在xml中的,这样自然比写死在代码里来得灵活。要注意(*)这样的选项需要出现在模式的最前面,而(?ms)随便放了,这样都行:'(^\$MOVE)#(.*)#(\d+),(\d+)#(?m)(.*)\$$' 我把[0-9]*换成\d+了,意思基本相同,不同的在于*和+,前者允许零匹配,后者至少得有一个。 加了(?s)了,再试…… 0: $MOVE#robot#100,100#haha test$ 1: $MOVE 2: robot 3: 100 4: 100 5: haha test 好了 ovector中是每个字串的起始位置和结束位置,最前面那一对是匹配到的全长,假如输入串完全满足模式,那么第一对就是输入串的开始与结尾了。 试试吧,程序员都会爱上正则表达式这玩意的。 11月1日 c/c++结合lua制造NPC lua是大虾推荐的脚本引擎,高效小巧,很容易和c语言交互。 我的工程起初的设计比较粗陋,并没有考虑太多扩展性,在面对增加task子系统的需求时,对于到底是保证时间还是更改结构颇为犹豫,最后还是狠下心决定编写一个扩展task子系统,代价就是花了3个通宵。 当时只是粗略的看了一下lua,边翻手册边google连抄带试的做,很多lua特性,比如关于coroutine(lua里一个有趣的伪并行机制),以及结合os thread的方式,完全没能理解,于是采取了一个低效但安全的方式回避了问题。 由于几乎全部的用户数据、状态都在server里维护,我将通信模块处理生成的事件中的一部分再次转译为一组消息派发给另外一套处理系统,这套处理系统可接受消息处理接口的注册请求,将这些接口实例组织成一个响应链,传递并处理消息,而可选的,这些实例可以实现为一个队列,转为异步处理消息。 task子系统实现了同步消息处理接口,每个task编写一个对应的lua脚本,其中除了task的各种定义数据之外,就是一组消息响应函数,根据需要实现。c则导出一些函数供lua调用,用来访问用户的状态和数据。 c通过lua_State指针与lua交互,lua_State以下简称lua状态,不考虑其实现细节,可以看作是一个栈,lua里一切都是对栈上的元素进行操作,而lua本身为了尽量减少系统依赖,并没有增加同步机制,所以多线程环境里,只能外部对lua状态加锁,或者给每一个线程创建一个单独的lua状态,不过虽然消息处理系统的线程数量固定,但线程池本身与task系统没什么关系,考虑到目前task脚本都很小,数量短期内也不会增加太多,而玩家是否拥有一个task,是在进行响应之前就判断过的,加上task中的响应函数本身是无状态的,并不参与维护任何用户数据和状态,所以我最后对于每个task消息请求,都加载了一次脚本。每次调用的时间开销小于1ms,可以接受。 实现NPC系统就不能这么偷懒了,我希望npc能够自己维护自己的状态,不需要c来记录,这样npc脚本就必须保持运行。c加载运行lua脚本之后,如果脚本保持运行状态,c线程将被阻塞,为每一个npc创建一个线程代价就太大了点儿。没办法,只能啃coroutine了,不过这次就犯不着玩通宵了。 coroutine是一种伪并行机制,它以一个函数为参数建立一个具有一般线程特性的lua状态,拥有自己的数据和栈,同时又共享全局数据,通过yield暂停,resume运行,在一个脚本里可以创建多个coroutine,不过并不存在真正的同时运行,仍然需要自己编写切换线程的逻辑。那么假如在c中可以控制coroutine,使得脚本在yield之后返回c线程,再从c线程调用resume恢复coroutine,如此反复,就可以虚拟出帧循环时间片,逐帧响应的npc就能够以投骰子的方式决定自己的行为。 非常幸运,lua具有这样的功能。 /* C code - start */ lua_getglobal(lua, "coroutine"); lua_pushstring(lua, "create"); lua_gettable(lua, -2); lua_getglobal(lua, "thread_body"); lua_call(lua, 1, 1); lua_State* co = lua_tothread(lua, -1); lua_setglobal(lua, "co"); ...... for(;TRUE;) { while(npc) { lua_resume(npc->co, 0); npc = npc->next; } // wait for next frame } /* C code - end */ -------------------------- -- lua code - start -------------------------- function on_frame() print ("Moving or Talking or Attacking ... ") end function thread_body() while 1 do on_frame() coroutine.yield() end end -------------------------- -- lua code - end -------------------------- 这样一个c线程就可以轮询数个npc了。 从lua_tothread这个函数就可以知道co这个状态是被lua定义为thread类型的,而创建一个thread不是只有coroutine.create能做,C API lua_newthread也可以,睡觉了,下回继续说吧。 8月15日 有些清净的日子% 回北京后发觉这城市难得的清静,机场出口一路都没什么人,小小的意外。
% 天气有点闷,人也懒散,早上怎么都睡不醒,自然勤也就没怎么出。
% 不关心奥运,可内心里仍然热爱比赛们,电视开了就关不了,有点烦人。
% 活儿算是干完一期了,貌似这些天server们转的挺好,signal 11之流没来搅扰我看球的心情。
% 开始构思和整理filesystem的spec。
% 头疼,总头疼,我恨各种空调。
% 热烈庆祝cod终于回归了兄弟3人小组,不过我迷上了用rpg火箭筒一炮打出airstrike的爽快,彻底步入旁门左道。
% 中午看完谢大美女和张宁姐姐的比赛才恋恋不舍的出来上班,意外赶上寿星缺勤的生日会,大喜,狂吃8块西瓜。
% 想买10寸小笔电,编译一次代码花30分钟,期间可以吃喝拉撒。
% 屋里蟑螂社会飞速发展,完成了从种群到部落的转变,已经上我床了。
% animorphs小说还不错,简单e文,我一目3行。
% 我很臭,1个月没洗澡了,哇哈哈哈…… 7月28日 歪招需要从server检查client超时,另开一个线程定时轮询所有client从实现上讲是简单的,不过心理上是有无法逾越的障碍的,我怎么就那么烦轮询,懒人就应该等通知而不是反复刷。咨询了几位大牛,给我的建议也是设置队列,无奈只好奋斗了一下,又把client结构纵横交错了一条队列出来,外加一个邪恶的监视线程,算是完事了。 不过心里总觉得疙瘩,躺在床上辗转不能安眠,我不要轮询我不要轮询…… 突然就灵光一现,假如我在client活动的时候更新一个定时器,如果定时器触发就认为是超时了岂不就成了事件驱动了么?不过client不能太多,不然就要命了。
|
||||||
|
|