skynet框架 源碼分析 二

        一個遊戲服務器系統的數據流向有很多種。在skynet中,我主要看到了三種,一種是從客戶端流到服務器,而後服務器處理完畢之後,發送回客戶端。第二種是一個harbor流向另外一個harbor,這應該就是服務進程之間通過套接字的通信了。第三種就是服務進程保存數據到數據庫中,而後返回(這部分我還沒看完)。

        本章主要講解第一種數據流向,客戶端到服務端中某個節點。服務端如何返回數據到客戶端。
        要把這個講明白,我覺得必須要先得從skynet_start.c文件開始看起。
作者在函數_start中,啓動了多個工作線程(_worker),一個moniter線程(_moniter),一個定時器線程(_timer),一個網絡線程(_socket)。我們來看看這些線程都在做什麼?

        worker線程:
        從全局消息隊列中取一條消息隊列,而後找到該消息隊列的處理器(handle),通過處理器找到對應節點的上下文,而後,更新處理器的狀態值,確定其不是無窮執行的狀態。接下來執行消息請求。即接口(_dispatch_message),在該接口中會判定是否是廣播消息,若不是,則執行節點的回調接口_cb(callback)。處理完畢之後,查看是否需要返回數據,若需要,則將處理結果壓到目標節點的消息隊列中。

        moniter線程:
        我覺得則個線程,只是判定節點是否出錯了,如果出錯,進行修復一下。

        timer線程:
        定時器線程我就不解釋了,每個服務器系統都應該有該功能。

        socket線程:
        該線程主要是從epoll_wait的結果中讀取消息,若有需要處理的消息則進行相關的處理。這裏面的消息目前要說明的有三個:第一個是管道,作者把管道的讀端放到了epoll中進行管理,也就是說,每次向管道中寫數據,都是socket線程讀取並處理的;第二個是gate產生的監聽端口,也是由epoll管理,並且一旦產生了數據也是由socket處理;第三個是accept客戶端的socket後,客戶端發送到服務端的數據,此數據也由socket線程處理。


        看完這些線程,我們開始跟蹤一個用戶從登入到發送消息真個流程。
        服務器啓動,大概監聽端口詳細的步驟可在watchdog.lua中看到。通過模塊service_gate.c打開監聽,監聽本地端口8888。假設此時,有用戶嘗試連接服務器。服務器端收到一個連接請求。在epoll_wait中產生了一個結果。socket線程發現有消息需要處理時,開始處理消息。

