目录
在上一篇博客已经实现了对于yuv数据的输出实现。那么现在就使用ZeroMQ发布到本机网络端口,并且使用Python+OpenCV订阅出来吧。
0x01 什么是ZeroMQ?
ZeroMQ(简称ZMQ)是一个基于消息队列的多线程网络库,其对套接字类型、连接处理、帧、甚至路由的底层细节进行抽象,提供跨越多种传输协议的套接字。
ZMQ以嵌入式网络编程库的形式实现了一个并行开发框架,对于普通的socket是端到端,一对一的模式,但是对于ZMQ,他可以实现N:M的关系。普通的BSD套接字的了解较多是点对点的连接,对于点对点的连接需要显示地建立连接、销毁连接、选择协议(TCP/UDP)和处理错误等,而ZMQ屏蔽了这些细节。
ZeroMQ可以提供什么?
-
能够提供进程内(inproc)、进程间(IPC)、网络(TCP)和广播方式的消息信道。
-
支持扇出(fan-out)、发布-订阅(pub-sub)、任务分发(task distribution)、请求/响应(request-reply)等通信模式。
使用这种ZeroMQ的中间件进行传输,大大简化了消息传输的中间过程,简化了很多链接的操作。
ZeroMQ是网络通信中新的一层,介于应用层和传输层之间(按照TCP/IP划分),其是一个可伸缩层,可并行运行,分三在分布式系统间。
ZMQ不是单独的服务,而是一个嵌入式库,它封装了网络通信、消息队列、线程调度等功能,向上层提供简洁的API,应用程序通过加载库文件,调用API函数来实现高性能网络通信。
0x02 ZeroMQ的消息模型
-
一对一模型(Exclusive-Pair)
最简单的1:1消息通信模型,可以认为是一个TCP Connection,但是TCP Server只能接受一个连接。数据可以双向流动,这点不同于后面的请求回应模型。
-
请求回应模型
由请求端发起请求,然后等待回应端应答。一个请求必须对应一个回应,从请求端的角度来看是发-收配对,从回应端的角度是收-发配对。跟一对一结对模型的区别在于请求端可以是1~N个。该模型主要用于远程调用及任务分配等。Echo服务就是这种经典模型的应用。
-
发布订阅模型
发布端单向分发数据,且不关心是否把全部信息发送给订阅端。如果发布端开始发布信息时,订阅端尚未连接上来,则这些信息会被直接丢弃。订阅端未连接导致信息丢失的问题,可以通过与请求回应模型组合来解决,订阅端只负责接受,而不能反馈,且订阅端消费速度慢于发布端的情况下,会在订阅端堆积数据。该模型主要用于数据分发。
-
推拉模型
Server端作为push端,而client端作为pull端,如果有多个client端同时连接到Server端,则server端会在内部做一个负载均衡,采用平均分配的算法,将所有消息均衡发布到client端上。与发布订阅模型相比,推拉模型在没有消费者的情况下,发布的消息不会被消耗掉。在消费者能力不够的情况下,能够提供多消费者并行消费解决方案。该模型主要用于多任务并行。
0x03 回到任务
那么对于ZeroMQ的了解就到这一步了,回到使用ZeroMQ传输yuv数据的时候了。作为一个强大的传输中间件,我们需要确定其传输模式,由于我们的scrcpy是源源不断的发送一帧一帧的图片,而接收端是否接收到数据,对于发送端来说都不会停止,那么这里选择发布订阅模型是比较合适的。对于发送者(PUB)我们可以确定在scrcpy的这一端,对于接收者(SUB),可以确定在Python+OpenCV订阅的这一端。
0x04 封装我们的yuv图像以及发布者
上次提取yuv图像时,我们需要的变量有如下:
-
手机的分辨率
-
图像宽度
-
图像高度
-
图像的Y/U/V三个分量
那么我们对应一个结构体,对他进行封装:
#define yuv_size 1080*2400
typedef struct{
int width;
int height;
uint8_t data_Y[yuv_size];
uint8_t data_U[yuv_size/4];
uint8_t data_V[yuv_size/4];
}myframe;
以上的结构体存储在decoder.h中,接下来回到decoder.c中进行对结构体的初始化:
//声明结构体
myframe yuv_frame;
//初始化长宽
yuv_frame.width= decoder->codec_ctx->width;
yuv_frame.height=decoder->codec_ctx->height;
//读取数据
int offest=0;
for(int i=0;i<yuv_frame.height;i++)
{
memcpy(yuv_frame.data_Y+offest,frame->data[0]+frame->linesize[0]*i,yuv_frame.width);
//指向下一行
offest += yuv_frame.width;
}
offest=0;
for(int i=0;i<yuv_frame.height/2;i++)
{
memcpy(yuv_frame.data_U+offest,frame->data[1]+frame->linesize[1]*i,yuv_frame.width/2);
offest += yuv_frame.width/2;
}
offset=0;
for(int i=0;i<yuv_frame.height/2;i++)
{
memcpy(yuv_frame.data_V+offset,frame->data[2]+frame->linesize[2]*i,yuv_frame.width/2);
offest += yuv_frame.width/2;
}
在Scrcpy中创建的ZeroMQ的发布者:
对于一个多线程的项目,对于一个创建发布者对象,我们需要找到一个函数初始化的位置进行实现,而不是随随便便的写在decoder.c中,这样会使其重复调用多次,导致出现bug。那么我们就得去查找关于ZeroMQ初始化所对应的文件,之后在他要执行下一步的时候创建好发布者的对象。在此之前,我们研究一下scrcpy的函数吧:
下面是scrcpy客户端初始化的函数:
bool
scrcpy(struct scrcpy_options *options) {
static struct scrcpy scrcpy;
struct scrcpy *s = &scrcpy;
// Minimal SDL initialization
if (SDL_Init(SDL_INIT_EVENTS)) {
LOGE("Could not initialize SDL: %s", SDL_GetError());
return false;
}
atexit(SDL_Quit);
bool ret = false;
bool server_started = false;
bool file_pusher_initialized = false;
bool recorder_initialized = false;
#ifdef HAVE_V4L2
bool v4l2_sink_initialized = false;
#endif
bool demuxer_started = false;
#ifdef HAVE_USB
bool aoa_hid_initialized = false;
bool hid_keyboard_initialized = false;
bool hid_mouse_initialized = false;
#endif
bool controller_initialized = false;
bool controller_started = false;
bool screen_initialized = false;
struct sc_acksync *acksync = NULL;
//1.server_init()
struct sc_server_params params = {
.req_serial = options->serial,
.select_usb = options->select_usb,
.select_tcpip = options->select_tcpip,
.log_level = options->log_level,
.crop = options->crop,
.port_range = options->port_range,
.tunnel_host = options->tunnel_host,
.tunnel_port = options->tunnel_port,
.max_size = options->max_size,
.bit_rate = options->bit_rate,
.max_fps = options->max_fps,
.lock_video_orientation = options->lock_video_orientation,
.control = options->control,
.display_id = options->display_id,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
.codec_options = options->codec_options,
.encoder_name = options->encoder_name,
.force_adb_forward = options->force_adb_forward,
.power_off_on_close = options->power_off_on_close,
.clipboard_autosync = options->clipboard_autosync,
.downsize_on_error = options->downsize_on_error,
.tcpip = options->tcpip,
.tcpip_dst = options->tcpip_dst,
.cleanup = options->cleanup,
};
static const struct sc_server_callbacks cbs = {
.on_connection_failed = sc_server_on_connection_failed,
.on_connected = sc_server_on_connected,
.on_disconnected = sc_server_on_disconnected,
};
if (!sc_server_init(&s->server, ¶ms, &cbs, NULL)) {
return false;
}
if (!sc_server_start(&s->server)) {
goto end;
}
//2.server_start()
server_started = true;
if (options->display) {
sdl_set_hints(options->render_driver);
}
// Initialize SDL video in addition if display is enabled
if (options->display && SDL_Init(SDL_INIT_VIDEO)) {
LOGE("Could not initialize SDL: %s", SDL_GetError());
goto end;
}
sdl_configure(options->display, options->disable_screensaver);
// Await for server without blocking Ctrl+C handling
if (!await_for_server()) {
goto end;
}
//3.server_connect_to
// It is necessarily initialized here, since the device is connected
struct sc_server_info *info = &s->server.info;
const char *serial = s->server.serial;
assert(serial);
//4.file_handler_init
struct sc_file_pusher *fp = NULL;
if (options->display && options->control) {
if (!sc_file_pusher_init(&s->file_pusher, serial,
options->push_target)) {
goto end;
}
fp = &s->file_pusher;
file_pusher_initialized = true;
}
//5.decoder_init()
struct sc_decoder *dec = NULL;
bool needs_decoder = options->display;
#ifdef HAVE_V4L2
needs_decoder |= !!options->v4l2_device;
#endif
if (needs_decoder) {
sc_decoder_init(&s->decoder);
dec = &s->decoder;
}
struct sc_recorder *rec = NULL;
if (options->record_filename) {
if (!sc_recorder_init(&s->recorder,
options->record_filename,
options->record_format,
info->frame_size)) {
goto end;
}
rec = &s->recorder;
recorder_initialized = true;
}
//6.av_log_set_callback()
av_log_set_callback(av_log_callback);
//7.sc_demuxer_init()
static const struct sc_demuxer_callbacks demuxer_cbs = {
.on_eos = sc_demuxer_on_eos,
};
sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL);
//8.sc_demuxer_add_sink(dec)
if (dec) {
sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink);
}
//9.sc_demuxer_add_sink(rec)
if (rec) {
sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink);
}
//10.sc_controller_init();control_socket
struct sc_controller *controller = NULL;
struct sc_key_processor *kp = NULL;
struct sc_mouse_processor *mp = NULL;
//11.sc_controller_start();
if (options->control) {
#ifdef HAVE_USB
bool use_hid_keyboard =
options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_HID;
bool use_hid_mouse =
options->mouse_input_mode == SC_MOUSE_INPUT_MODE_HID;
if (use_hid_keyboard || use_hid_mouse) {
bool ok = sc_acksync_init(&s->acksync);
if (!ok) {
goto end;
}
ok = sc_usb_init(&s->usb);
if (!ok) {
LOGE("Failed to initialize USB");
sc_acksync_destroy(&s->acksync);
goto aoa_hid_end;
}
assert(serial);
struct sc_usb_device usb_device;
ok = sc_usb_select_device(&s->usb, serial, &usb_device);
if (!ok) {
sc_usb_destroy(&s->usb);
goto aoa_hid_end;
}
LOGI("USB device: %s (%04" PRIx16 ":%04" PRIx16 ") %s %s",
usb_device.serial, usb_device.vid, usb_device.pid,
usb_device.manufacturer, usb_device.product);
ok = sc_usb_connect(&s->usb, usb_device.device, NULL, NULL);
sc_usb_device_destroy(&usb_device);
if (!ok) {
LOGE("Failed to connect to USB device %s", serial);
sc_usb_destroy(&s->usb);
sc_acksync_destroy(&s->acksync);
goto aoa_hid_end;
}
ok = sc_aoa_init(&s->aoa, &s->usb, &s->acksync);
if (!ok) {
LOGE("Failed to enable HID over AOA");
sc_usb_disconnect(&s->usb);
sc_usb_destroy(&s->usb);
sc_acksync_destroy(&s->acksync);
goto aoa_hid_end;
}
if (use_hid_keyboard) {
if (sc_hid_keyboard_init(&s->keyboard_hid, &s->aoa)) {
hid_keyboard_initialized = true;
kp = &s->keyboard_hid.key_processor;
} else {
LOGE("Could not initialize HID keyboard");
}
}
if (use_hid_mouse) {
if (sc_hid_mouse_init(&s->mouse_hid, &s->aoa)) {
hid_mouse_initialized = true;
mp = &s->mouse_hid.mouse_processor;
} else {
LOGE("Could not initialized HID mouse");
}
}
bool need_aoa = hid_keyboard_initialized || hid_mouse_initialized;
if (!need_aoa || !sc_aoa_start(&s->aoa)) {
sc_acksync_destroy(&s->acksync);
sc_usb_disconnect(&s->usb);
sc_usb_destroy(&s->usb);
sc_aoa_destroy(&s->aoa);
goto aoa_hid_end;
}
acksync = &s->acksync;
aoa_hid_initialized = true;
aoa_hid_end:
if (!aoa_hid_initialized) {
if (hid_keyboard_initialized) {
sc_hid_keyboard_destroy(&s->keyboard_hid);
hid_keyboard_initialized = false;
}
if (hid_mouse_initialized) {
sc_hid_mouse_destroy(&s->mouse_hid);
hid_mouse_initialized = false;
}
}
if (use_hid_keyboard && !hid_keyboard_initialized) {
LOGE("Fallback to default keyboard injection method "
"(-K/--hid-keyboard ignored)");
options->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT;
}
if (use_hid_mouse && !hid_mouse_initialized) {
LOGE("Fallback to default mouse injection method "
"(-M/--hid-mouse ignored)");
options->mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT;
}
}
#else
assert(options->keyboard_input_mode != SC_KEYBOARD_INPUT_MODE_HID);
assert(options->mouse_input_mode != SC_MOUSE_INPUT_MODE_HID);
#endif
// keyboard_input_mode may have been reset if HID mode failed
if (options->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_INJECT) {
sc_keyboard_inject_init(&s->keyboard_inject, &s->controller,
options->key_inject_mode,
options->forward_key_repeat);
kp = &s->keyboard_inject.key_processor;
}
// mouse_input_mode may have been reset if HID mode failed
if (options->mouse_input_mode == SC_MOUSE_INPUT_MODE_INJECT) {
sc_mouse_inject_init(&s->mouse_inject, &s->controller);
mp = &s->mouse_inject.mouse_processor;
}
if (!sc_controller_init(&s->controller, s->server.control_socket,
acksync)) {
goto end;
}
controller_initialized = true;
if (!sc_controller_start(&s->controller)) {
goto end;
}
controller_started = true;
controller = &s->controller;
if (options->turn_screen_off) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF;
if (!sc_controller_push_msg(&s->controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
}
}
}
// There is a controller if and only if control is enabled
assert(options->control == !!controller);
if (options->display) {
const char *window_title =
options->window_title ? options->window_title : info->device_name;
struct sc_screen_params screen_params = {
.controller = controller,
.fp = fp,
.kp = kp,
.mp = mp,
.forward_all_clicks = options->forward_all_clicks,
.legacy_paste = options->legacy_paste,
.clipboard_autosync = options->clipboard_autosync,
.shortcut_mods = &options->shortcut_mods,
.window_title = window_title,
.frame_size = info->frame_size,
.always_on_top = options->always_on_top,
.window_x = options->window_x,
.window_y = options->window_y,
.window_width = options->window_width,
.window_height = options->window_height,
.window_borderless = options->window_borderless,
.rotation = options->rotation,
.mipmaps = options->mipmaps,
.fullscreen = options->fullscreen,
.start_fps_counter = options->start_fps_counter,
.buffering_time = options->display_buffer,
};
//12.sc_screen_init()
if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
}
screen_initialized = true;
//13.sc_decoder_add_sink()
sc_decoder_add_sink(&s->decoder, &s->screen.frame_sink);
}
//14.sc_v4l2_sink_init()
#ifdef HAVE_V4L2
if (options->v4l2_device) {
if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device,
info->frame_size, options->v4l2_buffer)) {
goto end;
}
sc_decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink);
v4l2_sink_initialized = true;
}
#endif
//15.启动流配置
// now we consumed the header values, the socket receives the video stream
// start the demuxer
if (!sc_demuxer_start(&s->demuxer)) {
goto end;
}
demuxer_started = true;
//16.event_loop()
ret = event_loop(s);
LOGD("quit...");
// Close the window immediately on closing, because screen_destroy() may
// only be called once the demuxer thread is joined (it may take time)
sc_screen_hide_window(&s->screen);
end:
// The demuxer is not stopped explicitly, because it will stop by itself on
// end-of-stream
#ifdef HAVE_USB
if (aoa_hid_initialized) {
if (hid_keyboard_initialized) {
sc_hid_keyboard_destroy(&s->keyboard_hid);
}
if (hid_mouse_initialized) {
sc_hid_mouse_destroy(&s->mouse_hid);
}
sc_aoa_stop(&s->aoa);
sc_usb_stop(&s->usb);
}
if (acksync) {
sc_acksync_destroy(acksync);
}
#endif
if (controller_started) {
sc_controller_stop(&s->controller);
}
if (file_pusher_initialized) {
sc_file_pusher_stop(&s->file_pusher);
}
if (screen_initialized) {
sc_screen_interrupt(&s->screen);
}
if (server_started) {
// shutdown the sockets and kill the server
sc_server_stop(&s->server);
}
// now that the sockets are shutdown, the demuxer and controller are
// interrupted, we can join them
if (demuxer_started) {
sc_demuxer_join(&s->demuxer);
}
#ifdef HAVE_V4L2
if (v4l2_sink_initialized) {
sc_v4l2_sink_destroy(&s->v4l2_sink);
}
#endif
#ifdef HAVE_USB
if (aoa_hid_initialized) {
sc_aoa_join(&s->aoa);
sc_aoa_destroy(&s->aoa);
sc_usb_join(&s->usb);
sc_usb_disconnect(&s->usb);
sc_usb_destroy(&s->usb);
}
#endif
// Destroy the screen only after the demuxer is guaranteed to be finished,
// because otherwise the screen could receive new frames after destruction
if (screen_initialized) {
sc_screen_join(&s->screen);
sc_screen_destroy(&s->screen);
}
if (controller_started) {
sc_controller_join(&s->controller);
}
if (controller_initialized) {
sc_controller_destroy(&s->controller);
}
if (recorder_initialized) {
sc_recorder_destroy(&s->recorder);
}
if (file_pusher_initialized) {
sc_file_pusher_join(&s->file_pusher);
sc_file_pusher_destroy(&s->file_pusher);
}
//17.销毁server
sc_server_destroy(&s->server);
return ret;
}
之后我们就在无条件循环for(;;)前创建ZeroMQ对象:
对于ZeroMQ的使用可以参考官网:ZeroMQ API - 0MQ Api
对于ZeroMQ的中文教程:ZeroMQ教程中文版_神马_逗_浮云的博客-CSDN博客_zeromq中文
//新建一个ZeroMQ对象
void *context = zmq_ctx_new();
//在使用任何ØMQ库函数之前,必须创建一个ØMQ context。在结束应用程序时,必须销毁(删除)这个context。
//创建ZMQ套接字,设定为发布订阅模式
//在.h中初始化void *response;
response=zmq_socket(context,ZMQ_PUB);
//绑定一个socket,接收发来的链接请求
int rc = zmq_bind(response,"tcp://127.0.0.1:5555");
//链接失败则返回0
assert(rc==0);
//在for循环中添加
//使用ZeroMQ发送数据
zmq_send(response,&yuv_frame,sizeof(yuv_frame),ZMQ_DONTWAIT);
//在for循环的最后关闭端口
zmq_close(response);
到这里我们的发送端就已经设置完成了,那么接下来就看接收端吧。
0x05 使用Python订阅ZeroMQ的发布
使用Python进行订阅:
import zmq
context = zmq.context()
socket = context.socket(zmq.SUB)
socket.connect("tcp://127.0.0.1:5555")
socket.setsokopt(zmq.SUBSCRIBE,'')
while True:
printf socket.rev()
在“发布-订阅模式”下,发布者绑定一个指定的网络端口,订阅者则连接到该地址,该模式下消息流是单向的,只允许从发布者流向订阅者,发布者只管发消息,不会理会是否存在订阅者。一个发布者可以拥有多个订阅者,一个订阅者也可以订阅多个发布者。
一些特点:
-
公平排队:一个“订阅者”链接到多个发布者时,会均衡地从每个发布者读取消息,不会出现一个“发布者”淹没其他“发布者”的情况。
-
ZMQ3.0以上版本,过滤规则发生在“发布方”。ZMQ3.0以下版本,过滤规则则发生在订阅方。其实也就是处理消息的位置。
0x06 需要注意的
在编写自己的程序时可能会出现这个问题:
[-Wimplicit-function-declaration] 118 | zmq_send(response,&yuv_frame,sizeof(yuv_frame),ZMQ_DONTWAIT); | ^~~~ ../app/src/decoder.c:118:48: error: ‘ZMQ_DONTWAIT’ undeclared (first use in this function); did you mean ‘MSG_DONTWAIT’? 118 | zmq_send(response,&yuv_frame,sizeof(yuv_frame),ZMQ_DONTWAIT); | ^~~~ | MSG_DONTWAIT ../app/src/decoder.c:118:48: note: each undeclared identifier is reported only once for each function it appears in
之后发现包含了zmq.h也不行
/src_decoder.c.o -c ../app/src/decoder.c ../app/src/decoder.c:5:10: fatal error: zmq.h: 没有那个文件或目录 5 | #include <zmq.h> | ^~~ compilation terminated.
这个问题的原因是我们的Ubuntu上并没有安装zmq.h,那么安装:
sudo apt-get install libzmq3-dev
就可以解决以上问题。