前言

介绍

  1. [NetMQ](https://github.com/zeromq/netmq.git)是ZeroMQ的C#移植版本,它是对标准socket接口的扩展。它提供了一种异步消息队列,多消息模式,消息过滤(订阅),对多种传输协议的无缝访问。
  2. 当前有2个版本正在维护,版本3最新版为3.3.4,版本4最新版本为4.0.1。本文档是对4.0.1分支代码进行分析。

zeromq的英文文档

NetMQ的英文文档

目的

对NetMQ的源码进行学习并分析理解,因此写下该系列文章,本系列文章暂定编写计划如下:

  1. 消息队列NetMQ 原理分析1-Context和ZObject
  2. 消息队列NetMQ 原理分析2-IO线程和完成端口
  3. 消息队列NetMQ 原理分析3-命令产生/处理、创建Socket和回收线程
  4. 消息队列NetMQ 原理分析4-Socket、Session、Option和Pipe
  5. 消息队列NetMQ 原理分析5-Engine,Encord和Decord
  6. 消息队列NetMQ 原理分析6-TCP和Inpoc实现
  7. 消息队列NetMQ 原理分析7-Device
  8. 消息队列NetMQ 原理分析8-不同类型的Socket
  9. 消息队列NetMQ 原理分析9-实战

友情提示: 看本系列文章时最好获取源码,更有助于理解。


Socket

上一章最后我们简单介绍了SocketBaseSessionBase的创建和回收,这一张我们详细介绍SocketBaseSessionBase

首先SocketBase继承自Own,即也是ZObject对象,同时由于SocketBase需要进行消息的传输,因此它实现了一些结构,包括IPollEventsPipe.IPipeEvents

接口实现

  1. internal abstract class SocketBase : Own, IPollEvents, Pipe.IPipeEvents{
  2. ...
  3. }
  • IPollEvents事件上一章回收线程已经介绍过,这里不再做过多说明了,简单讲SocketBase实现该事件只有在回收线程回收Socket的时候会触发。
  • Pipe.IPipeEvents:是管道事件,它的签名如下
  1. public interface IPipeEvents
  2. {
  3. void ReadActivated([NotNull] Pipe pipe);
  4. void WriteActivated([NotNull] Pipe pipe);
  5. void Hiccuped([NotNull] Pipe pipe);
  6. void Terminated([NotNull] Pipe pipe);
  7. }
  • ReadActivated:表示管道可读,管道实际调用SocketBaseSessionBaseReadActivated方法,而SocketBase实际会调用XReadActivated方法。
  • WriteActivated:表示管道可写,管道实际调用SocketBaseSessionBaseWriteActivated方法,而SocketBase实际会调用XWriteActivated方法。
  • Hiccuped:当连接突然中断时会调用此方法。
  • WriteActivated:表示管道终止。

内部结构

SocketBase的内部维护着一个字段,用于存放连接/绑定地址和它的管道(若当前SocketBaseTCPListener,则无需初始化管道,管道为空)。

  1. private readonly Dictionary<string, Endpoint> m_endpoints = new Dictionary<string, Endpoint>();
  2. private readonly Dictionary<string, Pipe> m_inprocs = new Dictionary<string, Pipe>();

Endpoint对象用于存放SessionBasePipeListener的引用

  1. private class Endpoint
  2. {
  3. public Endpoint(Own own, Pipe pipe)
  4. {
  5. Own = own;
  6. Pipe = pipe;
  7. }
  8. public Own Own { get; }
  9. public Pipe Pipe { get; }
  10. }

SocketBase连接或绑定最后会向将Endpoint保存到字典中

  1. private void AddEndpoint([NotNull] string address, [NotNull] Own endpoint, Pipe pipe)
  2. {
  3. LaunchChild(endpoint);
  4. m_endpoints[address] = new Endpoint(endpoint, pipe);
  5. }

SocketBase断开连接时会移除它

  1. public void TermEndpoint([NotNull] string addr)
  2. {
  3. ...
  4. if (protocol == Address.InProcProtocol)
  5. {
  6. ...
  7. m_inprocs.Remove(addr);
  8. }
  9. else
  10. {
  11. ...
  12. m_endpoints.Remove(addr);
  13. }
  14. }

m_inprocs也是一个字典用于存放inproc协议的连接。

第一章创建SocketBase我们介绍了Context创建SocketBase所做的一些工作,初始化SocketBase时,会创建MailBox,用于传输Command

  1. protected SocketBase([NotNull] Ctx parent, int threadId, int socketId)
  2. : base(parent, threadId)
  3. {
  4. m_options.SocketId = socketId;
  5. m_mailbox = new Mailbox("socket-" + socketId);
  6. }

每个SocketBase的命令处理实际都是在工作线程中进行。因此理论上(忽略线程上下文切换时造成的性能损失)线程数越多,NetMQ的IO吞吐量和工作线程数成正比关系。

Context创建SocketBase会根据Create静态方法根据不同类型创建不同的SocketBase

  1. public static SocketBase Create(ZmqSocketType type, [NotNull] Ctx parent, int threadId, int socketId)
  2. {
  3. switch (type)
  4. {
  5. case ZmqSocketType.Pair:
  6. return new Pair(parent, threadId, socketId);
  7. case ZmqSocketType.Pub:
  8. return new Pub(parent, threadId, socketId);
  9. case ZmqSocketType.Sub:
  10. return new Sub(parent, threadId, socketId);
  11. case ZmqSocketType.Req:
  12. return new Req(parent, threadId, socketId);
  13. case ZmqSocketType.Rep:
  14. return new Rep(parent, threadId, socketId);
  15. case ZmqSocketType.Dealer:
  16. return new Dealer(parent, threadId, socketId);
  17. case ZmqSocketType.Router:
  18. return new Router(parent, threadId, socketId);
  19. case ZmqSocketType.Pull:
  20. return new Pull(parent, threadId, socketId);
  21. case ZmqSocketType.Push:
  22. return new Push(parent, threadId, socketId);
  23. case ZmqSocketType.Xpub:
  24. return new XPub(parent, threadId, socketId);
  25. case ZmqSocketType.Xsub:
  26. return new XSub(parent, threadId, socketId);
  27. case ZmqSocketType.Stream:
  28. return new Stream(parent, threadId, socketId);
  29. default:
  30. throw new InvalidException("SocketBase.Create called with invalid type of " + type);
  31. }
  32. }

具体创建SocketBase的工作在上一章已经做了详细的介绍,这里不再复述。

Session

首先和SocketBase一样,SessionBase也继承自Own,即也是ZObject对象,同时由于SessionBaseSocketBase存在消息传输,所以它也实现了IPipeEvents接口,同时它实现了IProactorEvents接口,在消息收发是会接收到通知。SessionBase一端和SocketBase进行消息的通讯,另一端和Engine存在消息通讯,它实现了IMsgSinkIMsgSource接口和Engine进行消息传输。

  1. internal class SessionBase : Own,
  2. Pipe.IPipeEvents, IProactorEvents,
  3. IMsgSink, IMsgSource{
  4. }
  1. internal interface IMsgSink
  2. {
  3. /// <summary>
  4. /// 传输消息.成功时返回true.
  5. /// </summary>
  6. /// <param name="msg">将msg消息写入到管道中</param>
  7. bool PushMsg(ref Msg msg);
  8. }
  1. internal interface IMsgSource
  2. {
  3. /// <summary>
  4. /// 取一个消息。成功时返回,从管道获取消息写入msg参数中;若失败则返回false,将null写入到msg参数中。
  5. /// </summary>
  6. /// <param name="msg">从管道获取消息写入Msg中</param>
  7. /// <returns>true if successful - and writes the message to the msg argument</returns>
  8. bool PullMsg(ref Msg msg);
  9. }

SocketBase将消息写入到写管道时,对应的SessionBase会从读管道读到SocketBase写入的数据,然后将数据从管道取出生成一个Msg,Engine会和AsyncSocket交互传输数据,关于Engine下一章再做介绍。

Option

option参数如下

  1. Affinity

    表示哪个线程是可用的,默认为0,表示所有线程在负载均衡都可使用。
  2. Backlog

    最大Socket待连接数
  3. DelayAttachOnConnect

    在创建连接时,延迟在SocketSession之间创建双向的管道,默认创建连接时立即创建管道
  4. DelayOnClose

    若为true,则在Socket关闭时Session先从管道接收所有消息发送出去。

    否则直接关闭,默认为true
  5. DelayOnDisconnect

    若为true,则在Pipe通知我们中断时Socket先将接收所有入队管道消息。

    否则直接中断管道。默认为true.
  6. Endianness

    字节序,数据在内存中是高到低排还是低到高排。
  7. Identity

    响应的Identity,每个Identity用于查找SocketIdentiy是一个重复的随机32位整形数字,转换为字节5位字节数组。每个消息的第一部分是Identity,
  8. IdentitySize

    1个字节用于保存Identity的长度。
  9. IPv4Only
  10. Linger

    当Socket关闭时,是否延迟一段时间等待数据发送完毕后再关闭管道
  11. MaxMessageSize

    每个消息包最大消息大小
  12. RawSocket

    若设置为true,RouterSocket可以接收非NetMQ发送来的tcp连接。

    默认是false,Stream在构造函数时会设置为true,设置为true时会将RecvIdentity修改为false(用NetMQ接收其他系统发送来的Socket请求应该用StreamSocekt,否则由于应用层协议不一样可能会导致一些问题。)
  13. RecvIdentity

    若为true,Identity转发给Socket
  14. ReconnectIvl

    设置最小重连时间间隔,单位ms。默认100ms
  15. ReconnectIvlMax

    设置最大重连时间间隔,单位ms。默认0(无用)
  16. RecoveryIvl

    PgmSocket用的
  17. SendBuffer

    发送缓存大小,设置底层传输Socket的发送缓存大小,初始为0
  18. ReceiveBuffer

    接收缓存大小,设置底层传输Socket的接收缓存大小,初始为0
  19. SendHighWatermark

    Socket发送的管道的最大消息数,当发送水位达到最大时会阻塞发送。
  20. ReceiveHighWatermark

    Socket接收管道的最大消息数
  21. SendLowWatermark

    Socket发送低水位,消息的最小数量单位,每次达到多少消息数量才向Session管道才激活写事件。默认1000
  22. ReceiveLowWatermark

    Socket接收低水位,消息的最小数量单位,每次达到多少消息数量Session管道才激活读事件。默认1000
  23. SendTimeout

    Socket发送操作超时时间
  24. TcpKeepalive

    TCP保持连接设置,默认-1不修改配置
  25. TcpKeepaliveIdle

    TCP心跳包在空闲时的时间间隔,默认-1不修改配置
  26. TcpKeepaliveIntvl

    TCP心跳包时间间隔,默认-1不修改配置
  27. DisableTimeWait

    客户端断开连接时禁用TIME_WAIT TCP状态

Pipe

上一章我们讲到过在SocketBaseSessionBase是通过2条单向管道进行消息传输,传输的消息单位是Msg,消息管道是YPipe<Msg>类型,那么YPipe<>又是什么呢?

YPipe

Ypipe内部实际维护这一个YQueue类型的先进先出队列,YPipe向外暴露了一下方法:

  1. TryRead

    该方法用于判断当前队列是否可读,可读的话第一个对象出队
  1. public bool TryRead(out T value)
  2. {
  3. if (!CheckRead())
  4. {
  5. value = default(T);
  6. return false;
  7. }
  8. value = m_queue.Pop();
  9. return true;
  10. }
  1. Unwrite

    取消写入消息
  1. public bool Unwrite(ref T value)
  2. {
  3. if (m_flushToIndex == m_queue.BackPos)
  4. return false;
  5. value = m_queue.Unpush();
  6. return true;
  7. }
  1. 写入消息

    将消息写入到队列中,若写入未完成则当前消息的指针索引指向当前队列块的后一位。
  1. public void Write(ref T value, bool incomplete)
  2. {
  3. m_queue.Push(ref value);
  4. // Move the "flush up to here" pointer.
  5. if (!incomplete)
  6. {
  7. m_flushToIndex = m_queue.BackPos;
  8. }
  9. }
  1. 完成写入

    当该部分消息写完时,则会调用Flush完成写入并通知另一个管道消息可读
  1. public void Flush()
  2. {
  3. if (m_state == State.Terminating)
  4. return;
  5. if (m_outboundPipe != null && !m_outboundPipe.Flush())
  6. SendActivateRead(m_peer);
  7. }

Msg

写入的消息单位是Msg,它实现了多条数据的存储,当每次数据写完还有数据带写入时通过将Flag标记为More表示消息还没写入完。

YQueue

YQueue是由一个个trunk组成的,每个trunk就是一个消息块,每个消息块可能包含多个Msg,主要由写入消息时是否还有更多消息带写入(Flag)决定。trunk是一个双向循环链表,内部维护着一个数组用于存放数据,每个数据会有2个指针,分别指向前一个块和后一个块,每个块还有一个索引,表示当前块在队列中的位置。

  1. private sealed class Chunk
  2. {
  3. public Chunk(int size, int globalIndex)
  4. {
  5. Values = new T[size];
  6. GlobalOffset = globalIndex;
  7. Debug.Assert(Values != null);
  8. }
  9. /// <summary>数据</summary>
  10. public T[] Values { get; }
  11. /// <summary>当前块在队列中的位置</summary>
  12. public int GlobalOffset { get; }
  13. /// <summary>前一个块</summary>
  14. [CanBeNull]
  15. public Chunk Previous { get; set; }
  16. /// <summary>下一个块</summary>
  17. [CanBeNull]
  18. public Chunk Next { get; set; }
  19. }

每个chunk默认最多可保存256个部分。

由于每次向SocketBase写入的Msg可能有多个部分,因此消息会写入到数组中,所有消息写完后指向trunk的指针才会后移一位。

YQueue有以下字段

  1. //用于记录当前块消息的个数,默认为256
  2. private readonly int m_chunkSize;
  3. // 当队列是空的时,下一个块指向null,首尾块都指向初始化的一个块,开始位置的块仅用于队列的读取(front/pop),最后位置的仅用于队列的写入(back/push)。
  4. // 开始位置
  5. private volatile Chunk m_beginChunk;
  6. //chunk的当前可读位置索引
  7. private int m_beginPositionInChunk;
  8. //指向后一个块
  9. private Chunk m_backChunk;
  10. //chunk的最后一个可读位置索引
  11. private int m_backPositionInChunk;
  12. //指向后一个块
  13. private Chunk m_endChunk;
  14. //chunk的下一个可写位置索引
  15. private int m_endPosition;
  16. //当达到最大Msg数量时,扩展一个chunk,最大为256个块
  17. private Chunk m_spareChunk;
  18. 当前trunk头部在整个队列中的的索引位置
  19. private int m_nextGlobalIndex;

YPipe写入Msg实际是向YQueue入队

  1. public void Push(ref T val)
  2. {
  3. m_backChunk.Values[m_backPositionInChunk] = val;
  4. //指向后一个块
  5. m_backChunk = m_endChunk;
  6. //索引更新到最后可读位置
  7. m_backPositionInChunk = m_endPosition;
  8. //下一个可写位置向后移动一位
  9. m_endPosition++;
  10. if (m_endPosition != m_chunkSize)
  11. return;
  12. //到达最后一个位置则需要扩充一个块
  13. Chunk sc = m_spareChunk;
  14. if (sc != m_beginChunk)
  15. {
  16. //已经扩充了块则更新下一个块的位置
  17. m_spareChunk = m_spareChunk.Next;
  18. m_endChunk.Next = sc;
  19. sc.Previous = m_endChunk;
  20. }
  21. else
  22. {
  23. //新建一个块,并更新索引位置
  24. m_endChunk.Next = new Chunk(m_chunkSize, m_nextGlobalIndex);
  25. m_nextGlobalIndex += m_chunkSize;
  26. m_endChunk.Next.Previous = m_endChunk;
  27. }
  28. m_endChunk = m_endChunk.Next;
  29. 当前块的局部位置从0开始
  30. m_endPosition = 0;
  31. }

每次消息写完消息时调用YPipeFlush方法完成当前消息的写入

  1. public bool Flush()
  2. {
  3. //只有一条Msg
  4. if (m_flushFromIndex == m_flushToIndex)
  5. {
  6. return true;
  7. }
  8. //将m_lastAllowedToReadIndex更新为flushToIndex
  9. if (Interlocked.CompareExchange(ref m_lastAllowedToReadIndex, m_flushToIndex, m_flushFromIndex) != m_flushFromIndex)
  10. {
  11. //没有数据写入时,lastAllowedToReadIndex为-1,表示没有数据可读,因此这里不需要关系线程安全
  12. Interlocked.Exchange(ref m_lastAllowedToReadIndex, m_flushToIndex);
  13. m_flushFromIndex = m_flushToIndex;
  14. return false;
  15. }
  16. 有数据写入时更新指针
  17. m_flushFromIndex = m_flushToIndex;
  18. return true;
  19. }

总结

该篇在上一片的基础上对SocketBaseSessionBase进行了一些细节上的补充。同时,对NetMQ的配置参数进行了一些介绍,最后对消息管道进行了简单讲解。




微信扫一扫二维码关注订阅号杰哥技术分享

本文地址:https://www.cnblogs.com/Jack-Blog/p/7117798.html

作者博客:杰哥很忙

欢迎转载,请在明显位置给出出处及链接

消息队列NetMQ 原理分析4-Socket、Session、Option和Pipe的更多相关文章

  1. 消息队列NetMQ 原理分析5-StreamEngine、Encord和Decord

    消息队列NetMQ 原理分析5-StreamEngine,Encord和Decord 前言 介绍 目的 StreamEngine 发送数据 接收数据 流程分析 Encoder V2Encoder V1 ...

  2. 消息队列NetMQ 原理分析1-Context和ZObject

    前言 介绍 NetMQ是ZeroMQ的C#移植版本,它是对标准socket接口的扩展.它提供了一种异步消息队列,多消息模式,消息过滤(订阅),对多种传输协议的无缝访问. 当前有2个版本正在维护,版本3 ...

  3. 消息队列NetMQ 原理分析3-命令产生/处理和回收线程

    消息队列NetMQ 原理分析3-命令产生/处理和回收线程 前言 介绍 目的 命令 命令结构 命令产生 命令处理 创建Socket(SocketBase) 创建连接 创建绑定 回收线程 释放Socket ...

  4. 消息队列NetMQ 原理分析2-IO线程和完成端口

    消息队列NetMQ 原理分析2-IO线程和完成端口 前言 介绍 目的 IO线程 初始化IO线程 Proactor 启动Procator线程轮询 处理socket 获取超时时间 从完成端口获取处理完的状 ...

  5. Netty构建分布式消息队列实现原理浅析

    在本人的上一篇博客文章:Netty构建分布式消息队列(AvatarMQ)设计指南之架构篇 中,重点向大家介绍了AvatarMQ主要构成模块以及目前存在的优缺点.最后以一个生产者.消费者传递消息的例子, ...

  6. PHP消息队列用法实例分析

    这篇文章主要介绍了PHP消息队列用法,结合实例形式分析了PHP消息队列用于Linux下进程间通信的相关技巧,需要的朋友可以参考下   该消息队列用于linux下,进程通信 队列状态信息:具体参考手册

  7. Rabbimq必备基础之对高级消息队列协议AMQP分析及Rabbitmq本质介绍

    MQ的一个产品... [消息队列] 1. MSMQ windows自带的一个服务... [petshop],message存放在文件系统中. 最原始的消息队列... [集群,消息确认,内存化,高可用, ...

  8. redis作为消息队列的原理

    Redis队列功能介绍 List 转:https://blog.csdn.net/cestlavieqiang/article/details/84197736 常用命令: Blpop删除,并获得该列 ...

  9. 自制MFC消息响应定位器+原理分析

    mfc里面有张消息映射表(MESSAGE_MAP),消息都是通过这张表来分发到相应函数里的. 这个是我自制的定位器,从vc6.0到现在的2013生成的mfc都可以用,全静态扫描并已处理动态基址. 下面 ...

随机推荐

  1. Hive load from hdfs 出错

    hive 加载HDFS的数据时出现错误, FATAL:SemanticException [Error 10028] search了一下,跟他一样Hive load from hdfs 出错. 我按照 ...

  2. kotlin 语言入门指南(二)--代码风格

    语言风格 这里整理了 kotlin 惯用的代码风格,如果你有喜爱的代码风格,可以在 github 上给 kotlin 提 pull request . 创建DTOs(POJSs/POCOs) 文件: ...

  3. 为什么要使用addEventListener而不是on监听事件

    昨天回答了一个关于vue的问题 vue 除了input 其他可以用keyup事件嘛? 在vue中没有提供除表单之外其它的keyup绑定方法,可以使用原生的监控键盘的事件,于是给出了代码: mounte ...

  4. Spring Security教程系列(一)基础篇-1

    第 1 章 一个简单的HelloWorld 第 1 章 一个简单的HelloWorld Spring Security中可以使用Acegi-1.x时代的普通配置方式,也可以使用从2.0时代才出现的命名 ...

  5. 一、 开篇(ASP.NET MVC5 系列)

    这个教程将教你一些用VS2013创建ASP.NET MVC 5 Web应用程序基础知识.为了避免开发工具的不一致而带来的一些小麻烦,建议你使用和我一样的开发工具VS2013英文版. 开发工具:Visu ...

  6. 微信第三方登录(原生)demo

    在一家ecstore二开公司有一段时间了,公司希望往自己研发产品上面走,一直在培养新人.   最近要自己去微信登录,自己就在ectore的框架基础上,写的原生微信第三方登录以此来熟悉微信第三方登录,在 ...

  7. css重构之旅

    css重构之旅 >前言: 今年我大一,马上就要大二了.从高三毕业暑假到大学的这一年马上过去,马上迎来大二生活学习前端也有将近一年了.一昧去追求那些视觉的效果和相对高端和新颖的技术,反而忽略了最基 ...

  8. [asp.net mvc 奇淫巧技] 04 - 你真的会用Action的模型绑定吗?

    在QQ群或者一些程序的交流平台,经常会有人问:我怎么传一个数组在Action中接收.我传的数组为什么Action的model中接收不到.或者我在ajax的data中设置了一些数组,为什么后台还是接收不 ...

  9. JParticles 2.0 发布,打造炫酷的粒子特效

    JParticles 2.0 发布,打造炫酷的粒子特效.不好意思哈,在这么繁花似锦的世界里,标题不得不取得吸引眼球一点哈,不然...还是不啰嗦了,我们进入正题吧 简单介绍一下 JParticles 2 ...

  10. 简化布隆过滤器——BitMap

    简化布隆过滤器--BitMap 前言 前段开发项目试就发现,一部分的代码实现存在着一些性能上的隐患.但当时忙于赶进度和由于卡发中的不稳定因素,想了许多解决方案也没有机会实施.最近,正好趁个机会进行一系 ...