當前位置:
首頁 > 知識 > 跨平台長連接組件設計及可插拔改造

跨平台長連接組件設計及可插拔改造



(點擊

上方公眾號

,可快速關注)





來源:宮城 ,


zeeyang.com/2018/04/03/cross-platform-architecture%20design-and-pluggable/



背景




我們在提出開發跨平台組件之前, iOS 和 Android 客戶端分別使用一套長連接組件,需要雙倍的人力開發和維護;在產品需求調整上,為了在實現細節上保持一致性也具有一定的難度;Web 端與客戶端長連接的形式不同,前者使用 WebSocket,後者使用 Socket ,無形中也增加了後端的維護成本。為了解決這些問題,我們基於 WebSocket 協議開發了一套跨平台的長連接組件。




架構介紹



組件自上而下分為五層:






  • Native 層:負責業務請求封裝和數據解析,與原生進行交互



  • Chat 層:負責提供底層通信使用的 c 介面,包含連接、讀寫和關閉



  • Websocket 層:實現 websocket 協議及維護心跳



  • TLS 層 :基於 mbedTLS 實現 TLS 協議及數據加解密



  • TCP 層:基於 libuv 實現 TCP 連接和數據的讀寫




整體架構如下圖所示:






TCP 層




TCP 層我們是基於 libuv 進行開發, libuv 是一個非同步 I/O 庫,並且支持了多個平台( Linux ,Windows 和 Darwin ),一開始主要應用於開發 Node.js ,後來逐漸在其他項目也開始使用。文件、 網路和管道 等操作是 I/O 操作 ,libuv 為此抽象出了相關的介面,底層使用各平台上最優的 I/O 模型實現。




它的核心是提供了一個 event loop ,每個 event loop 包含了六個階段:





  • timers 階段:這個階段執行 timer( setTimeout 、 setInterval )的回調



  • I/O callbacks 階段:執行一些系統調用錯誤,比如網路通信的錯誤回調



  • idle , prepare 階段:僅 node 內部使用



  • poll 階段:獲取新的 I/O 事件, 適當的條件下 node 將阻塞在這裡



  • check 階段:執行 setImmediate() 的回調



  • close callbacks 階段:執行 socket 的 close 事件回調







TLS 層




mbedTLS(前身PolarSSL)是實現了一套易用的加解密演算法和 SSL / TLS 庫。TLS 以及前身 SSL 是傳輸層安全協議,給網路通信提供安全和數據完整性的保障,所以它能很好的解決數據明文和劫持篡改的問題。並且其分為記錄層和傳輸層,記錄層用來確定傳輸層數據的封裝格式,傳輸層則用於數據傳輸,而在傳輸之前,通信雙方需要經過握手,其包含了雙方身份驗證,協商加密演算法,交換加密密鑰。







Websocket 層



Websocket 層包含了對協議的實現和心跳的維護。




其最新的協議是 13 RFC 6455。協議的實現分為握手,數據發送/讀取,關閉連接。




握手




握手要從請求頭去理解。



WebSocket 首先發起一個 HTTP 請求,在請求頭加上 Upgrade 欄位,該欄位用於改變 HTTP 協議版本或者是換用其他協議,這裡我們把 Upgrade 的值設為 websocket ,將它升級為 WebSocket 協議。




同時要注意 Sec-WebSocket-Key 欄位,它由客戶端生成並發給服務端,用於證明服務端接收到的是一個可受信的連接握手,可以幫助服務端排除自身接收到的由非 WebSocket 客戶端發起的連接,該值是一串隨機經過 base64 編碼的字元串。





GET /chat HTTP/1.1


Host: server.example.com


Upgrade: websocket

Connection: Upgrade


Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==


Origin: http://example.com


Sec-WebSocket-Protocol: chat, superchat


Sec-WebSocket-Version: 13




收到請求後,服務端也會做一次響應:





HTTP/1.1 101 Switching Protocols


Upgrade: websocket


Connection: Upgrade


Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=




裡面重要的是 Sec-WebSocket-Accept ,服務端通過從客戶端請求頭中讀取 Sec-WebSocket-Key 與一串全局唯一的標識字元串(俗稱魔串)「258EAFA5-E914-47DA- 95CA-C5AB0DC85B11」做拼接,生成長度為160位的 SHA-1 字元串,然後進行 base64 編碼,作為 Sec-WebSocket-Accept 的值回傳給客戶端,客戶端再去解析這個值,與自己加密編碼後的字元串進行比較。




