码迷,mamicode.com
首页 > 其他好文 > 详细

libevent学习六

时间:2015-01-29 21:12:13      阅读:231      评论:0      收藏:0      [点我收藏+]

标签:libevent   c   

events

Libevent的基本操作单元是event。每个event都意味着一个条件集合,包括:

1. 一个准备好读或写的文件描述符。

2. 一个“将”准备好读或写的文件描述符(边缘模式下)

3. 一个超时事件

4. 一个信号事件

5. 一个人为触发的事件

Event有相似的生命周期。一旦你通过Libevent方法去构造了一个event,并把它与event_base相关联,那么这个event就变成“被初始化”。然后你可以把它“添加”到event_base,把它变成“准备好的(pending:悬而未决的)”。当一个event是“准备好的”时,如果此时某个条件发生(例:它的文件描述符状态发生变化或它的时间过期),这个event将会变成“激活的”,然后它的回调(用户提供)将会运行。如果这个event被配置成“持续的(persistent)”,它会保留“准备好的”状态。如果没有配置成“持续的”,当它的回调结束后,它也将变成“未准备好的”。你可以通过从event_base中“删除”某个event,使它成为“未准备好的”,同理你也可以通过向event_base中添加一个“未准备好的”event,使它变成“准备好的”状态。


构造event对象

可以通过event_new()接口去构建一个新的event对象。

接口

#define EV_TIMEOUT      0x01
#define EV_READ         0x02
#define EV_WRITE        0x04
#define EV_SIGNAL       0x08
#define EV_PERSIST      0x10
#define EV_ET           0x20

typedef void (*event_callback_fn)(evutil_socket_t, short, void *);

struct event *event_new(struct event_base *base, evutil_socket_t fd,
    short what, event_callback_fn cb,
    void *arg);

void event_free(struct event *event);
event_new()方法分配和构造一个供event_base使用的新的event对象。what参数是一系列flag的集合。如果fd是个非负数,它表示的是一个将用来关注读和写事件的文件描述符。当event是激活的状态,Libevent会调用cb提供的方法,并且以fd这个文件描述符、所激活的事件标志和arg做为参数。

当发生内部错误,或无效的参数,event_new()会返回NULL。

所有新的event对象都是初始化的和未准备好的。如果想要使event从“未准备好的”状态变成“准备好的”状态,可以调用event_add()方法。

event_free()可以释放一个event对象。调用event_free()去释放一个“准备好的”或“激活的”的event是安全的:这样做会使event变成“未准备好的”和“未激活的”.
例:

#include <event2/event.h>

void cb_func(evutil_socket_t fd, short what, void *arg)
{
        const char *data = arg;
        printf("Got an event on socket %d:%s%s%s%s [%s]",
            (int) fd,
            (what&EV_TIMEOUT) ? " timeout" : "",
            (what&EV_READ)    ? " read" : "",
            (what&EV_WRITE)   ? " write" : "",
            (what&EV_SIGNAL)  ? " signal" : "",
            data);
}

void main_loop(evutil_socket_t fd1, evutil_socket_t fd2)
{
        struct event *ev1, *ev2;
        struct timeval five_seconds = {5,0};
        struct event_base *base = event_base_new();

        /* The caller has already set up fd1, fd2 somehow, and make them
           nonblocking. */

        ev1 = event_new(base, fd1, EV_TIMEOUT|EV_READ|EV_PERSIST, cb_func,
           (char*)"Reading event");
        ev2 = event_new(base, fd2, EV_WRITE|EV_PERSIST, cb_func,
           (char*)"Writing event");

        event_add(ev1, &five_seconds);
        event_add(ev2, NULL);
        event_base_dispatch(base);
}
event标志(flag)

1. EV_TIMEOUT:表示在一段时间到期后,激活这个event。

EV_TIMEOUT在构造一个event对象时,此标志是被忽略的:你可以在“添加”event时区设置一个超时时间,也可以不设置。把它设置在“what”参数是为了告诉event_base当发生一个超时时,调用cb这个回调。

2. EV_READ:表示当文件描述符变为可读时,激活这个event。

3. EV_WRITE:表示当文件描述符变为可写时,激活这个event。

4. EV_SIGNAL:用来检测信号事件。

5. EV_PERSIST:表示event是持续的。

