消息中间件学习十--ZeroMQ源码

1.概述: 

ZeroMQ是一种基于消息队列的多线程网络库。
它对套接字类型、连接处理、帧、甚至路由的底层细节进行抽象,
提供跨越多种传输协议的套接字。ZeroMQ是网络通信中新的一层,
介于应用层和传输层之间。它是一个可伸缩层,可并行运行,分散在分布式系统间。

 

 

2.套接字  

网络整理: 
书上说的端口是数据结构和I/O缓存区”是指硬件端口,网络编程里的端口可以理解为应用程序的ID。 

说得形象点,套接字就类似我们人类的门 
我们打开门,通过门外面的人可以进来 
我们推开门,里面的人也可以出去 
同样,外面的数据可以通过socket把它存储在本地机器的缓冲区里等待本地机器接收 
本地机器的数据可以通过socket放在缓冲区里等待发送到对方机器上 
当我们把门给关上时,就拒绝了和外面世界的交往。 

概括来说,socket可以看程序访问系统网络组件的接口,从类型上来看是一个句柄。
我们都知道,在windows系统中每一个句柄对应一个对象,每一个对象都对应内存中的一块内存,
其中存放了该对象的各种属性。具体到socket上来说,
可以用setsocketopt来设置每种属性(参见《windows网络编程技术》第9章套接字选项和I
/O控制命令)。

系统将对socket的各种操作转换对网络组件的操作向网络发出数据。

另一种说法比较书面化:
应用层通过传输层进行数据通信时,TCP和UDP会遇到同时为多个应用程序进程提供并发服务的问题。
多个TCP连接或多个应用程序进程可能需要 通过同一个TCP协议端口传输数据。
为了区别不同的应用程序进程和连接,
许多计算机操作系统为应用程序与TCP/IP协议交互提供了称为套接字 (Socket)的接口,
区分不同应用程序进程间的网络通信和连接。

     生成套接字,主要有3个参数:通信的目的IP地址、使用的传输 层协议(TCP或UDP)和使用的端口号。

     Socket原意是“插座”。通过将这3个参数结合起来,与一个“插座”Socket绑定,

     应用层就可以和传输 层通过套接字接口,区分来自不同应用程序进程或网络连接的通信,实现数据传输的并发服务。

 

3..套接字的类型  

概述:
  套接字的类型其实是指套接字的一种传输数据的方式,它们就是面向连接和面向消息。

第一种:面向连接的套接字
  代号SOCK_STREAM。它的特征就是传输的数据是什么顺序接收的时候就是什么顺序;
当在传输的过程中出现问题,那么问题点的数据就会停止传输并阻塞后面的数据,
等问题解决了再从该点继续传输,这样保证了数据在传输过程中不会丢失;
按这种排序的方式一直传输下去的话,数据是可以没有边界的
(只要传的人有那么多数据,接收的人也有那么多空间,两端条件满足)。
这种传输方式还有一个特点,就是传输的数据量大时,
传输这边觉得一次传不了那么多(每次传输的数据大小不能没法没天啊,是有限制的),
那么就会把数据分成几份,先后传出去。而接收这边,
有一个地方专门放接收到的数据(接数据到达的顺序存放)的地方(人称缓冲区),
当要从这个存放的地方取数据的时候可不一定按你发来的是几份我就取几次,
我可能觉得我能力大,一次就从里面取完了,也可能没错得取两次也行。这完全取决于接收者。 像上面说的,你也发现了,它每一个传输数据者都会对应一个数据接收者。也就是两端的套接字是一一对应的。 人们总结这种传输方式:可靠、按顺序的、基于字节的面向连接传输方式。
第二种:面向消息的套接字 代号SOCK_DGRAM。它的特征就是非顺序、还有可能丢失传输过程中的数据、有传输边界但是速度快的传输方式。 为什么它的速度比面向连接的要快呢。不从原理上看,单类比一个常景:
要从五袋米到一个地方,五个人同时出发送总比五个人按顺序一个送完一个接着这种要快吧。 总结: 面向连接的套接字是比较耗资源的,耗的是哪些资源呢:时间、传输通道。
它的前提是两端的套接字必需连接好了之后才进行数据传输。
在传输的过程中就一直占着此传输通道(可以把网线想像成由五条管子扎成一扎组成,两个连接的套接字就相当于在传输的这段时间占着其中一根)。
又因为它要保证顺序又要保证失败重传,所以它的传输过程有很多规矩,大家都知道,
按规矩办事肯定是慢的。要顺序,那肯定得一个传完再传一个;
要失败重传,那必需要一个传完之后等特对方应答后再看情况传输。
面向消息的套接字则快了,因为它不需要事先连接,更没有保证顺序和重传这么规矩。
数据一个个传出去就不管了,哪个先到看自己造化了。所以显得效率更高,但是它的责任少了,你的责任就大了。
很简单的道理,有一个要求,传数据
-保证不丢失。

