发布-订阅(Publish-Subscribe)模式是一种消息传递模式,用于实现组件之间的松耦合通信。在这种模式中,发送者(发布者)不需要知道接收者(订阅者)的存在,反之亦然。发布者将消息发布到频道上,而订阅者可以订阅一个或多个频道以接收这些消息。这是一种常用的机制,可类比C#中事件和响应回调函数。
一. C#事件与响应回调函数
C# 事件 / 回调只能局限当前进程, 本质是内存里的函数指针列表, 只存在当前进程, 当前应用程序域, 不能跨进程、不能跨电脑、不能跨程序, 另一个exe 完全感知不到,连不上、收不到事件。如下是一个演示例子。
cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp1
{
public class A
{
public delegate void DelRun(string info);
public event DelRun EventRun;
public void trigger(string s)
{
if (EventRun != null)
EventRun(s);
}
}
public class B
{
private A a;
public B()
{
a = new A();
}
private void Output(string s)
{
Console.WriteLine(s);
}
public void Blind()
{
a.EventRun += Output;
}
public void Trigger()
{
a.trigger("hello world");
}
}
class Program
{
static void Main(string[] args)
{
B b= new B();
b.Blind();
b.Trigger();
}
}
}
运行效果如下:

二. Python的发布与订阅
- Python还可以跨进程、跨程序、跨电脑、跨语言。如下介绍通过Redis来实现分布式,Redis是一个跑在电脑上的一个独立小程序,专门用来存数据、发消息、做中间件。官网下载地址如下:
https://github.com/tporadowski/redis/releases
下载完毕后,直接运行redis-server.exe即可


接下来,pip安装Redis

这边在Pycharm中创建两份.py文件,一份名为Publisher_1.py,用作发布功能,代码为:
python
import redis
import json
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
# 发布消息1
tcp_msg1 = "hello world"
r.publish("topic1", tcp_msg1)
# 发布消息2
tcp_msg2 = "welcom"
r.publish("topic2", tcp_msg2)
print("over")
一份名为Subscriber_1.py,用作订阅功能,代码为:
python
import redis
import json
# 连接Redis
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
pubsub = r.pubsub()
# 订阅主题\事件\消息
pubsub.subscribe("topic1", "topic2")
print("等待消息...")
# 监听消息
for msg in pubsub.listen():
if msg["type"] == "message":
topic = msg["channel"]
data = msg["data"]
print(topic + ":" + data)
执行后效果如下(先执行Subscriber_1.py,再执行Publisher_1.py):

- 接下来用第三方库pyPubSub, 此仅支持进程内,不支持跨进程。
首先先用pip安装该库

然后用Pycharm创建Test_Pypubsub.py,里面代码为:
python
from pubsub import pub
# 订阅者回调函数
def listener1(msg):
print(msg)
def listener2(msg):
print(msg)
# 订阅主题
pub.subscribe(listener1, "topic1")
pub.subscribe(listener2, "topic2")
# 发布消息
pub.sendMessage("topic1", msg="Hello PyPubSub1")
pub.sendMessage("topic1", msg="Hello PyPubSub2")
运行后效果如下:

- 不调用第三方库,自己手写(类似一个简单的两点距离计算,不用调opencv的库)
python
from collections import defaultdict
import threading
class LocalPubSub:
def __init__(self):
# key:主题,value:订阅者回调函数列表
self.subscribers = defaultdict(list)
self.lock = threading.Lock()
# 订阅主题
def subscribe(self, topic, callback):
with self.lock:
self.subscribers[topic].append(callback)
# 发布消息
def publish(self, topic, *args, **kwargs):
with self.lock:
callbacks = self.subscribers[topic].copy()
# 异步通知所有订阅者
for callback in callbacks:
threading.Thread(target=callback, args=args, kwargs=kwargs).start()
# 测试
if __name__ == "__main__":
pubsub = LocalPubSub()
# 订阅者1
def on_news(msg):
print(f"订阅者1 收到:{msg}")
# 订阅者2
def on_news2(msg):
print(f"订阅者2 收到:{msg}")
pubsub.subscribe("news", on_news)
pubsub.subscribe("news", on_news2)
# 发布消息
pubsub.publish("news", "Python Pub/Sub 测试消息")
执行效果如下:

可看到不管哪种方式,subscribe要注明收到消息后,要干什么活(对应可以是封装的函数或执行语句)。
注: ROS2 中也存在类似的发布和订阅机制功能,在后面博客中再介绍