先认识下ZeroMQ
参考:ZeroMQ详解 - 南哥的天下 - 博客园 (cnblogs.com)
ZeroMQ(简称ZMQ)是一个基于消息队列的多线程网络库,其对套接字类型、连接处理、帧、甚至路由的底层细节进行抽象,提供跨越多种传输协议的套接字。
ZMQ不是单独的服务,而是一个嵌入式库,工作在应用层和传输层之间(按照TCP/IP划分),它封装了网络通信、消息队列、线程调度等功能,向上层提供简洁的API,应用程序通过加载库文件,调用API函数来实现高性能网络通信。
为什么要使用ZeroMQ
当今的许多应用程序都包含了跨越某种网络的组件,无论这种网络是局域网还是互联网。因此,许多应用程序开发者最终都会处理某种类型的消息传递。一些开发人员使用消息队列产品,但大多数时候,他们使用TCP或UDP自己做。这些协议并不难用,但是,从A 发送几个字节到B和以任何一种可靠的方式处理消息,这两者之间有很大的区别。
让我们来看看当开始使用原始的TCP连接部件的时候,我们要面对的典型问题。任何可复用的消息层都需要解决如下所有这些问题或其中的大部分问题:
- 我们如何处理I/O呢?是让我们的应用程序阻塞,还是在后台处理I/O呢?这是一个关键的设计决策。阻塞式I/O 创建的架构不能很好地扩展,但后台I/O 也是非常难以正确做到的。
- 我们如何处理动态组件(例如,暂时撤除的块)呢?我们需要正式将组件划分为“客户端”和“服务器”,并强制该服务器不能撤除吗?那么,如果我们想将服务器连接到服务器时该怎么办呢?我们需要每隔几秒钟就尝试重新连接吗?
- 我们如何表示在线路上的消息呢?我们应该怎样将数据组织为帧,才能使得它很容易写入和读取,避免缓冲区溢出,既对小型消息高效,也足以处理非常大的戴着聚会礼帽的跳舞猫的视频呢?
- 我们如何处理不能立即传递的消息呢?特别是当我们在等待一个组件的联机回应时如何处理呢?我们需要丢弃消息,把它们放入一个数据库,或者把它们放到一个内存队列吗?
- 我们在哪里存储消息队列呢?如果组件从队列中读取很慢,导致我们的队列堆积,这会发生什么情况?我们的策略是什么呢?
- 我们如何处理丢失的消息呢?我们应该等待新的数据,要求重发,还是应该建立某种可靠性层,确保信息不会丢失呢?如果该层本身崩溃了该怎么办呢?
- 如果我们需要使用一个不同的网络传输,比如说,用多播来取代TCP 单播,或IPv6,该怎么办呢?我们需要重写应用程序吗?还是将传输抽象到某个层中呢?
- 我们如何路由消息呢?我们可以发送同样的消息到多个接收者吗?我们可以发送应答给原来的请求者吗?
- 我们如何编写出另一种语言的API 呢?我们应该重新实现一个线路级协议,还是重新包装一个库?如果是前者,我们怎么能保证协议栈的高效稳定呢?如果是后者,我们又怎么能保证互操作性呢?
- 我们应该如何表示数据,以便它可以在不同的架构之间读取呢?我们应该对数据类型强制执行特定的编码吗?究竟到什么程度,才是消息传递系统的工作,而不是更高一层的工作呢?
- 我们应该如何处理网络错误呢?是等待并重试,默默地忽略它们,还是终止它们呢?
ZeroMQ解决传统网络编程的问题:
- 调用的socket接口较多。
- TCP是一对一的连接。
- 编程需要关注很多socket细节问题。
- 不支持跨平台编程。
- 需要自行处理分包、组包问题。
- 流式传输时需处理粘包、半包问题。
- 需自行处理网络异常,比如连接异常中断、重连等。
- 服务端和客户端启动有先后。
- 自行处理IO模型。
- 自行实现消息的缓存。
- 自行实现对消息的加密。
构建可复用的消息传递系统是十分困难的
但是,我们如何才能做一个可复用的消息传递层呢?为什么有那么多的项目都需要这项技术,人们都还在通过在他们的代码中驱动TCP套接字费力地做它,并重复解决一长串清单中的问题呢?(见下图)。
实证明,构建可复用的消息传递系统是非常困难的,这就是为什么很少有自由和开放源码(FOSS)项目尝试做这项工作的原因,它也是商业通信产品复杂、昂贵、灵活性差,而且脆弱的原因。2006 年,iMatix 设计了高级消息队列协议,或AMQP,开始给FOSS 开发者提供也许是首个可复用的消息传递系统方法。AMQP 工作得比许多其他设计更好,但仍然相对复杂、昂贵,而且脆弱。学会如何使用它需要几个星期,而要用它来建立在事情变得很麻烦时也不会崩溃的稳定架构则需要几个月。
大多数消息传递项目(如AMQP)都在尝试以可重用的方式解决上面这个冗长的清单上的问题,它们通过发明一种负责寻址、路由和排队的新概念——代理,来做到这一点。这将导致一个客户端/服务器协议或一些未在文档中记录的协议之上的一组API,它允许应用程序与这个代理交流。在降低大型网络的复杂性方面,代理是一个很好的东西。但把以代理为基础的消息传递添加到像ZooKeeper 这样的产品会使情况变得更糟,而不是更好。这将意味着增加一台额外的大电脑和一个新的单点故障。代理迅速成为一个瓶颈和一个要管理的新风险。如果软件支持的话,我们可以添加第二个、第三个和第四个代理,并提出一些故障切换方案。人们这么做了。然而,它产生了更多的变动部件,变得更复杂,有更多的东西会被破坏。
此外,以代理为中心的设置都需要自己的运营团队。你真的需要日夜注意代理,并在它们开始“行为不端”时,用棍子敲打它们。你需要电脑,你需要备份的电脑,你需要人来管理那些电脑。只有那些由多个团队在数年内建成的,带有许多变动部件的大型应用程序,才值得这样做
因此,中小型应用程序开发人员陷入了困境。他们要么避免网络编程,制作不可扩展的单一应用程序,要么跳进网络编程,制作脆弱、复杂、很难维护的应用程序。他们还可以把赌注压在消息传递产品上,并最终获得依赖于昂贵且易破坏的技术的可扩展的应用程序。目前还没有非常好的选择,这也许可以解释为什么消息传递主要还停留在上个世纪,并激起强烈的情绪——对用户是消极的,而对那些销售技术支持和许可的厂商则是欢乐的、喜悦。
ZeroMQ的优点
我们需要的是做消息传递工作的东西,但需要它以下面这种简单和廉价的方式完成工作:
- 它可以在任何应用程序中以接近零的消耗开展工作。
- 它应该是不需要任何其他依赖就可以链接的库。
- 无须额外的变动部件,所以没有额外的风险。
- 它应该能运行在任何操作系统上,并能用任何编程语言开展工作。
而这就是ZeroMQ :一个高效的可嵌入库,它解决了大部分应用程序需要解决的问题,变得在网络上有良好的可伸缩性,而没有多少成本。
具体做法:
它在后台线程异步处理I/O,这些线程使用无锁数据结构与应用程序进行通信,所以并发ZeroMQ应用程序不需要锁、信号量、或者其他等待状态。
组件可以动态地来去自如,而ZeroMQ会自动重新连接,这意味着你可以以任何顺序启动组件,你可以创建“面向服务的架构”(SOA),其中的服务可以在任何时间加入和离开网络。
它根据需要自动对消息排队。为此,它会智能地在对消息排队之前,将消息尽可能地推进到接收者。它有一个处理过满队列(称为“高水位标志”)的方法。当队列满时,ZeroMQ会自动阻止发件人,或丢弃消息,这取决于你正在做的是哪种消息传递(即所谓的“模式”)。
它可以让你的应用程序通过任意传输协议来互相交流,这些协议可以是:TCP、多播、进程内、进程间。你不需要更改代码以使用不同的传输工具。
它使用依赖于消息传递模式的不同策略,安全地处理速度慢/阻塞的读取者。
它可以让你采用多种模式,如请求-应答和发布-订阅来将消息路由。这些模式是指你如何创建拓扑结构和网络结构。
它可以让你创建代理(proxy)来排队、转发,或通过一个调用来捕获消息。代理可以降低网络互联的复杂性。
它使用在线路上的简单组帧原封不动地传递整个消息。如果你写了一个10KB 的消息,那么你将收到一个10KB 的消息。
它不对消息强加任何格式。它们是零字节到千兆字节的二进制大对象。当你想表示你的数据时,可以选择其上的其他一些产品,如谷歌的协议缓冲区、XDR 等。
它能智能地处理网络错误。有时候它会重试,有时它会告诉你某个操作失败。
它可以减少你的能源消耗。少花CPU多办事意味着使用电脑更少的能源,你可以让你的旧电脑使用更长的时间。
实际上,ZeroMQ做的比这更多。它对你如何开发网络功能的应用程序有颠覆性的影响:
从表面上看,这是一个在其上做zmq_msg_recv()和zmq_msg_send()的套接字风格的API。
但该消息处理循环迅速成为中心循环,而你的应用程序很快就会分解成一组消息处理任务。它是优雅和自然的。
而且,它可扩展:每个任务对应一个节点,节点通过任意传输方式互相交谈。在一个进程中的两个节点(节点是一个线程),在一台电脑中的两个节点(节点是一个进程),或一个网络上的两台电脑(节点是一台电脑),所有的处理方式都是相同的,不需要更改应用程序代码。
消息模型
ZeroMQ将消息通信分成4种模型,分别是一对一结对模型(Exclusive-Pair)、请求回应模型(Request-Reply)、发布订阅模型(Publish-Subscribe)、推拉模型(Push-Pull)。这4种模型总结出了通用的网络通信模型,在实际中可以根据应用需要,组合其中的2种或多种模型来形成自己的解决方案。
一对一结对模型
最简单的1:1消息通信模型,可以认为是一个TCP Connection,但是TCP Server只能接受一个连接。数据可以双向流动,这点不同于后面的请求回应模型。
请求回应模型
由请求端发起请求,然后等待回应端应答。一个请求必须对应一个回应,从请求端的角度来看是发-收配对,从回应端的角度是收-发对。跟一对一结对模型的区别在于请求端可以是1~N个。该模型主要用于远程调用及任务分配等。Echo服务就是这种经典模型的应用。
发布订阅模型
发布端单向分发数据,且不关心是否把全部信息发送给订阅端。如果发布端开始发布信息时,订阅端尚未连接上来,则这些信息会被直接丢弃。订阅端未连接导致信息丢失的问题,可以通过与请求回应模型组合来解决。订阅端只负责接收,而不能反馈,且在订阅端消费速度慢于发布端的情况下,会在订阅端堆积数据。该模型主要用于数据分发。天气预报、微博明星粉丝可以应用这种经典模型。
推拉模型
Server端作为Push端,而Client端作为Pull端,如果有多个Client端同时连接到Server端,则Server端会在内部做一个负载均衡,采用平均分配的算法,将所有消息均衡发布到Client端上。与发布订阅模型相比,推拉模型在没有消费者的情况下,发布的消息不会被消耗掉;在消费者能力不够的情况下,能够提供多消费者并行消费解决方案。该模型主要用于多任务并行。
生产者-消费者模式是一个普遍宽泛的数据传输模式,表达的其实就是计算机中的数据总会有人生产,然后有人使用,有人发而且有人收,实际中会根据不同的情况有特定的一些数据传送模型,比如发布订阅模式、请求响应模式和客户服务端模式等等,那么发布订阅模式、请求响应模式和客户服务端模式有什么区别
发布订阅模式、请求响应模式和客户服务端模式是三种常见的消息传递或通信模式,它们在通信方式、应用场景以及实现复杂度等方面存在区别。以下是具体分析:
通信方式
发布订阅模式:异步通信,发布者和订阅者之间无直接联系。
请求响应模式:同步通信,请求者需等待响应者回应后才能继续执行。
客户服务端模式:通常为同步通信,但也可以设计成异步通信,取决于具体实现。
应用场景
发布订阅模式:适用于需要解耦和异步通信的场景,如消息通知系统和物联网监控。
请求响应模式:适用于需要明确请求和响应关系的场景,如银行取钱等。
客户服务端模式:广泛应用于各种网络服务和分布式系统中,如网页浏览和在线交易系统。
实现复杂度
发布订阅模式:相对复杂,需要维护主题和订阅者列表,通常需要消息代理来处理消息传递。
请求响应模式:实现相对简单,只需建立客户端和服务器的连接即可进行通信。
客户服务端模式:复杂度介于两者之间,需要考虑资源共享和安全管理等问题。
扩展性
发布订阅模式:扩展性好,增减角色对系统核心结构影响小。
请求响应模式:扩展性相对较差,每个新请求都需要建立新的连接。
客户服务端模式:扩展性较好,但受限于服务器的处理能力和网络带宽。
总的来说,发布订阅模式适合需要高解耦和异步通信的场景,而请求响应模式则适用于需要明确请求和响应关系的同步通信场景。客户服务端模式则是一种更为通用的网络架构模型,适用于多种不同的应用场景。
订阅发布和请求响应都是消息传递模式,但它们之间有很大的区别。
订阅发布模式(Publish-Subscribe)是一种异步消息传递模式,其中发布者将消息发送到主题,而订阅者则从该主题接收消息。发布者和订阅者之间没有直接的联系,发布者只需要将消息发布到主题,而订阅者只需要订阅感兴趣的主题即可。这种模式可以实现一对多的消息传递,即一个消息可以被多个订阅者接收。
请求响应模式(Request-Response)是一种同步消息传递模式,其中请求者发送请求消息,而响应者则返回响应消息。请求者和响应者之间需要建立直接的联系,请求者需要等待响应者的响应才能继续执行。这种模式通常用于需要请求和响应之间有明确的关系的场景,例如银行取钱的例子中,输入金额后需要等待银行返回钱才能离开。
更多参考:
另外注意,客户端服务器模式通常基于请求响应模型。
客户端服务器模式是一种计算机网络架构,它描述了客户端和服务器之间的通信方式。在这种模式中,客户端是指向服务器发送请求的设备或应用程序,而服务器则是接收并响应这些请求的设备或应用程序。这种通信方式确保了数据的可靠性和实时性,适用于各种应用场景。
请求响应模型是客户端服务器模式的核心特征。在这个模型中,客户端发送一个请求到服务器,服务器接收请求并进行相应的处理,然后将结果返回给客户端。这种模式允许客户端和服务器之间的相互通信和交互,是实现服务提供和消费的基础。
可以认为,请求响应和发布订阅都是基础的消息模型,是对所有消息传递模式的一种总结和基础抽象,我们可以基于这些模型发展更高层的一些通信模式,基于不同的应用场景来使用不同的模式,而客户端服务器模式就可以看作是基于请求响应模型的一种更高层模式。
另外,不必过于纠结,实际中,根据不同的产品要求来使用即可。
通信协议
提供进程内、进程间、机器间、广播等四种通信协议。通信协议配置简单,用类似于URL形式的字符串指定即可,格式分别为inproc://、ipc://、tcp://、pgm://。ZeroMQ会自动根据指定的字符串解析出协议、地址、端口号等信息。
优点
简单
1、仅仅提供24个API接口,风格类似于BSD Socket。
2、处理了网络异常,包括连接异常中断、重连等。
3、改变TCP基于字节流收发数据的方式,处理了粘包、半包等问题,以msg为单位收发数据,结合Protocol Buffers,可以对应用层彻底屏蔽网络通信层。
4、对大数据通过SENDMORE/RECVMORE提供分包收发机制。
5、通过线程间数据流动来保证同一时刻任何数据都只会被一个线程持有,以此实现多线程的“去锁化”。
6、通过高水位HWM来控制流量,用交换SWAP来转储内存数据,弥补HWM丢失数据的缺陷。
7、服务器端和客户端的启动没有先后顺序。
灵活
1、支持多种通信协议,可以灵活地适应多种通信环境,包括进程内、进程间、机器间、广播。
2、支持多种消息模型,消息模型之间可以相互组合,形成特定的解决方案。
跨平台
1、支持Linux、Windows、OS X等。
等等。
再来看看nanomsg
nanomsg也是一个基于
Socket的通讯库
,它是 zeromq 作者重新用 C 语言重新实现的, 是对 zeromq 的经验教训的各种提炼和反思。可以认为,nanomsg 则是流行的 ZMQ的优化版,但在某些方面有所不同。zeromq vs nanomsg
zeromq是作者用cpp早期创作,作者承认存在一些明显问题;nanomsg则是用C写的,更为成熟。
当前版本nanomsg支持以下协议:
配对模式:简单的一对一的通信;
总线模式:简单的多对多的通信;
请求/回复模式:支持组建大规模的集群服务来处理用户请求;
扇入模式:支持从多个源聚合请求消息;
扇出模式:支持分配到多个节点以支持负载均衡;
调查模式:允许在一个单一的请求里检查多个应用的状态;
可扩展协议是在网络通信协议之上实现的,当前版本nanomsg支持以下几种传输机制:
INPROC:单进程内通信;
IPC:单机内多进程的通信;
TCP:通过tcp协议的网络通信;
nanomsg用c实现,不依赖系统特性,所以支持多个操作系统。
nanomsg 的所有操作都是基于不同类型的 Socket,而 Socket 的类型决定了 nanomsg 使用了哪种通信模式和传输机制。
不过,nanomsg不是这篇文章的主角。
我们再来看看NNG
nanomsg 是一个由 Garrett D'Amore 创建的开源项目,最初于 2012 年发布。然而,nanomsg 项目的开发已经在一段时间内停滞。因此,几位 nanomsg 的贡献者创建了 NNG 项目,旨在取代 nanomsg 并继续发展。目前,NNG 项目更加活跃,并且在持续地进行开发和维护。
NNG(Nanomsg Next Generation)是 nanomsg 的后继项目,是一个轻量级的、可扩展的消息传递库,旨在提供简单而可靠的消息传递机制,用于构建分布式系统中的通信。NNG 支持点对点通信、发布-订阅模式、请求-回复等模式。
可点击download下载源码。
注意,不管是ZMQ还是nanomsg还是NNG,其实都是一种基于socket的框架,但是,使用socket并不是最终目的,最终的目的还是正确有效地处理各种消息数据。就好比,我们要发快递包给别人,最终目的是正确地将快递包发送到目标,而快递公司采用什么样的方式来送快递我们作为应用层并不用太关心,所基于的socket就是通信方式,好比快递公司用飞机来送快递包。所以,这些框架既可以叫网络框架,但更实质的是一种消息库一种消息框架。
NNG(Next Generation Nanomsg)是一个轻量级的消息传递库,但它并不是传统意义上的消息队列。
注意:消息传递并不等同于消息队列,消息队列只是消息传递的一种途径。
NNG主要特点
模块化设计:NNG 的模块化设计使得它更加灵活和可扩展,可以根据需要选择性地启用或禁用不同的功能模块。
可靠性:NNG 实现了可靠的消息传递机制,保证了消息的完整性和可靠性。
性能优异:NNG 在设计时考虑了性能因素,表现出色,适用于对性能有较高要求的应用场景。
可扩展性:NNG 提供了灵活的配置选项,使得可以根据需要进行扩展和定制,以满足不同应用程序的需求。
线程安全:NNG 的设计考虑了线程安全性,可以在多线程环境下安全地使用。
跨平台支持:NNG 可以在多种操作系统上运行,包括 Linux、Windows、macOS 等,具有良好的跨平台性。
语言无关性:NNG 不仅提供了 C 语言的 API,还支持多种其他语言的绑定,如 Python、Java、C# 等,使得开发者可以在不同的编程语言中使用它。
开源免费:NNG 是一个开源项目,采用了开放的许可证,可以免费获取源代码并在自己的项目中使用。
同时如 NNG 描述所言 “light-weight brokerless messaging”,NNG 中的通信各方是不需要第三方程序介入的,这与 MQTT/Redis 通信需要服务器不同。这样很适合作为通信库来使用而没有其他依赖。
通讯协议
- PAIR 一对一双向通信(点对点通信模式)。
- PIPELINE(PUSH/PULL) 单向通信,类似与生产者消费者模型的消息队列(消息队列模式)。
- PUB/SUB 单向广播(发布订阅模式)。
- REQ/REP 请求-应答模式。req-request,请求;rep-reply,答复、回答;
- BUS 网状连接通信,每个加入节点都可以发送/接受广播消息。
- SURVEY 用于多节点表决或者服务发现。
传输模式
- inproc 进程内线程间传输(线程间通信)
- ipc 主机内进程间传输(同主机上多进程间通信)
- tcp 网络内主机间传输(多主机通信)
目录和内容
NNG 协议基本上囊括了常见的通信需求,一些特殊的需求,也可以通过组合协议来实现。这样一来,如果在程序中使用 NNG,不管是多进程,还是多线程,通过设计,可以进一步增强模块化,同时不乏灵活性。如果环境变化,程序不管是由多进程改成多线程,还是由多线程改成多主机,都很容易实现。
常见模块/进程/线程间通信,可以依据具体需求来使用 PIPELINE(消息队列) 还是 REQ/REP(过程调用),而不是锁+全局变量,每个模块单元只需要做单一相关的具体事务,无需知晓全局状态。
1.8.0版源码地址:GitHub - nanomsg/nng at v1.8.0
目录结构如下
src路径内容
网上资料很少。
只有自行实践总结了。
参考手册:
NNG Reference Manual
nng.nanomsg.org/man/v1.8.0/index.html
实际使用时,先研究下demo和tests这些,然后遇到api就查询即可。
看下demo
async,异步io(即aio);
http_client,http请求;
pubsub_forwarder,发布订阅;
reqrep:rpc;
更多待补充。
nng源码阅读参考:
貌似NNG也提供了一些任务创建和线程同步的方式,和posix、systemv等是并列的关系,我们可以使用其中任意一种风格。我们实际使用时,可以根据需要来选用。比如,我们可以使用posix风格来创建线程,然后用systemv风格的消息队列进行通信,我们也可以使用posix风格创建线程,然后使用NNG风格的互斥锁和条件变量。
补充参考:POSIX 和 SYSTEM V-云社区-华为云 (huaweicloud.com)
NNG在core目录下有taskq.c和thread.c,这里面就有些多线程相关的内容,比如:
thread & mutex & cv
互斥锁,提供
nni_mtx_init
,nni_mtx_fini
,nni_mtx_lock
,nni_mtx_unlock
接口.条件变量,提供
nni_cv_init
,nni_cv_fini
,nni_cv_wake
,nni_cv_wake1
,nni_cv_until
接口.int nni_thr_init(nni_thr *thr, nni_thr_func fn, void *arg);
nni_thr_run
,nni_thr_fini
,nni_thr_wait
,nni_thr_is_self
线程有init, start, stop, done4个状态, 执行
nni_thr_init
后, 进入init状态. 执行nni_thr_run
进入start状态. 执行nni_thr_wait
后, 首先设置为stop状态, 然后等待线程变成done状态.nni_thr并不直接执行
nni_thr_func
函数, 而是执行nni_thr_wrap
函数, 该函数内部再调用nni_thr_init
提供的回调函数
nni_thr_wrap
- 如果没有start并且stop, 那么等待一个线程关联的条件变量.
- 如果
nni_thr_run
设置了start, 那么执行nni_thr_init
提供的回调函数.- 回调函数执行完成设置为done状态, 然后wake线程的条件变量.
taskq为任务队列, 内部包含一个链表, 链表中为需要执行的task, 每个taskq里面会有多个线程来处理链表里面的task
struct nni_taskq { nni_list tq_tasks; // 任务列表, 双向链表实现 nni_mtx tq_mtx; // 锁 nni_cv tq_sched_cv; // 条件变量 nni_cv tq_wait_cv; // 条件变量 nni_taskq_thr *tq_threads; // 线程数组 int tq_nthreads; // 线程数 bool tq_run; // true表示线程开始执行. }; struct nni_task { nni_list_node task_node; // tq_tasks的节点 void * task_arg; // 任务的参数 nni_cb task_cb; // 该任务的回调函数 nni_taskq * task_tq; // 任务所属taskq nni_thr * task_thr; // 执行任务的线程. non-NULL if the task is running unsigned task_busy; bool task_prep; bool task_reap; // task执行完后是否需要销毁. nni_mtx task_mtx; // task的锁. nni_cv task_cv; };
更多待补充。
当一个应用程序中集成了很多的框架的时候,就必然会出现很多重复的实现,我们可以根据实际情况和各自的优缺点使用合适的接口。
补充
nng_msg
nng_msg是NNG(Next Generation Nanomsg)库中的一个关键概念,它代表消息(message),是通信过程中的基本单位。以下是对nng_msg的详细介绍:
基本概述
定义:在NNG中,nng_msg是一个结构体,用于表示一条消息。它是NNG通信过程中的核心数据单元,包含了消息的内容、长度、类型等信息。
作用:nng_msg作为通信的基本单位,在发送方和接收方之间传递数据。发送方将数据封装成nng_msg结构体,并通过NNG提供的API发送出去;接收方则通过相应的API接收nng_msg,并从中提取出数据进行处理。
功能特性
数据封装:nng_msg结构体能够封装各种类型的数据,包括字符串、二进制数据等。这使得NNG能够支持多种不同的通信需求。
灵活性:由于nng_msg只是数据的载体,并不关心数据的具体含义,因此它具有很强的灵活性。开发者可以根据需要自定义消息的格式和内容。
高效性:NNG通过优化nng_msg的处理流程,实现了高效的数据传输。无论是发送还是接收消息,都能够在短时间内完成,满足高性能通信的需求。
使用方式
创建与销毁:在使用nng_msg之前,需要先创建一个nng_msg实例。在不再需要时,应及时销毁该实例以释放资源。
发送与接收:通过NNG提供的API,如nng_sendmsg和nng_recvmsg,可以分别实现消息的发送和接收。这些API内部会处理nng_msg的序列化和反序列化过程,使得开发者无需关心底层细节。
数据处理:在接收到nng_msg后,开发者可以通过访问其成员变量来获取消息的内容和相关信息。根据需要,可以对接收到的数据进行解析、处理或存储等操作。
应用场景
进程间通信:NNG作为一个轻量级的通信库,适用于进程间通信场景。通过nng_msg,不同进程之间可以方便地传递数据和信息。
分布式系统:在分布式系统中,各个节点之间需要进行频繁的数据交换。NNG的高效性和灵活性使得它成为构建分布式系统的理想选择之一。通过nng_msg,各个节点可以快速、准确地传递数据和指令。
综上所述,nng_msg是NNG库中的一个重要概念,它代表了通信过程中的基本单位——消息。通过nng_msg,NNG实现了高效、灵活的数据传输机制,适用于进程间通信和分布式系统等多种场景。
nng的消息传递本质上是基于socket的,不管是发布订阅还是请求响应,都需要先建立连接。在NNG(Nanomsg Next Generation)中,无论是实现发布订阅模式还是请求响应模式,都需要在通信双方之间建立连接。以下是对这两种模式建立连接过程的详细说明:
发布订阅模式
创建套接字:
发布者使用
nng_pub0_open
函数创建一个发布者套接字。
订阅者使用
nng_sub0_open
函数创建一个订阅者套接字。设置主题(对于订阅者):
订阅者需要指定要订阅的主题。这可以通过
nng_setopt
函数并指定NNG_OPT_SUB_SUBSCRIBE
选项来实现。例如,如果希望订阅"topic"主题,可以使用以下代码:int rv = nng_setopt(sock, NNG_OPT_SUB_SUBSCRIBE, "topic", strlen("topic"));
绑定和监听(对于发布者):
发布者通常需要绑定到一个地址,以便订阅者可以连接过来。这可以通过调用
nng_listen
函数来实现,并传入发布者的地址(如"tcp://localhost:8000")。连接到发布者(对于订阅者):
订阅者需要连接到发布者的端点,这通常通过调用
nng_dial
函数来实现,并传入发布者的地址。发送和接收消息:
发布者可以使用
nng_send
函数发送消息,并在发送时指定消息的主题。
订阅者则循环接收消息,并根据需要处理。
请求响应模式
创建套接字:
请求者使用
nng_req0_open
函数创建一个请求者套接字。
响应者使用
nng_rep0_open
函数创建一个响应者套接字。绑定和监听(对于响应者):
响应者需要绑定到一个地址,以便请求者可以连接过来。这同样通过调用
nng_listen
函数来实现,并传入响应者的地址。连接到响应者(对于请求者):
请求者需要连接到响应者的端点,通过调用
nng_dial
函数实现,并传入响应者的地址。发送和接收消息:
请求者发送一个请求消息,然后等待响应者的回复。
响应者接收到请求消息后,处理请求并发送回复消息。
综上所述,无论是发布订阅模式还是请求响应模式,在NNG中都需要在通信双方之间建立连接。这是通过创建套接字、设置必要的选项(如订阅主题)、绑定和监听地址以及发起连接等步骤来实现的。
请求响应模型跟常规的socket有点像,就是建立连接,然后接收发送;发布订阅模型里面,除了建立连接,后面在发送和接收时,都有个指定的主题。
nng请求响应模式使用示例
好的,下面是一个使用 NNG(Nanomsg Next Generation)库实现请求响应模式的示例。这个示例包括两个部分:一个请求者(客户端)和一个应答者(服务器)。
应答者(服务器)代码
#include <nng/nng.h> #include <nng/protocol/reqrep0/rep.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main() { nng_socket rep_sock; int rv; // 创建 REP 套接字 if ((rv = nng_rep0_open(&rep_sock)) != 0) { fprintf(stderr, "无法创建 REP 套接字: %s\n", nng_strerror(rv)); return 1; } // 绑定到本地地址 if ((rv = nng_listen(rep_sock, "tcp://127.0.0.1:5555", NULL, 0)) != 0) { fprintf(stderr, "无法绑定地址: %s\n", nng_strerror(rv)); nng_close(rep_sock); return 1; } printf("服务器已启动,等待请求...\n"); while (1) { char *buf = NULL; size_t buf_size; // 接收请求 if ((rv = nng_recv(rep_sock, &buf, &buf_size, NNG_FLAG_ALLOC)) != 0) { fprintf(stderr, "接收请求失败: %s\n", nng_strerror(rv)); continue; } printf("收到请求: %.*s\n", (int)buf_size, buf); // 发送响应 const char *response = "Hello from server!"; if ((rv = nng_send(rep_sock, response, strlen(response), 0)) != 0) { fprintf(stderr, "发送响应失败: %s\n", nng_strerror(rv)); } else { printf("发送响应: %s\n", response); } // 释放缓冲区 nng_free(buf, buf_size); } // 关闭套接字 nng_close(rep_sock); return 0; }
请求者(客户端)代码
#include <nng/nng.h> #include <nng/protocol/reqrep0/req.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main() { nng_socket req_sock; int rv; // 创建 REQ 套接字 if ((rv = nng_req0_open(&req_sock)) != 0) { fprintf(stderr, "无法创建 REQ 套接字: %s\n", nng_strerror(rv)); return 1; } // 连接到服务器 if ((rv = nng_dial(req_sock, "tcp://127.0.0.1:5555", NULL, 0)) != 0) { fprintf(stderr, "无法连接到服务器: %s\n", nng_strerror(rv)); nng_close(req_sock); return 1; } // 发送请求 const char *request = "Hello from client!"; if ((rv = nng_send(req_sock, request, strlen(request), 0)) != 0) { fprintf(stderr, "发送请求失败: %s\n", nng_strerror(rv)); nng_close(req_sock); return 1; } else { printf("发送请求: %s\n", request); } // 接收响应 char *buf = NULL; size_t buf_size; if ((rv = nng_recv(req_sock, &buf, &buf_size, NNG_FLAG_ALLOC)) != 0) { fprintf(stderr, "接收响应失败: %s\n", nng_strerror(rv)); nng_close(req_sock); return 1; } else { printf("收到响应: %.*s\n", (int)buf_size, buf); } // 释放缓冲区 nng_free(buf, buf_size); // 关闭套接字 nng_close(req_sock); return 0; }
你应该会看到客户端发送请求并接收到服务器的响应。这个示例展示了如何使用 NNG 库实现基本的请求响应模式。
nng发布订阅模式使用示例
NNG(Nanomsg Next Generation)中的发布-订阅模式是一种一对多的通信模式,允许一个发布者向多个订阅者发送消息。这种模式适用于需要广播消息给多个接收者的场景,如新闻发布、实时数据更新等。
以下是使用NNG发布-订阅模式的示例代码:
发布者(Publisher)
#include <nng/nng.h> #include <nng/protocol/pubsub0/pub.h> #include <stdio.h> #include <string.h> #include <unistd.h> int main() { nng_socket pub_sock; int rv; // 创建发布者套接字 if ((rv = nng_pub0_open(&pub_sock)) != 0) { fprintf(stderr, "无法创建发布者套接字: %s\n", nng_strerror(rv)); return 1; } // 绑定到本地地址 if ((rv = nng_listen(pub_sock, "tcp://127.0.0.1:5555", NULL, 0)) != 0) { fprintf(stderr, "无法绑定地址: %s\n", nng_strerror(rv)); nng_close(pub_sock); return 1; } // 发布消息 const char *msg = "Hello, World!"; for (int i = 0; i < 10; ++i) { if ((rv = nng_send(pub_sock, msg, strlen(msg), 0)) != 0) { fprintf(stderr, "发送消息失败: %s\n", nng_strerror(rv)); break; } printf("已发送消息: %s\n", msg); sleep(1); // 每秒发送一次消息 } // 关闭套接字 nng_close(pub_sock); return 0; }
订阅者(Subscriber)
#include <nng/nng.h> #include <nng/protocol/pubsub0/sub.h> #include <stdio.h> #include <string.h> #include <unistd.h> int main() { nng_socket sub_sock; int rv; // 创建订阅者套接字 if ((rv = nng_sub0_open(&sub_sock)) != 0) { fprintf(stderr, "无法创建订阅者套接字: %s\n", nng_strerror(rv)); return 1; } // 订阅所有主题 if ((rv = nng_setopt(sub_sock, NNG_OPT_SUB_SUBSCRIBE, "", 0)) != 0) { fprintf(stderr, "设置订阅选项失败: %s\n", nng_strerror(rv)); nng_close(sub_sock); return 1; } // 连接到发布者 if ((rv = nng_dial(sub_sock, "tcp://127.0.0.1:5555", NULL, 0)) != 0) { fprintf(stderr, "无法连接到发布者: %s\n", nng_strerror(rv)); nng_close(sub_sock); return 1; } // 接收消息 char *buf = NULL; size_t buf_size = 0; for (int i = 0; i < 10; ++i) { if ((rv = nng_recv(sub_sock, &buf, &buf_size, NNG_FLAG_ALLOC)) != 0) { fprintf(stderr, "接收消息失败: %s\n", nng_strerror(rv)); break; } printf("收到消息: %.*s\n", (int)buf_size, buf); nng_free(buf, buf_size); // 释放缓冲区 } // 关闭套接字 nng_close(sub_sock); return 0; }
这个示例展示了如何使用NNG实现一个简单的发布-订阅模式。发布者会每秒发送一条消息,订阅者会接收并打印这些消息。
nng的发布订阅如何绑定到一个主题的?
在NNG中,发布者和订阅者通过主题来过滤和接收消息。下面将详细介绍如何在NNG的发布订阅模式中绑定到特定主题:
创建套接字:使用
nng_sub0_open
函数创建一个订阅者套接字,或者使用nng_pub0_open
函数创建一个发布者套接字。设置主题:对于订阅者,使用
nng_setopt
函数并指定NNG_OPT_SUB_SUBSCRIBE
选项来设置要订阅的主题。例如,如果希望订阅"topic"主题,可以使用以下代码:int rv = nng_setopt(sock, NNG_OPT_SUB_SUBSCRIBE, "topic", strlen("topic"));
连接到发布者:对于订阅者,使用
nng_dial
函数连接到发布者的端点。发布者则使用nng_listen
函数等待订阅者的连接。发送和接收消息:发布者可以通过调用
nng_send
函数并指定消息的主题来发送消息。订阅者则循环接收消息,并根据需要处理。总的来说,通过上述步骤可以在NNG的发布订阅模式中实现基于主题的消息过滤和接收。
nng的aio使用示例
Nanomsg(NNG)是一个高性能的异步消息库,支持多种通信模式。在NNG中,异步I/O(AIO)是一种重要的机制,允许应用程序在不阻塞的情况下进行消息传递。下面将介绍如何使用NNG的异步I/O功能,并提供一个示例代码来演示其基本用法。
基本概念
异步I/O:异步I/O操作不会阻塞调用线程,而是通过回调函数或事件通知机制来处理完成的事件。这有助于提高应用程序的并发性和响应速度。
nng_aio:NNG中的异步I/O操作是通过
nng_aio
结构体和相关函数来实现的。这些函数允许你在后台执行I/O操作,并在操作完成时通过回调函数通知你。使用步骤
初始化套接字:首先需要创建一个NNG套接字,并设置其为异步模式。
配置异步I/O:使用
nng_aio
结构体来配置异步I/O操作,包括指定回调函数、数据缓冲区等。启动异步操作:调用相应的异步I/O函数(如
nng_send_aio
或nng_recv_aio
)来启动异步操作。处理完成事件:在回调函数中处理异步操作的结果。
示例代码
以下是一个使用NNG异步I/O功能的简单示例,展示了如何发送和接收消息。
#include <nng/nng.h> #include <nng/protocol/reqrep0/rep.h> #include <stdio.h> #include <stdlib.h> #include <string.h> // 回调函数,当异步操作完成时调用 void aio_callback(void *arg) { nng_aio *aio = arg; int rv = nng_aio_result(aio); if (rv == 0) { printf("Operation completed successfully\n"); } else { fprintf(stderr, "Operation failed: %s\n", nng_strerror(rv)); } nng_aio_free(aio); } int main() { nng_socket sock; int rv; // 打开REP模式的套接字 if ((rv = nng_rep0_open(&sock)) != 0) { fprintf(stderr, "Failed to open REP socket: %s\n", nng_strerror(rv)); exit(1); } // 绑定地址 if ((rv = nng_listen(sock, "tcp://127.0.0.1:5555", NULL, 0)) != 0) { fprintf(stderr, "Failed to bind socket: %s\n", nng_strerror(rv)); nng_close(sock); exit(1); } printf("REP socket listening on tcp://127.0.0.1:5555\n"); // 创建异步I/O对象 nng_aio *aio = NULL; if ((rv = nng_aio_alloc(&aio, aio_callback, NULL)) != 0) { fprintf(stderr, "Failed to allocate AIO: %s\n", nng_strerror(rv)); nng_close(sock); exit(1); } // 准备要发送的消息 const char *msg = "Hello, client!"; nng_msg *nmsg = NULL; if ((rv = nng_msg_alloc(&nmsg, 0)) != 0 || (rv = nng_msg_append(nmsg, msg, strlen(msg))) != 0) { fprintf(stderr, "Failed to allocate or append message: %s\n", nng_strerror(rv)); nng_aio_free(aio); nng_close(sock); exit(1); } // 启动异步发送操作 if ((rv = nng_send_aio(sock, nmsg, aio)) != 0) { fprintf(stderr, "Failed to send message asynchronously: %s\n", nng_strerror(rv)); nng_msg_free(nmsg); nng_aio_free(aio); nng_close(sock); exit(1); } // 等待异步操作完成(在实际应用中,可以使用事件循环或其他机制来处理) nng_msleep(1000); // 简单的延时,模拟等待异步操作完成 // 清理资源 nng_close(sock); return 0; }
总结
初始化和配置:在使用NNG的异步I/O功能之前,需要正确初始化套接字并配置异步I/O对象。
启动异步操作:通过调用
nng_send_aio
或nng_recv_aio
等函数启动异步操作,并指定回调函数来处理操作完成事件。处理结果:在回调函数中处理异步操作的结果,并进行必要的资源清理。
通过上述步骤和示例代码,你可以了解如何在NNG中使用异步I/O功能,从而构建高效的并发应用程序。
nx_gen_reqrep_name
nx_gen_reqrep_name
是一个函数名,通常出现在使用 Nanomsg Next Generation (NNG) 库的代码中。NNG 是一个高性能的消息传递库,类似于 ZeroMQ,但提供了更现代的 API 和一些额外的功能。在 NNG 中,REQ/REP(请求/应答)模式是一种常见的通信模式,用于实现同步的请求-响应交互。在这种模式下,客户端发送一个请求消息,服务器接收并处理该请求,然后返回一个应答消息。
nx_gen_reqrep_name
可能是一个生成或处理 REQ/REP 套接字名称的函数。具体来说,它可能用于:
生成唯一的套接字名称:在分布式系统中,确保每个 REQ/REP 套接字都有一个唯一的标识符,以避免冲突。
解析或验证套接字名称:检查套接字名称是否符合预期格式或规则。
日志记录或调试:在开发过程中,帮助开发者跟踪和调试 REQ/REP 套接字的使用情况。
由于
nx_gen_reqrep_name
并不是一个标准的 NNG 函数,具体的功能和实现细节可能会因不同的项目或库版本而有所不同。如果你需要了解这个函数的具体用途和实现,建议查看相关的源代码或文档。以下是一个假设的示例,展示如何可能实现一个简单的
nx_gen_reqrep_name
函数:#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> // 生成一个唯一的 REQ/REP 套接字名称 char* nx_gen_reqrep_name() { // 获取当前时间戳 time_t t = time(NULL); struct tm *tm = localtime(&t); // 分配内存以存储套接字名称 char *name = malloc(256); if (!name) { perror("malloc"); exit(EXIT_FAILURE); } // 格式化套接字名称,包含时间戳以确保唯一性 snprintf(name, 256, "reqrep_socket_%04d%02d%02d%02d%02d%02d", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec); return name; } int main() { char *socket_name = nx_gen_reqrep_name(); printf("Generated REQ/REP socket name: %s\n", socket_name); free(socket_name); // 记得释放内存 return 0; }
这个示例展示了如何生成一个基于当前时间戳的唯一 REQ/REP 套接字名称。实际的
nx_gen_reqrep_name
函数可能会有不同的实现方式,具体取决于其设计目的和使用场景。
nng_ctx_open函数
nng_ctx_open
是NNG(Nanomsg Next Generation)库中的一个函数,用于打开一个上下文(context)在NNG(Nanomsg Next Generation)库中,打开一个单独的上下文通常指的是创建一个独立的上下文对象,这个对象可以与特定的套接字关联,用于管理该套接字的异步I/O操作。这里的“单独”意味着这个上下文是独立于其他上下文和套接字的,它专门用于处理与之关联的套接字的异步事件。
为什么需要单独的上下文?
资源隔离:每个上下文可以独立管理其资源,如内存、文件描述符等,这有助于避免资源冲突和泄露。
并发控制:在多线程或多任务环境中,使用单独的上下文可以更容易地实现并发控制,确保不同线程或任务不会相互干扰。
错误处理:单独的上下文允许更精细的错误处理。如果某个上下文出现错误,可以单独处理,而不影响其他上下文。
性能优化:通过为不同的套接字创建单独的上下文,可以根据套接字的具体需求进行性能优化,例如调整缓冲区大小、超时设置等。
如何打开一个单独的上下文?
在NNG中,可以通过
nng_ctx_open
函数来打开一个单独的上下文。以下是一个简单的示例,展示了如何为一个REP模式的套接字创建一个单独的上下文:#include <nng/nng.h> #include <nng/protocol/reqrep0/rep.h> #include <stdio.h> #include <stdlib.h> int main() { nng_socket sock; nng_ctx *ctx = NULL; int rv; // 打开REP模式的套接字 if ((rv = nng_rep0_open(&sock)) != 0) { fprintf(stderr, "Failed to open REP socket: %s ", nng_strerror(rv)); exit(1); } // 绑定地址 if ((rv = nng_listen(sock, "tcp://127.0.0.1:5555", NULL, 0)) != 0) { fprintf(stderr, "Failed to bind socket: %s ", nng_strerror(rv)); nng_close(sock); exit(1); } printf("REP socket listening on tcp://127.0.0.1:5555 "); // 创建并打开上下文 if ((rv = nng_ctx_open(&ctx, sock)) != 0) { fprintf(stderr, "Failed to open context: %s ", nng_strerror(rv)); nng_close(sock); exit(1); } // 在这里可以添加更多的逻辑来处理异步I/O操作 // 清理资源 nng_close(sock); nng_ctx_close(ctx); return 0; }
在这个示例中,我们首先打开了一个REP模式的套接字,并将其绑定到一个TCP地址。然后,我们使用
nng_ctx_open
函数为这个套接字创建了一个单独的上下文。这样,我们就可以在这个上下文中独立地管理与该套接字相关的异步I/O操作了。总结
单独的上下文提供了一个独立于其他上下文的环境,用于管理特定套接字的异步I/O操作。
使用单独的上下文可以帮助实现资源隔离、并发控制、错误处理和性能优化。
在NNG中,可以通过
nng_ctx_open
函数来打开一个单独的上下文,并将其与特定的套接字关联。上下文切换:是指操作系统在多个进程或线程之间进行调度时,保存当前进程或线程的状态(如寄存器、程序计数器、堆栈指针等),并加载下一个进程或线程的状态,以便继续执行。
一个上下文可以简单理解成一个线程。