6. EV_ET:表示event是边缘模式。(只有在支持边缘模式的系统中可以使用)它将会改变EV_READ和EV_WRITE的语义。

持续的event
默认情况下,一个被“激活的”event,在它的回调被执行后,它将会变成为“未准备好的”。因此,如果你想让一个event再次变成“准备好的”,可以通过在回调方法中调用event_add()去实现。

如果EV_PERSIST标志被设置,这个event就变成了“持续的”。这意味着即使它的回调被调用,它也还是“准备好的”状态。如果你想在回调中使这个event变成“未准备好的”,可以调用event_del()方法去删除它。

如果一个超时应用在一个“持续的”event上,每次回调后都会重置event状态。因此,如果你的event被设置为EV_READ|EV_PERSIST和5秒的超时,event将会被激活,当发生:

1. 当一个socket准备好读。

2. 从上次被激活计时,达到5秒钟。

把event自己作为回调的参数

很多时候,你可能想创建一个以接收它本身作为回调函数参数的event。你不能只传一个指向event对象的指针,因为调用event_new()时这个event还不存在。为了解决这个问题你可以使用event_self_cbarg()。

接口

void *event_self_cbarg();
这个方法会返回一个“魔术”指针。用于作为event的回调参数,并告诉event_new()去创建一个event接收它回调的参数。
#include <event2/event.h>

static int n_calls = 0;

void cb_func(evutil_socket_t fd, short what, void *arg)
{
    struct event *me = arg;

    printf("cb_func called %d times so far.\n", ++n_calls);

    if (n_calls > 100)
       event_del(me);
}

void run(struct event_base *base)
{
    struct timeval one_sec = { 1, 0 };
    struct event *ev;
    /* We're going to set up a repeating timer to get called called 100
       times. */
    ev = event_new(base, -1, EV_PERSIST, cb_func, event_self_cbarg());
    event_add(ev, &one_sec);
    event_base_dispatch(base);
}
这个方法也可以用于event_new(),evtimer_new(),evsignal_new(),evtimer_assign(),和evsignal_assign()。然而如果用在非event的回调中,它不会起作用。

纯超时event(只存在超时)

为了方便使用,Libevent提供了一些宏,以提供对纯超时event的分配和管理。它们以evtimer_开头。使用这些宏只会提高代码的清晰度,除此之外,没有其他性能上的好处。

接口

#define evtimer_new(base, callback, arg)     event_new((base), -1, 0, (callback), (arg))
#define evtimer_add(ev, tv)     event_add((ev),(tv))
#define evtimer_del(ev)     event_del(ev)
#define evtimer_pending(ev, tv_out)     event_pending((ev), EV_TIMEOUT, (tv_out))
构建信号event

Libevent也提供对POSIX信号的支持。构造一个信号handler:

接口

#define evsignal_new(base, signum, cb, arg)     event_new(base, signum, EV_SIGNAL|EV_PERSIST, cb, arg)
大部分参数类似event_new,除了用信号值替代文件描述符。


struct event *hup_event;
struct event_base *base = event_base_new();

/* call sighup_function on a HUP signal */
hup_event = evsignal_new(base, SIGHUP, sighup_function, NULL);
注意:信号的回调是在信号发生之后,运行在event循环里面的,所以在常规的信号处理函数中不能调用的函数,都可以在回调中使用。

警告:不要给一个信号设置一个超时。现在还不支持。
使用信号时,可以使用下面提供的一些宏。

接口

#define evsignal_add(ev, tv)     event_add((ev),(tv))
#define evsignal_del(ev)     event_del(ev)
#define evsignal_pending(ev, what, tv_out)     event_pending((ev), (what), (tv_out))
信号相关的注意事项
在绝大多数的系统中,在某个时刻,同一进程只有一个event_base能够监听信号。如果你添加一个信号到两个event_base,及时添加的信号是不同的!也只会有一个event_base会收到信号。

kqueue后端不会有这个限制。

构造一个非堆内存的event对象

为了性能或其他的原因,一些人喜欢把events作为一个大的结构体的一部分来使用。这会节省:

1. 在堆上分配一个对象的内存花销

2. 指针的解引用的时间花销

3. 如果event不在缓存中,会产生额外的缓存未命中的时间花销

如果不同的版本的event结构体大小不同,使用这个方法会存在破坏版本间二进制兼容性的风险。

