一 前言

  本文是《JDFS:一款分布式文件管理實(shí)用程序》系列博客的第二篇,在上一篇博客中,筆者向讀者展示了JDFS的核心功能部分,包括:服務(wù)端與客戶端部分的上傳、下載功能的實(shí)現(xiàn),epoll的運(yùn)用,線程池的運(yùn)用等。當(dāng)然目前JDFS還僅僅支持上傳、下載功能,還不具備分布式文件管理的功能,這些都會在后續(xù)的開發(fā)過程中加進(jìn)來。在寫博客的過程中,筆者發(fā)現(xiàn)最好是每完成一個小的功能就及時(shí)用博客記錄下來,如果等功能全部實(shí)現(xiàn)完成后再寫博客的話,一方面由于功能點(diǎn)比較多,博客寫起來會比較費(fèi)力;另一方面由于時(shí)間間隔太長,有些有價(jià)值的細(xì)節(jié)恐怕會忘記。所以最好是增量式寫博客,每實(shí)現(xiàn)一個關(guān)鍵點(diǎn)的功能,及時(shí)用博客記錄下來。本文是在該系列博客第一篇的基礎(chǔ)上做了部分更新升級以及解決一些小bug.當(dāng)然主要針對的是上傳部分的功能。如果讀者對這篇博客的背景不是太了解的話,請先移步筆者的上一篇博客:點(diǎn)擊我 。

  根據(jù)上一篇博客我們知道,JDFS的服務(wù)端主程序在epoll里面先recv客戶端的數(shù)據(jù),然后解析頭部,根據(jù)請求類型,把作業(yè)交給線程池來執(zhí)行。對于查詢、下載部分的功能這是沒有問題的,因?yàn)椴樵?、下載部分客服端只是發(fā)送一個頭部過來,服務(wù)端接收后解析的過程不會太占用多少時(shí)間。而如果是上傳功能的話,服務(wù)端recv到的數(shù)據(jù)不僅包含頭部而且包含客服端期望上傳的文件實(shí)體的數(shù)據(jù),而筆者的本意是讓線程池來接收數(shù)據(jù)的,所以這個代碼的實(shí)現(xiàn)與筆者的期望是矛盾的。本文首先就會對這一點(diǎn)進(jìn)行更新改進(jìn),使得查詢、上傳、下載都可以并行的被線程池來執(zhí)行。

  另外在上一篇博客中,上傳部分的功能代碼比較粗糙,這次也進(jìn)行了一些更新改進(jìn)。在筆者測試上傳功能的時(shí)候,發(fā)現(xiàn)了一些偶爾出現(xiàn)而且不容易重現(xiàn)的bug,而下載功能目前為止在筆者的測試過程當(dāng)中還沒有遇到過bug。所以從代碼實(shí)現(xiàn)以及測試的過程來看,上傳部分的功能要比下載部分復(fù)雜、更難調(diào)試。具體代碼實(shí)現(xiàn)請移步筆者的github鏈接:點(diǎn)擊我。

   PS: 本篇博客是博客園用戶“cs小學(xué)生”的原創(chuàng)作品,轉(zhuǎn)載請注明原作者和原文鏈接,謝謝。

二 上傳功能演示

  在上一篇博客中,筆者展示了上傳功能的截圖,但那個只有一個客戶端在向服務(wù)端上傳文件,在這里再補(bǔ)充一個同時(shí)有兩個客服端向服務(wù)端上傳文件的截圖。

