Nginx事件管理之事件处理流程

摘要:
无效ngx_event_接受(ngx_event_t*ev){socklen_socklen;ngx_uint_level;ngx_event_conf_t*ecf;将*非活动读取事件添加到epoll侦听器对象*/if(ngx_enable_Accept_events((ngx_cycle_t*)ngx_cycle)!ngx_event_core_module);
1. 概述

事件处理要解决的两个问题:

  1. "惊群" 问题,即多个 worker 子进程监听相同端口时,在 accept 建立新连接时会有争抢,引发不必要的上下文切换,
    增加系统开销。
  2. 负载均衡问题。

这两个问题的解决需要依靠 Nginx 的 post 事件处理机制。Nginx 设计了两个 post 队列,一个是由被触发的监听连接的读事
件构成的 ngx_posted_accept_events 队列,另一个是由普通读/写事件构成的 ngx_posted_events 队列。这样的 post 事件
可以让用户完成:

  • 将 epoll_wait 产生的一批事件,分到这两个队列中,让存放着新连接事件的 ngx_posted_accept_events 队列优先执行,
    存放普通事件的 ngx_posted_events 队列最后执行,这是解决 "惊群" 和负载均衡两个问题的关键。
  • 如果在处理一个事件的过程中产生了另一个事件,而我们希望这个事件随后执行(不是立刻执行),就可以把它放到 post
    队列中。
2. 建立新连接

每个监听待连接事件的回调函数都是 ngx_event_accept,一旦监听到客户端发来的连接请求,就会调用该回调方法。

void ngx_event_accept(ngx_event_t *ev)
{
    socklen_t          socklen;
    ngx_err_t          err;
    ngx_log_t         *log;
    ngx_uint_t         level;
    ngx_socket_t       s;
    ngx_event_t       *rev, *wev;
    ngx_sockaddr_t     sa;
    ngx_listening_t   *ls;
    ngx_connection_t  *c, *lc;
    ngx_event_conf_t  *ecf;
#if (NGX_HAVE_ACCEPT4)
    static ngx_uint_t  use_accept4 = 1;
#endif

    /* 若事件已经超时 */
    if (ev->timedout) {
        /* 则遍历 ngx_cycle_t 成员 listening 保存的需要监听的端口,将
         * 还未活跃的读事件添加到 epoll 监听对象中 */
        if (ngx_enable_accept_events((ngx_cycle_t *) ngx_cycle) != NGX_OK) {
            return;
        }

        ev->timedout = 0;
    }

    ecf = ngx_event_get_conf(ngx_cycle->conf_ctx, ngx_event_core_module);

    if (!(ngx_event_flags & NGX_USE_KQUEUE_EVENT)) {
        ev->available = ecf->multi_accept;
    }

    lc = ev->data;
    ls = lc->listening;
    ev->ready = 0;

    ngx_log_debug2(NGX_LOG_DEBUG_EVENT, ev->log, 0,
                   "accept on %V, ready: %d", &ls->addr_text, ev->available);

    do {
        socklen = sizeof(ngx_sockaddr_t);

#if (NGX_HAVE_ACCEPT4)
        if (use_accept4) {
            s = accept4(lc->fd, &sa.sockaddr, &socklen, SOCK_NONBLOCK);
        } else {
            s = accept(lc->fd, &sa.sockaddr, &socklen);
        }
#else
        /* 调用 accept 方法试图建立新连接,如果没有准备好的新连接事件,则直接返回 */
        s = accept(lc->fd, &sa.sockaddr, &socklen);
#endif

        if (s == (ngx_socket_t) -1) {
            err = ngx_socket_errno;

            if (err == NGX_EAGAIN) {
                ngx_log_debug0(NGX_LOG_DEBUG_EVENT, ev->log, err,
                               "accept() not ready");
                return;
            }

            level = NGX_LOG_ALERT;

            if (err == NGX_ECONNABORTED) {
                level = NGX_LOG_ERR;

            } else if (err == NGX_EMFILE || err == NGX_ENFILE) {
                level = NGX_LOG_CRIT;
            }

#if (NGX_HAVE_ACCEPT4)
            ngx_log_error(level, ev->log, err,
                          use_accept4 ? "accept4() failed" : "accept() failed");

            if (use_accept4 && err == NGX_ENOSYS) {
                use_accept4 = 0;
                ngx_inherited_nonblocking = 0;
                continue;
            }
#else
            ngx_log_error(level, ev->log, err, "accept() failed");
#endif

            if (err == NGX_ECONNABORTED) {
                if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
                    ev->available--;
                }

                if (ev->available) {
                    continue;
                }
            }

            if (err == NGX_EMFILE || err == NGX_ENFILE) {
                if (ngx_disable_accept_events((ngx_cycle_t *) ngx_cycle, 1)
                    != NGX_OK)
                {
                    return;
                }

                if (ngx_use_accept_mutex) {
                    if (ngx_accept_mutex_held) {
                        ngx_shmtx_unlock(&ngx_accept_mutex);
                        ngx_accept_mutex_held = 0;
                    }

                    ngx_accept_disabled = 1;

                } else {
                    ngx_add_timer(ev, ecf->accept_mutex_delay);
                }
            }

            return;
        }

#if (NGX_STAT_STUB)
        (void) ngx_atomic_fetch_add(ngx_stat_accepted, 1);
#endif
        
        /* 设置负载均衡阈值 ngx_accept_disabled,这个阈值是进程允许的总连接数的 1/8 减去
         * 空闲连接数,这个值越大表示过载越大,当前进程的负载越重 */
        ngx_accept_disabled = ngx_cycle->connection_n / 8
                              - ngx_cycle->free_connection_n;

        /* 从连接池中获取一个 ngx_connection_t 连接对象 */
        c = ngx_get_connection(s, ev->log);

        if (c == NULL) {
            if (ngx_close_socket(s) == -1) {
                ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno,
                              ngx_close_socket_n " failed");
            }

            return;
        }

        c->type = SOCK_STREAM;

#if (NGX_STAT_STUB)
        (void) ngx_atomic_fetch_add(ngx_stat_active, 1);
#endif

        /* 为该连接建立内存池,在这个连接释放到空闲连接池时,释放 pool 内存池 */
        c->pool = ngx_create_pool(ls->pool_size, ev->log);
        if (c->pool == NULL) {
            ngx_close_accepted_connection(c);
            return;
        }

        c->sockaddr = ngx_palloc(c->pool, socklen);
        if (c->sockaddr == NULL) {
            ngx_close_accepted_connection(c);
            return;
        }

        /* 将客户端的地址信息拷贝到 c->sockaddr 中 */
        ngx_memcpy(c->sockaddr, &sa, socklen);

        log = ngx_palloc(c->pool, sizeof(ngx_log_t));
        if (log == NULL) {
            ngx_close_accepted_connection(c);
            return;
        }

        /* set a blocking mode for iocp and non-blocking mode for others */

        if (ngx_inherited_nonblocking) {
            if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
                if (ngx_blocking(s) == -1) {
                    ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno,
                                  ngx_blocking_n " failed");
                    ngx_close_accepted_connection(c);
                    return;
                }
            }

        } else {
            /* 设置套接字的属性,如设为非阻塞套接字 */
            if (!(ngx_event_flags & NGX_USE_IOCP_EVENT)) {
                if (ngx_nonblocking(s) == -1) {
                    ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_socket_errno,
                                  ngx_nonblocking_n " failed");
                    ngx_close_accepted_connection(c);
                    return;
                }
            }
        }

        *log = ls->log;

        /* 初始化该连接处理 I/O 的方法 */
        c->recv = ngx_recv;
        c->send = ngx_send;
        c->recv_chain = ngx_recv_chain;
        c->send_chain = ngx_send_chain;

        c->log = log;
        c->pool->log = log;

        c->socklen   = socklen;
        c->listening = ls;
        c->local_sockaddr = ls->sockaddr;
        c->local_socklen  = ls->socklen;

