前言

介绍

  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-StreamEngine,Encord和Decord
  6. 消息队列NetMQ 原理分析6-TCP和Inpoc实现
  7. 消息队列NetMQ 原理分析7-Device
  8. 消息队列NetMQ 原理分析8-不同类型的Socket
  9. 消息队列NetMQ 原理分析9-实战

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


StreamEngine

SocketBaseMsg发送给SessionBase之后需要将Msg转化为byte[]进行传输,Engine就是做转换的工作,转换完成之后就会和实际的底层Socket进行消息传输。

NetMQTcp协议消息转换使用的是StreamEngine

  1. internal sealed class StreamEngine : IEngine, IProactorEvents, IMsgSink
  2. {
  3. }

上一章介绍到管道事件。

发送数据

当出管道有数据可读时,会调用SessionBaseReadActivated事件

  1. public void ReadActivated(Pipe pipe)
  2. {
  3. ...
  4. if (m_engine != null)
  5. m_engine.ActivateOut();
  6. else
  7. m_pipe.CheckRead();
  8. }

然后会调用对应m_engine的ActivateOut事件

  1. public void ActivateOut()
  2. {
  3. FeedAction(Action.ActivateOut, SocketError.Success, 0);
  4. }
  5. public void FeedAction(){
  6. ...
  7. case State.Active:
  8. switch (action)
  9. {
  10. case Action.OutCompleted:
  11. int bytesSent = EndWrite(socketError, bytesTransferred);
  12. // IO error has occurred. We stop waiting for output events.
  13. // The engine is not terminated until we detect input error;
  14. // this is necessary to prevent losing incoming messages.
  15. if (bytesSent == -1)
  16. {
  17. m_sendingState = SendState.Error;
  18. }
  19. else
  20. {
  21. m_outpos.AdvanceOffset(bytesSent);
  22. m_outsize -= bytesSent;
  23. BeginSending();
  24. }
  25. break;
  26. ...
  27. }
  28. ...
  29. }

TCPConnect客户端发送请求完成时,会调用OutCompleted事件

  1. private void Loop()
  2. {
  3. ...
  4. switch (completion.OperationType)
  5. {
  6. ...
  7. case OperationType.Connect:
  8. case OperationType.Disconnect:
  9. case OperationType.Send:
  10. item.ProactorEvents.OutCompleted(
  11. completion.SocketError,
  12. completion.BytesTransferred);
  13. }
  14. }
  15. ...
  1. public void OutCompleted(SocketError socketError, int bytesTransferred)
  2. {
  3. ...
  4. // Create the engine object for this connection.
  5. var engine = new StreamEngine(m_s, m_options, m_endpoint);
  6. ...
  7. // Attach the engine to the corresponding session object.
  8. SendAttach(m_session, engine);
  9. ...
  10. }

此时会创建一个StreamEngine和请求的SessionBase对象进行关联。

  1. protected override void ProcessAttach(IEngine engine)
  2. {
  3. Debug.Assert(engine != null);
  4. // Create the pipe if it does not exist yet.
  5. if (m_pipe == null && !IsTerminating)
  6. {
  7. ZObject[] parents = { this, m_socket };
  8. int[] highWaterMarks = { m_options.ReceiveHighWatermark, m_options.SendHighWatermark };
  9. int[] lowWaterMarks = { m_options.ReceiveLowWatermark, m_options.SendLowWatermark };
  10. bool[] delays = { m_options.DelayOnClose, m_options.DelayOnDisconnect };
  11. Pipe[] pipes = Pipe.PipePair(parents, highWaterMarks, lowWaterMarks, delays);
  12. // Plug the local end of the pipe.
  13. pipes[0].SetEventSink(this);
  14. // Remember the local end of the pipe.
  15. Debug.Assert(m_pipe == null);
  16. m_pipe = pipes[0];
  17. // Ask socket to plug into the remote end of the pipe.
  18. SendBind(m_socket, pipes[1]);
  19. }
  20. // Plug in the engine.
  21. Debug.Assert(m_engine == null);
  22. m_engine = engine;
  23. m_engine.Plug(m_ioThread, this);
  24. }

接收数据