这些小的性能消耗,对大部分程序来说都不是问题。所以你应该坚持使用event_new()去构建event对象,除非你确认使用堆分配会给你带来明显的性能损失如果以后的Libevent使用了一个比当前event大的结构,event_assign()会引发难以判定的错误

接口

int event_assign(struct event *event, struct event_base *base,
    evutil_socket_t fd, short what,
    void (*callback)(evutil_socket_t, short, void *), void *arg);
所有的参数都与event_new()相同。除了event参数,它必须指向一个未初始化的event。成功返回0,失败返回-1。


#include <event2/event.h>
/* Watch out!  Including event_struct.h means that your code will not
 * be binary-compatible with future versions of Libevent. */
#include <event2/event_struct.h>
#include <stdlib.h>

struct event_pair {
         evutil_socket_t fd;
         struct event read_event;
         struct event write_event;
};
void readcb(evutil_socket_t, short, void *);
void writecb(evutil_socket_t, short, void *);
struct event_pair *event_pair_new(struct event_base *base, evutil_socket_t fd)
{
        struct event_pair *p = malloc(sizeof(struct event_pair));
        if (!p) return NULL;
        p->fd = fd;
        event_assign(&p->read_event, base, fd, EV_READ|EV_PERSIST, readcb, p);
        event_assign(&p->write_event, base, fd, EV_WRITE|EV_PERSIST, writecb, p);
        return p;
}
你也可以用event_assign()去初始化栈上分配的或静态分配的event对象。

警告:
千万不要用event_assign()去初始化一个在event_base准备好的event。如果这样做,会导致一些极端难以断定的错误。如果一个event已经初始化并且已经是准备好的,可以先通过event_del()去删除它,然后在用event_assign()去初始化它。

下面有些宏可以用在纯超时或信号event上:

接口

#define evtimer_assign(event, base, callback, arg)     event_assign(event, base, -1, 0, callback, arg)
#define evsignal_assign(event, base, signum, callback, arg)     event_assign(event, base, signum, EV_SIGNAL|EV_PERSIST, callback, arg)
如果你需要在将来的版本既使用event_assign()并且也保留二进制兼容性,你可以要求Libevent在运行时告诉你struct event的大小:

接口

size_t event_get_struct_event_size(void);
这个方法返回event结构体的大小。使用之前,你要确认使用堆分配会给你的程序带来明显的性能损失,因为使用它会让你的代码变得异常难读难写。

注意在未来的版本中,event_get_struct_event_size()可能会返回给你一个比sizeof(struct event)小的值。如果在未来的Libevent版本中遇到这种情况,意味着在struct event结尾的多余字节只用做保留填充字节。

下面这个例子与上个例子相同,只不过它使用event_get_struct_size()在运行时提供struct event的大小而不是依赖event_struct.h中的struct event的大小。

#include <event2/event.h>
#include <stdlib.h>

/* When we allocate an event_pair in memory, we'll actually allocate
 * more space at the end of the structure.  We define some macros
 * to make accessing those events less error-prone. */
struct event_pair {
         evutil_socket_t fd;
};

/* Macro: yield the struct event 'offset' bytes from the start of 'p' */
#define EVENT_AT_OFFSET(p, offset)             ((struct event*) ( ((char*)(p)) + (offset) ))
/* Macro: yield the read event of an event_pair */
#define READEV_PTR(pair)             EVENT_AT_OFFSET((pair), sizeof(struct event_pair))
/* Macro: yield the write event of an event_pair */
#define WRITEEV_PTR(pair)             EVENT_AT_OFFSET((pair),                 sizeof(struct event_pair)+event_get_struct_event_size())

/* Macro: yield the actual size to allocate for an event_pair */
#define EVENT_PAIR_SIZE()             (sizeof(struct event_pair)+2*event_get_struct_event_size())

void readcb(evutil_socket_t, short, void *);
void writecb(evutil_socket_t, short, void *);
struct event_pair *event_pair_new(struct event_base *base, evutil_socket_t fd)
{
        struct event_pair *p = malloc(EVENT_PAIR_SIZE());
        if (!p) return NULL;
        p->fd = fd;
        event_assign(READEV_PTR(p), base, fd, EV_READ|EV_PERSIST, readcb, p);
        event_assign(WRITEEV_PTR(p), base, fd, EV_WRITE|EV_PERSIST, writecb, p);
        return p;
}