#if (NGX_HAVE_UNIX_DOMAIN)
        if (c->sockaddr->sa_family == AF_UNIX) {
            c->tcp_nopush = NGX_TCP_NOPUSH_DISABLED;
            c->tcp_nodelay = NGX_TCP_NODELAY_DISABLED;
#if (NGX_SOLARIS)
            /* Solaris's sendfilev() supports AF_NCA, AF_INET, and AF_INET6 */
            c->sendfile = 0;
#endif
        }
#endif

        rev = c->read;
        wev = c->write;
        
        /* 置为 1,表示当前写事件已经准备就绪 */
        wev->ready = 1;

        if (ngx_event_flags & NGX_USE_IOCP_EVENT) {
            rev->ready = 1;
        }

        if (ev->deferred_accept) {
            rev->ready = 1;
#if (NGX_HAVE_KQUEUE || NGX_HAVE_EPOLLRDHUP)
            rev->available = 1;
#endif
        }

        rev->log = log;
        wev->log = log;

        /*
         * TODO: MT: - ngx_atomic_fetch_add()
         *             or protection by critical section or light mutex
         *
         * TODO: MP: - allocated in a shared memory
         *           - ngx_atomic_fetch_add()
         *             or protection by critical section or light mutex
         */

        c->number = ngx_atomic_fetch_add(ngx_connection_counter, 1);