当完成端口通知数据接收完成时,会调用ProactorInCompleted事件,实际就是调用的对应的StreamEngineInCompleted事件

  1. public void InCompleted(SocketError socketError, int bytesTransferred)
  2. {
  3. FeedAction(Action.InCompleted, socketError, bytesTransferred);
  4. }
  1. public void FeedAction(){
  2. ...
  3. case State.Active:
  4. switch (action)
  5. {
  6. case Action.InCompleted:
  7. m_insize = EndRead(socketError, bytesTransferred);
  8. ProcessInput();
  9. break;
  10. ...
  11. }
  12. ...
  13. }

接收完成后会对接收到的数据进行处理

  1. private void ProcessInput()
  2. {
  3. ...
  4. if (m_options.RawSocket)
  5. {
  6. if (m_insize == 0 || !m_decoder.MessageReadySize(m_insize))
  7. {
  8. processed = 0;
  9. }
  10. else
  11. {
  12. processed = m_decoder.ProcessBuffer(m_inpos, m_insize);
  13. }
  14. }
  15. else
  16. {
  17. // Push the data to the decoder.
  18. processed = m_decoder.ProcessBuffer(m_inpos, m_insize);
  19. }
  20. ...
  21. // Flush all messages the decoder may have produced.
  22. m_session.Flush();
  23. ...
  24. }
  25. public override bool MessageReadySize(int msgSize)
  26. {
  27. m_inProgress = new Msg();
  28. m_inProgress.InitPool(msgSize);
  29. NextStep(new ByteArraySegment(m_inProgress.Data, m_inProgress.Offset),
  30. m_inProgress.Size, RawMessageReadyState);
  31. return true;
  32. }

读取数据到Msg后会调用DecoderProcessBuffer方法

PS:由于NetMQ有自己的传输协议格式,因此当使用NetMQ和其他程序进行Socket传输时,必须使用StreamSocket


  1. public int ProcessBuffer(ByteArraySegment data, int size)
  2. {
  3. ...
  4. while (m_toRead == 0)
  5. {
  6. if (!Next())
  7. {
  8. if (State < 0)
  9. {
  10. return -1;
  11. }
  12. return size;
  13. }
  14. }
  15. return size;
  16. ...
  17. }
  18. protected override bool Next()
  19. {
  20. if (State == RawMessageReadyState)
  21. {
  22. return RawMessageReady();
  23. }
  24. return false;
  25. }
  26. private bool RawMessageReady()
  27. {
  28. ...
  29. bool isMessagedPushed = m_msgSink.PushMsg(ref m_inProgress);
  30. if (isMessagedPushed)
  31. {
  32. // NOTE: This is just to break out of process_buffer
  33. // raw_message_ready should never get called in state machine w/o
  34. // message_ready_size from stream_engine.
  35. NextStep(new ByteArraySegment(m_inProgress.Data, m_inProgress.Offset),
  36. 1, RawMessageReadyState);
  37. }
  38. return isMessagedPushed;
  39. ...
  40. }

对读到的数据进行处理调用RawDecoderNext的方法,将获取到的Msg放入到SeesionBase的管道中。

流程分析

读写数据流程图如下图所示:



我们使用WireShark进行验证。

我们监听15557地址,然后创建一个客户端连接15557地址



前面3条是三次握手。第四条是客户端向服务器发送了10字节长度的请求头部,以0xff开头,0x7f结尾。中间是8字节是Identitysize长度

  1. ...
  2. switch (m_handshakeState)
  3. {
  4. case HandshakeState.Closed:
  5. switch (action)
  6. {
  7. case Action.Start:
  8. // Send the 'length' and 'flags' fields of the identity message.
  9. // The 'length' field is encoded in the long format.
  10. m_greetingOutputBuffer[m_outsize++] = 0xff;
  11. m_greetingOutputBuffer.PutLong(m_options.Endian, (long)m_options.IdentitySize + 1, 1);
  12. m_outsize += 8;
  13. m_greetingOutputBuffer[m_outsize++] = 0x7f;
  14. ...
  15. }
  16. ...
  17. }
  18. ...



第6条是服务器向客户端发送的10字节长度的请求头部,以0xff开头,0x7f结尾。中间是8字节是identitysize的信息

I

