memcache源码分析系列之二 网络通信及状态变化

memcache的网络通信主要是通过libevent来实现的。

我们结合源代码来看下,有tcp、udp两种方式,主要讲解tcp

1.创建tcp的监听

if (IS_UDP(transport)) {
            int c;

            for (c = 0; c < settings.num_threads_per_udp; c++) {
                /* Allocate one UDP file descriptor per worker thread;
                 * this allows "stats conns" to separately list multiple
                 * parallel UDP requests in progress.
                 *
                 * The dispatch code round-robins new connection requests
                 * among threads, so this is guaranteed to assign one
                 * FD to each thread.
                 */
                int per_thread_fd = c ? dup(sfd) : sfd;
                dispatch_conn_new(per_thread_fd, conn_read,
                                  EV_READ | EV_PERSIST,
                                  UDP_READ_BUFFER_SIZE, transport);
            }
        } else {
            if (!(listen_conn_add = conn_new(sfd, conn_listening,
                                             EV_READ | EV_PERSIST, 1,
                                             transport, main_base))) {
                fprintf(stderr, "failed to create listening connection\n");
                exit(EXIT_FAILURE);
            }
            listen_conn_add->next = listen_conn;
            listen_conn = listen_conn_add;
        }

通过以上代码,可以发现udp调用dispatch_conn_new,寻找一个工作线程来处理;tcp调用conn_new,添加事件监听。

注意conn_new传入的第二个参数是conn_listening,表示监听的conn连接的初始状态是conn_listening;

最后一个参数是main_base,main_base在主线程初始化,并在主线程调用event_base_loop(main_base, 0),可见tcp监听的事件循环是在主线程完成的;

对sfd事件的回调是event_handler。


2.tcp监听到连接的处理

监听的conn的fd有事件到达,event_handler调用drive_machine,c->state的状态是conn_listening,

调用accept并进行一些初始化之后,调用dispatch_conn_new进行后续处理。

dispatch_conn_new(sfd, conn_new_cmd, EV_READ | EV_PERSIST, DATA_BUFFER_SIZE, tcp_transport);

dispatch首先创建CQ_ITEM对象,循环选择一个线程,然后添加到thread->new_conn_queue中,接着往

线程的管道中写入数据通知线程有数据要处理,注意此时写入的是c,表示有新的连接。之前对管道的fd添

加了事件监听,回调是thread_libevent_process。

    int tid = (last_thread + 1) % settings.num_threads;

    LIBEVENT_THREAD *thread = threads + tid;

    last_thread = tid;

    item->sfd = sfd;
    item->init_state = init_state;
    item->event_flags = event_flags;
    item->read_buffer_size = read_buffer_size;
    item->transport = transport;

    cq_push(thread->new_conn_queue, item);

    MEMCACHED_CONN_DISPATCH(sfd, thread->thread_id);
    buf[0] = 'c';
    if (write(thread->notify_send_fd, buf, 1) != 1) {
        perror("Writing to thread notify pipe");
    }

thread_libevent_process从管道中读取数据,c表示有新连接,从new_conn_queue中读取CQ_ITEM,

获得连接的信息,调用conn_new创建新连接,此时的初始状态是conn_new_cmd,回调函数就像创建

监听处理一样(因为调用同样函数),也是event_handler,关联的事件循环就是当前的工作线程。

conn *c = conn_new(item->sfd, item->init_state, item->event_flags,
                           item->read_buffer_size, item->transport, me->base);

3.服务器处理客户端发送的数据

conn_new_cmd状态:

如果nreqs大于0,则reset_cmd_handler进行一些处理(清理item,conn_shrink瘦身处理,有数据则

进入conn_parse_cmd状态,没有数据进入conn_waiting状态。如果nreqs小于等于0,则更新事件监听

update_event(c, EV_WRITE | EV_PERSIST),注意添加了写的监听。

conn_waiting状态:

update_event(c, EV_READ | EV_PERSIST),添加读的监听。

进入conn_read状态。

conn_read状态:

tcp调用try_read_network接收数据。try_read_network中有些变量意义要说明一下。

rbuf:数据存储在数组中的头位置。

rcurr:数据处理rbuf到什么位置

rsize:rbuf的长度

rbytes:已经read读取到的数据长度

rlbytes:还需要读取的长度,接收命令可能不完整

(如telnet的时候,回车就发送,value的话需再接收一次)


try_read_network的流程:

判断是否需要移动rcurr,是否需要分配空间,读取read数据。

static enum try_read_result try_read_network(conn *c) {
    enum try_read_result gotdata = READ_NO_DATA_RECEIVED;
    int res;
    int num_allocs = 0;
    assert(c != NULL);

    if (c->rcurr != c->rbuf) {
        if (c->rbytes != 0) /* 判断读取到数据的长度 */
            memmove(c->rbuf, c->rcurr, c->rbytes);
        c->rcurr = c->rbuf;
    }
    
    while (1) {
        if (c->rbytes >= c->rsize) {//空间不足时,重新分配空间
            if (num_allocs == 4) {//最多重新分配四次,初始大小为
                return gotdata;
            }
            ++num_allocs;
            char *new_rbuf = realloc(c->rbuf, c->rsize * 2);
            if (!new_rbuf) {
                STATS_LOCK();
                stats.malloc_fails++;
                STATS_UNLOCK();
                if (settings.verbose > 0) {
                    fprintf(stderr, "Couldn't realloc input buffer\n");
                }
                c->rbytes = 0; /* ignore what we read */
                out_of_memory(c, "SERVER_ERROR out of memory reading request");
                c->write_and_go = conn_closing;
                return READ_MEMORY_ERROR;
            }
            c->rcurr = c->rbuf = new_rbuf;
            c->rsize *= 2;
        }
        //开始接收数据
        int avail = c->rsize - c->rbytes;
        res = read(c->sfd, c->rbuf + c->rbytes, avail);
        if (res > 0) {
            pthread_mutex_lock(&c->thread->stats.mutex);
            c->thread->stats.bytes_read += res;
            pthread_mutex_unlock(&c->thread->stats.mutex);
            gotdata = READ_DATA_RECEIVED;
            c->rbytes += res;//更新rbytes的值
            if (res == avail) {
                continue;
            } else {
                break;
            }
        }
        if (res == 0) {
            return READ_ERROR;
        }
        if (res == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                break;
            }
            return READ_ERROR;
        }
    }
    return gotdata;
}