#if (NGX_STAT_STUB)
        (void) ngx_atomic_fetch_add(ngx_stat_handled, 1);
#endif

        /* 将网络字节序的地址转换为主机字节序的字符串形式的地址 */
        if (ls->addr_ntop) {
            c->addr_text.data = ngx_pnalloc(c->pool, ls->addr_text_max_len);
            if (c->addr_text.data == NULL) {
                ngx_close_accepted_connection(c);
                return;
            }

            c->addr_text.len = ngx_sock_ntop(c->sockaddr, c->socklen,
                                             c->addr_text.data,
                                             ls->addr_text_max_len, 0);
            if (c->addr_text.len == 0) {
                ngx_close_accepted_connection(c);
                return;
            }
        }

#if (NGX_DEBUG)
        {
        ngx_str_t  addr;
        u_char     text[NGX_SOCKADDR_STRLEN];

        ngx_debug_accepted_connection(ecf, c);

        if (log->log_level & NGX_LOG_DEBUG_EVENT) {
            addr.data = text;
            addr.len = ngx_sock_ntop(c->sockaddr, c->socklen, text,
                                     NGX_SOCKADDR_STRLEN, 1);

            ngx_log_debug3(NGX_LOG_DEBUG_EVENT, log, 0,
                           "*%uA accept: %V fd:%d", c->number, &addr, s);
        }

        }
#endif
        
        /* 将这个连接的读/写事件都添加到 epoll 等事件驱动模块中,这样,在这个连接上
         * 如果接收到用户请求,epoll_wait 就会收集到这个事件 */
        if (ngx_add_conn && (ngx_event_flags & NGX_USE_EPOLL_EVENT) == 0) {
            if (ngx_add_conn(c) == NGX_ERROR) {
                ngx_close_accepted_connection(c);
                return;
            }
        }

        log->data    = NULL;
        log->handler = NULL;

        /* 调用监听对象的 ngx_listening_t 中的 handler 回调方法。ngx_listening_t 结构体 
         * 的 handler 回调方法就是当新的 TCP 连接刚刚建立完成时在这里调用的 */
        ls->handler(c);

        if (ngx_event_flags & NGX_USE_KQUEUE_EVENT) {
            ev->available--;
        }

    /* 如果监听事件的 available 标志位为 1,再次循环到开始,否则结束。
     * 当 available 为 1 时,表示尽可能一次性尽量多地建立新连接 */
    } while (ev->available);
}
3. "惊群" 问题的解决

