转载请注明出处:http://blog.csdn.net/luotuo44/article/details/44217383
本文以get命令为例子,探讨memcached是如何处理命令的。本文只是探讨memcached处理命令的工作流程,具体的代码细节在不影响阅读的前提下能省略的就省略、能取默认值就取默认值、内存是足够的(不需要动态申请空间就够用了)。涉及到数组、缓存区的就假设已经分配好了。
现在假定memcached里面有了一个键值为”tk”的item,此时我们使用命令”get tk”获取对应item的内容。
《网络模型》展示了当memcached进程accept一个新客户端连接时,会把该连接的一些信息封装成一个conn结构体,并且把新连接的初始状态设置成conn_new_cmd。此时,worker线程等待客户端命令的到来。conn结构体有很多成员变量,后文只会列出使用到的成员。
读取命令:
等待有数据可读:
当客户端发送get命令后,memcached的event_base就会监听到客户端对应的socket fd变成可读了,接着就会调用回调函数event_handler处理这个可读事件。实际上回调函数event_handler只是一个傀儡函数,它会调用drive_machine函数进行处理。drive_machine是一个有限状态机,在真正读数据之前它会在几个状态中跳转。
void event_handler(const int fd, const short which, void *arg) {
conn *c;
c = (conn *)arg;
c->which = which;
/* sanity */
if (fd != c->sfd) {
conn_close(c);
return;
}
drive_machine(c);
/* wait for next event */
return;
}
struct conn {
int sfd;//该conn对应的socket fd
enum conn_states state;//当前状态
struct event event;//该conn对应的event
short ev_flags;//event当前监听的事件类型
short which; /** which events were just triggered */ //触发event回调函数的原因
//读缓冲区
char *rbuf; /** buffer to read commands into */
//有效数据的开始位置。从rbuf到rcurr之间的数据是已经处理的了,变成无效数据了
char *rcurr; /** but if we parsed some already, this is where we stopped */
//读缓冲区的总长度
int rsize; /** total allocated size of rbuf */
//有效数据的长度。初始值为0
int rbytes; /** how much data, starting from rcur, do we have unparsed */
...
LIBEVENT_THREAD *thread;//这个conn属于哪个worker线程
};
static void drive_machine(conn *c) {
bool stop = false;
int sfd;
int nreqs = settings.reqs_per_event;//20
int res;
const char *str;
//drive_machine被调用会进行状态判断,并进行一些处理。但也可能发生状态的转换
//此时就需要一个循环,当进行状态转换时,也能处理
while (!stop) {
switch(c->state) {
...
case conn_waiting://等待socket变成可读的
if (!update_event(c, EV_READ | EV_PERSIST)) {//更新监听事件失败
conn_set_state(c, conn_closing);
break;
}
conn_set_state(c, conn_read);
//居然stop循环,不过没关系,因为event的可读事件是水平触发的。
//马上又会再次进入有限状态机,并且进入下面的conn_read case中。
stop = true;
break;
case conn_new_cmd:
--nreqs;
if (nreqs >= 0) {//简单起见,不考虑nreqs小于0的情况
//如果该conn的读缓冲区没有数据,那么将状态改成conn_waiting
//如果该conn的读缓冲区有数据, 那么将状态改成conn_pase_cmd
reset_cmd_handler(c);
}
break;
...
}
}
return;
}
static void reset_cmd_handler(conn *c) {
c->cmd = -1;
...
//为了简单,这里假设没有数据
if (c->rbytes > 0) {//读缓冲区里面有数据
conn_set_state(c, conn_parse_cmd);//解析读到的数据
} else {
conn_set_state(c, conn_waiting);//否则等待数据的到来
}
}
//设置conn的状态
static void conn_set_state(conn *c, enum conn_states state) {
...
if (state != c->state) {
c->state = state;
}
}
读取数据:
在前面,conn的状态跳转到了conn_read。在case conn_read中,worker线程会调用try_read_network函数读取客户端发送的数据。try_read_network函数会尽可能地把所有的数据都读进conn的读缓存区中(当然也是有一个最大限度的)。
static void drive_machine(conn *c) {
bool stop = false;
int sfd;
int nreqs = settings.reqs_per_event;//20
int res;
const char *str;
//drive_machine被调用会进行状态判断,并进行一些处理。但也可能发生状态的转换
//此时就需要一个循环,当进行状态转换时,也能处理
while (!stop) {
switch(c->state) {
...
case conn_read:
//这里假定为TCP
res = IS_UDP(c->transport) ? try_read_udp(c) : try_read_network(c);
switch (res) {
…
case READ_DATA_RECEIVED://读取到了数据,接着就去解析数据