前言
本篇文章講述客戶端與服務端的具體設計細節。有細心的小夥伴發現,客戶端和服務端的工作方式不一樣:服務端是多執行緒計算模型,利用工作執行緒完成資料的讀取,而客戶端是單執行緒(利用Reactor執行緒完成資料的讀取)。這麼做的原因有二:首先我們認為我們的使用RPC的初衷是由於CPU計算是瓶頸,不得已把計算放到多臺機器上,所以服務端採用多執行緒計算模型;其次我們認為網路IO只要不是客戶端故意阻塞,那麼無論是請求資料還是響應資料只需要一次接收就可以收全,不會有執行緒長時間阻塞在網路上,所以客戶端就使用反應器執行緒進行接收響應資料。
客戶端同步和非同步呼叫
SimpleRpc提供了同步呼叫和非同步呼叫的方法,使用區別在於傳遞的引數不同,如下所示。
//非同步請求 int async_request(Server &server, Request *request, Response *response, ResultHandler *handler); //同步請求 int sync_request(Server &server, Request *request, Response *response);
那麼SimpleRpc對於同步和非同步呼叫是如何支援的呢?我們重新看一下DownstreamHandler對資料的處理方式:
void DownstreamHandler::handle_read(int fd) { char head[4]; Connection conn(fd); conn.recv_n(head, 4); int size = *((int *)head); char *buf = new char[size]; conn.recv_n(buf, size); close(fd); printf("Downstream Handler close fd:%d\n", fd); //下游響應 _response->deserialize(buf, size); //如果有result_handler,則呼叫data_comeback鉤子函式 if(_result_handler != NULL) { _result_handler->data_comeback(); //對於同步呼叫,這個方法會喚醒客戶端使其從wait中返回 } delete[] buf; //自殺 delete this; }
result_handler的呼叫是關鍵,我們正是利用這一點做到同步呼叫和非同步呼叫。ResultHandler的類UML如下:
DefaultResultHandler是SimpleRpc的預設結果處理方式,UserDefinedResultHandler由使用者自己選擇性的定義並實現。當客戶端工作執行緒對服務端相應資料處理完畢後,呼叫ResultHandler的data_comeback方法執行這個鉤子函式。
- 同步呼叫的實現:
int SimpleRpcClient::sync_request(Server &server, Request *request, Response *response) { Mutex mutex; Connection conn; Condition cond(&mutex); InetAddr addr(server.get_port_str(), server.get_ip_str()); Connector conntor(addr); int ret = conntor.Connect(conn); //建立與服務端的連線 if(ret == -1){ LOG("connect error\n"); return -1; } int size = request->bytes(); //獲取請求序列化後的位元組數 char *buf = new char[size + 4]; //用額外4位元組存放資料長度,方便接收端校驗 if(buf == NULL) { LOG("request oom, request need %d bytes\n", size + 4); conn.Close(); return -1; } int payload = request->serialize(buf + 4, size); //序列化 memcpy(buf, &payload, sizeof(int)); ret = conn.send_n(buf, payload + 4); //傳送序列化資料 if(ret != 0) { LOG("connection send error\n"); return -1; } DefaultResultHandler *handler = new DefaultResultHandler(&cond, &mutex); DownstreamHandler *down_handler = new DownstreamHandler(conn.sock(), response, Reactor::get_instance(), handler); Reactor::get_instance()->regist(conn.sock(), down_handler); //註冊到reactor中等待響應事件的通知 handler->finish(); //阻塞呼叫,直到cond得到喚醒通知 delete[] buf; delete handler; return 0; }
我們的DefautlResultHandler擁有系統等待條件(Condition),並且作為DownstreamHandler的成員之一。客戶端傳送請求資料後,構造DownstreamHandler並註冊到reactor中,等待服務端響應事件的通知。幹完以上的事情之後,客戶端應用執行緒呼叫DefaultResultHandler的finish方法阻塞直到得到完成通知,這樣達到了同步呼叫的效果。
- 非同步呼叫的實現:
非同步呼叫沒有使用DefaultResultHandler作為引數傳遞給DownstreamHandler,而是把使用者自定義的ResultHanlder傳遞進去,具體的控制流程(data_comeback函式)由使用者自己定義。
int SimpleRpcClient::async_request( Server &server, Request *request, Response *response, ResultHandler *handler) { ... DownstreamHandler *down_handler = new DownstreamHandler(conn.sock(), response, Reactor::get_instance(), handler); Reactor::get_instance()->regist(conn.sock(), down_handler); ... }
服務端工作執行緒計算模型
我們知道服務端使用多執行緒進行資料的處理,那麼每個執行緒的工作內容是什麼呢?
template<class REQUEST, class RESPONSE> class Processor : public Worker<StreamEvent> { public: virtual int process(REQUEST &request, RESPONSE &response) = 0; void run() { while(true){ StreamEvent e = get_event(); //佇列中獲取待處理事件 char head[4]; Connection conn(e.fd); int payload = conn.recv_n(head, 4); //接收資料長度 if(payload == -1) { close(e.fd); printf("Error Processor close fd:%d\n", e.fd); return; } REQUEST request; RESPONSE response; int size = *((int *)head); char *recv_buf = new char[size]; conn.recv_n(recv_buf, size); //接收請求資料 request.deserialize(recv_buf, size); //反序列化 process(request, response); //進行使用者程式碼邏輯計算,由使用者實現 size = response.bytes(); char *send_buf = new char[size + 4]; memcpy(send_buf, &size, sizeof(int)); payload = response.serialize(send_buf + 4, size); //序列化響應資料 conn.send_n(send_buf, size + 4); //傳送響應資料 //為了正常關閉該連結,需要重新註冊回reactor UpstreamHandler *upHandler = new UpstreamHandler(e.fd, Reactor::get_instance()); Reactor::get_instance()->regist(e.fd, upHandler); delete recv_buf; delete send_buf; } }
virutal ~Processor(){}
}