Nginx 的解决方法为:规定在同一时刻只能有唯一一个 worker 子进程监听端口,这样就不会发生 "惊群" 了,此时新连接
事件只能唤醒唯一正在监听端口的 worker 子进程。

具体实现看 ngx_trylock_accept_mutex 方法:

ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{
    /* 使用进程间的同步锁,试图获取 accept_mutex 锁。注意,ngx_shmtx_trylock 返回 1 表示成功拿到锁,
     * 返回 0 表示获取锁失败。这个获取锁的过程是非阻塞的,此时一旦锁被其他 worker 子进程占用,
     * ngx_shmtx_trylock 方法会立即返回 */
    if (ngx_shmtx_trylock(&ngx_accept_mutex)) {

        ngx_log_debug0(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                       "accept mutex locked");

        /* 如果获取到 accept_mutex 锁,但 ngx_accept_mutex_held 为 1,则立刻返回。
         * ngx_accept_mutex_held 是一个标志位,当它为 1 时,表示当前进程已经获取到锁了 */
        if (ngx_accept_mutex_held && ngx_accept_events == 0) {
            /* ngx_accept_mutex 锁之前已经获取到了,立刻返回 */
            return NGX_OK;
        }

        /* 将所有监听连接的读事件添加到当前的 epoll 等事件驱动模块中 */
        if (ngx_enable_accept_events(cycle) == NGX_ERROR) {
            /* 若是将监听句柄添加到事件驱动模块中失败了,则应释放 ngx_accept_mutex 锁 */
            ngx_shmtx_unlock(&ngx_accept_mutex);
            return NGX_ERROR;
        }

        /* 经过 ngx_enable_accept_events 方法的调用,当前进程的事件驱动模块已经开始监听所有的端口,
         * 这时需要把 ngx_accept_mutex_held 标志位置为 1,方便本进程的其他模块了解它目前已经获取
         * 到了锁 */
        ngx_accept_events = 0;
        ngx_accept_mutex_held = 1;

        return NGX_OK;
    }

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
                   "accept mutex lock failed: %ui", ngx_accept_mutex_held);

    /* 如果 ngx_shmtx_trylock 返回 0,则表明获取 ngx_accept_mutex 锁失败,这时如果
     * ngx_accept_mutex_held 标志位还为 1,即当前进程还在获取到锁的状态,这是不正确的 */
    if (ngx_accept_mutex_held) {
        /* ngx_disable_accept_events 会将所有监听连接的读事件从事件驱动模块中移除 */
        if (ngx_disable_accept_events(cycle, 0) == NGX_ERROR) {
            return NGX_ERROR;
        }

        /* 在没有获取到 ngx_accept_mutex 锁时,必须把 ngx_accept_mutex_he 置为 0 */
        ngx_accept_mutex_held = 0;
    }

    return NGX_OK;
}

如果 ngx_trylock_accept_mutex 方法没有获取到锁,接下来调用事件驱动模块的 process_events 方法时只能处理已有的
连接上的事件;如果获取到了锁,调用 process_events 方法时就会既处理已有连接上的事件,也处理新连接的事件。

4. 负载均衡

只有打开了 accept_mutex 锁,才能实现 worker 子进程间的负载均衡。在接收到一个客户的新连接请求的处理函数
ngx_event_accept 中初始化了一个全局变量 ngx_accept_disabled,它是负载均衡机制实现的关键阈值:

ngx_accept_disabled = ngx_cycle->connection_n / 8 - ngx_cycle->free_connection_n;

在 Nginx 启动时,ngx_accept_disabled 的值是一个负数,其值为连接总数的 7/8。当它为负数时,不会进行触发负载均
衡操作;而当 ngx_accept_disabled 是正数时,就会触发 Nginx 进行负载均衡操作了。

