Linux c 開發,linux開發
前言
從我們上一章《Linux c 開發 - Memcached源碼分析之基於Libevent的網路模型》我們基本瞭解了Memcached的網路模型。這一章節,我們需要詳細解讀Memcached的命令解析。
我們回顧上一章發現Memcached會分成主線程和N個背景工作執行緒。主線程主要用於監聽accpet用戶端的Socket串連,而背景工作執行緒主要用於接管具體的用戶端串連。
主線程和背景工作執行緒之間主要通過基於Libevent的pipe的讀寫事件來監聽,當有串連練上來的時候,主線程會將串連交個某一個背景工作執行緒去接管,後期用戶端和服務端的讀寫工作都會在這個背景工作執行緒中進行。
背景工作執行緒也是基於Libevent的事件的,當有讀或者寫的事件進來的時候,就會觸發事件的回呼函數。
那麼Memcached是如何來解析用戶端上傳的命令資料報文的呢?下面我們會詳細講解。
Memcached的命令解析源碼分析
從drive_machine開始
我們上一節看到用戶端串連的讀寫事件回呼函數:event_handler,這個方法中最終調用的是drive_machine。
void event_handler(const int fd, const short which, void *arg) {conn *c;//組裝conn結構c = (conn *) arg;assert(c != NULL);c->which = which;/* sanity */if (fd != c->sfd) {if (settings.verbose > 0)fprintf(stderr, "Catastrophic: event fd doesn't match conn fd!\n");conn_close(c);return;}//最終轉交給了drive_machine這個方法drive_machine(c);/* wait for next event */return;}
conn資料結構
每一個串連都會有自己的一個conn資料結構。這個結構主要儲存每個串連的基本資料。
這一章中用到的幾個比較重要的參數:
char * rbuf:用於儲存用戶端資料報文中的命令。
int rsize:rbuf的大小。
char * rcurr:未解析的命令的字元指標。
int rbytes:為解析的命令的長度。
typedef struct conn conn;struct conn { int sfd; sasl_conn_t *sasl_conn; bool authenticated; enum conn_states state; enum bin_substates substate; rel_time_t last_cmd_time; struct event event; short ev_flags; short which; /** which events were just triggered */ char *rbuf; /** buffer to read commands into */ char *rcurr; /** but if we parsed some already, this is where we stopped */ int rsize; /** total allocated size of rbuf */ int rbytes; /** how much data, starting from rcur, do we have unparsed */ char *wbuf; char *wcurr; int wsize; int wbytes; /** which state to go into after finishing current write */ enum conn_states write_and_go; void *write_and_free; /** free this memory after finishing writing */ char *ritem; /** when we read in an item's value, it goes here */ int rlbytes; /* data for the nread state */ /** * item is used to hold an item structure created after reading the command * line of set/add/replace commands, but before we finished reading the actual * data. The data is read into ITEM_data(item) to avoid extra copying. */ void *item; /* for commands set/add/replace */ /* data for the swallow state */ int sbytes; /* how many bytes to swallow */ /* data for the mwrite state */ struct iovec *iov; int iovsize; /* number of elements allocated in iov[] */ int iovused; /* number of elements used in iov[] */ struct msghdr *msglist; int msgsize; /* number of elements allocated in msglist[] */ int msgused; /* number of elements used in msglist[] */ int msgcurr; /* element in msglist[] being transmitted now */ int msgbytes; /* number of bytes in current msg */ item **ilist; /* list of items to write out */ int isize; item **icurr; int ileft; char **suffixlist; int suffixsize; char **suffixcurr; int suffixleft; enum protocol protocol; /* which protocol this connection speaks */ enum network_transport transport; /* what transport is used by this connection */ /* data for UDP clients */ int request_id; /* Incoming UDP request ID, if this is a UDP "connection" */ struct sockaddr_in6 request_addr; /* udp: Who sent the most recent request */ socklen_t request_addr_size; unsigned char *hdrbuf; /* udp packet headers */ int hdrsize; /* number of headers' worth of space is allocated */ bool noreply; /* True if the reply should not be sent. */ /* current stats command */ struct { char *buffer; size_t size; size_t offset; } stats; /* Binary protocol stuff */ /* This is where the binary header goes */ protocol_binary_request_header binary_header; uint64_t cas; /* the cas to return */ short cmd; /* current command being processed */ int opaque; int keylen; conn *next; /* Used for generating a list of conn structures */ LIBEVENT_THREAD *thread; /* Pointer to the thread object serving this connection */};
drive_machine:
drive_machine這個方法中,都是通過c->state來判斷需要處理的邏輯。
conn_listening:監聽狀態
conn_waiting:等待狀態
conn_read:讀取狀態
conn_parse_cmd:命令列解析
static void drive_machine(conn *c) {bool stop = false;int sfd;socklen_t addrlen;struct sockaddr_storage addr;int nreqs = settings.reqs_per_event;int res;const char *str;#ifdef HAVE_ACCEPT4static int use_accept4 = 1;#elsestatic int use_accept4 = 0;#endifassert(c != NULL);while (!stop) {switch (c->state) {case conn_listening://.......更多代碼}
我們繼續看一下conn_read、conn_wait和conn_parse_cmd狀態的代碼。
1. 當用戶端有資料報文上來的時候,會觸發conn_read這個case。
2. conn_read會去讀取socket的資料,如果沒有讀取到資料,則會去調用conn_waiting這個case,會繼續等待用戶端資料報文上報。
3. 如果conn_read中發生意外錯誤,則會調用conn_close這個case,會關閉用戶端串連。
4. 如果conn_read讀取到了資料,或者本身buf中有資料,則會去解析命令,調用conn_parse_cmd這個case。
//這邊是繼續等待用戶端的資料報文到來case conn_waiting:if (!update_event(c, EV_READ | EV_PERSIST)) {if (settings.verbose > 0)fprintf(stderr, "Couldn't update event\n");conn_set_state(c, conn_closing);break;}//等待的過程中,將串連狀態設定為讀取狀態,並且stop設定為true,退出while(stop)的迴圈conn_set_state(c, conn_read);stop = true;break;//讀取資料的事件,當用戶端有資料報文上傳的時候,就會觸發libevent的讀事件case conn_read://try_read_network 主要讀取TCP資料//返回try_read_result的枚舉類型結構,通過這個枚舉類型,來判斷是否已經讀取到資料,是否讀取失敗等情況res = IS_UDP(c->transport) ? try_read_udp(c) : try_read_network(c);switch (res) {//沒有讀取到資料,那麼繼續將事件設定為等待。//while(stop)會繼續迴圈,去調用conn_waiting這個casecase READ_NO_DATA_RECEIVED:conn_set_state(c, conn_waiting);break;//如果有資料讀取到了,這個時候就需要調用conn_parse_cmd邏輯//conn_parse_cmd:主要用來解析讀取到的命令case READ_DATA_RECEIVED:conn_set_state(c, conn_parse_cmd);break;//讀取失敗的狀態,則直接調用conn_closing 關閉用戶端的串連case READ_ERROR:conn_set_state(c, conn_closing);break;case READ_MEMORY_ERROR: /* Failed to allocate more memory *//* State already set by try_read_network */break;}break;//這邊是解析Memcached的用戶端命令,例如解析:set username zhulicase conn_parse_cmd://try_read_command方法很關鍵,用來讀取命令//如果這個方法返回為0,則表示解析命令失敗(因為TCP粘包拆包的原因,可能命令不完整,需要繼續等待資料到來)if (try_read_command(c) == 0) {/* wee need more data! *///這邊的注釋貌似寫錯誤了吧,應該是we need more data!conn_set_state(c, conn_waiting);}break;
try_read_network
這個方法主要是讀取TCP網路資料。讀取到的資料會放進c->rbuf的buf中。
如果buf沒有空間儲存更多資料的時候,就會觸發記憶體塊的重新分配。重新分配,memcached限制了4次,估計是擔憂用戶端的而已攻擊導致儲存命令列資料報文的buf不斷的ralloc。
//這個方法是通過TCP的方式讀取用戶端傳遞過來的命令資料static enum try_read_result try_read_network(conn *c) {//這個方法會最終返回try_read_result的枚舉類型//預設設定READ_NO_DATA_RECEIVED:沒有接受到資料enum try_read_result gotdata = READ_NO_DATA_RECEIVED;int res;int num_allocs = 0;assert(c != NULL);//c->rcurr 存放未解析命令內容指標 c->rbytes 還有多少沒解析過的資料//c->rbuf 用於讀取命令的buf,儲存命令字串的指標 c->rsize rbuf的sizeif (c->rcurr != c->rbuf) {if (c->rbytes != 0) /* otherwise there's nothing to copy */memmove(c->rbuf, c->rcurr, c->rbytes);c->rcurr = c->rbuf;}//迴圈從fd中讀取資料while (1) {//如果buf滿了,則需要重新分配一塊更大的記憶體//當未解析的資料size 大於等於 buf塊的size,則需要重新分配if (c->rbytes >= c->rsize) {//最多分配4次if (num_allocs == 4) {return gotdata;}++num_allocs;//從新分配一塊新的記憶體塊,記憶體大小為rsize的兩倍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指向到新的buf塊c->rcurr = c->rbuf = new_rbuf;c->rsize *= 2; //rsize則乘以2}//avail可以計算出buf塊中剩餘的空間多大int avail = c->rsize - c->rbytes;//這邊我們可以看到Socket的讀取方法//c->sfd為Socket的ID//c->rbuf + c->rbytes 意思是從buf塊中空餘的記憶體位址開始存放新讀取到的資料//avail 每次接收最大能讀取多大的資料res = read(c->sfd, c->rbuf + c->rbytes, avail);//如果接受到的結果res大於0,則說明Socket中讀取到了資料//設定成READ_DATA_RECEIVED枚舉類型,表明讀取到了資料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; //未處理的資料量 + 當前讀取到的命令sizeif (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;}
try_read_command
這個方法主要是用來讀取rbuf中的命令的。
例如命令:set username zhuli\r\n get username \n
則會通過\n這個分行符號來分隔資料報文中的命令。因為資料報文會有粘包和拆包的特性,所以只有等到命令列完整了才能進行解析。所有只有匹配到了\n符號,才能匹配一個完整的命令。
//如果我們已經在c->rbuf中有可以處理的命令列了,則就可以調用此函數來處理命令解析static int try_read_command(conn *c) {assert(c != NULL);assert(c->rcurr <= (c->rbuf + c->rsize)); //這邊斷言assert(c->rbytes > 0);if (c->protocol == negotiating_prot || c->transport == udp_transport) {if ((unsigned char) c->rbuf[0] == (unsigned char) PROTOCOL_BINARY_REQ) {c->protocol = binary_prot;} else {c->protocol = ascii_prot;}if (settings.verbose > 1) {fprintf(stderr, "%d: Client using the %s protocol\n", c->sfd,prot_text(c->protocol));}}//有兩種模式,是否是二進位模式還是ascii模式if (c->protocol == binary_prot) {//更多代碼} else {//這邊主要處理非二進位模式的命令解析char *el, *cont;//如果c->rbytes==0 表示buf容器中沒有可以處理的命令報文,則返回0//0 是讓程式繼續等待接收新的用戶端報文if (c->rbytes == 0)return 0;//尋找命令中是否有\n,memcache的命令通過\n來分割//當用戶端的資料報文過來的時候,Memcached通過尋找接收到的資料中是否有\n分行符號來判斷收到的命令資料包是否完整//例如命令:set username 10234344 \n get username \n//這個命令就可以分割成兩個命令,分別是set和get的命令//el返回\n的字元指標地址el = memchr(c->rcurr, '\n', c->rbytes);//如果沒有找到\n,說明命令不完整,則返回0,繼續等待接收新的用戶端資料報文if (!el) {//c->rbytes是接收到的資料包的長度//這邊非常有趣,如果一次接收的資料報文大於了1K,則Memcached回去判斷這個請求是否太大了,是否有問題?//然後會關閉這個用戶端的連結if (c->rbytes > 1024) {/* * We didn't have a '\n' in the first k. This _has_ to be a * large multiget, if not we should just nuke the connection. */char *ptr = c->rcurr;while (*ptr == ' ') { /* ignore leading whitespaces */++ptr;}if (ptr - c->rcurr > 100|| (strncmp(ptr, "get ", 4) && strncmp(ptr, "gets ", 5))) {conn_set_state(c, conn_closing);return 1;}}return 0;}//如果找到了\n,說明c->rcurr中有完整的命令了cont = el + 1; //下一個命令開始的指標節點//這邊判斷是否是\r\n,如果是\r\n,則el往前移一位if ((el - c->rcurr) > 1 && *(el - 1) == '\r') {el--;}//然後將命令的最後一個字元用 \0(字串結束符號)來分隔*el = '\0';assert(cont <= (c->rcurr + c->rbytes));c->last_cmd_time = current_time; //最後命令時間//處理命令,c->rcurr就是命令process_command(c, c->rcurr);c->rbytes -= (cont - c->rcurr); //這個地方為何不這樣寫?c->rbytes = c->rcurr - contc->rcurr = cont; //將c->rcurr指向到下一個命令的指標節點assert(c->rcurr <= (c->rbuf + c->rsize));}return 1;}
process_command
這個方法主要用來處理具體的命令。將命令分解後,分發到不同的具體操作中去。
//命令處理函數//前一個方法中,我們找到了rbuf中\n的字元,然後將其替換成\0static void process_command(conn *c, char *command) {//tokens結構,這邊會將c->rcurr(command)命令拆分出來//並且將命令通過空格符號來分隔成多個元素//例如:set username zhuli,則會拆分成3個元素,分別是set和username和zhuli//MAX_TOKENS最大值為8,說明memcached的命令列,最多可以拆分成8個元素token_t tokens[MAX_TOKENS];size_t ntokens;int comm;assert(c != NULL);MEMCACHED_PROCESS_COMMAND_START(c->sfd, c->rcurr, c->rbytes);if (settings.verbose > 1)fprintf(stderr, "<%d %s\n", c->sfd, command);/* * for commands set/add/replace, we build an item and read the data * directly into it, then continue in nread_complete(). */c->msgcurr = 0;c->msgused = 0;c->iovused = 0;if (add_msghdr(c) != 0) {out_of_memory(c, "SERVER_ERROR out of memory preparing response");return;}//tokenize_command非常重要,主要就是拆分命令的//並且將拆分出來的命令元素放進tokens的數組中//參數:command為命令ntokens = tokenize_command(command, tokens, MAX_TOKENS);//tokens[COMMAND_TOKEN] COMMAND_TOKEN=0//分解出來的命令的第一個參數為操作方法if (ntokens >= 3&& ((strcmp(tokens[COMMAND_TOKEN].value, "get") == 0)|| (strcmp(tokens[COMMAND_TOKEN].value, "bget") == 0))) {//處理get命令process_get_command(c, tokens, ntokens, false);} else if ((ntokens == 6 || ntokens == 7)&& ((strcmp(tokens[COMMAND_TOKEN].value, "add") == 0 && (comm =NREAD_ADD))|| (strcmp(tokens[COMMAND_TOKEN].value, "set") == 0&& (comm = NREAD_SET))|| (strcmp(tokens[COMMAND_TOKEN].value, "replace") == 0&& (comm = NREAD_REPLACE))|| (strcmp(tokens[COMMAND_TOKEN].value, "prepend") == 0&& (comm = NREAD_PREPEND))|| (strcmp(tokens[COMMAND_TOKEN].value, "append") == 0&& (comm = NREAD_APPEND)))) {//處理更新命令process_update_command(c, tokens, ntokens, comm, false);//更多代碼....}
tokenize_command:
這個方法主要用於分解命令。具體是將一個命令語句分解成多個元素。
例如:set username zhuli\n
則會分解成三個元素:set和username和zhuli這三個元素。
//拆分命令方法static size_t tokenize_command(char *command, token_t *tokens,const size_t max_tokens) {char *s, *e;size_t ntokens = 0; //命令參數遊標size_t len = strlen(command); //命令長度unsigned int i = 0;assert(command != NULL && tokens != NULL && max_tokens > 1);s = e = command;for (i = 0; i < len; i++) {//指標不停往前走,如果遇到空格,則會停下來,將命令元素拆分出來,放進tokens這個數組中if (*e == ' ') {if (s != e) {tokens[ntokens].value = s;tokens[ntokens].length = e - s;ntokens++;//這邊將空格替換成\0//Memcached這邊的代碼寫的非常的好,這邊的命令進行切割的時候,並沒有將記憶體塊進行拷貝,而是在原來的記憶體塊上進行切割*e = '\0';//最多8個元素if (ntokens == max_tokens - 1) {e++;s = e; /* so we don't add an extra token */break;}}s = e + 1;}e++;}if (s != e) {tokens[ntokens].value = s;tokens[ntokens].length = e - s;ntokens++;}/* * If we scanned the whole string, the terminal value pointer is null, * otherwise it is the first unprocessed character. */tokens[ntokens].value = *e == '\0' ? NULL : e;tokens[ntokens].length = 0;ntokens++;//傳回值為參數個數,例如分解出3個元素,則返回3return ntokens;}
process_get_command
get的命令例子:
//處理GET請求的命令static inline void process_get_command(conn *c, token_t *tokens, size_t ntokens,bool return_cas) {//處理GET命令char *key;size_t nkey;int i = 0;item *it;//&tokens[0] 是操作的方法//&tokens[1] 為key//token_t 儲存了value和lengthtoken_t *key_token = &tokens[KEY_TOKEN];char *suffix;assert(c != NULL);do {//如果key的長度不為0while (key_token->length != 0) {key = key_token->value;nkey = key_token->length;//判斷key的長度是否超過了最大的長度,memcache key的最大長度為250//這個地方需要非常注意,我們在平常的使用中,還是要注意key的位元組長度的if (nkey > KEY_MAX_LENGTH) {//out_string 向外部輸出資料out_string(c, "CLIENT_ERROR bad command line format");while (i-- > 0) {item_remove(*(c->ilist + i));}return;}//這邊是從Memcached的記憶體儲存快中去取資料it = item_get(key, nkey);if (settings.detail_enabled) {//狀態記錄,key的記錄數的方法stats_prefix_record_get(key, nkey, NULL != it);}//如果擷取到了資料if (it) {//c->ilist 存放用於向外部寫資料的buf//如果ilist太小,則重新分配一塊記憶體if (i >= c->isize) {item **new_list = realloc(c->ilist,sizeof(item *) * c->isize * 2);if (new_list) {c->isize *= 2;c->ilist = new_list;} else {STATS_LOCK();stats.malloc_fails++;STATS_UNLOCK();item_remove(it);break;}}/* * Construct the response. Each hit adds three elements to the * outgoing data list: * "VALUE " * key * " " + flags + " " + data length + "\r\n" + data (with \r\n) *///初始化返回出去的資料結構if (return_cas) {//更多代碼....}/* * If the command string hasn't been fully processed, get the next set * of tokens. *///如果命令列中的命令沒有全部被處理,則繼續下一個命令//一個命令列中,可以get多個元素if (key_token->value != NULL) {ntokens = tokenize_command(key_token->value, tokens, MAX_TOKENS);key_token = tokens;}} while (key_token->value != NULL);c->icurr = c->ilist;c->ileft = i;if (return_cas) {c->suffixcurr = c->suffixlist;c->suffixleft = i;}if (settings.verbose > 1)fprintf(stderr, ">%d END\n", c->sfd);/* If the loop was terminated because of out-of-memory, it is not reliable to add END\r\n to the buffer, because it might not end in \r\n. So we send SERVER_ERROR instead. */if (key_token->value != NULL || add_iov(c, "END\r\n", 5) != 0|| (IS_UDP(c->transport) && build_udp_headers(c) != 0)) {out_of_memory(c, "SERVER_ERROR out of memory writing get response");} else {conn_set_state(c, conn_mwrite);c->msgcurr = 0;}}