原文地址:http://blog.csdn.net/ffb/article/details/17437101
使用nc訪問memcache服務的時候,發現set命令總是失敗,但是其他命令可用。換telnet一切正常,疑似伺服器bug,值得研究。
nc使用的命令是:
nc 127.0.0.1 11212
telnet使用的命令是:
telnet 127.0.0.1 11212
於是嘗試使用wireshark抓包分析資料差異,
telnet的請求資料如下:
0000 73 65 74 20 70 79 77 20 31 20 30 20 33 0d 0a 31 set pyw 1 0 3..10010 30 30 0d 0a 00..
nc的請求資料如下:
0000 73 65 74 20 70 79 77 20 31 20 30 20 33 0a set pyw 1 0 3.0000 31 30 30 0a 100.
發現nc發送的斷行符號符是0A,telnet發送的斷行符號符是0D 0A,應該是這個差異導致的問題,但是nc發送其他命令都可以正確執行,更加懷疑伺服器代碼有bug。
於是查看原始碼,處理命令格式的代碼在:
memcached.c::try_read_command
2455 cont = el + 1; 2456 if ((el - c->rcurr) > 1 && *(el - 1) == '\r') { 2457 el--; 2458 } 2459 *el = '\0';
其中,el是
2433 el = memchr(c->rcurr, '\n', c->rbytes);
看似代碼沒啥問題。於是向上看,讀取命令和判斷的位置在drive_machine(),其中c->state是用來控制處理流程,所以只要跟蹤這個變數的變化即可。於是跟蹤:
$ gdb ./memcached(gdb) set args -A 11212(gdb) b try_read_commandBreakpoint 1 at 0x409e79: file memcached.c, line 2422.(gdb) r
用戶端輸入
get pywENDset pyw 0 0 3100
可以看出,對於來自ns的set命令,try_read_command之後,狀態值變為conn_nread而不是conn_mwrite,意思是還需要繼續讀取命令參數。所以增加conn_nread的斷點
(gdb) b 2848
調試之後看到了這段代碼:
2848 if (c->rlbytes == 0) { 2849 complete_nread(c); 2850 break; 2851 }跟進去,就發現了非常可疑的代碼:
948 if (strncmp(ITEM_data(it) + it->nbytes - 2, "\r\n", 2) != 0) {
修改為
948 if (strncmp(ITEM_data(it) + it->nbytes - 2, "\r\n", 2) != 0 && strncmp(ITEM_data(it) + it->nbytes - 1, "\n", 1) != 0) {編譯運行,重新執行上面的命令,多斷行符號一次,探索資料成功的儲存了。
而為什麼要多斷行符號一次,是因為memcache伺服器端整個狀態的流轉,都是靠已經讀取的位元組數來驅動的,當使用者輸入了set命令,會由process_update_command函數設定一個期望接收的位元組數到變數conn::rlbytes,而這個期望數值預設是按照\r\n來計算的,比如希望再輸入三個位元組,這個值就會被填寫為5。
由於代碼中多處處理方法都和\r\n高度綁定,比如item_make_header(),如果想徹底解決這個問題,最好是在首個命令之後分析終端的斷行符號符類型,統一標識,在標識的基礎上對相關的代碼進行相容性改進。但是由於涉及的修改比較多,本次研究就到這裡了。
在結束前,我又換了個思路。這裡涉及的問題實際是memcache的通訊協議規定了\r\n是標準的命令結束方法,所以伺服器端代碼實際上不需要進行任何修改,只要用戶端按照協議要求發送\r\n作為斷行符號即可(在改變之前不妨先看看是否可以遵守),man了一下nc,嘗試如下命令:
$ nc -C 127.0.0.1 11211set pyw 1 0 3100STORED
OK,問題解決。