event的准备好和未准备好状态

构造出一个event对象后,此时它还不能做任何事情。直到你通过添加它到event_base,把它变成准备好的状态。你可以通过event_add来添加:

接口

int event_add(struct event *ev, const struct timeval *tv);
在一个未准备好的event上调用event_add会把它的状态变成准备好的。此方法成功返回0,失败返回-1。如果tv是NULL,event会被直接加入event_base。如果非NULL,tv表示延迟加入的时间。

如果应用event_add()在一个准备好的event上,这个event还会保留准备好的状态并且用提供的tv超时重新调度它。如果tv为NULL,event_add()不会有任何影响。

注:tv表示的是时间段,不是某个具体的时间。不要这样做“tv->tv_sec = time(NULL)+10”。

接口

int event_del(struct event *ev);
调用event_del在一个已经初始化的event上,可以使它变成未准备好的和未激活的状态。如果event本来就是未准备好的或未激活的,它不会产生什么作用。成功返回0,失败返回-1。
注:如果你删除一个已经是活跃的但是还未运行回调的event,那么删除后这个event的回调就不会再被执行。

接口

int event_remove_timer(struct event *ev);
你可以通过event_remove_timer()单独的移除一个准备好的event的超时部分,而不需要删除它的IO或信号部分。如果event没有设置超时,此方法无效。如果event只有一个超时没有IO或信号部分,event_remove_timer()就与event_del()同义。成功返回0,失败返回-1。

带优先级的event

在某一时刻,多个event被同时触发,此时Libevent并不会定义什么执行回调的顺序。你可以通过优先级来定义一些event比另一些event更重要。

前几节讨论过,每个event_base有1个或多个优先级。在一个event被初始化后,在这个event被添加到event_base之前,你可以通过以下方法设置它的优先级。

接口

int event_priority_set(struct event *event, int priority);
event的优先级是一个基于0到event_base的优先级数量减1之间的一个数值。此方法成功返回0,失败返回-1。

当带有不同优先级的多个event同时被激活,带有低优先级的event是不允许运行的。Libevent会运行那些高优先级的event,然后再重新检测。只有当没有高优先级的event处于激活的状态,才会去运行低优先级的event。


#include <event2/event.h>

void read_cb(evutil_socket_t, short, void *);
void write_cb(evutil_socket_t, short, void *);

void main_loop(evutil_socket_t fd)
{
  struct event *important, *unimportant;
  struct event_base *base;

  base = event_base_new();
  event_base_priority_init(base, 2);
  /* Now base has priority 0, and priority 1 */
  important = event_new(base, fd, EV_WRITE|EV_PERSIST, write_cb, NULL);
  unimportant = event_new(base, fd, EV_READ|EV_PERSIST, read_cb, NULL);
  event_priority_set(important, 0);
  event_priority_set(unimportant, 1);

  /* Now, whenever the fd is ready for writing, the write callback will
     happen before the read callback.  The read callback won't happen at
     all until the write callback is no longer active. */
}
如果你不主动设置一个event的优先级,那么它默认的优先级的值是event_base的队列数量,再除2。


检测event状态
有些时候,你想知道一个event是否被添加,或检测它的标志位。

接口

int event_pending(const struct event *ev, short what, struct timeval *tv_out);

#define event_get_signal(ev) /* ... */
evutil_socket_t event_get_fd(const struct event *ev);
struct event_base *event_get_base(const struct event *ev);
short event_get_events(const struct event *ev);
event_callback_fn event_get_callback(const struct event *ev);
void *event_get_callback_arg(const struct event *ev);
int event_get_priority(const struct event *ev);

void event_get_assignment(const struct event *event,
        struct event_base **base_out,
        evutil_socket_t *fd_out,
        short *events_out,
        event_callback_fn *callback_out,
        void **arg_out);
event_pending方法可以确定给定的event是不是准备好的或是不是激活的状态。如果what参数被设置了EV_READ,EV_WRITE,EV_SIGNAL,和EV_TIMEOUT,此方法会返回event的相关状态标志。如果tv_out不为NULL,并且EV_TIMEOUT被设置在了what参数上,并且event是准备好的或已经被超时激活的状态,tv_out会被设置为event的超时时间。
event_get_fd()和event_get_signal()方法返回文件描述符或信号值。event_get_base()方法返回它的event_base。event_get_events()方法返回flag(例如EV_READ,EV_WRITE等)。event_get_callback()方法和event_get_callback_arg()方法返回回调方法和参数指针。event_get_priority()方法返回当前的优先级。

