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发送数据。