ZeroMQ笔记

ZeroMQ笔记

zmq是一个轻量级消息传输内核。可以理解为封装后的网络库,把一些在实践中总结出来的消息通信模型封装成不同类型的套接字,以供我们使用。

通信单元是消息,业务层收到的消息一定是一个完整的包,不用考虑分包和粘包。

不同模型可以按照一定的条件组合,并且接口统一。

安装

python为例:

#安装
pip3 install pyzmq

#查看是否安装成功
import zmq
print(zmq.__version__)
print(zmq.zmq_version_info())

传输方式

  1. inproc:同一个进程的线程之间,bind("inproc://#1.inproc")
  2. ipc:本地进程之间,bind("ipc:///tmp/GameLogicServer.ipc")
  3. tcp:远程进程之间,bind("tcp://*:5555")
  4. pgm:PGM datagrams are layered directly on top of IP datagrams as defined by RFC 3208
  5. epgm: EPGM where PGM datagrams are encapsulated inside UDP datagrams

(Encapsulated Pragmatic General Multicast)

如果两个进程在同一主机且在同一个操作平台,可选择IPC或TCI/IP两种通讯方式都可以,但IPC效率高于TCP/IP。

采用IPC通讯,进程1直接把通讯包发给进程2。

采用TCP/IP通讯,进程1将要先把通讯包发给“LO”即本地环路接口,通过"LO"再把通讯包发给进程2。

基础的消息模式

有三种,其他模式都是在这些基础上改进的。

req-rep

pub-sub

push-pull

router-dealer基于req-rep

Request-Reply (请求响应模式)

消息双向的,同步。

两个角色:请求端、回应端,请求端相当于客户端,回应端相当于服务端。

请求端和回应端无论谁先启动,效果是相同的。

由请求端发起请求,然后等待回应端应答。

一个请求必须对应一个回应,从请求端的角度来看是发-收配对,从回应端的角度是收-发对。

Server和Client是1:N的模型。

该模式主要用于远程调用及任务分配。

img

#client.py
import zmq

ctx = zmq.Context()

print("Connecting to hello world server…")

s = ctx.socket(zmq.REQ)
s.connect("tcp://localhost:5555")
s.send(b"Hello")

msg =s.recv()
print(f"Recv reply [{msg}]")
# server.py
import time
import zmq

ctx = zmq.Context()
s = ctx.socket(zmq.REP)
s.bind("tcp://*:5555")

print("Listening …")

while True:
    msg = s.recv()
    print("Recv request:%s"%msg)

    time.sleep(1)

    s.send(b"World")

Publish/Subscribe(订阅-发布模式 )

消息单向。

两个角色:发布端、订阅端。

一个发布端,多个订阅端;发布端只管产生数据。

发布端发布一条消息,可被多个订阅端同时收到。

发布者不必关心订阅者的加入和离开,消息会以 1:N 的方式扩散到每个订阅者。

广播所有client,没有队列缓存,断开连接数据将永远丢失。

PUB和SUB谁bind谁connect并无严格要求(虽本质并无区别),但仍建议PUB使用bind,SUB使用connect。

从ZMQ v3.x开始,在使用连接的协议是tcp或者ipc时,过滤发生在发布端。使用epgm协议,过滤发生在订阅端。但在ZMQ v2.x版本中,所有过滤都发生在订阅端。

订阅端可以使用zmq_connect()同时连接到多个发布端。不同发布者推送的消息将交错到达。

订阅端调用zmq_send()来发送消息是会报错的,同样发布端使用zmq_recv()来接收消息也会报错。

img

# publish.py
import time
import zmq
from random import randrange

ctx = zmq.Context()
s = ctx.socket(zmq.PUB)
s.bind("tcp://*:5556")
print("Listening …")

while True:
    a = randrange(100,103)
    b = randrange(200,300)
    c = randrange(300,400)

    s.send_string("%i %i %i" %(a,b,c))
    time.sleep(0.1)
# subscribe.py
import sys
import zmq

ctx = zmq.Context()

print("Collecting …")

s = ctx.socket(zmq.SUB)
s.connect("tcp://localhost:5556")

filter = sys.argv[1]
s.setsockopt_string(zmq.SUBSCRIBE, filter)