第8条是服务器向客户端发送的版本号和Socket类型,01表示版本号1,06表示当前是RouterSocket

  1. ...
  2. case HandshakeState.ReceivingGreeting:
  3. switch (action)
  4. {
  5. case Action.InCompleted:
  6. ...
  7. if (m_greeting[0] != 0xff || (m_greetingBytesRead == 10 && (m_greeting[9] & 0x01) == 0)){
  8. ...
  9. }
  10. else if (m_greetingBytesRead < 10)
  11. {
  12. var greetingSegment = new ByteArraySegment(m_greeting, m_greetingBytesRead);
  13. BeginRead(greetingSegment, PreambleSize - m_greetingBytesRead);
  14. }
  15. else
  16. {
  17. ...
  18. m_outpos[m_outsize++] = 1; // Protocol version
  19. m_outpos[m_outsize++] = (byte)m_options.SocketType;
  20. ...
  21. }
  22. ...
  23. }
  24. ...



第10条是客户端向服务器发送的版本号和socket类型,05表示当前是DealSocket

  1. ...
  2. case HandshakeState.ReceivingRestOfGreeting:
  3. switch (action)
  4. {
  5. case Action.InCompleted:
  6. ...
  7. if (m_greeting[VersionPos] == 0)
  8. {
  9. // ZMTP/1.0 framing.
  10. m_encoder = new V1Encoder(Config.OutBatchSize, m_options.Endian);
  11. m_encoder.SetMsgSource(m_session);
  12. m_decoder = new V1Decoder(Config.InBatchSize, m_options.MaxMessageSize, m_options.Endian);
  13. m_decoder.SetMsgSink(m_session);
  14. }
  15. else
  16. {
  17. // v1 framing protocol.
  18. m_encoder = new V2Encoder(Config.OutBatchSize, m_session, m_options.Endian);
  19. m_decoder = new V2Decoder(Config.InBatchSize, m_options.MaxMessageSize, m_session, m_options.Endian);
  20. }
  21. Activate();
  22. ...
  23. }
  24. ...

Encoder

V2Encoder

接下来就是数据传输。

  1. public V2Encoder(int bufferSize, IMsgSource session, Endianness endian)
  2. : base(bufferSize, endian)
  3. {
  4. m_inProgress = new Msg();
  5. m_inProgress.InitEmpty();
  6. m_msgSource = session;
  7. // Write 0 bytes to the batch and go to message_ready state.
  8. NextStep(m_tmpbuf, 0, MessageReadyState, true);
  9. }

由于NetMQ使用的是版本1,用的是V2EncoderV2Decoder进行编码和解码。

在初始化Encoder的时候会向报文写入2个0字节数据,暂时不明白为何要这样做。


  1. int protocolFlags = 0;
  2. if (m_inProgress.HasMore)
  3. protocolFlags |= V2Protocol.MoreFlag;
  4. if (m_inProgress.Size > 255)
  5. protocolFlags |= V2Protocol.LargeFlag;
  6. m_tmpbuf[0] = (byte)protocolFlags;
  7. // Encode the message length. For messages less then 256 bytes,
  8. // the length is encoded as 8-bit unsigned integer. For larger
  9. // messages, 64-bit unsigned integer in network byte order is used.
  10. int size = m_inProgress.Size;
  11. if (size > 255)
  12. {
  13. m_tmpbuf.PutLong(Endian, size, 1);
  14. NextStep(m_tmpbuf, 9, SizeReadyState, false);
  15. }
  16. else
  17. {
  18. m_tmpbuf[1] = (byte)(size);
  19. NextStep(m_tmpbuf, 2, SizeReadyState, false);
  20. }

第一个字节是Flags用于标记该报文是否为大报文,超过过255个字节就会标记为大包标记,是否还有更多报文。若报文长度小于256,则第二个字节用于存储报文长度。但是若是大报文,则会8个字节保存报文长度。

下面就开始发送数据

我们用客户端发一个字符串test1,然后服务端原样返回该字符串



可以看到如我们上面分析的一样,第一个字节为0,第二个字节为大小test1为5个字节长度。由于CMD命令单行输入最长字符限制长度为255,因此我们没办法在CMD命令下输入更长数据进行测试。暂时就不做验证。

V1Encoder