處理握手 HTTP 響應解析的時候,可以用 http-paser ,解析方式也比較簡單,就是對頭信息的逐字讀取再處理,具體處理你可以看一下它的狀態機實現。解析完成後你需要對其內容進行解析,看返回是否正確,同時去管理你的握手狀態。




數據發送/讀取




數據的處理需要用幀協議圖來說明:





0                   1                   2                   3


0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1


+-+-+-+-+-------+-+-------------+-------------------------------+


|F|R|R|R| opcode|M| Payload len |    Extended payload length    |


|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |


|N|V|V|V|       |S|             |   (if payload len==126/127)   |


| |1|2|3|       |K|             |                               |


+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +


|     Extended payload length continued, if payload len == 127  |


+ - - - - - - - - - - - - - - - +-------------------------------+


|                               |Masking-key, if MASK set to 1  |


+-------------------------------+-------------------------------+


| Masking-key (continued)       |          Payload Data         |


+-------------------------------- - - - - - - - - - - - - - - - +


:                     Payload Data continued ...                :


+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +


|                     Payload Data continued ...                |


+---------------------------------------------------------------+




首先我們來看看數字的含義,數字表示位,0-7表示有8位,等於1個位元組。





0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1




所以如果要組裝一個幀數據可以這樣子:





char *rev = (rev *)malloc(4);


rev[0] = (char)(0x81 & 0xff);


rev[1] = 126 & 0x7f;


rev[2] = 1;


rev[3] = 0;




ok,了解了幀數據的樣子,我們反過來去理解值對應的幀欄位。




首先0x81是什麼,這個是十六進位數據,轉換成二進位就是1000 0001, 是一個位元組的長度,也就是這一段裡面每一位的值:





0 1 2 3 4 5 6 7 8


+-+-+-+-+-------+


|F|R|R|R| opcode|


|I|S|S|S|  (4)  |


|N|V|V|V|       |


| |1|2|3|       |


+-+-+-+-+-------+






  • FIN 表示該幀是不是消息的最後一幀,1表示結束,0表示還有下一幀。



  • RSV1, RSV2, RSV3 必須為0,除非擴展協商定義了一個非0的值,如果沒有定義非0值,且收到了非0的 RSV ,那麼 WebSocket 的連接會失效,建議是斷開連接。



  • opcode 用來描述 Payload data 的定義,如果收到了一個未知的 opcode ,同樣會使 WebSocket 連接失效,協議定義了以下值:




  • %x0 表示連續的幀



  • %x1 表示 text 幀



  • %x2 表示二進位幀



  • %x3-7 預留給非控制幀



  • %x8 表示關閉連接幀



  • %x9 表示 ping



  • %xA 表示 pong



  • %xB-F 預留給控制幀




連續幀是和 FIN 值相關聯的,它表明可能由於消息分片的原因,將原本一個幀的數據分為多個幀,這時候前一幀的 opcode 就是0,FIN 也是0,最後一幀的 opcode 就不再是0,FIN 就是1了。




再可以看到 opcode 預留了非控制幀和控制幀,這兩個又是什麼?




控制幀表示 WebSocket 的狀態信息,像是定義的分片,關閉連接,ping和pong。




非控制幀就是數據幀,像是 text 幀,二進位幀。




0xff 作用就是取出需要的二進位值。




下面再來看126,126則表示的是 Payload len ,也就是 Payload 的長度:





                8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1


                +-+-------------+-------------------------------+


                |M| Payload len |    Extended payload length    |


                |A|     (7)     |             (16/64)           |


                |S|             |   (if payload len==126/127)   |


                |K|             |                               |


+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +


|     Extended payload length continued, if payload len == 127  |


+ - - - - - - - - - - - - - - - +-------------------------------+


|                               |Masking-key, if MASK set to 1  |


+-------------------------------+-------------------------------+


| Masking-key (continued)       |           Payload Data        |


+-------------------------------- - - - - - - - - - - - - - - - +


:                     Payload Data continued ...                :


+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +


|                     Payload Data continued ...                |


+---------------------------------------------------------------+