面向连接的套接字两个都帮你做了,你什么都不用管;面向消息的只帮你做了前一个,
那么后一个就得你自己想办法做。
所以单纯评论两种方式的好坏是没意义的,按情景使用即可。守恒定律在哪都适用。

 

4.帧的概念   

1.网络上的帧数据在网络上是以很小的称为帧(Frame)的单位传输的,
帧由几部分组成,不同的部分执行不同的功能。
帧通过特定的称为网络驱动程序的软件进行成型,
然后通过网卡发送到网线上,通过网线到达它们的目的机器,
在目的机器的一端执行相反的过程。
接收端机器的以太网卡捕获到这些帧,
并告诉操作系统帧已到达,然后对其进行存储。
就是在这个传输和接收的过程中,嗅探器会带来安全方面的问题。

2.数据帧Frame,数据链路层的协议数据(protocoldataunit)单元。
数据链路层的主要职责是控制相邻系统之间的物理链路,
它在传送“比特”信息的基础上,在相邻节点间保证可靠的数据通信。
为了保证数据的可靠传输,把用户数据封装成帧。

3.如果是FLASH的帧帧——就是影像动画中最小单位的单幅影像画面,相当于电影胶片上的每一格镜头。
关键帧——任何动画要表现运动或变化,至少前后要给出两个不同的关键状态,
而中间状态的变化和衔接电脑可以自动完成,在Flash中,表示关键状态的帧叫做关键帧。
过渡帧——在两个关键帧之间,电脑自动完成过渡画面的帧叫做过渡帧。
关键帧和过渡帧的联系和区别两个关键帧的中间可以没有过渡帧(如逐帧动画),
但过渡帧前后肯定有关键帧,因为过渡帧附属于关键帧;关键帧可以修改该帧的内容,
但过渡帧无法修改该帧内容。关键帧中可以包含形状、剪辑、组等多种类型的元素或诸多元素,
但过渡帧中对象只能是剪辑(影片剪辑、图形剪辑、按钮)或独立形状。
影片是由一张张连续的图片组成的,每幅图片就是一帧,PAL制式每秒钟25帧,NTSC制式每秒钟30帧。

 

5.上面是一些概念,下面是源码的分析 

分析一:

1. 传送message和command的管道

 

 

过程:

   (1)mailbox_t 持有command queue, pipe_t 持有message_queue;

 (2)yqueue_t 和它的包装类ypipe_t提供queue的实现。

      mailbox_t使用的queue传递command_t, pipe_t使用的queue传递msg_t。 

  (3)signaler_t用eventfd机制创建一对fd_t,用于线程间的信号通知。
           mailbox_t的通知方式是:发送者写入command后,
           应该写入signaler_t的w成员变量,然后在它的r成员变量上监听的线程将会被唤醒。
 (4)pipe_t没有自己的通知机制,发送者写入message后,
      应该写入一个command,并写signaler_t,从而唤醒监听线程。
 
2. 线程间传送message和command的机制
  

 

 

 

    过程:

       (1)poller_t(实际上是epoll_t,用typedef定义)用epoll机制监听fd_t列表的信号。

               fd_t的数量和信号种类可以动态地改变。它还包含一个线程的worker routine,监听在这个线程上进行。

            (io_thread_t没有worker routine,虽然它的名字包含”thread”)

       (2)io_thread_t持有poller_t并负责启动它。

                在poller_t捕捉到信号时,io_thread_t的处理函数in_event()被调用,

               于是从mailbox_t读入command并处理。

      (3)io_thread_t派生自i_poll_events,所以能被poller_t引用。

   (4)command_t中包含目标object。 io_thread_t调用object_t的command处理函数process_command()。

        在该函数中,会依据command的类别派发,从而调用其派生类的虚拟处理函数process_XXX()。

   (5)对于单纯的command处理,如发给session_base_t的command,session_base_t应当实现其处理函数process_XXX()。
   (6)
