• libevent简介[翻译]7 使用事件循环


    http://www.wangafu.net/~nickm/libevent-book/Ref4_event.html

    使用事件

    libevent操作的单元是event,每一个event都代表了一组条件:

    • 一个文件描述符可读或是可写

    • 一个文件描述符变成可读或是可写(边界触发模式)

    • 超时

    • 信号中断

    • 自定义触发事件

    event都有生命周期。当我们设置好event并且把它关联到event base后,它就被初始化了。把event增加到event base中,会使event挂起。如果可以激活事件的条件触发了,比如文件描述符更改了状态或是定时器超时了,事件就会变成激活状态,用户提供的回调函数就会执行。如果时间是不断触发的,那么就会保持挂起状态。如果事件不再是持续触发,那么当回调的时候也会停止挂起。可以通过删除挂起的事件来使挂起的事件不再挂起,同样也可以把非挂起的事件添加到挂起事件中。

    构造事件结构体

    使用event_new()创建一个事件结构体

    接口

    #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()函数尝试申请一块内存并创建一个基于base的事件结构体。what参数就是上面列出标识,每个标识的意识在下面介绍。如果fd是非负数,那么就表示是一个文件,我们可以接收到它的读写事件。当事件激活的时候,Libevent就会调用cb指向的函数,并且把对应的参数传递进去:文件描述符fd,用位表示的触发的事件,通过arg传递进来的值。

    遇到内部错误或是无效的参数,event_new()返回NULL

    所有新创建的事件都是非挂起的,如果想让它挂起,需要调用event_add()(在下面会介绍)。

    调用event_free()释放时间,不管事件是挂起还是激活状态,都可以安全的调用event_free(),它会把事件设置为非挂起并且非激活状态,然后再释放空间。

    示例

    #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);
    }
    

    事件标识

    • EV_TIMEOUT

    这个是定时器事件,当指定的时间超时后,事件就会触发。

    在构造这个事件时,EV_TIMEOUT标识是被忽略的,也就是你可以在把event添加到事件循环列表的时候再设置超时,当你通过what设置的超时事件触发时,就会调用回调函数。

    • EV_READ

    当指定的文件描述符可读时,这个事件就会触发。

    • EV_WRITE

    当指定的文件描述符可写时,这个事件就会触发。

    • EV_SIGNAL

    用于实现信号检测的功能。下面的构造信号事件会介绍。

    • EV_PERSIST

    表示这个事件是持久的。下面的关于持久事件会介绍。

    • EV_ET

    标明事件是边界触发,当然如果底层支持边界触发事件的话。这个标识影响EV_READEV_WRITE

    关于持久事件

    默认情况下,当一个挂起的事件变成活动状态时(因为fd变得可读或是可写,或是超时),它就会变成非挂起状态,在它的回调函数没有执行之前。如果想把事件再次挂起,需要调用event_add(),在回调函数内部。

    如果EV_PERSIST标识设置了。这个事件就是持久事件。也就是即使事件回调了,也是挂起的,可以通过event_del()把事件设置为非挂起状态。

    在持久事件中,超时时钟会在每次回调的时候重置。比如你又一个标识EV_READ|EV_PERSIST的超时事件,超时时间是5秒,那么事件会在如下情况激活:

    • socket可读

    • 从上一次激活后,过去了5秒

    创建一个事件作为它自己的回调参数

    有时候,你可能想创建一个事件,然后把自己作为参数回调。我们可以把指向这个事件的指针通过event_new()传递进去,但是呢,在创建的时候,这个事件还不存在。所以,为了解决这个问题,可以使用event_self_cbarg()

    接口

    void *event_self_cbarg();
    

    这个函数返回一个魔法的指针,可以当做事件的回调函数的参数,告诉event_new()创建一个事件,接受他自己作为回调参数。

    示例

    #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_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() event_assign() evtimer_assign() and evsignal_assign(),但是不能用在没有事件作为参数的回调函数中。

    超时事件

    为了方便,有一组宏,前缀是evtimer_,可以用来替代event_*,可以是代码更清晰。

    接口

    #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))
    

    构造信号事件

    Libevent可以监听POSIX的信号。

    接口

    #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);
    

    注意,事件回调会在信号发生时在事件循环中执行,所以调用在标准的POSIX信号句柄中不建议的函数也是安全的。

    警告

    不要为信号事件创建超时。

    同样也有一些便利的宏用作信号事件

    接口

    #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))
    

    警告

    当前版本的Libevent,只能有一个event_base在监听信号。如果增加信号到两个event_base,即使信号不同,也只会有一个收到。

    队列不会有这个限制。

    不通过堆申请空间来设置事件

    为了性能或是一些其他原因,有些人喜欢把一批事件作为一个大的结构体同一申请,对每一个事件使用,保存了:

    • 从堆上申请小的结构体的开销

    • 把指针指向结构体事件的开销

    • 如果事件不在缓冲区的开销

    这样做会增加兼容其他版本的类库的风险,因为有可能事件结构体的大小不一样了。

    这样做资源消耗非常小,不用担心很多应用使用。你可以坚持使用event_new(),除非你知道这样有很大的性能消耗对于在堆上申请事件。使用event_assign()会导致你很难去发现错误,如果将来的libevent版本中事件结构体比你使用的时候更大。

    接口

    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_assign()的参数与event_new()的意义都一样,除了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_assign()`去初始化一个已经挂起的事件。这样做会导致非常难以解决的错误。如果这个事件已经初始化并且挂起,先调用`event_del()`从事件循环中删除掉,在调用`event_assign()`。
    
    同样有一些宏用作`event_assign()`去申请超时和信号事件。
    
    ### 接口
    
    ```cpp
    #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()还想兼容后面的新版本,可以在运行时动态的获取事件结构体的大小:

    接口

    size_t event_get_struct_event_size(void);
    

    这个函数可以告诉你事件结构体的大小。就和前面一样,你只有在知道有堆申请的错误时,才使用这个,这样,你的代码将会变得更加健壮。

    可能event_get_struct_event_size()在将来的实现中给定的大小与sizeof(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;
    }
    

    使事件挂起或是非挂起

    当我们创建了一个事件,它不会做任何事情,除非我们把它加入到队列中挂起。

    接口

    int event_add(struct event *ev, const struct timeval *tv);
    

    调用event_add把一个非挂起的事件设置为挂起。返回0表示成功,返回-1表示失败。如果tvNULL,就表示是没有超时时间。tv的超时时间是以秒或是毫秒计算的。

    如果调用event_add()增加一个已经挂起的事件,它会先使它离开挂起,然后再重新按照给定的超时时间配置。如果这个事件已经挂起,并且你重新增加到里面,超时的参数是NULL,那么event_add()将会什么都不做。

    注意

    不要把tv中的事件设置为你想要超时的时间,它表示过了多少秒超时,如果你设置成tv→tv_sec = time(NULL)+10;,那么你的超时事件会在40年之后才会到达,而不是10秒。

    接口

    int event_del(struct event *ev);
    

    调用这个函数会使初始化的事件变成非挂起,非激活状态。如果事件没挂起也没有激活,那么这个函数就没有任何操作。返回0表示成功,-1表示失败。

    注意

    如果你在这个事件已经激活但是还没有回调的时候删除了,那么它也不会再回调了。

    接口

    int event_remove_timer(struct event *ev);
    

    最后,你可以立马移除一个挂起的事件,而不用删除他的IO或是信号内容。如果事件没有超时挂起,event_remove_timer()没有任何操作,如果事件已经超时,还没有IO或是信号内容,event_remove_timer()event_del()效果一样。

    附带优先级的事件

    当很多事件触发时,libevent并没有指定任何顺序,哪一个回调函数会先执行。如果你需要某些事件更重要,要比其他事件先触发,可以使用事件的优先级。

    就如前面章节讨论的那样,每个event_base都有一个或是多个优先级。在增加事件到event_base中时,在初始化之后,可以设置它的优先级。

    接口

    int event_priority_set(struct event *event, int priority);
    

    这个priority参数的值是0和event_base中设置的最高的优先级权限数值减一之间。

    当多个事件的多个优先级触发,低优先级的事件不会运行。代替的是,libevent运行高优先级的事件。然后再次检测事件。只有当没有更高优先级的事件时,才会执行低优先级的事件。

    示例

    #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 base的最高优先级除以2.
    
    ## 检测事件状态
    
    有时候你需要知道事件是不是被添加到队列
    
    ### 接口
    
    ```cpp
    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函数用来确定事件是挂起还是激活。如果是,任何一个EV_READ EV_WRITE EV_SIGNALEV_TIMEOUT标识设置了,这个函数就会返回所有的这个事件的标识,当前是挂起还是激活。如果tv_out提供了,EV_TIMEOUT已经设置了,当前时间已经挂起或是因为超时激活,这个tv_out被设置到里面用来接收超时触发的时间。

    event_get_fd()event_get_signal()返回配置的文件描述符或是信号。event_get_base()返回配置的event_baseevent_get_events()函数返回EV_READEV_WRITE等标识。event_get_callback()event_get_callback_arg()返回回调函数和回调函数的参数指针。event_get_priority()返回事件当前的优先级。

    event_get_assignment()把所有事件相关的内容拷贝到提供的指针空间中,如果指针是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!
    ");
            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;
    }
    

    查找当前的运行事件

    为了调试或是其他原因,你可能要获得一个当前正在运行的事件指针。

    接口

    struct event *event_base_get_running_event(struct event_base *base);
    

    注意这个函数的行为只有在event_base的循环中才有效,从另外一个线程调用是无效的,会触发不可预料的行为。

    设置只执行一次的事件

    如果你不想每次都增加事件到队列,然后删除事件从队列中,当它不再是持久的。可以调用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。这个事件被添加进去并且按照默认的优先级跑。当回调函数触发时,libevent在内部会释放事件结构体。

    通过event_base_once添加的事件不能呗删除或是手动激活。

    手动激活事件

    很少情况下,你可能需要手动激活一个事件,即使它的触发条件还没有达到。

    接口

    void event_active(struct event *ev, int what, short ncalls);
    

    这个函数使ev激活,激活的事件是what,可以是EV_READ EV_WRITE EV_TIMEOUT。 这个函数不需要事件提前挂起,并且激活它也不会使事件挂起。

    警告:递归调用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;
    }
    

    示例:用超时来解决上面的问题

    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用了堆算法来跟踪超时事件。堆算法提供了O(lgn)的复杂度对于增加和删除。如果你随机的不规律增加超时事件,这是比较优的方案,如果你是大量的同一时间增加,会有性能问题。

    比如,你有一万个事件,每个都是5秒钟超时触发。这样的话用双链表队列性能更好,可以达到O(1)。

    当然,你并不是需要对所有的超时事件都使用队列。因为队列仅仅对于不变的超时数值有效。如果超时的事件是随机的,有的多,有的少,那么用双向链表队列的复杂度是O(n),没有堆的性能好。

    libevent可以让你选择使用哪种数据结构。

    接口

    const struct timeval *event_base_init_common_timeout(
        struct event_base *base, const struct timeval *duration);
    

    这个函数参数有一个event_base和一个超时时间范围。返回一个特殊的timeval,你可以指定它给一个事件,这个事件使用O(1)的队列。这个特殊的timeval可以拷贝或是复制。只能用作构造特定的基类。不要依赖它的实际内容: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);
    }
    

    除了清理内存外还可以告诉一个好的事件

    libevent提供了一个方法,可以区分初始化的事件和内存中已经被清除设置位0的空间。比如,通过calloc()申请或是通过memset()清理或是通过bzero()申请

    接口

    int event_initialized(const struct event *ev);
    
    #define evsignal_initialized(ev) event_initialized(ev)
    #define evtimer_initialized(ev) event_initialized(ev)
    

    警告

    这个函数不能可靠的区分初始化的事件和一大块未初始化的内存。除非你确定内存已经清零或是被事件初始化,不然不可以调用。

    一般情况,你不需要调用这个函数,除非你得到了一个非常特殊的程序。event_new()返回的事件总是被初始化的。

    示例

    #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);
    }
    
  • 相关阅读:
    php5升级到php7 后对于mysql数据库的关联出现问题的解决方案
    关于js与php互相传值的介绍【转载+自身总结】
    PHP页面间参数传递的四种方法详解
    很久没更新博客了, 明天开始恢复更新。
    SQL 行转列
    oracle 记录被别的用户锁住
    IIS32位,64位模式下切换
    Oracle 分页
    Oracel 提取数字
    Win8 做无线热点
  • 原文地址:https://www.cnblogs.com/studywithallofyou/p/13100340.html
Copyright © 2020-2023  润新知