V1Encoder编码如下所示

  1. if (size < 255)
  2. {
  3. m_tmpbuf[0] = (byte)size;
  4. m_tmpbuf[1] = (byte)(m_inProgress.Flags & MsgFlags.More);
  5. NextStep(m_tmpbuf, 2, SizeReadyState, false);
  6. }
  7. else
  8. {
  9. m_tmpbuf[0] = 0xff;
  10. m_tmpbuf.PutLong(Endian, size, 1);
  11. m_tmpbuf[9] = (byte)(m_inProgress.Flags & MsgFlags.More);
  12. NextStep(m_tmpbuf, 10, SizeReadyState, false);
  13. }

当小于255字符,首字符是长度,第二个字符是Flags,超过255字符,首字符为0xff,然后跟着8个字符长度的长度值,接下来是Flags

RawEncoder

使用RawEncoder会将原始数据原样发送不会增加任何其他字符。

Decoder

V2Decoder

接收到数据会先接收第一个字节Flags判断是否有后续包以及是小包还是打包,若是小包,则解析第一个字节长度位,否则读取8个字节长度位。

V1Decoder

接收到数据收先会判断第一个字节是不是Oxff,若为Oxff则表示为打包,获取8位字节长度,否则获取1位字节长度处理。

RawDecoder

使用RawDecoder会读取数据保存到管道中。

总结

本片介绍了NetMQ的报文格式并阐述了底层Msg如何转换为流进行发送和接收。




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

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

作者博客:杰哥很忙

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

消息队列NetMQ 原理分析5-StreamEngine、Encord和Decord的更多相关文章

  1. 消息队列NetMQ 原理分析4-Socket、Session、Option和Pipe

    消息队列NetMQ 原理分析4-Socket.Session.Option和Pipe 前言 介绍 目的 Socket 接口实现 内部结构 Session Option Pipe YPipe Msg Y ...

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

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

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

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

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

    消息队列NetMQ 原理分析3-命令产生/处理和回收线程 前言 介绍 目的 命令 命令结构 命令产生 命令处理 创建Socket(SocketBase) 创建连接 创建绑定 回收线程 释放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. php访问数据库$result=$mysql_qurey('')判断是否是空值

    在访问数据库后得到result值,前提是$relust为true,就是没有发生错误连接情况,但是查询的条件由于不满足导致返回值为空,此时判断就不能用!来决定是否有返回数据了,经过书籍搜索,用resul ...

  2. 关于Handler的理解,子线程不能更新UI的纠正和回调的思考

    开发Android这么久了,总会听到有人说:主线程不能访问网络,子线程不能更新UI.Android的主线程的确不能长时间阻塞,但是子线程为什么不能更新UI呢?今天把这些东西整理,顺便在子线程更新UI. ...

  3. 检查服务器是否开启GD库

    1.把以下代码复制到记事本中并保存成A.php<?phpOb_end_flush();header ("Content-type: image/png");$im = @im ...

  4. 【2017-06-27】Js中获取地址栏参数、Js中字符串截取

    一.Js中获取地址栏参数 //从地址栏获取想要的参数 function GetQueryString(name) { var reg = new RegExp("(^|&)" ...

  5. mkdirs自动创建文件夹

    //保存路径 如file路径是E:\filer\a.jpg File toSaveFile=new File("filePath"); if(toSaveFile!=null){ ...

  6. SOD开源框架MSF(消息服务框架)进阶篇

    复习:在上一篇我介绍了MSF的基本订阅,模式就是,客户端A,订阅服务器.客户端B,订阅服务器.通过服务器广播消息, 所有订阅过的客户端都能接到消息. 进阶:在上一篇的基础上,增加客户端A,发送信息到服 ...

  7. 【LeetCode】220. Contains Duplicate III

    题目: Given an array of integers, find out whether there are two distinct indices i and j in the array ...

  8. JS函数和对象(一)

    在本文章中,将对JS中的函数和对象进行一些讲解,不对之处还请之处 一.JS中的函数 1.1无参函数 其形式如下代码所示 function box(){ alert("我是一个函数,只有被调用 ...

  9. Ext viewport的渲染

    Ext viewport的渲染 1.在app.js里创建 Ext.application({ name: 'MySecurity', extend: 'MySecurity.Application', ...

  10. maven编译常见错误解决方法整理

    程序包com.sun.xml.internal.ws.spi不存在 当maven项目里面有用到JDK内部的一些类,接口(如:com.sun.xml.internal.ws.spi.ProviderIm ...