event_get_assignment()方法拷贝所有event的字段到所给出的指针上。如果某个指针为NULL,它会被忽略。

#include <event2/event.h>
#include <stdio.h>

/* Change the callback and callback_arg of 'ev', which must not be
 * pending. */
int replace_callback(struct event *ev, event_callback_fn new_callback,
    void *new_callback_arg)
{
    struct event_base *base;
    evutil_socket_t fd;
    short events;

    int pending;

    pending = event_pending(ev, EV_READ|EV_WRITE|EV_SIGNAL|EV_TIMEOUT,
                            NULL);
    if (pending) {
        /* We want to catch this here so that we do not re-assign a
         * pending event.  That would be very very bad. */
        fprintf(stderr,
                "Error! replace_callback called on a pending event!\n");
        return -1;
    }

    event_get_assignment(ev, &base, &fd, &events,
                         NULL /* ignore old callback */ ,
                         NULL /* ignore old callback argument */);

    event_assign(ev, base, fd, events, new_callback, new_callback_arg);
    return 0;
}


获取当前正在运行的event
为了调试或其他的目的,你可以通过以下方法获得当前正在运行的event的指针。

接口

struct event *event_base_get_running_event(struct event_base *base);
注:这个方法只能被用在event_base的循环中。不支持其他线程的调用,如果通过其他线程调用,会导致未定义的行为。

配置一次性event

如果你不需要多次添加一个event到event_base,或不需要再删除它,并且它不需要是持续的(persistent),你可以用event_base_once()。

接口

int event_base_once(struct event_base *, evutil_socket_t, short,
  void (*)(evutil_socket_t, short, void *), void *, const struct timeval *);
这个方法的接口与event_new()相同,除了它不支持EV_SIGNAL或EV_PERSIST。需要调度的event以默认的优先级被插入和运行。当回调运行结束,Libevent自行释放event对象。成功返回0,失败返回-1。
用event_base_once生成的event不能被删除或人为激活:如果你想取消它,那你从一开始就应该用event_new()或event_assign()去创建它。

注意:如果这个event永远都没被触发,它所占用的内存就不会被释放。


人为激活event

极少情况下,你想去激活一个event,即使它的条件未达成。

接口

void event_active(struct event *ev, int what, short ncalls);
此方法激活ev的what标志(EV_READ,EV_WRITE和EV_TIMEOUT的组合)。这个event不需要是准备好的状态,并且激活它也不会使它变成准备好的状态。

警告:在同一个event上,递归的调用event_active()会导致资源枯竭。下面就是一个非正确使用event_active的例子。

错误的例子:用event_active()制造了一个无限循环

struct event *ev;

static void cb(int sock, short which, void *arg) {
        /* Whoops: Calling event_active on the same event unconditionally
           from within its callback means that no other events might not get
           run! */

        event_active(ev, EV_WRITE, 0);
}

int main(int argc, char **argv) {
        struct event_base *base = event_base_new();

        ev = event_new(base, -1, EV_PERSIST | EV_READ, cb, NULL);

        event_add(ev, NULL);

        event_active(ev, EV_WRITE, 0);

        event_base_loop(base, 0);

        return 0;
}
这里的event循环只会执行一次,然后永远执行“cb”方法。
例子:使用定时器解决上面的问题

struct event *ev;
struct timeval tv;

static void cb(int sock, short which, void *arg) {
   if (!evtimer_pending(ev, NULL)) {
       event_del(ev);
       evtimer_add(ev, &tv);
   }
}

int main(int argc, char **argv) {
   struct event_base *base = event_base_new();

   tv.tv_sec = 0;
   tv.tv_usec = 0;

   ev = evtimer_new(base, cb, NULL);

   evtimer_add(ev, &tv);

   event_base_loop(base, 0);

   return 0;
}
例子:使用event_config_set_max_dispatch_interval()解决上面的问题

struct event *ev;

static void cb(int sock, short which, void *arg) {
        event_active(ev, EV_WRITE, 0);
}