開始讀代碼:

  1. int  
  2. socket_server_poll(struct socket_server *ss, struct socket_message * result, int * more) {  
  3.     for (;;) {  
  4.         if (ss->event_index == ss->event_n) {  
  5.             ss->event_n = sp_wait(ss->event_fd, ss->ev, MAX_EVENT);  
  6.             if (more) {  
  7.                 *more = 0;  
  8.             }  
  9.             ss->event_index = 0;  
  10.             if (ss->event_n <= 0) {  
  11.                 ss->event_n = 0;  
  12.                 return -1;  
  13.             }  
  14.         }  
  15.         struct event *e = &ss->ev[ss->event_index++];  
  16.         struct socket *s = e->s;  
  17.         if (s == NULL) {  
  18.             int type = ctrl_cmd(ss, result);  
  19.             if (type != -1)  
  20.                 return type;  
  21.             else  
  22.                 continue;  
  23.         }  
  24.         switch (s->type) {  
  25.         case SOCKET_TYPE_CONNECTING:  
  26.             return report_connect(ss, s, result);  
  27.         case SOCKET_TYPE_LISTEN:  
  28.             if (report_accept(ss, s, result)) {  
  29.                 return SOCKET_ACCEPT;  
  30.             }   
  31.             break;  
  32.         case SOCKET_TYPE_INVALID:  
  33.             fprintf(stderr, "socket-server: invalid socket\n");  
  34.             break;  
  35.         default:  

        socket線程:
        若,此時只有一個用戶,那麼event_n的值爲1,直接執行到case SOCKET_TYPE_LISTEN: 因爲watchdog中的gate在監聽,所以走到這一句停住。執行report_accept邏輯。在report_accept中,系統保留了accept文件描述符,用來讀寫操作,但,現在還沒有把它放到epoll中,而是做了一次記錄。標誌了一下哪個槽被引用了。並且,該消息由哪個服務處理(handle),接着依次返回,先由socket_server_poll接口返回一個類型值,socket線程的skynet_soket_poll捕捉到了返回類型,並在skynet_socket.c文件中的接口forward_message中標誌該消息是來源於PTYPE_SOCKET的,而後把消息壓到watchdog的消息隊列中,socket線程處理完畢。

此時,socket的身份其實就是生產者,而worker線程就是消費者。

        worker線程:
        接下來,worker線程上場,發現有消息可以處理,其中一個線程被喚醒,開始處理消息,發現是gate節點的服務請求,調用gate的_cb(callback,每個節點都有一個消息處理函數,_cb就是gate的消息處理函數,也被稱爲回調)接口來處理數據。接下來我們看看service_gate.c是如何處理該消息的,gate模塊發現消息是類型是PTYPE_SOCKET,於是乎直接走了接口dispatch_socket_message,在該接口中,繼續執行邏輯,發現message->type爲SKYNET_SOCKET_TYPE_ACCEPT,如果沒有邏輯錯誤,而後,向現有的管道中寫入一個start_socket的消息請求worker線程執行完畢。

        socket線程:
        epoll在監聽到該請求後,由於連接是新建立的event中的socket(用戶數據)是NULL,執行ctrl_cmd接口,在裏面將新建立的socket的accept端放到epoll中,並返回一個消息類型位SOCKET_OPEN,而後返回給skynet_socket_poll接口,該接口捕捉到返回類型後,向gate節點發送了一個socket消息類型爲SKYNET_SOCKET_TYPE_CONNECT的消息,socket線程處理完畢。

        workder線程:
        在gate中執行_cb接口回調,發現有消息類型爲PTYPE_SOCKET的消息,同時其數據中存放的是skynet_socket_message並且其類型爲SKYNET_SOCKET_TYPE_CONNECT,在gate模塊中的dispatch_socket_message接口中,捕捉到該類型的消息時,用_report接口來處理消息。該接口則是向watchdog節點發送了一條文本消息,該文本消息則執行watchdog中的command:open接口,這時候,爲用戶載入agent節點,和client節點。同時,向gate模塊發送一條文本消息告訴gate將兩者連接起來。worker處理完畢。

        worker線程:
        由於此時已經不是向管道中寫消息,所以,繼續worker線程處理邏輯。worker線程抓取到gate的消息組,執行相關的邏輯,發現一條文本消息,中間含有forward命令,於是拆開參數,將agent和client連接起來,並且保存在該gate節點中。

        經過以上步驟,客戶端與服務端建立連接完畢。等待兩者互相發送消息。

        假設服務端需要向客戶端發送消息:

               agent向client節點發送一個服務請求,比如例子中的
               skynet.send(client,"text","Welcome to skynet")

               agent向服務節點client一條文本消息,"Welcome to skynet",這是在腳本中執行的。首先調用lua-skynet模塊的_command接口向節點client壓一條服務消息,在worker線程處理到client節點時,取出消息,並調用client的_cb(callback)接口處理該消息,而後client節點向管道寫入消息。在下次socket線程從管道中取出數據時,發消息類型是發送消息時會調用send_socket接口,向套接字中寫入消息。消息發送完畢。

        假設客戶端向服務端發送消息:

               客戶端向socket中發送消息。socket線程首先捕捉到消息。取出消息,而後將該消息壓到gate節點的消息組中。socket線程處理完畢。worker線程執行消息回調的時候,發現消息類型是PTYPE_SOCKET,其來源於socket。而後,檢查該消息的數據部分,發現數據類型是SKYNET_SOCKET_TYPE_DATA,於是調用dispatch_message接口繼續執行,當數據沒有超出16M時,將數據傳入_forward接口,在_forward接口中,用skynet_send接口發送消息到agent,agent在收到消息之後,發現消息類型是"client"。調用agent.lua中的dispatch接口處理。

[plain] view plain copy
 print?
  1. skynet.register_protocol {  
  2.     name = "client",  
  3.     id = 3,  
  4.     pack = function(...) return ... end,  
  5.     unpack = skynet.tostring,  
  6.     dispatch = function (session, address, text)  
  7.         -- It's client, there is no session  
  8.         skynet.send("LOG", "text", "client message :" .. text)  
  9.         local result = skynet.call("SIMPLEDB", "text", text)  
  10.         skynet.ret(result)  
  11.     end  
  12. }  

        agent節點中,dispatch接口首先將文本消息發送給log節點,而後調用simpledb執行文本指令,最後返回執行結果給客戶端。消息返回完畢。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章