pipe_t在其处理函数process_command()中, 会调用真正的接收对象,如session_base_t的处理函数XXX_activated(),
 
              session_base_t应当派生自i_pipe_events,并实现处理函数XXX_activated(),开始读写等操作。
 
3. socket的读写机制
   

 

 

 

   (1)读写socket的类,如tcp_connecter_t和stream_engine_t,也依赖poller_t监听其状态。

        poller_t来自它们挂接的io_thread_t。它们在步骤开始时选择一个io_thread_t。

   (2)与io_thread_t一样,要得到socket状态信号,这些socket读写类应该派生自i_poll_events,

        在其处理函数(in_event(),out_event()等)中开始socket读写操作。

   (3)io_object_t是这个派生关系的中间层,持有poller_t,以便将socket加入监听。   

 

4. 通信Session的建立

   

 

 

 

 

 

  (1)ctx_t负责保存socket_base_t实例和其他各种资源,如线程io_thread_t和它的mailbox_t。

  (2)一个socket_base_t对应一个socket。对于tcp客户端,一个连接创建一个socket_base_t;

      对于tcp服务器,除了监听的socket需要一个socket_base_t,

      每个接受的客户连接会产生一个新的socket_base_t。

  (3)socket_base_t为每个连接创建一个session_base_t。

      session_base_t根据连接类型,创建tcp_conneter_t(客户端)或tcp_listener_t(服务器),

      后者将完成连接或接受连接的过程,并创建stream_egine_t。

      engine被直接挂接到session_base_t上,后续的数据读写由engine直接处理,
      不再经过tcp_connnter_t或tcp_listener_t。
   (4)socket, session, connecter, listener和engine之间的command传送,用前面所述的io_thread_t逻辑完成。
        socket_base_t与session_base_t之间的message传送,用前述的pipe_t逻辑完成。
        session_base_t与engine之间直接引用,不需要特别处理。 
 
5. 在Engine中的数据打包和解包
   

 

 

 

  (1)客户端和服务器的engine交互前需要鉴权(握手),由engine自己完成。

  (2)Msg_t中的数据发送前需要转成ZMTP(ZeroMQ Message Transport Protocol)格式,

      接收后需要将ZMTP格式转回msg_t。engine调用encoder和decoder_t完成格式转换。

      i_encoder和i_decoder的不同派生类实现ZMTP的不同版本。

 

 分析二 

1. PUB & SUB模式

 

 

 (1) pub & sub模式的核心在于:

    subscriber向publisher注册filter,publisher根据filter向对应的subscriber发布消息。 

(2) ZeroMQ使用了两组socket_base_t的派生类。
      xsub_t和sub_t用于sub端。
     
      sub_t的xsetsockopt()用于设置filter选项,
      它会调用xsub_t的xsend()向pub端发送一个注册请求。
      当sub端收到发布的消息时,会暂存在xsbub_t的fq_t成员中,
      以备以后用户调用xrecv()来获取。
       
      xpub_t和pub_t用于pub端。 mtrie_t成员和dist_t成员配合使用,用于过滤和发布消息。
      mtrie_t是一种字典树,支持prefix match模式。sub端注册filter时,
      对应的pipe_t同时保存在mtrie_t和dist_t中,在mtrie_t中根据消息头(可以理解为topic)
      找到期望的目标pipe_t时,会调用mark_as_matching()在dist_t中标记pipe_t,然后dist_t根据标记发送消息。
 
  2. 在Engine中的数据打包和解包(REVIEW)
     

 

   (1)客户端和服务器的engine的交互分三个阶段:greeting,handshake和ready。

      engine自己完成greeting,接着它调用mechanism_t完成handshake,最后进入ready,可以开始真正的消息交互。

   (2)进入ready时,如果socket要求,engine会发送一个identity消息给socket。

      socket是否要求由socket的options.recv_identity指定。一个要求identity的socket例子是router_t。

   (3)msg_t中的数据发送前需要转成ZMTP(ZeroMQ Message Transport Protocol)格式,

      接收后需要将ZMTP格式转回msg_t。engine调用encoder和decoder_t完成格式转换。

      i_encoder和i_decoder的不同派生类实现ZMTP的不同版本。

   

  3. 在pipe_t中传送的消息格式

     

 

   (1)msg_t的union成员包含一个base或其派生类的成员。

     (这里的“派生”是指逻辑上的派生,实际上它们只是由同样的大小并共享部分成员,如type,flags)

      这些派生类的不同之处可能包括数据存储方式。

      如vsm将数据保存在结构内部,imsg将数据保存在用malloc额外分配的内存中。

   (2)base.type指定派生类的类型。base.flags则指定msg_t的其他特性,
      如msg_t::more可以将连续的一组msg_t组成一个multiple part的消息。
      又如:msg_t::command可以指定消息的类型是command而不是message。
     (ZeroMQ的消息有两种: command和message,command用于engine之间的数据交互,message是上层使用者(如pub和sub)的数据交互)
 
 