電腦培訓(xùn),計(jì)算機(jī)培訓(xùn),平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

  在此次的上傳中(使用shell腳本來提交),兩個客服端分別同時(shí)向服務(wù)端上傳不同的文件:CRLS-en.pdf和CRLS-e.pdf. 圖左半邊是服務(wù)端打印的一些信息,我們可以清晰的看到服務(wù)端交叉的接收CRLS-e.pdf和CRLS-en.pdf。圖右半邊是客服端打印的一些消息,我們也可以清晰的看到客服端也是交叉上傳兩個文件。服務(wù)端最后三次接收是:CRLS-e.pdf、CRLS-en.pdf、CRLS-en.pdf,客服端最后三次上傳的是CRLS-en.pdf、CRLS-e.pdf、CRLS-en.pdf,可見客服端上傳和服務(wù)端接收的文件的次序并不是一致的。但是從圖中我們也可以很容易的發(fā)現(xiàn):對于同一個文件,服務(wù)端接收的次序和客服端發(fā)送的次序是一致的。

  下圖是服務(wù)端接收完成后的截圖:

電腦培訓(xùn),計(jì)算機(jī)培訓(xùn),平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

三 改進(jìn)

1. 修改服務(wù)端epoll框架,使得上傳也能并行化

  

電腦培訓(xùn),計(jì)算機(jī)培訓(xùn),平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

 1     for(int i=0;i<num_of_events_to_happen;i++){ 2             struct sockaddr_in client_socket; 3             int client_socket_len=sizeof(client_socket); 4             if(*server_listen_fd==event_for_epoll_wait[i].data.fd){ 5                 int client_socket_fd=accept(*server_listen_fd,(struct sockaddr *)&client_socket,&client_socket_len); 6                 if(client_socket_fd==-1){ 7                     perror("Http_server_body,accept"); 8                     continue; 9                 }10 11                 event_for_epoll_ctl.data.fd=client_socket_fd;12                 event_for_epoll_ctl.events=EPOLLIN;13 14                 epoll_ctl(epoll_fd,EPOLL_CTL_ADD,client_socket_fd,&event_for_epoll_ctl);15 16             }else if(event_for_epoll_wait[i].events & EPOLLIN){17 18                 int client_socket_fd=event_for_epoll_wait[i].data.fd;19                 if(client_socket_fd<0){20                     continue;21                 }22 23                 callback_arg *cb_arg=(callback_arg *)malloc(sizeof(callback_arg));24                 cb_arg->socket_fd=client_socket_fd;25                 threadpool_add_jobs_to_taskqueue(pool, Http_server_callback, (void *)cb_arg);26                 27                 //epoll delete client_fd28 29             }30         }

電腦培訓(xùn),計(jì)算機(jī)培訓(xùn),平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

  如上述代碼是服務(wù)端主程序使用epoll不斷接收客服端連接、發(fā)送數(shù)據(jù)的主要邏輯部分。在第16行,原來的代碼是先接收,然后解析頭部,根據(jù)具體的請求再將之打包成作業(yè)加入到作業(yè)隊(duì)列,然后線程池的線程就會來執(zhí)行之?,F(xiàn)在不這樣做了,只要服務(wù)端epoll監(jiān)聽到讀請求,就把該讀請求打包成作業(yè)交給線程池來處理。線程相關(guān)函數(shù)會負(fù)責(zé)從傳入的客戶端連接的socket fd上讀數(shù)據(jù),然后根據(jù)具體請求再做不同操作。整個邏輯很簡單,就是把要干的事情從服務(wù)端推遲到線程池里面去做。前文提到的打包作業(yè)是這樣的:以前服務(wù)端解析完頭部后,分別把三個代表查詢、下載、上傳的回調(diào)函數(shù)指針設(shè)置好,加入到作業(yè)隊(duì)列里面。按照本文所述的新方法,服務(wù)端不用關(guān)心具體的操作是什么,只需要把回調(diào)函數(shù)指針Http_server_callback()和參數(shù)client_socket_fd傳遞到線程池就行了。而Http_server_callback()是新添加的一個函數(shù),其代碼如下:

電腦培訓(xùn),計(jì)算機(jī)培訓(xùn),平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

 1 void *Http_server_callback(void *arg){ 2  3     if(arg==NULL){ 4         printf("Http_server_callback,argument error\n"); 5         exit(0); 6     } 7  8     callback_arg *cb_arg=(callback_arg *)arg; 9     int client_socket_fd=cb_arg->socket_fd;10     memset(cb_arg->server_buffer, 0, sizeof(http_request_buffer)+4);11     int ret=recv(client_socket_fd,cb_arg->server_buffer,sizeof(http_request_buffer)+4,0);12     if(ret!=(4+sizeof(http_request_buffer))){13         close(client_socket_fd);14         return (void *)0;15     }16 17     http_request_buffer *hrb=(http_request_buffer *)(cb_arg->server_buffer);18     if(hrb->request_kind==0){19 20         callback_arg_query cb_arg_query;21         cb_arg_query.socket_fd=client_socket_fd;22         cb_arg_query.server_buffer=cb_arg->server_buffer;23         cb_arg_query.server_buffer_size=cb_arg->server_buffer_size;24         strcpy(cb_arg_query.file_name, hrb->file_name);25 26         Http_server_callback_query((void *)(&cb_arg_query));        
27 28     }else if(hrb->request_kind==1){29 30         callback_arg_upload cb_arg_upload;31         cb_arg_upload.socket_fd=client_socket_fd;32         cb_arg_upload.server_buffer=cb_arg->server_buffer;33         cb_arg_upload.server_buffer_size=cb_arg->server_buffer_size;34         cb_arg_upload.range_begin=hrb->num1;35         cb_arg_upload.range_end=hrb->num2;36         37         strcpy(cb_arg_upload.file_name, hrb->file_name);38 39         Http_server_callback_upload((void *)(&cb_arg_upload));40 41     }else if(hrb->request_kind==2){42 43         callback_arg_download cb_arg_download;44         cb_arg_download.socket_fd=client_socket_fd;45         cb_arg_download.server_buffer=cb_arg->server_buffer;46         cb_arg_download.server_buffer_size=cb_arg->server_buffer_size;47         cb_arg_download.range_begin=hrb->num1;48         cb_arg_download.range_end=hrb->num2;49 50         strcpy(cb_arg_download.file_name, hrb->file_name);51 52         Http_server_callback_download((void *)(&cb_arg_download));53 54     }else{55 56     }57 58 59 }

電腦培訓(xùn),計(jì)算機(jī)培訓(xùn),平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

2.  上傳部分代碼的改進(jìn)

電腦培訓(xùn),計(jì)算機(jī)培訓(xùn),平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

 (              ret=recv(client_socket_fd,server_buffer+recv_size,range_end-range_begin+-recv_size,             (ret<=                 perror(                                            recv_size+=             (recv_size==(range_end-range_begin+                            }

電腦培訓(xùn),計(jì)算機(jī)培訓(xùn),平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

  上面是服務(wù)端上傳功能的部分邏輯,在一個while無限循環(huán)中,服務(wù)端接收客服端發(fā)送過來的[n,m]區(qū)間的數(shù)據(jù),因?yàn)閞ecv一次不一定能夠接收完整個[n,m]區(qū)間的數(shù)據(jù),因此需要在循環(huán)里面不斷地接收,直到接收到的數(shù)據(jù)達(dá)到m-n+1的長度,這個時(shí)候就用break跳出循環(huán)。另外如果recv的返回值ret<=0,則表明網(wǎng)絡(luò)出錯或者客服端斷開網(wǎng)絡(luò),此時(shí)也要break出去。

  跳出while循環(huán)后,要判斷是正確接收到了完整數(shù)據(jù)還是出錯了,并且給客服端發(fā)送一個ack消息,客服端根據(jù)ack消息來決定下一步的走向,該部分邏輯如下:

電腦培訓(xùn),計(jì)算機(jī)培訓(xùn),平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

 1 if(recv_size==(range_end-range_begin+1)){ 2  3             int ret1=fwrite(server_buffer,range_end-range_begin+1, 1, fp); 4             memset(server_buffer, 0, sizeof(http_request_buffer)+4); 5             http_request_buffer *hrb=(http_request_buffer *)server_buffer; 6             if(ret1==1){ 7                 hrb->request_kind=3; 8                 hrb->num1=range_begin; 9                 hrb->num2=range_end;10             }else{11                 hrb->request_kind=4;12             }13 14             int ret=send(client_socket_fd,server_buffer,sizeof(http_request_buffer)+4,0);15             16             if(ret!=(sizeof(http_request_buffer)+4)){17 18                 perror("Http_server_callback_upload, send ack to client");19 20             }else{21 22 23             }24 25             26         }else{

電腦培訓(xùn),計(jì)算機(jī)培訓(xùn),平面設(shè)計(jì)培訓(xùn),網(wǎng)頁設(shè)計(jì)培訓(xùn),美工培訓(xùn),Web培訓(xùn),Web前端開發(fā)培訓(xùn)

  在第6行判斷如果數(shù)據(jù)接收完畢并且成功寫入到服務(wù)端,則給客服端發(fā)送一個正確接收并寫入的消息,或者設(shè)置request_kind=4,代表服務(wù)端接收失敗,客服端需要重新發(fā)送數(shù)據(jù)。更詳細(xì)的代碼請讀者直接閱讀github里面的源代碼。

四 一些遇到的問題、bug

1. 在后來跑JDFS的時(shí)候,發(fā)現(xiàn)會提示no host to route的錯誤,發(fā)現(xiàn)原因是虛擬Ubuntu的ip地址發(fā)生了變化,執(zhí)行下ifconfig命令,用最新的ip地址就可以了。

2. 

1 int ret=fread(upload_buffer+sizeof(http_request_buffer)+4, size_of_last_piece, 1, fp);2         if(ret!=1 && ret!=0){3             printf("JDFS_http_upload,fread failed,ret=%d\n",ret);4             exit(0);5         }

  上面這段代碼是客戶端讀取文件的最后一段數(shù)據(jù)準(zhǔn)備上傳,下面是一個if判斷語句,之前判斷語句是if(ret!=1){...},而且之前一直上傳也沒出現(xiàn)過fread的錯誤,但是這次卻發(fā)生客戶端上傳文件都能成功但是到了傳送最后一部分?jǐn)?shù)據(jù)的時(shí)候fread老是提示錯誤,經(jīng)分析fread由于到達(dá)了文件尾部,所以返回0,在if語句里面加上這個判斷就沒問題了。但是奇怪的是,筆者之前好多次上傳都成功并沒有提示這個錯誤啊。

3.  在下載功能部分,客戶端從服務(wù)端recv數(shù)據(jù),一旦recv返回值小于等于0,則不管錯誤原因的類型,客戶端無條件重新連接到服務(wù)端,并請求數(shù)據(jù)。而客戶端上傳數(shù)據(jù)到服務(wù)端,某種程度上比較像服務(wù)端從客戶端下載數(shù)據(jù),不同的是服務(wù)端此時(shí)是被動從客戶端下載數(shù)據(jù)。那么此時(shí)如果服務(wù)端接收數(shù)據(jù)時(shí)recv返回錯誤的結(jié)果,服務(wù)端不應(yīng)該重新連接客戶端請求那部分失敗的數(shù)據(jù),而應(yīng)該是告訴客戶端數(shù)據(jù)接收失敗,由客戶端決定此時(shí)應(yīng)該怎么辦。為什么呢?一方面,服務(wù)端是被動的服務(wù)客戶端的請求,如果服務(wù)端主動向客服端重新連接,并請求那部分失敗的數(shù)據(jù),此時(shí)服務(wù)端變成了客戶端,客戶端變成了服務(wù)端,這不符合C/S的模型;另一方面,服務(wù)端應(yīng)該是服務(wù)大量并發(fā)的請求,也不應(yīng)該因?yàn)槟骋粋€請求服務(wù)失敗,就主動重新連接客戶端,請求數(shù)據(jù),萬一這個過程老是出錯,服務(wù)端豈不是一直陷入特定的請求泥潭,而大量其他的請求得不到服務(wù)?

  所以,服務(wù)端只需要告訴客戶端該請求服務(wù)失敗就行了,剩下的客戶端要么重新向服務(wù)端提交請求,要么終止執(zhí)行或者其他。筆者一開始想的比較簡單,在服務(wù)失敗的時(shí)候,服務(wù)端關(guān)閉socket fd,這樣客戶端檢測到鏈接被關(guān)閉,自然就知道服務(wù)失敗了。在客戶端邏輯里,如果send失敗,只發(fā)送了部分?jǐn)?shù)據(jù),也關(guān)閉鏈接,這樣服務(wù)端就檢測到socket fd鏈接被關(guān)閉。這么做結(jié)果引發(fā)了很多問題,原因在于send()端一次發(fā)送的數(shù)據(jù),recv()端可能要分好幾次才能接收完畢,如果一方已經(jīng)close套接字,而另一方還沒有接收完數(shù)據(jù),就因?yàn)閷Χ薱lose了而接收失敗,因此close的時(shí)機(jī)不好協(xié)調(diào)。

  于是取消了用close()傳遞消息的方法,而改為:線程池Job的一次操作結(jié)果,無論有沒有達(dá)到目的,都給客戶端發(fā)送一條ack確認(rèn)信息,客戶端根據(jù)ack信息如果成功則繼續(xù),否則重新上傳失敗的數(shù)據(jù)。這么做基本上解決了問題,但是很奇怪的是,非常偶然的情況下會出現(xiàn)一個bug:在重新啟動server端,然后執(zhí)行客戶端的時(shí)候,服務(wù)端調(diào)用recv的時(shí)候會提示bad file descriptor的錯誤;在重新啟動server一到兩次后又恢復(fù)正常了,這個錯誤由于非常難重新,目前還沒有找到問題的根源所在。

4. 在調(diào)試的過程中,還遇到過另外一個問題:客戶端是執(zhí)行的上傳功能,而服務(wù)端有時(shí)候會解析為查詢的操作。經(jīng)分析可能原因如下:有可能客戶端發(fā)送了[n1,m1] [n2,m2]兩段數(shù)據(jù), 而服務(wù)端接收的序列很可能是這樣的,[n1,b],[b+1,m1],[n2,m2]. 服務(wù)端在接收完[n1,b]后(由于網(wǎng)絡(luò)原因[n1,m1]很可能不是一次接收完成),下一次接收[b+1,m1]的時(shí)候誤以為是一段新的數(shù)據(jù),并把開頭若干字節(jié)的數(shù)據(jù)當(dāng)做頭部處理,而頭部恰好有一部分?jǐn)?shù)據(jù)是0,而0就代表著查詢請求。但是經(jīng)過研究代碼并沒有發(fā)現(xiàn)明顯會產(chǎn)生上述場景的條件。由于修復(fù)其他bug后,導(dǎo)致這個錯誤沒能繼續(xù)重現(xiàn),現(xiàn)在也很難找到根源,也留到以后再研究吧。

五 結(jié)束語

  至此本篇博客就結(jié)束了,此篇博客主要解決了上傳功能的并行化問題,以及修復(fù)了一些bug,當(dāng)然還有一些不容易重新的bug,其原因有待進(jìn)一步的分析解決。截止目前JDFS的上傳、下載功能已趨于完成了,下一篇博客開始將在此基礎(chǔ)上增加分布式文件管理的功能,比如把本地文件冗余地存儲于不同的虛擬節(jié)點(diǎn)上,查詢虛擬文件系統(tǒng),從虛擬文件系統(tǒng)上讀取目標(biāo)文件等。歡迎繼續(xù)關(guān)注本系列博客,我們下期再見。

標(biāo)簽: 上傳下載epoll線程池socket

http://www.cnblogs.com/junhuster/p/JDFS2.html