MASK 表示Playload data 是否要加掩碼,如果設成1,則需要賦值 Masking-key 。所有從客戶端發到服務端的幀都要加掩碼





Playload len




表示 Payload 的長度,這裡分為三種情況




長度小於126,則只需要7位


長度是126,則需要額外2個位元組的大小,也就是 Extended payload length


長度是127,則需要額外8個位元組的大小,也就是 Extended payload length + Extended payload length continued ,Extended payload length 是2個位元組,Extended payload length continued 是6個位元組


Playload len 則表示 Extension data 與 Application data 的和




Masking-key 是在 MASK 設置成1之後,隨機生成的4位元組長度的數據,然後和 Payload Data做異或運算


Payload Data 就是我們發送的數據


而數據的發送和讀取就是對幀的封裝和解析。




關閉連接




關閉連接分為兩種:服務端發起關閉和客戶端主動關閉。




服務端跟客戶端的處理基本一致,以服務端為例:




服務端發起關閉的時候,會客戶端發送一個關閉幀,客戶端在接收到幀的時候通過解析出幀的opcode來判斷是否是關閉幀,然後同樣向服務端再發送一個關閉幀作為回應。




Chat 層




Chat 層比較簡單,只是提供一些通用的連接、讀寫數據和斷開介面和回調,同時維護一個 loop 用於重連。




Native 層




這一層負責和原生進行交互,由於組件是用 c 代碼編寫的,所以為了調用原生方法,Android 採用 JNI 的方式,iOS 採用 runtime 的方式來實現。




JNI :





JNIEXPORT void JNICALL


Java_com_youzan_mobile_im_network_Channel_nativeDisconnect(JNIEnv *env, jobject jobj) {


    jclass clazz = env->GetObjectClass(jobj);


    jfieldID fieldID = env->GetFieldID(clazz, CONTEXT_VARIABLE, "J");


    context *c = (context *) env->GetLongField(jobj, fieldID);


    im_close(c);


}




runtime:





void sendData(int cId, int mId, int version, int mv, const char *req_id, const char *data {


    context *ctx = (context *)objc_msgSend(g_obj, sel_registerName("ctx"));


    send_request(ctx, cId, mId, version, mv, req_id, data);


}




插拔式架構改造




在實現了一套跨端長連接組件之後,最近我們又完成了其插件化的改造,為什麼要做這樣的改造呢?由於業務環境複雜和運維的相關限制,有的業務方可以配置 TLS 組成 WSS;有的業務方不能配置,只能以明文 WebSocket 的方式傳輸;有的業務方甚至連 WebSocket 的承載也不要,轉而使用自定義的協議。隨著對接的業務方增多,我們沒辦法進行為他們一一定製。我們當初設計的結構是 Worker (負責和業務層通信) -> WebSocket -> TLS -> TCP ,這四層結構是耦合在一起的,這時候如果需要剔除 TLS 或者擴展一個新的功能,就會改動相當多的代碼。基於以上幾點,我們發現,原先的定向設計完全不符合要求,為了接下來可能會有新增協議解析的預期,同時又不改變使用 libuv 進行跨平台的初衷,所以我們就實施了插件化的改造,最重要的目的是為了解耦,同時也為了提高組件的靈活性,實現可插拔(冷插拔)。




解耦




首先我們要對四層結構的職責進行明確






  • Worker :提供業務介面和回調



  • WebSocket :負責 WebSocket 握手,封裝/解析幀數據和維護心跳



  • TLS :負責 TLS 握手和數據的加解密



  • TCP:TCP 連接和數據的讀寫




以及整理出結構間的執行調用:







其中 connect 包含了連接和握手兩個過程。在完成鏈路層連接後,我們認為協議層握手完成,才算是真正的連接成功。




同樣的,數據讀寫、連接關閉、連接銷毀和重置都會嚴格按照結構的順序依次調用。




可插拔改造




解耦完成之後我們發現對於介面的調用都是顯式的,比如 Worker send data 中調用 WebSocket send data , WebSocket send data 中又調用 TLS send data ,這樣的顯式調用是因為我們知道這些介面是可用的,但在插件化中某個插件可能沒有被使用,這樣介面的調用會在某一層中斷而導致整個組件的不可用。




結構體改造




所以我們首先考慮到的是抽象出一個結構體,將插件的介面及回調統一,然後利用函數指針實現插件方法的調用,以下是對函數指針聲明:





/* handle */


typedef int (*node_init)(dul_node_t *node, map_t params);


typedef void (*node_conn)(dul_node_t *node);  


typedef void (*node_write_data)(dul_node_t *node,


                                const char *payload,


                                unsigned long long payload_size,


                                void *params);


typedef int (*node_read_data)(dul_node_t *node, 


                              void *params, 


                              char *payload, 


                              uint64_t size);                    


typedef void (*node_close)(dul_node_t *node);                           


typedef void (*node_destroy)(dul_node_t *node);


typedef void (*node_reset)(dul_node_t *node);


  


/* callback */


typedef void (*node_conn_cb)(dul_node_t *node, int status);


typedef void (*node_write_cb)(dul_node_t *node, int status);                         


typedef int (*node_recv_cb)(dul_node_t *node, void *params, uv_buf_t *buf, ssize_t size);


typedef void (*node_close_cb)(dul_node_t *node);




但如果僅僅聲明這些函數指針,在使用時還必須知道插件的結構體類型才能調用到函數的實現,這樣插件之間仍然是耦合的。所以我們必須將插件提前關聯起來,通過結構體指針來尋找上一個或者下一個插件,OK,這樣就很容易聯想到雙向鏈表正好能夠滿足我們的需求。所以加上 pre 、 next 以及一些必要參數後,最終我們整理的結構體為:





typedef struct dul_node_s {


    // 前、後插件


    dul_node_t *pre;


    dul_node_t *next;


 


    // 必要參數


    char *host;


    int port;


    map_t params;


 


    node_init init;


    node_conn conn;


    node_write_data write_data;


    node_read_data read_data;


    node_close close;


    node_destroy destroy;


    node_reset reset;


 


    node_conn_cb conn_cb;


    node_write_cb write_cb;


    node_recv_cb recv_cb;


    node_close_cb close_cb;


} dul_node_t;




接著我們再對原有的結構體進行調整,將結構體前面的成員調整為 dul_node_s 結構體的成員,後面再加上自己的成員。這樣在插件初始化的時候統一以 dul_node_s 結構體初始化,而在用到具體某一個插件時我們進行結構體類型強轉即可,這裡有點像繼承里父類和子類的概念。




插件註冊




在插件使用前我們按需配置好用到的插件,但如果把插件介面直接暴露給業務方來配置,就需要讓業務方接觸到 C 代碼,這點比較難以控制。基於這個原因,我們討論了一下,想到前端裡面 webpack 對於插件配置的相關操作,於是我們查閱了 webpack 的相關文檔,最終我們仿照這個方式實現了我們的插件配置:"ws?path=/!tls!uv" 。不同插件以 ! 分割,通過循環將插件依次創建:





void separate_loaders(tokenizer_t *tokenizer, char *loaders, context *c) {


    char *outer_ptr = NULL;


     


    char *p = strtok_r(loaders, "!", &outer_ptr);


    dul_node_t *pre_loader = (dul_node_t *)c;


    while (p) {


        pre_loader = processor_loader(tokenizer, p, pre_loader);


        p = strtok_r(NULL, "!", &outer_ptr);


    }


}




單個插件所需要額外的 params 以 query string 形式拼接,在插件創建中用 ? 分割出來 ,以 kv 形式放入到一個 hashmap 中。再根據插件的名稱調用對應的初始化方法,並根據傳入的 pre_loader 綁定雙向鏈表的前後關係:





void (*oper_func[])(dul_node_t **) = {


    ws_alloc,


    tls_alloc,


    uv_alloc,


};


 


char const *loaders[] = {


    "ws", "tls", "uv"


};


 


dul_node_t *processor_loader(tokenizer_t *tokenizer, const char *loader, dul_node_t *pre_loader) {


    char *p = loader;


    char *inner_ptr = NULL;


 


    /* params 提取組裝 */


    p = strtok_r(p, "?", &inner_ptr);


    dul_node_t *node = NULL;


    map_t params = hashmap_new();


    params_parser(inner_ptr, params);


 


    /* 這裡採用轉移表,進行插件初始化 */


    while (strcmp(loaders[sqe], p) != 0) {


        sqe++;


    }


    oper_func[sqe](&node);


     


    if (node == NULL) {


        return NULL;


    }


    node->init(node, params);


    hashmap_free(params);


 


    // 雙向鏈表前後關係綁定


    pre_loader->next = node;


    node->pre = pre_loader;


    return node;


}


 


/* params string 解析 */


void params_parser(char *query, map_t params) {


    char *outer_ptr = NULL;


    char *p = strtok_r(query, "&", &outer_ptr);


    while (p) {


        char *inner_ptr = NULL;


        char *key =  strtok_r(p, "=", &inner_ptr);


        hashmap_put(params, key, inner_ptr);


        p = strtok_r(NULL, "&", &outer_ptr);


    }


}




Tips:隨著插件的增加,對應初始化的代碼也會越來越多,而且都是重複代碼,為了減少這部分工作,我們可以採取宏來定義函數。後續如果增加一個插件,只需要在底下加一行 LOADER_ALLOC(zim_xx, xx) 即可。





#define LOADER_ALLOC(type, name)                   


    void name##_alloc(dul_node_t **ctx) {           


        type##_t **loader = (type##_t **)ctx;       


        (*loader) = malloc(sizeof(type##_t));       


        (*loader)->init = &name##_init;             


        (*loader)->next = NULL;                     


        (*loader)->pre = NULL;                     


    }                       


 


LOADER_ALLOC(websocket, ws);


LOADER_ALLOC(zim_tls, tls);


LOADER_ALLOC(zim_uv, uv);




介面調用




再回到一開始我們思考介面調用的問題,由於有了函數指針變數,我們就需要在插件的初始化中把函數的地址存儲在這些變數中:





int ws_init(dul_node_t *ctx, map_t params) {


    websocket_t *ws = (websocket_t *)ctx;


    bzero(ws, sizeof(websocket_t));


     


    // 省略中間初始化過程


   


    ws->init = &ws_init;


    ws->conn = &ws_connect;


    ws->close = &ws_close;


    ws->destroy = &ws_destroy;


    ws->reset = &ws_reset;


    ws->write_data = &ws_send;


    ws->read_data = &ws_read;


    ws->conn_cb = &ws_conn_cb;


    ws->write_cb = &ws_send_cb;


    ws->recv_cb = &ws_recv_cb;


    ws->close_cb = &ws_close_cb;


    return OK;


}




對比介面前後調用的方式,前者需要知道下一個 connect 函數,並進行顯式調用,如果在 TLS 和 TCP 中新增一層,就需要改動 connect 函數的調用。但後者完全沒有這個顧慮,不論是新增還是刪除插件,它都可以通過指針找到對應的結構體,調用其 connect 函數,插件內部無需任何改動,豈不妙哉。





/* 改造前 */


int tls_ws_connect(tls_ws_t *handle,


                   tls_ws_conn_cb conn_cb,


                   tls_ws_close_cb close_cb) {


    ...


 


    return uv_tls_connect(tls,


                          handle->host,


                          handle->port,


                          on__tls_connect);


}


 


/* 改造後 */


static void tls_connect(dul_node_t *ctx) {


    zim_tls_t *tls = (zim_tls_t *)ctx;


     


    ...


 


    if (tls->next && tls->next->conn) {


        tls->next->host = tls->host;


        tls->next->port = tls->port;


        tls->next->conn(tls->next);


    }


}




新增插件




基於改造後組件,新增插件只需要改動三處,以日誌插件為例:




增加日誌文件




在頭文件中定義 zim_log_s 結構體(這裡沒有額外的成員):





typedef struct zim_log_s zim_log_t;


 


struct zim_log_s {


    dul_node_t *pre;


    dul_node_t *next;


 


    char *host;


    int port;


    map_t params;


 


    node_init init;


    node_conn conn;


    node_write_data write_data;


    node_read_data read_data;


    node_close close;


    node_destroy destroy;


    node_reset reset;


 


    node_conn_cb conn_cb;


    node_write_cb write_cb;


    node_recv_cb recv_cb;


    node_close_cb close_cb;


};




在實現文件中實現介面及回調,注意:即使介面或回調內沒有額外的操作,仍然需要實現,例如此處的 log_conn_cb 和 log_connect ,否則上一個插件或下一個插件在日誌層調用時會中斷:





/* callback */


void log_conn_cb(dul_node_t *ctx, int status) {


    zim_log_t *log = (zim_log_t *)ctx;


    if (log->pre && log->pre->conn_cb) {


        log->pre->conn_cb(log->pre, status);


    }


}


 


/* 省略中間直接回調 */


 


int log_recv_cb(dul_node_t *ctx, void *params, uv_buf_t *buf, ssize_t size) {


    /* 收集接收到的數據 */


    recv_data_from_server(buf->base, params, size);


     


    /* 繼續向上一層插件回調接收到的數據 */


    zim_log_t *log = (zim_log_t *)ctx;


    if (log->pre && log->pre->recv_cb) {


        log->pre->recv_cb(log->pre, opcode, buf, size);


    }


    return OK;


}


 


/* log hanlder */


int log_init(dul_node_t *ctx, map_t params) {


    zim_log_t *log = (zim_log_t *)ctx;


    bzero(log, sizeof(zim_log_t));


 


    log->init = &log_init;


    log->conn = &log_connect;


    log->write_data = &log_write;


    log->read_data = &log_read;


    log->close = &log_close;


    log->destroy = &log_destroy;


    log->reset = &log_reset;


    log->conn_cb = &log_conn_cb;


    log->write_cb = &log_write_cb;


    log->recv_cb = &log_recv_cb;


    log->close_cb = &log_close_cb;


 


    return OK;


}


 


static void log_connect(dul_node_t *ctx) {


    zim_log_t *log = (zim_log_t *)ctx;


    if (log->next && log->next->conn) {


        log->next->host = log->host;


        log->next->port = log->port;


        log->next->conn(log->next);


    }


}


 


/* 省略中間直接調用 */


 


static void log_write(dul_node_t *ctx, 


                      const char *payload, 


                      unsigned long long payload_size,


                      void *params) {


    /* 收集發送數據 */


    send_data_to_server(payload, payload_size, params);


 


    /* 繼續往下一層插件寫入數據 */


    zim_log_t *log = (zim_log_t *)ctx;


    if (log->next && log->next->write_data) {


        log->next->write_data(log->next, payload, payload_size, flags);


    }                           


}




增加日誌初始化函數及修改轉移表





LOADER_ALLOC(zim_log, log);


     


void (*oper_func[])(dul_node_t **) = {


    ws_alloc,


    tls_alloc,


    uv_alloc,


    log_alloc,


};


 


char const *loaders[] = {


    "ws", "tls", "uv", "log"


};




修改插件註冊





/* 增加日誌前 */


char loaders[] = "ws?path=/!tls!uv";


context_init(c, "127.0.0.1", 443, "", "", "", "", NULL, loaders);


 


/* 增加日誌後 */


char loaders[] = "log!ws?path=/!log!tls!uv";


context_init(c, "127.0.0.1", 443, "", "", "", "", NULL, loaders);


 




我們重新運行程序,就能發現日誌功能已經成功的配置上去,能夠將接受和發送的數據上報:







總結




回顧一下跨平台長連接組件的設計,我們使用 libuv 和 mbedtls 分別實現 TCP 和 TLS ,參照 WebSocket 協議實現了其握手及數據讀寫,同時抽象出通信介面及回調,為了和原生層交互,iOS 和 Android 分別採用 runtime 消息發送和 JNI 進行原生方法調用。




但這樣的定向設計完全不符合後期可能會有新增協議解析的預期,所以我們進行了插件化改造,其三個核心點是結構體改造、雙向鏈表和函數指針。




我們通過將插件行為抽象出一個結構體,利用雙向鏈表將前後插件綁定在一起,使用函數指針調用具體插件的函數或回調。




這樣做的優點是使得插件之間不存在耦合關係,只需保持邏輯順序上的關係,同時通過修改插件的註冊提高了靈活性,使得組件具有可插拔性(冷插拔)。




但在新增組件中我們需要實現所有的介面和回調,如果數量多的話,這還真是一件比較繁瑣的事情。




看完本文有收穫?請轉發分享給更多人


關注「ImportNew」,提升Java技能


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 ImportNew 的精彩文章:

偵探劇場:堆內存神秘溢出事件
通向架構師的道路(第十八天)萬能框架 Spring ( 一 )(上)

TAG:ImportNew |