5. ngx_process_events_and_timers
/**
 * 在开启负载均衡的情况下,在ngx_event_process_init()函数中跳过了将监听套接口加入到
 * 事件监控机制,真正将监听套接口加入到事件监控机制是在ngx_process_events_and_timers()
 * 里。工作进程的主要执行体是一个无限的for循环,而在该循环内最重要的函数调用就是
 * ngx_process_events_and_timers(),所以在该函数内动态添加或删除监听套接口是一种很灵活
 * 的方式。如果当前工作进程负载比较小,就将监听套接口加入到自身的事件监控机制里,从而
 * 带来新的客户端请求;而如果当前工作进程负载比较大,就将监听套接口从自身的事件监控机制里
 * 删除,避免引入新的客户端请求而带来更大的负载。
 */
 /*
  * 参数含义:
  * - cycle是当前进程的ngx_cycle_t结构体指针
  * 
  * 执行意义:
  * 使用事件模块处理截止到现在已经收集到的事件.
  */
void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{
    ngx_uint_t  flags;
    ngx_msec_t  timer, delta;

    /*
     * Nginx具体使用哪种超时检测方案主要取决于一个nginx.conf的配置指令timer_resolution,即对应
     * 的全局变量 ngx_timer_resolution。 */
    
    /* 如果配置文件中使用了 timer_resolution 配置项,也就是 ngx_timer_resolution 值大于 0,
     * 则说明用户希望服务器时间精确度为 ngx_timer_resolution 毫秒。这时,将 ngx_process_events 的 
     *  timer 参数设置为 -1,告诉 ngx_process_events 方法在检测事件时不要等待,直接收集所有已经
     * 就绪的事件然后返回;同时将 flags 参数置为 0,即告诉 ngx_process_events 没有任何附加动作。
     */
    if (ngx_timer_resolution)
    {
        timer = NGX_TIMER_INFINITE;
        flags = 0;
    }
    else
    {
        /* 如果没有使用 timer_resolution,那么将调用 ngx_event_find_timer() 方法获取最近一个将要
         * 触发的事件距离现在有多少毫秒,然后把这个值赋予 timer 参数,告诉 ngx_process_events 
         * 方法在检测事件时如果没有任何事件,最多等待 timer 毫秒就返回;将 flags 参数设置为 
         * NGX_UPDATE_TIME,告诉 ngx_process_events 方法更新缓存的时间 */
        timer = ngx_event_find_timer();
        flags = NGX_UPDATE_TIME;

#if (NGX_WIN32)

        /* handle signals from master in case of network inactivity */

        if (timer == NGX_TIMER_INFINITE || timer > 500)
        {
            timer = 500;
        }
        
#endif
    }

    /* 开启了负载均衡的情况下,若当前使用的连接到达总连接数的7/8时,就不会再处理
     * 新连接了,同时,在每次调用process_events时都会将ngx_accept_disabled减1,
     * 直到ngx_accept_disabled降到总连接数的7/8以下时,才会调用ngx_trylock_accept_mutex
     * 试图去处理新连接事件 */
    if (ngx_use_accept_mutex)
    {          
        /* 
         * 检测变量 ngx_accept_disabled 值是否大于0来判断当前进程是否
         * 已经过载,为什么可以这样判断需要理解变量ngx_accept_disabled
         * 值的含义,这在accept()接受新连接请求的处理函数ngx_event_accept()
         * 内可以看到。
         * 当ngx_accept_disabled大于0,表示处于过载状态,因为仅仅是自减一,
         * 当经过一段时间又降到0以下,便可争用锁获取新的请求连接。
         */
        if (ngx_accept_disabled > 0)
        {
            ngx_accept_disabled--;
        }
        else
        {
            /* 
             * 若进程没有处于过载状态,那么就会尝试争用该锁获取新的请求连接。
             * 实际上是争用监听套接口的监控权,争锁成功就会把所有监听套接口
             * (注意,是所有的监听套接口,它们总是作为一个整体被加入或删除)
             * 加入到自身的事件监控机制里(如果原本不在);争锁失败就会把监听
             * 套接口从自身的事件监控机制里删除(如果原本就在)。从下面的函数
             * 可以看到这点。 
             */
            if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR)
            {
                /* 发生错误则直接返回 */
                return;
            }

            /* 若获取到锁,则给flags添加NGX_POST_EVENTS标记,表示所有发生的事件都将延后
             * 处理。这是任何架构设计都必须遵守的一个约定,即持锁者必须尽量缩短自身持锁的时
             * 间,Nginx亦如此,所以照此把大部分事件延迟到释放锁之后再去处理,把锁尽快释放,
             * 缩短自身持锁的时间能让其他进程尽可能的有机会获取到锁。*/
            if (ngx_accept_mutex_held)
            {
                flags |= NGX_POST_EVENTS;
            }
            else
            {
                /* 如果没有获取到 accept_mutex 锁,则意味着既不能让当前 worker 进程频繁地试图抢锁,
                 * 也不能让它经过太长时间再去抢锁。*/
                if (timer == NGX_TIMER_INFINITE
                    || timer > ngx_accept_mutex_delay)
                {
                    /* 这意味着,即使开启了 timer_resolution 时间精度,也需要让 
                     * ngx_process_events  方法在没有新事件的时候至少等待 ngx_accept_mutex_delay 
                     * 毫秒再去试图抢锁。而没有开启时间精度时,如果最近一个定时器事件的超时时间
                     * 距离现在超过了 ngx_accept_mutex_delay 毫秒的话,也要把 timer 设置为 
                     * ngx_accept_mutex_delay 毫秒,这是因为当前进程虽然没有抢到 accept_mutex  
                     * 锁,但也不能让 ngx_process_events 方法在没有新事件的时候等待的时间超过
                     *  ngx_accept_mutex_delay 毫秒,这会影响整个负载均衡机制 */
                    timer = ngx_accept_mutex_delay;
                }
            }
        }
    }

    /* 调用 ngx_process_events 方法,并计算 ngx_process_events 执行时消耗的时间 */
    delta = ngx_current_msec;

    /* 开始等待事件发生并进行相应处理(立即处理或先缓存所有接收到的事件) */
    (void)ngx_process_events(cycle, timer, flags);

    /* delta 的值即为 ngx_process_events 执行时消耗的毫秒数 */
    delta = ngx_current_msec - delta;

    ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0, 
                   "timer delta: %Mms", delta);

    /*
     * 接下来先处理新建连接缓存事件ngx_posted_accept_events,此时还不能释放锁,因为我们还在处理
     * 监听套接口上的事件,还要读取上面的请求数据,所以必须独占,一旦缓存的新建连接事件全部被处
     * 理完就必须马上释放持有的锁了,因为连接套接口只可能被某一个进程至始至终的占有,不会出现多
     * 进程之间的相互冲突,所以对于连接套接口上事件ngx_posted_events的处理可以在释放锁之后进行,
     * 虽然对于它们的具体处理与响应是最消耗时间的,不过在此之前已经释放了持有的锁,所以即使慢一点
     * 也不会影响到其他进程。
     */
    ngx_event_process_posted(cycle, &ngx_posted_accept_events);

    /* 将锁释放 */
    if (ngx_accept_mutex_held)
    {
        ngx_shmtx_unlock(&ngx_accept_mutex);
    }

    /* 若ngx_process_events方法执行时消耗的时间delta大于0,这时可能有新的定时器事件被触发,
     * 因此需要调用下面该函数处理所有满足条件的定时器事件 */
    if (delta)
    {
        /* 处理所有的超时事件 */
        ngx_event_expire_timers();
    }

    /* 释放锁后再处理耗时长的连接套接口上的事件 */
    ngx_event_process_posted(cycle, &ngx_posted_events);

    /*
     * 补充两点。
     * 一:如果在处理新建连接事件的过程中,在监听套接口上又来了新的请求会怎么样?这没有关系,当前
     *     进程只处理已缓存的事件,新的请求将被阻塞在监听套接口上,而前面曾提到监听套接口是以 ET
     *     方式加入到事件监控机制里的,所以等到下一轮被哪个进程争取到锁并加到事件监控机制里时才会
     *     触发而被抓取出来。
     * 二:上面的代码中进行ngx_process_events()处理并处理完新建连接事件后,只是释放锁而并没有将监听
     *     套接口从事件监控机制里删除,所以有可能在接下来处理ngx_posted_events缓存事件的过程中,互斥
     *     锁被另外一个进程争抢到并把所有监听套接口加入到它的事件监控机制里。因此严格说来,在同一
     *     时刻,监听套接口只可能被一个进程监控(也就是epoll_wait()这种),因此进程在处理完
     *     ngx_posted_event缓存事件后去争用锁,发现锁被其他进程占有而争用失败,会把所有监听套接口从
     *     自身的事件监控机制里删除,然后才进行事件监控。在同一时刻,监听套接口只可能被一个进程
     *     监控,这就意味着Nginx根本不会受到惊群的影响,而不论Linux内核是否已经解决惊群问题。
     */
}