读取到数据,跳到conn_parse_cmd状态

conn_parse_cmd状态:

调用try_read_command,telnet走的是ascii_prot协议。

try_read_command处理流程:

1.首先处理下读取到的数据,例如

发送的是set abc 0 0 8\r\n

处理后是set abc 0 0 8\0\n

2.调用process_cmd,首先调用tokenize_command,处理接收到的数据按空格分开存储到tokens中,

按tokens的长度及命名名进行处理,此时我们分析的是set,调用

process_update_command(c, tokens, ntokens, comm, false);

调用item_alloc,取得一个Item的存储空间,并初始化,此时key suffix等信息已经存到item的data中。

    it = item_alloc(key, nkey, flags, realtime(exptime), vlen);

    if (it == 0) {
        。。。//先忽略
    }
    ITEM_set_cas(it, req_cas_id);

    c->item = it;
    c->ritem = ITEM_data(it);//ritem表示value存放的位置
    c->rlbytes = it->nbytes;//设置为value的长度
    c->cmd = comm; //命令类型,int型
    conn_set_state(c, conn_nread);

conn_nread状态:(value可能还未读取)

1.数据未读取完读取数据,剩余的value读取到item->data中。

2.读取完调用 complete_nread,最终调用store_item

store_item首先上锁,调用do_store_item,set的话,实际调用do_item_link,然后解锁。

int do_item_link(item *it, const uint32_t hv) {
    MEMCACHED_ITEM_LINK(ITEM_key(it), it->nkey, it->nbytes);
    assert((it->it_flags & (ITEM_LINKED|ITEM_SLABBED)) == 0);
    mutex_lock(&cache_lock);
    it->it_flags |= ITEM_LINKED;
    it->time = current_time;

    STATS_LOCK();
    stats.curr_bytes += ITEM_ntotal(it);
    stats.curr_items += 1;
    stats.total_items += 1;
    STATS_UNLOCK();

    /* Allocate a new CAS ID on link. */
    ITEM_set_cas(it, (settings.use_cas) ? get_cas_id() : 0);
    assoc_insert(it, hv);//插入到hash表中
    item_link_q(it);//插入到最近最小使用lru的链表中
    refcount_incr(&it->refcount);
    mutex_unlock(&cache_lock);

    return 1;
}

如果set成功,调用out_string(c, "STORED");


outstring函数

调用add_msghdr,

将需要发送数据copy到wbuf,并添加\r\n

调用conn_set_state(c, conn_write)

conn_write状态:memcache发送数据

在conn_write之后并未break,所以继续执行到case-break为止

(即使c->state!=conn_mwrite,conn_mwrite仍然执行)

        case conn_write:
            /*
             * We want to write out a simple response. If we haven't already,
             * assemble it into a msgbuf list (this will be a single-entry
             * list for TCP or a two-entry list for UDP).
             */
            if (c->iovused == 0 || (IS_UDP(c->transport) && c->iovused == 1)) {
                if (add_iov(c, c->wcurr, c->wbytes) != 0) {
                    if (settings.verbose > 0)
                        fprintf(stderr, "Couldn't build response\n");
                    conn_set_state(c, conn_closing);
                    break;
                }
            }

            /* fall through... */

        case conn_mwrite:
          if (IS_UDP(c->transport) && c->msgcurr == 0 && build_udp_headers(c) != 0) {
            if (settings.verbose > 0)
              fprintf(stderr, "Failed to build UDP headers\n");
            conn_set_state(c, conn_closing);
            break;
          }
            switch (transmit(c)) {
调用transmit发送数据。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值