How a Plug‑In Architecture Transforms Cross‑Platform Long‑Connection Components
This article explains how a unified, cross‑platform long‑connection component built on libuv, mbedTLS and WebSocket was refactored into a plug‑in architecture that decouples layers, uses double‑linked structures and function pointers, and dramatically improves flexibility and maintainability.
Background
Before the cross‑platform component was created, iOS and Android each used a separate long‑connection module (both based on raw sockets), which doubled development and maintenance effort, made product‑level adjustments difficult, and required the backend to support both WebSocket (Web) and socket (mobile) protocols.
Architecture Overview
The component is organized into five layers from top to bottom:
Native layer : encapsulates business requests, parses data and interacts with the native side.
Chat layer : provides C‑style connect, read/write and close interfaces and maintains a reconnection loop.
WebSocket layer : implements the WebSocket protocol and heartbeat handling.
TLS layer : based on mbedTLS to perform TLS handshake and data encryption/decryption.
TCP layer : built on libuv to create TCP connections and perform asynchronous I/O.
The overall architecture is illustrated below:
TCP Layer
The TCP layer uses libuv, an asynchronous I/O library that abstracts file, network and pipe operations and selects the optimal I/O model on each platform (Linux, Windows, Darwin). Its core is an event loop that consists of six phases:
timers phase : executes callbacks for setTimeout and setInterval.
I/O callbacks phase : handles system‑call errors such as network errors.
idle / prepare phase : used internally by Node.js.
poll phase : retrieves new I/O events; Node may block here.
check phase : runs setImmediate() callbacks.
close callbacks phase : runs socket close callbacks.
Typical libuv event‑loop diagram:
TLS Layer
mbedTLS(formerly PolarSSL) provides an easy‑to‑use SSL/TLS implementation. TLS ensures confidentiality and integrity of network traffic. It consists of a record layer (defines the format of transmitted data) and a transport layer (handles the actual data transfer). Before data exchange, a handshake is performed that includes mutual authentication, cipher‑suite negotiation and key exchange.
WebSocket Layer
The WebSocket layer implements RFC 6455 (protocol version 13). It handles the handshake, data framing, and connection closure.
Handshake
The client sends an HTTP request with an Upgrade: websocket header. The Sec-WebSocket-Key header contains a base64‑encoded random string that the server uses to prove the request came from a legitimate WebSocket client.
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: 13The server replies with a 101 Switching Protocols response that includes Sec-WebSocket-Accept, which is the base64‑encoded SHA‑1 hash of the client key concatenated with the magic GUID.
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Data Frame
WebSocket frames consist of several fields: FIN, RSV1‑3, opcode, MASK, payload length, optional extended payload length, optional masking key, and payload data. The diagram below shows the bit layout.
Key field meanings:
FIN – 1 if this is the final fragment of a message.
RSV1‑3 – must be 0 unless an extension defines them.
opcode – defines the type of payload (e.g., 0x1 for text, 0x2 for binary, 0x8 for close, 0x9 for ping, 0xA for pong).
MASK – set to 1 for frames from client to server; a 4‑byte masking key follows.
Payload length – 7‑bit value; 126 indicates a 2‑byte extended length, 127 indicates an 8‑byte extended length.
Close Connection
Either side can initiate a close by sending a close frame (opcode 0x8). The peer replies with its own close frame, after which the TCP connection is terminated.
Chat Layer
The Chat layer offers simple connect, read/write and disconnect interfaces and maintains a reconnection loop.
Native Layer
Because the component is written in C, native interaction differs per platform: Android uses JNI, iOS uses the Objective‑C runtime.
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);
} 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);
}Plug‑In Architecture Refactor
After the initial component was completed, a plug‑in redesign was undertaken to address three problems:
Different business partners required TLS (WSS), plain WebSocket, or even custom protocols.
The original chain (Worker → WebSocket → TLS → TCP) was tightly coupled; removing or adding a layer required extensive code changes.
Future protocol extensions should be possible without touching existing code.
The solution introduced three core ideas: a unified structure, a double‑linked list of plugins, and function pointers for all operations.
Structure Redesign
A generic dul_node_t structure holds pointers to the previous and next plugins, common parameters (host, port, params) and a set of function pointers for init, connect, read, write, close, destroy, reset and callbacks.
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;Each concrete plugin (WebSocket, TLS, TCP, Log, etc.) embeds this generic node as its first member, effectively achieving inheritance.
Double‑Linked Plugin Chain
During initialization the plugins are linked together. The configuration string resembles Webpack loader syntax, e.g. "ws?path=/!tls!uv". The loader parser splits the string by !, creates each plugin, stores query‑string parameters in a hashmap, and links the nodes.
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);
}
}Parameter parsing turns a query string like ?path=/&mode=secure into a hashmap:
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);
}
}Function‑Pointer Calls
Each plugin’s conn implementation forwards the call to the next plugin if it exists, passing host and port values. This eliminates hard‑coded calls such as tls_ws_connect() and makes the chain fully dynamic.
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);
}
}Adding a New Plugin (Log Example)
To add a logging plugin the following steps are required:
Define a zim_log_t structure that embeds the generic node.
Implement all interface functions (init, conn, write_data, read_data, close, destroy, reset) and callbacks, even if they simply forward the call.
Add a macro allocation entry and extend the loader tables.
Update the configuration string to include log at the desired position.
Structure definition:
typedef 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;
} zim_log_t;Callback forwarding example:
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);
}
}Macro for allocation:
#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);
LOADER_ALLOC(zim_log, log);After rebuilding, the log plugin records incoming and outgoing payloads, as shown in the screenshot.
Conclusion
The unified component uses libuv for TCP, mbedTLS for TLS, and a full WebSocket implementation to provide a cross‑platform long‑connection solution. By refactoring the design into a plug‑in architecture with a double‑linked list and function pointers, the system becomes loosely coupled, easily extensible, and supports cold‑plug‑and‑play of new protocols or features without touching existing layers.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
21CTO
21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