int main(int argc, char **argv) {
        struct event_config *cfg = event_config_new();
        /* Run at most 16 callbacks before checking for other events. */
        event_config_set_max_dispatch_interval(cfg, NULL, 16, 0);
        struct event_base *base = event_base_new_with_config(cfg);
        ev = event_new(base, -1, EV_PERSIST | EV_READ, cb, NULL);

        event_add(ev, NULL);

        event_active(ev, EV_WRITE, 0);

        event_base_loop(base, 0);

        return 0;
}

优化超时
现在Libevent使用二叉树堆算法去跟踪event的超时。二叉树堆对增加和删除一个event timeout提供了O(lgN)的时间复杂度。如果添加的event都是尽量随机的值,使用二叉树堆是最佳的。如果有大量的event的超时时间都相同,那么二叉树堆的算法就很不高效了。

例如,假设你有1w个event,它们的超时都在被添加的5秒后。在这种情况下,如果使用双端队列去实现,它是O(1)复杂度。

一般来说,你不会用队列来管理所有的超时,因为队列只有在常量的情况下才会很快。如果存在一些event的timeout的值是随机的,然后添加一个event到队列里就要花费O(N)的时间,它明显不如二叉树堆高效。

Libevent可以允许你把一些event放在队列中,另外一些放在二叉树堆上。首先你需要向Libevent要求一个具体的“common timeout”时间。然后你就可以把是这个interval的event添加进入。注意:只有当你有很大数量的event有同一个timeout值时,使用这个优化才会对性能有所提高。

接口

const struct timeval *event_base_init_common_timeout(
    struct event_base *base, const struct timeval *duration);
此方法用event_base和时间值去初始化内部的队列。它返回一个指向特殊timeval的结构体的指针。你可以用这个指针去区分是添加到O(1)的队列中还是O(lgN)的二叉树堆中。这个结构可以被拷贝和赋值。它只能用在构造它的那个event_base中。不要依赖它的具体内容:Libevent使用它来区分使用哪个队列。

#include <event2/event.h>
#include <string.h>

/* We're going to create a very large number of events on a given base,
 * nearly all of which have a ten-second timeout.  If initialize_timeout
 * is called, we'll tell Libevent to add the ten-second ones to an O(1)
 * queue. */
struct timeval ten_seconds = { 10, 0 };

void initialize_timeout(struct event_base *base)
{
    struct timeval tv_in = { 10, 0 };
    const struct timeval *tv_out;
    tv_out = event_base_init_common_timeout(base, &tv_in);
    memcpy(&ten_seconds, tv_out, sizeof(struct timeval));
}

int my_event_add(struct event *ev, const struct timeval *tv)
{
    /* Note that ev must have the same event_base that we passed to
       initialize_timeout */
    if (tv && tv->tv_sec == 10 && tv->tv_usec == 0)
        return event_add(ev, &ten_seconds);
    else
        return event_add(ev, tv);
}
除非你确认使用common timeout可以带来好处,否则不要轻易使用它。

区分event是否被清除

Libevent提供区分event对象是否被清理的方法。

接口

int event_initialized(const struct event *ev);

#define evsignal_initialized(ev) event_initialized(ev)
#define evtimer_initialized(ev) event_initialized(ev)
警告:这些方法不能区分一个已初始化的event或一个未初始化的内存。

#include <event2/event.h>
#include <stdlib.h>

struct reader {
    evutil_socket_t fd;
};

#define READER_ACTUAL_SIZE()     (sizeof(struct reader) +      event_get_struct_event_size())

#define READER_EVENT_PTR(r)     ((struct event *) (((char*)(r))+sizeof(struct reader)))

struct reader *allocate_reader(evutil_socket_t fd)
{
    struct reader *r = calloc(1, READER_ACTUAL_SIZE());
    if (r)
        r->fd = fd;
    return r;
}

void readcb(evutil_socket_t, short, void *);
int add_reader(struct reader *r, struct event_base *b)
{
    struct event *ev = READER_EVENT_PTR(r);
    if (!event_initialized(ev))
        event_assign(ev, b, r->fd, EV_READ, readcb, r);
    return event_add(ev, NULL);
}

libevent学习六

标签:libevent   c   

原文地址:http://blog.csdn.net/huanzai2/article/details/43238173

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!