4. 在pub和sub间的数据交互
   阶段:
     Greeting阶段。pub端和sub端同时发送以下数据,由stream_engine自己完成。
     

 

     (1)首先发送10 octets的signature。收到对端的signature后,认定对端也是ZMTP协议。

     (2)然后发送1个octet的主版本号。收到对端的主板本号后,创建本地兼容的encoder/decoder。这里创建v2_encoder_t和v2_decoder_t。

     (3)如果对端也是版本号3,则发送1 octet的次版本号,20 octet的mechanism名称,和一些填充字节。

         mechanism这里使用NULL,对应null_mechanism_t。收到对端的mechanism,确认是否与本地mechanism相同。是则greeting完成。

 

      Handshake阶段。

      sub端发送如下数据:

      

 

      pub端发送如下数据:

      

 

      (1)首先是1 octet的长度标志: 1 octet的长度或8 octets的长度。然后是长度,这里是1 octet。这部分由encoder根据后面msg_t的数据添加。

      (2)接着是command-body。包括command-name “READY”,和command-data “Socket-Type=PUB/SUB”。这部分来自msg_t的数据,由null_mechanism_t构造。

      (3)注意command-data部分包括多组属性。每个属性包括:

            1 octet名字长度,名字,4 octets的值长度,iv.值。

       (4)Pub和sub发送以上相应数据。收到对端数据后,则handshake完成。
 
 
      ready阶段。
         sub端发送如下数据向pub端注册:

        

 

        (1)首先是1 octet的长度标志: 1 octet的长度或8 octets的长度。然后是长度,这里是1 octet。这部分由encoder根据后面msg_t的数据添加。

        (2)接着是message-body。1 octet的注册/反注册标志。1是注册,0是反注册。Topic是注册的filter:”topic”,希望得到所有以”topic”开头的消息。

        pub端发送如下数据,发布消息到sub端:

         

 

         (1)首先是1 octet的长度标志: 1 octet的长度或8 octets的长度。然后是长度,这里是1 octet。这部分由encoder根据后面msg_t的数据添加。

         (2)接着是message-body。消息内容是”topic nm val”。

     

       如下是sub和pub的数据交互的一个例子。ZMTP 3.1的完整语法定义。     

         The following ABNF grammar defines the protocol:

         ; The protocol consists of zero or more connections
         zmtp = *connection

         ; A connection is a greeting, a handshake, and traffic
         connection = greeting handshake traffic

         ; The greeting announces the protocol details
         greeting = signature version mechanism as-server filler
         signature = %xFF padding %x7F
         padding = 8OCTET ; Not significant
         version = version-major version-minor
         version-major = %x03
         version-minor = %x01

         ; The mechanism is a null padded string
         mechanism = 20mechanism-char
         mechanism-char = "A"-"Z" | DIGIT
         | "-" | "_" | "." | "+" | %x0

         ; Is the peer acting as server for security handshake?
         as-server = %x00 | %x01

         ; The filler extends the greeting to 64 octets
         filler = 31%x00 ; 31 zero octets

         ; The handshake consists of at least one command
         ; The actual grammar depends on the security mechanism
          handshake = 1*command

         ; Traffic consists of commands and messages intermixed
         traffic = *(command | message)

          ; A command is a single long or short frame
          command = command-size command-body
          command-size = %x04 short-size | %x06 long-size
          short-size = OCTET ; Body is 0 to 255 octets
          long-size = 8OCTET ; Body is 0 to 2^63-1 octets
          command-body = command-name command-data
          command-name = short-size 1*255command-name-char
          command-name-char = ALPHA
          command-data = *OCTET

          ; A message is one or more frames
          message = *message-more message-last
          message-more = ( %x01 short-size | %x03 long-size ) message-body
          message-last = ( %x00 short-size | %x02 long-size ) message-body
          message-body = *OCTET

分析三

