一、消息的传递
在现实的世界中,如果处理工作的安排,一般有两种情况,按计划的正常工作和突然发生的工作,这种突然发生的工作可能很紧急,也可能不紧急。所以收到通知消息后工作的分发就可以分成两种类型,即紧急消息必须马上去找相关工作人员或电话等方式将消息通知立刻执行;而非紧急的消息可能等相关人员收工后问询下一次工作的安排时再处理。如果把这种突发工作的处理说明一下,其实就是通知下发和问询安排。
二、推和拉
在计算机世界中也存在这种现象,不过它们换一个名词:推(PUSH)和拉(PULL)或者说推送和拉取。
1、推模式就是将消息主动通过某种手段(如WS)发送到相关各方,一般来说,发送者是服务端(生产者),接收者是客户端(消费者)
推模式的优点在于消息的主动推送,实时性较好,服务端可以自由控制相关数据的推送;缺点在于服务端需要一套较为复杂的推送机制,会消耗较多的资源
2、拉模式则是客户端(消费者)主动向服务端(生产端)请求数据
拉模式的优点在于客户端自主控制拉取的频繁和数量,服务端相对简单;缺点是周期较长,实时而且频繁的拉取可能浪费通信资源。
需要说明的是,推拉不是一定对应到大家传统认知的服务端和客户端,有时个儿它们是互相的,甚至在P2P中可能是平等的。不过,为了便于描述,就将其定义为服务端和客户端。大家不要被名字混淆就可以了。
三、推拉机制的应用
这两种模式在互联网中应用非常多,常见的如抖音,下滑页面就可以拉取新的数据,而如果自己关注的人有新视频则会推消息给当前屏幕提醒。不过一般来说,单纯的使用推或拉的方式的相对较少,一般都是二者混合使用。
象手机上的新闻客户端的数据处理机制和App的更新机制都是如此。说的更清楚一些其实就是取二者的所长,避二者的所短,达到一个相对的实时性、可控性和资源消耗等的平衡。比如对于一些重要的通知或影响使用的数据(如突然出现的热点新闻)要实时推送过来,而对于一些非重要的通知如一些统计数据,就可以通过拉取的方式来更新。
手机上常见的天气预报一般也是如此处理。
一般来说,常见的MQ框架都是二者混用,IOT中的数据上传使用Push方式,而一些使用轮询的场景下多是用Pull的方式。
而在实际的开发中,如何应用推和拉,是一个非常考验设计者的灵活适应能力。不要认为推和拉这种模式只有在互联网中应用,其实在常见的开发中很多,只是不如互联网中规模大、复杂度高罢了。比如日志的拉取分析,网络开发中的定时数据推送甚至心跳都可以看成一种推拉的机制。
正如前面的分析,除了一些特定的场景下,大多数都是推和拉共同使用的。特别是基于事件的驱动机制,对事件的处理过程,一定是推拉结合的。所以从简单入手,逐渐扩展对整个消息推拉的实际应用效果,不断的根据业务需求进行调整,就会形成一套较好的当前应用的推拉机制的流程逻辑。
另外一种比较常见应用推拉机制的是监控程序,如果监控程序越复杂,对推拉机制的应用可能要求就越高。比如一个交通监控程序,正常可能每几分钟更新一下相关的状态(拉),但如果遇到交通事故需要立刻上报(推)。
四、实现
推拉机制一般来说在互联网应用最为广泛,不过也不代表别的地方用得少。比如在C++网络通信中经常也可以遇到类似的问题,需要处理不同的事件,重要事件要及时的推送到指定的客户端,而非重要数据则由客户端按步就班的接收拉取并处理。
实现推和拉非常容易,但是做一个好的推拉机制则相当麻烦,它需要处理各种异常的情况和不同的场景下的配置。比如一个MQ框架,用在即时通信中,可能拉取的时间间隔就很短,而在天气预报的实现中拉取的时间就较长。
实现推和拉有很多种方法,最简单的推送模式可以使用观察者模式+队列和状态处理,然后基于事件或定时触发即可;而实现一个拉取模式的可以采用轮询或定时请求即可。当然,实现的方式各种各样,但目的是相同的。就是把消息安全及时的进行传递。
五、实例
下面看一个极简的混合应用的例子:
#include <iostream>
#include <unistd.h>
#include <thread>
class Client;
class Server {
public:
std::string updateData() {
return "new data!";
}
bool addNotify(Client*pc){
this->pc_=pc;
return true;
}
bool trigger(const std::string&msg);
private:
Client *pc_;
};
class Client {
public:
void Polling(Server& s) {
auto h = std::thread([&]{
while (true) {
sleep(3);
std::string d = s.updateData();
std::cout << "get new data: " << d << std::endl;
}
});
h.detach();
}
void updateClient(const std::string&msg){
std::cout << "push msg: " << msg << std::endl;
}
};
bool Server::trigger(const std::string&msg){
this->pc_->updateClient(msg);
return true;
}
int main(){
Server s;
Client c;
s.addNotify(&c);
s.trigger("trigger msg!");//真实的事件触发都是另外的线程进行
c.Polling(s);
getchar();
return 0;
}
上面的代码非常简单,既没有队列也没有使用MQ之类的方式,但整体的推拉的效果就是如此,定时器也是使用的最简单的sleep模拟实现的。而真正的应用场景下一定会有各种策略或机制来保证数据推拉的时机,包括数据如何产生后如何触发、如何保存、如何重试、如何删除已发送数据等等。可能涉及到多线程、队列、各种通信机制、数据同步等等。大家看一下开源的MQ中如何实现的,多做对比,就比较容易理解了。
六、总结
推和拉本身不难理解,也易于实现。但麻烦就在于实际应用场景中的复杂度和各种异常的处理,这才是重点中的重点。所以大家一定要根据实际情况进行平衡,保证所需。而不是为了应用推拉去实现推拉机制。