1. 前言
最近考慮到一個問題,項目中有同時處理socket、zeromq的邏輯需求,想通過libevent(I/O服用)一塊將zmq-socket的事件也放一個線程中處理。
網上了解了一些實現,大部分都是通過將zmq的sockfd
拿到,加入libevent_dispatch
中一併處理,但是存在問題是實時性不夠,寫法不對導致丟事件的情況。
上述的根本原因是:zeromq是通過底層獨立線程完成socket層面的收發,拷貝到內存隊列供給上層業務使用。強行監聽socket事件,但實際數據獲取還是從內存隊列拿取,肯定是存在時效性的問題。
所以,本文嘗試一種方法,服務端這塊直接剖析zeromq協議zmtp
,不要zmq子線程、內存隊列,直接從socket層面收取數據;客戶端仍然兼容libzmq
常規的寫法。
2. 相關知識
根據 zmtp官方說明 的解讀:
協議流程主要爲:打招呼greeting
、握手handshake
、通信traffic
;
通信內容主要分爲命令command
、消息message
;
2.1 Greeting格式
Greeting 又可以細分爲:簽名signature
、版本號version
、機器碼mechanism
、類型as-server
、擴展filter
;主要是zmq考慮到了多種協議版本、多種協議類型,考慮的字段信息就多了。
; The greeting announces the protocol details
greeting = signature version mechanism as-server filler
signature = %xFF padding %x7F
padding = 8OCTET ; Not significant
version = version-major version-minor
version-major = %x03
version-minor = %x00
; The mechanism is a null padded string
mechanism = 20mechanism-char
mechanism-char = "A"-"Z" | DIGIT
| "-" | "_" | "." | "+" | %x0
; Is the peer acting as server?
as-server = %x00 | %x01
; The filler extends the greeting to 64 octets
filler = 31%x00 ; 31 zero octets
2.1 Handshake格式
Handshake相比Greeting簡單多了,一個command搞定
; The handshake consists of at least one command
; The actual grammar depends on the security mechanism
handshake = 1*command
2.2 Command格式
Command提供了長命令、短命令進行選擇;與Message不同的是,Command數據域body
還拆分出命令名稱Command-name
和命令數據Command-data
,這塊主要用到的地方是包含在數據幀中,配合Message進行信息控制交互;
; A command is a single long or short frame
command = command-size command-body
command-size = %x04 short-size | %x06 long-size
short-size = OCTET ; Body is 0 to 255 octets
long-size = 8OCTET ; Body is 0 to 2^63-1 octets
command-body = command-name command-data
command-name = OCTET 1*255command-name-char
command-name-char = ALPHA
command-data = *OCTET
2.3 Message格式
Message內部其實提供了兩種類型(長消息、短消息)、兩種標記(獨立報文、組合報文)進行靈活選擇
; A message is one or more frames
message = *message-more message-last
message-more = ( %x01 short-size | %x03 long-size ) message-body
message-last = ( %x00 short-size | %x02 long-size ) message-body
message-body = *OCTET
3. 實踐
通過zmtp
協議的指導,我們選取一個方向來實踐一下:push-pull
協議、libevent::buffer_event
;
3.1 連接握手
這次先看一下主函數,有個大概的印象:socket連接__do_connect
,zmq握手__do_handshake
,回調函數拉數據on_recv
。
int main(int argc, char *argv[])
{
int res = -1;
struct bufferevent *bev = NULL;
struct event_base *base = NULL;
struct msg_ctx mctx = {0};
mctx.evbuf = evbuffer_new();
__do_reset(&mctx);
if (argc < 2) {
printf("%s <address>\n", argv[0], argv[1]);
exit(EXIT_FAILURE);
}
printf("Program start...\n");
base = event_base_new();
assert(base);
bev = __do_connect(base, argv[1]);
res = __do_handshark(bev);
assert(res == 0);
bufferevent_setwatermark(bev, EV_READ, 0, 0);
bufferevent_setcb(bev, on_recv, NULL, NULL, &mctx);
bufferevent_enable(bev, EV_READ);
event_base_dispatch(base);
printf("Program quit...\n");
sleep(5);
return EXIT_SUCCESS;
}
本端實現的是pull的功能,需要連接到push,然後進行拉取數據;
socket建立連接使用的常規的bufferevent_socket_connect
的方法
static struct bufferevent *__do_connect(struct event_base *base, const char *paddr)
{
int res = -1;
int dstlen = sizeof(struct sockaddr_storage);
struct bufferevent *bev = NULL;
struct sockaddr_storage dst;
res = evutil_parse_sockaddr_port(paddr, (struct sockaddr *)&dst, &dstlen);
assert(0 == res);
bev = bufferevent_socket_new(base, -1, BEV_OPT_CLOSE_ON_FREE);
assert(bev);
res = bufferevent_socket_connect(bev, (struct sockaddr *)&dst, dstlen);
assert(0 == res);
return bev;
}
下來準備看握手過程的實現,先看一下結構體的定義是怎麼跟zmtp
結合的;這裏有個地方注意一下,zmtp3.0與2.0協議格式還不一致,所以先制定約束:使用v3協議,不做v2的兼容。
/*
* ZMTP 3.0
* connection = greeting handshake traffic
*
* This is the 3.0 greeting (64 bytes)
* greeting = signature version mechanism as-server filler
*/
struct zmtp_greeting {
char signature [10]; // %xFF padding %x7F
char version [2]; // version-major %x03, version-minor %x00
char mechanism [20]; // The mechanism is a null padded string
char as_server [1]; // Is the peer acting as server
char filler [31]; // The filler extends the greeting to 64 octets
};
從第2小節可以看出,Message和Command是類似的,這裏我們全抽象爲:flags、size、data(柔性數組)
/*
* A message is one or more frames
*
* message = *message-more message-last
* message-more = ( %x01 short-size | %x03 long-size ) message-body
* message-last = ( %x00 short-size | %x02 long-size ) message-body
* message-body = *OCTET
*
* short-size = OCTET ; Body is 0 to 255 octets
* long-size = 8OCTET ; Body is 0 to 2^63-1 octets
*
*/
struct zmtp_msg_shdr {
u8 flags; // Must be zero
u8 size; // Size, 0 to 255 bytes
u8 data [0]; // Message data
};
struct zmtp_msg_lhdr {
u8 flags; // Must be zero
u64 size; // Size, 0 to 255 bytes
u8 data [0]; // Message data
};
結構體準備好了,我們看一個握手的快速實現,這裏說的快速
是指使用阻塞同步的形式完成;另外一種常規的做法是使用狀態機+事件回調的方式完成。
static int __do_handshark(struct bufferevent *bev)
{
int res = -1;
int fd = bufferevent_getfd(bev);
struct zmtp_greeting gt = ZMTP_GREETING_INIT;
char buffer[SIZE_LINE_NORMAL];
struct zmtp_msg_shdr *phead = (struct zmtp_msg_shdr *)buffer;
char pull_data[] = ZMTP_HANDSHAKE_PULL;
struct zmtp_msg_shdr pull_head = {
.flags = ZMTP_FLAGS_SCMD,
.size = sizeof(pull_data) - 1,
};
/* Greeting */
res = sdk_tcp_send_nbytes(fd, >, sizeof(struct zmtp_greeting), TIMEO);
assert(0 == res);
res = sdk_tcp_recv_nbytes(fd, >, sizeof(struct zmtp_greeting), TIMEO);
assert(0 == res);
printf("Greeting done...\n");
/* Handshark */
res = sdk_tcp_recv_nbytes(fd, phead, sizeof(struct zmtp_msg_shdr), TIMEO);
assert(0 == res);
assert(0x04 == phead->flags);
res = sdk_tcp_recv_nbytes(fd, phead->data, phead->size, TIMEO);
assert(0 == res);
printf("Handsharke size: %d\n\n", phead->size);
DSP_TOTAL(phead->data, phead->size);
res = sdk_tcp_send_nbytes(fd, &pull_head, sizeof(struct zmtp_msg_shdr), TIMEO);
assert(0 == res);
res = sdk_tcp_send_nbytes(fd, &pull_data, pull_head.size, TIMEO);
assert(0 == res);
printf("Handsharke done...\n");
return 0;
}
上述其實可以看到,報文填充非常暴力,通過宏直接將握手信息全部填充進去了。
#define ZMTP_GREETING_INIT { \
{ 0xFF, 0, 0, 0, 0, 0, 0, 0, 1, 0x7F }, \
{ 3, 0 }, \
{ 'N', 'U', 'L', 'L', 0 }, \
{ 0 }, \
{ 0 } \
}
#define ZMTP_HANDSHAKE_PULL { \
0x05, \
'R', 'E', 'A', 'D', 'Y', \
0x0b, \
'S', 'o', 'c', 'k', 'e', 't', '-', 'T', 'y', 'p', 'e', \
0x00, 0x00, 0x00, 0x04, \
'P', 'U', 'L', 'L', '0'\
}
3.2 數據拉取
數據拉取過程,由於Message
數據域是不定長的,所以我們通過狀態機
的形式剖報文
void on_recv(struct bufferevent *bev, void *args)
{
int res = 0;
struct msg_ctx *mctx = (struct msg_ctx *)args;
/* Et mode */
while (1) {
size_t length = evbuffer_get_length(bufferevent_get_input(bev));
if (length == 0) {
break;
}
printf("Evbuffer length: %u\n", length);
switch (mctx->status) {
case STATUS_MSG_HEAD:
if (0 == __do_head_parser(bev, mctx)) {
/* state transition */
mctx->status = STATUS_MSG_BODY;
printf("Head done, data: %u\n", mctx->size);
}
break;
case STATUS_MSG_BODY:
if (0 == __do_body_parser(bev, mctx)) {
/* state transition */
mctx->status = STATUS_MSG_HEAD;
printf("Body done, data: %u\n", mctx->size);
}
break;
default:
assert(0);
break;
}
}
return;
}
Message頭部信息,我們主要需要區分是長消息、還是短消息;是獨立消息、還是組合消息。
enum {
ZMTP_FLAGS_SCMD = 0x04,
ZMTP_FLAGS_LCMD = 0x06,
ZMTP_FLAGS_SMSG_MORE = 0x01,
ZMTP_FLAGS_LMSG_MORE = 0x03,
ZMTP_FLAGS_SMSG_LAST = 0x00,
ZMTP_FLAGS_LMSG_LAST = 0x02,
};
static int __do_head_parser(struct bufferevent *bev, struct msg_ctx *mctx)
{
if (mctx->flags == 0xFF) {
bufferevent_read(bev, &mctx->flags, sizeof(u8));
printf("Head flag: %x\n", mctx->flags);
}
if (mctx->size == 0x00) {
size_t expect = 0;
size_t length = evbuffer_get_length(bufferevent_get_input(bev));
switch (mctx->flags) {
case ZMTP_FLAGS_SMSG_MORE:
case ZMTP_FLAGS_SMSG_LAST:
expect = sizeof(u8);
printf("Body short\n");
break;
case ZMTP_FLAGS_LMSG_MORE:
case ZMTP_FLAGS_LMSG_LAST:
expect = sizeof(u64);
printf("Body long\n");
break;
default:
assert(0);
break;
}
if (length < expect) {
printf("Retry\n");
return RETRY;
}
size_t rlen = bufferevent_read(bev, &mctx->size, expect);
assert(rlen == expect);
if (expect == sizeof(u64)) {
mctx->size = ntohll(mctx->size);
}
assert(mctx->size > 0);
}
return 0;
}
然後是數據域狀態下的數據讀取了,這個主要考慮緩衝消息到evbuffer
中,消息收全了最後才調用__do_something
和__do_reset
函數
static int __do_body_parser(struct bufferevent *bev, struct msg_ctx *mctx)
{
char buffer[SIZE_LINE_LONG];
size_t rmax = _MIN(sizeof(buffer), mctx->size - evbuffer_get_length(mctx->evbuf));
size_t rlen = bufferevent_read(bev, buffer, rmax);
int res = evbuffer_add(mctx->evbuf, buffer, rlen);
assert(res == 0);
if (evbuffer_get_length(mctx->evbuf) >= mctx->size) {
printf("Body done, length: %u/%u\n",
evbuffer_get_length(mctx->evbuf), mctx->size);
__do_something(mctx);
__do_reset(mctx);
return 0;
}
printf("Retry\n");
return RETRY;
}
static void __do_reset(struct msg_ctx *mctx)
{
mctx->status = STATUS_MSG_HEAD;
mctx->flags = 0xFF;
mctx->size = 0x00;
evbuffer_drain(mctx->evbuf, evbuffer_get_length(mctx->evbuf));
}
static void __do_something(struct msg_ctx *mctx)
{
printf("-- 0x%x\n", mctx->flags);
printf("-- %d\n", mctx->size);
while (evbuffer_get_length(mctx->evbuf) > 0) {
char buffer[SIZE_LINE_LONG];
ssize_t rlen = evbuffer_remove(mctx->evbuf, buffer, sizeof(buffer));
printf("-- %s\n", buffer);
}
}
3.3 運行結果
短消息的拉取:
./pullx 127.0.0.1:5555
Program start...
Greeting done...
Handsharke size: 26
0000 05 52 45 41 44 59 0b 53 - 6f 63 6b 65 74 2d 54 79 .READY.Socket-Ty
0010 70 65 00 00 00 04 50 55 - 53 48 ** ** ** ** ** ** pe....PUSH
Handsharke done...
Evbuffer length: 14
Head flag: 0
Body short
Head done, data: 12
Evbuffer length: 12
Body done, length: 12/12
-- 0x0
-- 12
-- Data- -000
Body done, data: 0
Evbuffer length: 14
Head flag: 0
Body short
Head done, data: 12
Evbuffer length: 12
Body done, length: 12/12
-- 0x0
-- 12
-- Data- -001
Body done, data: 0
長消息的拉取:
./pullx 127.0.0.1:5555
Program start...
Greeting done...
Handsharke size: 26
0000 05 52 45 41 44 59 0b 53 - 6f 63 6b 65 74 2d 54 79 .READY.Socket-Ty
0010 70 65 00 00 00 04 50 55 - 53 48 ** ** ** ** ** ** pe....PUSH
Handsharke done...
Evbuffer length: 1033
Head flag: 2
Body long
Head done, data: 1024
Evbuffer length: 1024
Body done, length: 1024/1024
-- 0x2
-- 1024
-- Data- -000
Body done, data: 0
Evbuffer length: 1033
Head flag: 2
Body long
Head done, data: 1024
Evbuffer length: 1024
Body done, length: 1024/1024
-- 0x2
-- 1024
-- Data- -001
Body done, data: 0
Evbuffer length: 1033
Head flag: 2
Body long
Head done, data: 1024
Evbuffer length: 1024
Body done, length: 1024/1024
4. 結論
通過本次試驗,驗證了直接剖取zmtp
方法的可行性;但是要到工程中實踐,還得思考幾個問題:
- push-pull協議簡單,但router-dealer、pub-sub模式、加密、認證協議的剖析會更加複雜;
- socket維護的問題,需要考慮斷線重連、重新握手的問題;
- 協議版本的兼容性;