免责声明:文章转载自《Nginx事件管理之事件处理流程》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇控制input标签中只能输入数字以及小数点后两位document.write下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

js应用实现博客个性主页布局拖拽功能

Jquery的Interface elements for jQuery里面的拖拽布局存在一些bug,效率也比较低,GoogleUI google_drag.js有些乱,不是很容易理解,Discuz!NT Space代码满天飞,所以自己参考GoogleUI的思想,简化和优化了一些操作代码,实现了博客系统基本的拖拽布局的效果,暂时未考虑其他浏览器的兼容性问题...

前端(十九)—— Bootstrap框架

Bootstrap Bootstrap中文文档 一、简介 Bootstrap是美国Twitter公司的设计师Mark Otto和Jacob Thornton合作基于HTML、CSS、JavaScript 开发的简洁、直观、强悍的前端开发框架,使得 Web 开发更加快捷。 Bootstrap框架是基于jQuery的,在导入bootstrap框架的js时应先...

markdown语法---根据使用不断扩充中

markdown语法 标题 标题使用 #表示,几个#表示几级标题,最多六级标题。 斜体 使用 两个星号*括起来的文字是斜体字这是斜体字 粗体 使用四个 * 号括起来的是粗体字。 这是粗体字 引用 这个就是引用,以 > 开始。 超链接 以 []()的方式写,图片需要在前面加一个感叹号. eg: [百度](https://www.baidu.com)...

如何实现table表格中的button按钮有加载中的效果

一、如何实现table表格中的button按钮有加载中的效果 效果:  前端代码: <el-table-column label="送货单信息" align="center" width="110"> <template slot-scope="scope"> <el-button slo...

knockout前端经常用的功能

1.表单序号自增长 data-bind="text:$index()+1" 2.日期格式显示 datetime:字段名 3.实用的判断 <--  ko if: 判断条件 --> //代码块 <-- /ko --> 4.foreach循环,一般用于表格tbody使用遍历集合,形成列表 <tbody data-bind="fore...

JNI数据类型(转)

  本文原创,转载请注明出处:http://blog.csdn.net/qinjuning     在Java存在两种数据类型: 基本类型 和 引用类型 ,大家都懂的 。     在JNI的世界里也存在类似的数据类型,与Java比较起来,其范围更具严格性,如下:         1、primitive types ----基本数据类型,如:int、 flo...