1. plain_client_t和plain_server_t

 

 (1)用plain_XXX_t组替换null_mechanism_t会改变数据交互的handshake阶段。

    plain_client_t用户client端,plain_server_t用于server端。

 (2)Handshake阶段。

    client端发送如下数据(encode/decode部分略过):

   

 

   过程:    

     (1)client发送HELLO命令,带上自己的用户名”reader”和密码”secret”。

     (2)server接收到HELLO命令后,解析出client的用户名和密码,然后连接到ZAP服务端口进行验证。(请见下一节2. ZAP服务)

        如果验证成功,server端回复如下数据:

        

 

          server回复WELCOME命令。

      (3)client端再发送如下数据:

           

 

          client再发送INITIATE命令,带上Socket-Type类型”DEALER”,和自己的identity。这里的identity为空字符串。

      (4)server端回复如下数据:

          

 

          Server回复READY命令,带上Socket-Type类型”ROUTER”,和自己的identity。这里的identity为空字符串。

  

2. ZAP服务

   ZAP服务端口是公开端口,它绑定在inproc://zeromq.zap.01上。

   server端的plain_server_t接收到HELLO命令,得到client的用户名和密码。

   它会连接ZAP服务端口进行验证。plain_server_t发送如下消息组到ZAP服务端口:

   

 

   发给ZAP服务端口的消息组必须一个delimiter消息开头,它的内容为空。注意消息组包括用户名和密码。

   

   ZAP服务应该回复如下消息组:

   

 

   回复消息同样应该以delimiter消息开头。注意消息组包括status code,值200意味着认证成功。

 

 3. router_t

    

 

    router_t的关键在于要对所有接入的pipe编号,以便用户知道收到的数据从哪里来,发出的数据要发到哪里去。

    router_t要求engine在handshake后发送一个identity消息。

    在接入新的pipe时,router_t先接收这个identity消息,用identity标记这个pipe。

    消息中的identity可以为空,如果为空,router自动为pipe产生一个5字节的identity。

     pipe和它的identity保存在一个map中。

     
    
    接收消息时,router_t先取出pipe的identity返回给客户,再返回真正的消息。
    发送时,客户应该先写identity,router根据identity在map中找到对应的pipe,
    然后将后面的消息写入这个pipe。
    
     Router_t要求一组消息必须以一个delimiter消息开头。delimiter消息是内容为空的消息。
 
 
4. rep_t
  

 

 rep_t用于同步地给请求者回复。

 

 rep_t派生自router_t。如router_t一节所说,router_t的一组消息的开头是一个identity消息和一个delimeter消息。
 接收消息时,rep_t会调用router_t依次读取这两个消息并写回,这样发送的pipe也就确定了。
 router_t接着读取剩余的消息返回给客户。发送消息时,router_t调用router_t发送客户的消息。
 收到一个请求消息组后,客户应该同步发送一个回复消息组。这期间rep_t不允许客户接收新的消息组。
 
 
5. pair_t
   

 

  pair_t实际上不是真正的socket。它只是进程内部通信的一层包装。

   

   pair_t总是成对出现。一对pair_t应该绑定到同一个inproc的地址上,

   然后就可以开始互相收发消息了。Inproc的地址的格式如:inproc://XXX

  

分析四  

 The ZeroMQ Enterprise Messaging Broker
 https://github.com/zeromq/malamute

1. zactor_t

 


    (1)zactor_t是CZMQ提供的接口。CZMQ是对ZeroMQ的C接口包装。

    (2)zactor_t与shim_t总是成对出现。Zactor_t面向客户,shim_t在后台处理。

        创建zactor_t时,会同时创建shim_t和一个新的线程,shim_t在这个新线程中运行。

        aactor_t还会创建一对pair_t的socket,分别绑定到shim_t和它自己,以便两者通信。

    (3)客户可以通过zactor_new()指定shim_t运行的函数zactor_fn,从而处理真正的业务。

         typedef void (zactor_fn) (zsock_t* pipe, void* args);

         参数:第一个参数是shim_t的pair_t socket,用它可以接收来自客户的配置命令。

              第二个参数是客户通过zactor_new()指定的额外参数,

              这个参数一般被用来指定另外一个zsock_t。这样,

              zactor_t就有了两个zsock_t,一个用作配置命令,一个用作消息交互。

 
2. mlm_server (第一个zactor_fn)

 


   Malamute的server端的一个zactor_t以mlm_server作为zactor_fn。在mlm_server中,做如下事情:

     (1)创建一个rep_t类型的zsock_t,以便接收client的认证消息,并回复认证结果。

   (2)创建一个zloop_t,以便监听来自zactor_t的配置消息,和来自rep_t的client认证消息。
