Nginx事件管理之事件处理流程
1. 概述
事件处理要解决的两个问题:
- "惊群" 问题,即多个 worker 子进程监听相同端口时,在 accept 建立新连接时会有争抢,引发不必要的上下文切换,
增加系统开销。 - 负载均衡问题。
这两个问题的解决需要依靠 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事件管理之事件处理流程的更多相关文章
- Nginx事件管理之概念描述
1. Nginx事件管理概述 首先,Nginx定义了一个核心模块ngx_events_module,这样在Nginx启动时会调用ngx_init_cycle方法解析配置项,一旦在 nginx.conf ...
- Nginx事件管理之ngx_event_core_module模块
1. 概述 ngx_event_core_module 模块是一个事件类型的模块,它在所有事件模块中的顺序是第一位.它主要完成以下两点任务: 创建连接池(包括读/写事件): 决定究竟使用哪些事件驱动机 ...
- Nginx事件管理之定时器事件
1. 缓存时间 1.1 管理 Nginx 中的每个进程都会单独地管理当前时间.ngx_time_t 结构体是缓存时间变量的类型: typedef struct { /* 格林威治时间1970年1月1日 ...
- Nginx事件管理之epoll模块
1. epoll 原理 假设有 100 万用户同时与一个进程保持着 TCP 连接,而每一时刻只有几十个或几百个 TCP 连接时活跃的(接收到 TCP 包),也就是说,在每一时刻,进程只需要处理这 10 ...
- Nginx事件管理之核心模块ngx_events_module
1. ngx_events_module核心模块的功能介绍 ngx_events_module 模式是一个核心模块,它的功能如下: 定义新的事件类型 定义每个事件模块都需要实现的ngx_event_m ...
- Nginx事件管理机制-epoll
epoll的最大好处在于他不会随着被监控描述符的数目的增长而导致效率极致下降. select是遍历扫描来判断每个描述符是否有事件发生,当监控的描述付越多时,时间消耗就越多,并且由于系统的限制selec ...
- Cocoa Touch事件处理流程--响应者链
Cocoa Touch事件处理流程--响应者链 作者:wangzz 原文地址:http://blog.csdn.net/wzzvictory/article/details/9264335 转载请注明 ...
- View的事件处理流程
一直对view的事件处理流程迷迷糊糊,今天花了点时间写了个栗子把它弄明白了. 1.view的常用的事件分为:单击事件(onClick).长按事件(onLongClick).触摸事件(onTouch), ...
- Redis 内存管理与事件处理
1 Redis内存管理 Redis内存管理相关文件为zmalloc.c/zmalloc.h,其只是对C中内存管理函数做了简单的封装,屏蔽了底层平台的差异,并增加了内存使用情况统计的功能. void * ...
随机推荐
- mqtt协议实现 java服务端推送功能(二)java demo测试
上一篇写了安装mosQuitto和测试,但是用cmd命令很麻烦,有没有一个可视化软件呢? 有,需要在google浏览器下载一个叫MQTTLens的插件 打开MQTTLens后界面如下: 打开conne ...
- 09 Python之IO多路复用
四种常见IO模型 阻塞IO(blocking IO).非阻塞IO(nonblocking IO).IO多路复用(IOmultiplexing).异步IO(asynchronous IO) IO发生时涉 ...
- Centos7:solr伪集群(SolrCloud)搭建
JDK,tocmat环境搭建 zookeeper集群安装 解压缩zookeeper的压缩包 创建data目录 复制zoo_sample.cfg为zoo.cfg 修改confg/zoo.cfg中 dat ...
- VSCode中Markdown目录显示异常
更新最新的VSCode之后编辑Markdown文件发现TOC标签的目录格式异常,发现是因为行尾字符导致,必须设置行尾字符进行解决.
- SmartBinding与kbmMW#3
前言 在SmartBinding #2中,我介绍了新的自动绑定功能,支持在Form设计器中直接定义绑定.不仅如此,kbmMW SmartBind还有更多很酷的功能,即将发布的kbmMW中的SmartB ...
- MySQL增删改查语句
创建数据库:CREATE DATABASE 数据库名; 创建数据表:CREATE TABLE table_name (column_name column_type); 插入数据:INSERT INT ...
- fastadmin 列表展示时字段值截取
{field: '字段名', title: __('lang中的语言名'),formatter:function(value,row,index){ value=value?value:''; var ...
- C# 图像基本处理
使用第三方:AForge实现视频采集(实现视频采集.暂停) 实现图片的常用处理功能:旋转.反色.灰度.放大.缩小.模糊.拉伸.增强.锐化.裁剪...... 实现对图片进行文字编辑......
- zencart批量评论插件Easy Populate CSV add reviews使用教程
此插件在Easy Populate CSV 1.2.5.7b产品批量插件基础上开发,有1.3x与1.5x两个版本. zencart批量评论插件Easy Populate CSV add reviews ...
- MyBatis---join 查询
在实际业务中,经常能碰到多表关联查询 下面的Demo,讲举例join查询在MyBatis中的实现 User 类: package com.zy.domain; import java.io.Seria ...