sum = 0
for i in range(10):
    str = s.recv_string()
    a,b,c = str.split()
    print(a,b,c)
    sum += int(b)

print("filter=%s,sum=%d"%(filter,sum))

Push/Pull(流水线模式)

消息单向

三个角色:push work pull

模型描述:

  1. 上游(任务发布) 负载均衡
  2. 工人(中间,具体工作) 中途可以加入和退出
  3. 下游(信号采集或者工作结果收集)

img

push进行数据推送,work进行数据缓存,pull进行数据竞争获取处理。

Push的任何一个消息,始终只会有一个Pull端收到消息。

也叫管线模式。

主要用于多任务并行。

router-dealer模式(信封机制)

DEALER是对REQ的包装,本质就是在发送数据前发送一段bytes数据。

ROUTER是对REP的包装,是在接收数据前先接收第一段bytes数据。

消息双向,异步。

用于消息分发。

img

代码参考:zeromq中两个dealer 通过一个router进行通信

option设置

zmq_setsockopt(3) - 0MQ Api

用来设置socket的控制参数。

事件监控

zmq_socket_monitor(3) - 0MQ Api

用于定位问题的。

需要另开一个线程。

组合模式

  • PUB - SUB
  • REQ - REP
  • REQ - ROUTER
  • DEALER - REP
  • DEALER - ROUTER
  • DEALER - DEALER
  • ROUTER - ROUTER
  • PUSH - PULL
  • PAIR - PAIR

以上为合法的组合

消息帧

zmq_msg_send(&message, socket, ZMQ_SNDMORE );    //发第1帧
zmq_msg_send(&message, socket, ZMQ_SNDMORE );    //发第2帧
zmq_msg_send(&message, socket, ZMQ_DONTWAIT);    //发最后帧

零拷贝

只能在发送做,不能在接收做

void my_free (void *data, void *hint) {
    free(data);
}

auto len = pstServerMsg->ByteSizeLong();
char *buffer = (char *)malloc(len);
pstServerMsg->SerializeToArray(buffer, len);

zmq_msg_t msg;
zmq_msg_init_size(&msg, len);
zmq_msg_init_data(&msg, buffer, len, my_free, NULL);
zmq_msg_send(&msg, socket, ZMQ_DONTWAIT);

在接收消息的时候是无法使用零拷贝的:

ZMQ会将收到的消息放入一块内存区域供你读取,但不会将消息写入程序指定的内存区域 。

至于性能有多大提升,还有待确认。

Tips

线程间不可以共享socket,但可以共享上下文context。

谁bind谁connect,遵循“服务端是稳定的,客户端是灵活的“原则。

zmq_bind可以仅使用一个套接字就能绑定至多个端点,但不能多次绑定同一个端点。

不同的消息模式可以组合,从而适应具体的应用场景。

zmq_send是异步发送。函数有返回值,并不能代表消息已经发送。

发送端缓存消息,可以通过阈值控制消息处理策略。

socket断掉后,可以通过参数控制缓存中未发送的消息保存保留多长时间。

心跳

一般来说,应用层建议加心跳,“快速失败“思想的体现。

灵活端发ping

稳定端回pong

幂等性

常用的解决方法是在服务端检测并拒绝重复的请求:

  • 客户端为每个请求附加唯一的标识,包括客户端标识和消息标识;

  • 服务端在发送应答时使用客户端标识和消息标识作为键,保存应答内容;

  • 当服务端发现收到的请求已在应答哈希表中存在,它会跳过该次请求,直接返回应答内容。

核心思想

当解决一个复杂且多面化的问题时,单个通用型的解决方案可能并不是最好的方式。

可以把问题的领域想象成一个抽象层,并基于分层提供多个实现,每种实现只致力于解决一种情况。

划分范围时,明确什么在范围内,什么不在范围内。

过于严格则应用性受到限制。

过于宽泛则功能变得复杂,带来混乱。

https://github.com/anjuke/zguide-cn

https://dongshao.blog.csdn.net/article/details/106794294

https://www.cnblogs.com/hummersofdie/p/4597031.html

posted @ 2022-02-16 16:42  天下太平  阅读(784)  评论(0)    收藏  举报