(3)zloop_t使用zpoller_t,但实际上内部还是使用poll机制。


3. zauth (Malamute的第二个zactor_fn)

 


    Malamute的server端如果需要校验client的身份,它还会创建一个zactor_t,将zauth作为zactor_fn。在zauth中,做如下事情:

    (1)创建一个rep_t类型的zsock_t,以便接收client的认证消息,并回复认证结果。

    (2)创建一个zloop_t,以便监听来自zactor_t的配置消息,和来自rep_t的client认证消息。

    (3)zloop_t使用zpoller_t,但实际上内部还是使用poll机制。

 

4. mlm_client (第三个zactor_fn)

   

 

  Malamute的client端的对外接口是mlm_client_t,它是zactor_t的包装。它以mlm_client作为zactor_fun。在mlm_client,做如下事情:

  (1)创建一对pair_t类型的zsock_t,分别绑定在mlm_client_t和s_client_t上,用于消息交互。

  (2)创建一个dealer_t类型的zsock_t,与pair_t类型的zsock_t以接力的方式,完成消息收发。

  (3)zactor_t创建的一对pair_t类型的zsock_t用户配置命令。

 

5. mlm_stream_simple (第四个zactor_fn)

  

 

  Malamute的server端为每条数据流创建一个stream_t。zactor_t以mlm_stream_simple为zactor_fun。在mlm_stream_simple中,做如下事情:

   (1)创建stream_engine_t

   (2)创建zpoller_t,以便监听来自zactor的配置命令和来自stream_t的消息。

   (3)当客户注册过滤条件是,stream_engine_t负责保存;当客户发送消息时,stream_engine_t负责根据过滤条件找到目标客户。

 

6. server端和client端的数据交互

   (1)client端创建mlm_client_t,并设置mechanism。如果是plain mechanism,则需要设置用户名和密码。它们会写入zsock_t的选项。

   (2)连接server端。如果plain mechanism,则自动发送用户密码完成认证。

   (3)在server端,zauth函数从rep_t读取认证消息,在本地用户列表中查询,如果用户密码匹配,则认证成功。zauth的用户密码信息是从本地配置文件读取的。

   (4)连接成功后, client端发送注册请求MLM_PROTO_CONNECTION_OPEN(message type=0x01)。

       注意reader是用户的名字。注意这里的“用户”跟zauth的“用户”不同。连接和认证是互相独立的。

       每个连接有自己的用户名,但多个连接可以使用同一个zauth的用户名进行认证。

   

 


     (4)在server端,mlm_server函数从router_t读取注册消息。

         因为router_t的消息组总是以identity消息开头,所以mlm_server可以根据identity查询本地用户列表。

         如果查询不到,则增加新条目。

     (5)然后mlm_server发送注册请求的结果MLM_PROTO_OK(message type=0x0f)。0xc8(200)代表成功。

  

   (6)如果注册成功,client端继续发送自己的过滤条件消息MLM_PROTO_STREAM_READ(message type=0x06)。

       stream是流名字,pattern是client要求的消息格式。

    

 

    (7)在server端,mlm_server函数从router_t读取过滤条件消息。

         mlm_server根据stream名字查找本地stream列表。

         如果查询不到,则增加新条目,并创建一个mlm_stream_simple线程处理该stream的过滤业务。

 


    (8)上表是subscriber客户的注册消息。

         下表是publisher客户的注册消息MLM_PROTO_STREAM_WRITE(message type=0x05):

         

 

 

 

 

 

学习来源: https://www.jianshu.com/p/facc6a2b42d5

                  https://www.jianshu.com/p/f7cc2fe841ed

                  https://www.jianshu.com/p/427aafef9176

                  https://www.jianshu.com/p/76e846844520

                  https://blog.csdn.net/mottohlm/article/details/80993481

                  https://blog.csdn.net/haizhongyun/article/details/7621478

                  https://blog.csdn.net/luzhensmart/article/details/81838193

                  //帧的概念

                  https://zhidao.baidu.com/question/342519627.html

                 //路由

                 https://zhidao.baidu.com/question/7754074.html?fr=iks&word=%C2%B7%D3%C9%BA%AC%D2%E5&ie=gbk

 

 

posted @ 2020-09-27 13:07  小窝蜗  阅读(1217)  评论(1编辑  收藏  举报