• 产品与解决方案
  • 行业解决方案
  • 服务
  • 支持
  • 合作伙伴
  • 新华三人才研学中心
  • 关于我们

Punching的实现与应用

Hole Punching技术是一种借助于公网上的服务器完成NAT穿越的解决方案,一开始单为解决UDPNAT穿越而提出,但是在某些条件(主要是操作系统支持)满足的情况下,Hole Punching技术同样适用于TCPNAT穿越。相对其他NAT穿越方案来说,Hole Punching技术显得比较通用和简单。

1      UDP Hole Punching

在基于UDPHole Punching场景中,终端AB分别与公网上的服务器S建立UDP连接。当一个终端向服务器S注册时,服务器S记录下该终端的两对IP地址和端口信息,为描述方便,我们将一对IP地址和端口信息的组合称之为一个endpoint。一个endpoint是终端发起与服务器S通信的IP地址和端口;另一个endpoint是服务器S观察到的该终端实际与自己通信所用的IP地址和端口。我们可以把前一个endpoint看作是终端的私网IP地址和私网端口;把后一个endpoint看作是终端的私网IP地址和端口经过NAT转换后的公网IP地址和公网端口。服务器S可以从终端的注册报文中得到该终端的私网endpoint相关信息,可以通过对注册报文的源IP地址和UDP源端口字段获得该终端的公网endpoint。如果终端不是位于NAT设备后面,那么采用上述方法得到的两个endpoint应该是完全相同的。

也有一些的NAT设备会扫描UDP报文的数据字段,寻找4字节的位域,将看上去很像IP地址的位域,改为与IP头一样的地址。为了避免这种行为的NAT设备对UDP报文数据的修改,应用程序可以采用直接对IP地址的值进行加密的方式骗过NAT设备的检查。

当终端A希望与终端B建立连接时,Hole punching过程如下所示:

1)终端A最初并不知道如何向B发起连接,于是终端A向服务器S发送报文,请求服务器S帮助建立与终端BUDP连接。

2)服务器S将含有终端B的公网及私网的endpoint发给终端A,同时,服务器S将含有终端A的公网及私网的endpoint的用于请求连接的报文也发给B。一旦这些报文都顺利达到,终端A与终端B都知道了对方的公网和私网的endpoint

3)当终端A收到服务器S返回的包含终端B的公网和私网的endpoint的报文后,终端A开始分别向这些终端Bendpoint发送UDP报文,并且终端A会自动锁定第一个给出响应的终端Bendpoint。同理,当终端B收到服务器S发送的包含终端A的公网和私网的endpoint的报文后,也会开始向终端A的公网和私网的endpoint发送UDP报文,并且自动锁定第一个得到终端A的回应的endpoint。由于终端AB的互相向对方发送UDP报文的操作是异步的,所以终端AB发送报文的时间先后没有严格的时序要求。

下面我们就来看一下这三个角色之间是如果进行UDP Hole punching的。在这里,我们分为三种具体情景来讨论:第一种也是最简单的一种情景,两个终端都位于同一个NAT设备后面,位于同一个内网中;第二种也是最普遍的一种情景,两个终端分别位于不同的NAT设备后面,分属不同的内网;第三种是终端位于两层NAT设备之后,通常最上层的NAT是由ISP提供,第二层的NAT是家用的NAT路由器之类的设备。

通常情况下由应用程序自身确定的网络物理层连接方式是很困难的,有时甚至是不可能的,即使是上述的若干种情景下可以穿越NAT,也只是代表在一定时期内有效,而不是永久有效的。诸如STUN之类的网络协议或许可以提供必要的NAT信息,但在遇到多层NAT设备的时候,通常这些信息也不是完全完整和有效的。尽管如此,只要NAT设备的响应是合理的,在通常情况下Hole punching技术还是能够在应用程序对网络状况一无所知的前提下自动适用于多数场合。(合理NAT响应将在第3章中详细讨论)

1.1      终端位于同一个NAT设备后面

首先假设两个终端位于同一个NAT设备后面,并且位于相同的内网(相同的私有IP地址域)如图13所示。终端A与服务器S建立了UDP连接,经过NAT转换后,终端A的公网端口被映射为2000。终端B同样与服务器S建立了UDP连接,公网端口映射为2010

1 终端位于同一个NAT后面图, Hole punching

 

2 终端位于同一个NAT后面图, Hole punching

 

3 终端位于同一个NAT后面图, Hole punching

 

假设终端A想通过服务器S,利用Hole punching技术与终端B建立连接。终端A向服务器S发出请求报文与终端B进行连接。服务器S将终端B的公网和私网endpoint信息发给终端A,同时也把终端A的公网和私网的endpoint发给终端B。由终端AB发往对方公网endpointUDP报文能否被对方收到,这取决于当前的NAT设备是否支持回环转换(hairpin translation,详见1.3节)。但是终端AB往对端私网endpoint发送的UDP报文是一定可以到达的,无论如何,私网报文采用最短转发路径,要比经过NAT转换来的快。终端AB有很大的可能性采用私网的endpoint进行常规的通信。

假定NAT设备支持回环转换,应用程序也忽略私网endpoint间的连接,那么终端AB会采用公网endpoint作为通信的连接,这势必会造成数据报文不必要的经过NAT设备,这是一种对资源的浪费。就目前的网络情况而言,应用程序最好还是把公网和私网endpoint都实验一下。

1.2      终端位于不同的NAT设备后面

假定终端AB在不同的NAT设备后面,分属不同的内网,如图4-6所示。终端AB都经由各自的NAT设备与服务器S建立了UDP连接,AB的本地私网端口号均为1000,服务器S的公网端口号为9000。在NAT设备的映射关系中,终端A的公网IP被映射为100.0.0.1,公网端口为2000,终端B的公网IP被映射为101.0.0.1,公网端口为2000

如下所示:终端A-->本地私网IP10.0.0.1,本地私网端口:1000,公网IP100.0.0.1,公网端口:2000;终端B-->本地私网IP11.0.0.1,本地私网端口:1000,公网IP101.0.0.1,公网端口:2000

4 终端位于不同的NAT后面图, Hole punching

 

5 终端位于不同的NAT后面图, Hole punching

 

6 终端位于不同的NAT后面图, Hole punching

 

在终端A向服务器S发送的注册报文中,会包含终端A的私网endpoint信息,即10.0.0.1:1000;服务器S会记录下终端A的私网endpoint信息,同时会把自己观察到的终端A的公网endpoint记录下来,即100.0.0.1:2000。同理,服务器S会记录下终端B的私网endpoint信息,11.0.0.1:1000和由服务器S观察到的终端B的公网endpoint101.0.0.1:2000。无论终端AB二者任何一方向服务器S发送连接请求,服务器都会将其记录下来的上述的公网、私网endpoint信息发送给对方。

由于终端AB分属不同的内网,它们彼此的私网endpoint无法在公网中路由,所以发往各自私网endpointUDP报文会被发送到错误的主机或者根本不存在的主机。因此应用程序对于收到的报文必须经过授权和过滤,例如,可以在报文中加入对方的程序名称、加密算法,或者至少是一个双方都从服务器S上的预先得到的随机数字。

现在假定终端A的第一个报文将发往终端B的公网endpoint,如图5所示。该消息途经终端ANAT设备,并在该设备上生成了一个映射。新的映射源endpoint10.0.0.1:1000,该映射关系和终端A与服务器S的建立连接的时候NAT生成的映射关系的源endpoint是一样,但它的目的endpoint是不同。如果终端ANAT设备给出的响应是友好的,那么终端ANAT设备将保留终端A的私网endpoint,并且所有来自终端A的私网源endpoint10.0.0.1:1000)的数据报文都沿用终端A与服务器S事先建立起来的映射关系,公网endpoint均为(100.0.0.1:2000)。终端A向终端B的公网endpoint发送消息的过程就是打洞的过程,从终端A的内网的角度来看应为从(10.0.0.1:1000)发往(101.0.0.1:2000,从终端A的在其NAT设备上建立的映射来看,是从(100.0.0.1:2000)发到(101.0.0.1:2000)。

如果终端A发给终端B的公网endpoint的报文在终端B向终端A发送报文之前到达终端BNAT设备,终端BNAT会认为终端A发过来的报文是未经授权的公网报文,会丢弃掉该报文。终端B发往终端A的报文跟上述的过程一样,会在终端BNAT上建立一个(11.0.0.1:1000100.0.0.1:2000)的映射关系(通常也会沿用终端B与服务器S连接时建立的映射,只是该映射现在不光可以接受由服务器S发给终端B的报文,还可以接受从终端ANAT设备-100.0.0.12000发来的报文)。

一旦终端A与终端B都向对方的NAT设备在公网上的endpoint发送了报文,就打开了终端A与终端B之间的,终端A与终端B向对方的公网endpoint发送数据报文,等效为向对方的客户端直接发送UDP数据报文了。一旦应用程序确认已经可以通过往对方的公网endpoint发送数据报文的方式让数据报文到达NAT设备后面的目的应用程序,程序会自动停止继续发送用于“Hole punching”的数据报文,转而开始真正的数据通信。

1.3      终端位于多层NAT设备后面

有的网络拓扑结构包含了多个NAT设备,如果没有掌握该拓扑结构的详细信息,两个终端之间是无法建立最优化的点对点路由的。现在我们来讨论最后一种情况,如图7-9所示。假定NAT C是由ISP(Internet Service Provider)提供的工业级的NAT设备,NAT C提供将多个下属的用户NAT或用户节点映射到有限的几个公网IP的服务,NAT ANAT B作为NAT C的内网节点将把用户的家庭网络或内部网络接入NAT C的内网,然后用户的内部网络就可以经由NAT C访问公网了。从这种拓扑结构上来看,只有服务器SNAT C是真正拥有公网可路由IP地址的设备,而NAT ANAT B所使用的公网”IP地址,实际上是由ISP服务提供商设定的(相对于NAT C而言)私网地址(本文的后续部分,把这个由ISP提供的私网地址相对于NAT ANAT B称之为公网地址),同理隶属于NAT ANAT B的客户端,相对与NAT ANAT B而言,它们处于NAT ANAT B的内网,以此类推,客户端可以放到多层NAT设备后面。客户端A和客户端B发起对服务器S的连接的时候,就会依次在NAT ANAT B上建立向外的映射关系,而NAT ANAT B要联入公网的时候,会在NAT C上再建立向外的映射关系。

7 终端位于多层NAT后面图, Hole punching

 

8 终端位于多层NAT后面图, Hole punching

 

9 终端位于多层NAT后面图, Hole punching

 

现在假定终端AB希望通过UDP Hole punching完成两个终端间的P2P直连。最优化的路由策略是终端A向终端B伪公网”IP上发送数据包,即ISP服务提供商指定的私网IPNAT B公网endpoint101.0.0.1:2000。由于从服务器S的角度只能观察到真正的公网地址,也就是NAT ANAT BNAT C建立的映射时的真正的公网endpoint110.0.0.1:3000以及110.0.0.1:3010,所以非常不幸,终端AB是无法通过服务器S知道这些公网的endpoint。而且即使终端AB通过某种手段可以得到NAT ANAT B公网endpoint,我们仍然不建议采用上述的最优化的打洞方式,这是因为这些地址是由ISP服务提供商提供的,或许会存在与客户端本身所在的私网地址重复的可能性。(例如:NAT A的私网IP地址域恰好与NAT ANAT C公网IP地址域重复,这样就会导致打洞报文无法发出的问题)

因此终端别无选择,只能使用由公网服务器S观察到的终端AB的公网地址和端口进行打洞操作,用于打洞的报文将由NAT C进行转发,这里NAT C是否支持回环转换非常重要,否则数据包将无法由NAT C转发给NAT ANAT B,进而无法到达终端AB。当终端AB的公网地址(110.0.0.1:3010)发送UDP数据报文的时候,NAT A首先把数据包的源地址由A的私网endpoint10.0.0.1:1000)转换为公网endpoint100.0.0.1:2000),现在报文到了NAT CNAT C应该可以识别出来该数据包是要发往自身转换过的公网endpoint,如果NAT C可以给出合理响应的话,NAT C将把该数据包的源endpoint改为110.0.0.1:3000,目的endpoint改为101.0.0.1:2000,即NAT B公网endpointNAT B最后会将收到的报文发往终端B。同样,由终端B发往A的数据包也会经过类似的过程。也有很多NAT设备不支持类似这样的回环转换,但是已经有越来越多的NAT设备生产厂商开始加入对该转换的支持。

1.4      UDP在空闲状态下的超时问题

由于UDP转换协议提供的不是绝对可靠的,多数NAT设备内部都有一个UDP转换的空闲状态计时器,如果在一段时间内没有UDP数据通信,NAT设备会关掉由打洞操作打出来的,作为应用程序来讲如果想要做到与设备无关,就最好在穿越NAT后设定一个穿越的有效期。这个有效期与NAT设备内部的配置有关,目前没有标准有效期,最短的只有20秒左右。在这个有效期内,即使没有P2P数据报文需要传输,应用程序为了维持该可以正常工作,也必须向对方发送打洞维持报文。这个维持报文是需要双方应用都发送的,只有一方发送不会维持另一方的映射关系正常工作。除了频繁发送打洞维持报文以外,还有一个方法就是在当前的有效期过期之前,P2P客户端双方重新打洞,丢弃原有的,这也不失为一个有效的方法。

2      TCP Hole Punching

建立穿越NAT设备的P2PTCP连接只比UDP复杂一点点,TCP Hole punching从协议层来看是与UDP Hole punching过程非常相似的。尽管如此,基于TCP Hole punching至今为止还没有被很好的理解,这也造成了对其提供支持的NAT设备不是很多。在NAT设备支持的前提下,基于TCP Hole punching技术实际上与基于UDP Hole punching技术一样快捷、可靠。实际上,只要NAT设备支持的话,基于TCPP2P技术的健壮性将比基于UDP的技术的更强一些,因为TCP协议的状态机给出了一种标准的方法来精确的获取某个TCP映射关系的生命期,而UDP协议则无法做到这一点。

2.1      套接字和TCP端口的重用实现

基于TCP Hole punching过程中,最主要的问题不是来自于TCP协议,而是来自于应用程序的API接口。这是由于标准的伯克利(Berkeley)套接字的API是围绕着构建客户端/服务器程序而设计的,API允许TCP流套接字通过调用connect()函数来建立向外的连接,或者通过listen()accept()函数接受来自外部的连接,但是,API不提供类似UDP那样的,同一个端口既可以向外连接,又能够接受来自外部的连接。而且更糟的是,TCP的套接字通常仅允许建立11的响应,即应用程序在将一个套接字绑定到本地的一个端口以后,任何试图将第二个套接字绑定到该端口的操作都会失败。为了让TCP Hole punching能够顺利工作,我们需要使用一个本地的TCP端口来监听来自外部的TCP连接,同时建立多个向外的TCP连接。幸运的是,所有的主流操作系统都能够支持特殊的TCP套接字参数,通常叫做“SO_REUSEADDR”,该参数允许应用程序将多个套接字绑定到本地的一个IP地址和端口(只要所有要绑定的套接字都设置了SO_REUSEADDR参数即可)。BSD(Berkeley Software Distribution,伯克利软件套件)系统引入了SO_REUSEPORT参数,该参数用于区分端口重用还是地址重用,在这样的系统里面,上述所有的参数必须都设置才行。

2.2      打开P2PTCP

假定终端A希望建立与终端BTCP连接。我们像通常一样假定终端AB已经与公网上的已知服务器S建立了TCP连接。服务器记录下来每个注册的客户端的公网和私网的endpoint,如同为UDP服务的时候一样。从协议层来看,TCP Hole punchingUDP Hole punching是几乎完全相同的过程。

1)终端A使用其与服务器S的连接向服务器发送请求,要求服务器S协助其连接终端B

2)服务器S将终端B的公网和私网endpoint返回给终端A,同时,服务器S将终端A的公网和私网endpoint发送给终端B

3)客户端AB使用连接服务器STCP端口异步地发起向对方的公网、私网endpointTCP连接,同时监听各自的本地TCP端口是否有外部的连接联入。

4)终端AB开始等待向外的连接是否成功,检查是否有新连接联入。如果向外的连接由于某种网络错误而失败,如:连接被重置或者节点无法访问,终端只需要延迟一小段时间(例如延迟一秒钟),然后重新发起连接即可,延迟的时间和重复连接的次数可以由应用程序编写者来确定。

5)TCP连接建立起来以后,终端之间应该开始鉴权操作,确保目前联入的连接就是所希望的连接。如果鉴权失败,终端将关闭连接,并且继续等待新的连接联入。终端通常采用先入为主的策略,只接受第一个通过鉴权操作的终端,然后将进入通信过程不再继续等待是否有新的连接联入。

10 TCP Hole Punching

UDP不同的是,使用UDP协议的每个终端只需要一个套接字即可完成与服务器S通信,并同时与多个P2P终端通信的任务,而TCP终端必须解决多个套接字绑定到同一个本地TCP端口的问题,如图10所示。现在来看更加实际的一种情景,终端AB分别位于不同的NAT设备后面,如图5所示,并且假定图中的端口号是TCP协议的端口号,而不是UDP的端口号。图中向外的连接代表终端AB向对方的私网endpoint发起的连接,这些连接或许会失败或者无法连接到对方。如同使用UDP Hole Punching遇到的问题一样,TCP Hole Punching也会遇到私网IP公网IP重复造成连接失败或者错误连接之类的问题。客户端向彼此公网endpoint发起连接的操作,会使得各自的NAT设备打开新的允许终端ABTCP数据通过。如果NAT设备支持TCP“打洞操作的话,一个在终端之间的基于TCP协议的流通道就会自动建立起来。如果终端AB发送的第一个SYN报文发到了终端BNAT设备,而终端B在此前没有向终端A发送SYN报文,终端BNAT设备会丢弃这个报文,这会引起终端A连接失败无法连接问题。而此时,由于终端A已经向终端B发送过SYN报文,终端B发往终端ASYN报文将被看作是由终端A发往终端B的报文的回应的一部分,所以终端B发往终端ASYN报文会顺利地通过终端ANAT设备,到达终端A,从而建立起终端ABP2P连接。

2.3      从应用程序的角度来看TCP Hole punching

从应用程序的角度来看,在进行TCP Hole punching的时候都发生了什么呢?假定终端A首先向终端B发出SYN报文,该报文发往终端B的公网地址,并且被终端BNAT设备丢弃,但是终端B发往终端A的公网地址的SYN报文则通过终端ANAT到达了终端A,然后,会发生以下的两种结果中的一种,具体是哪一种取决于操作系统对TCP协议的实现:

1)终端ATCP实现会发现收到的SYN报文就是其发起连接并希望联入的终端BSYN报文,通俗一点来说就是说曹操,曹操到的意思,本来终端A要去找终端B,结果终端B自己找上门来了。终端A的程序调用的异步connect()函数将成功返回,终端Alisten()等待从外部联入的函数将没有任何反映。此时,终端B联入终端A的操作在终端A的程序内部被理解为终端A联入终端B连接成功,并且终端A开始使用这个连接与B开始P2P通信。由于收到的SYN报文中不包含终端A需要的ACK数据,因此,终端ATCP将用SYN-ACK报文回应终端B的公网endpoint,并且将使用先前终端A发向终端BSYN报文一样的序列号。一旦终端BTCP收到由终端A发来的SYN-ACK报文,则把自己的ACK报文发给终端A,然后两端建立起TCP连接。简单的说,第一种,就是即使终端A发往终端BSYN报文被终端BNAT丢弃了,但是由于终端B发往终端A的报文到达了终端A。结果是,终端A认为自己连接成功了,终端B也认为自己连接成功了,不管是谁成功了,总之连接是已经建立起来了。

2)另外一种结果是,终端ATCP实现没有像1).中所讲的那么智能,它没有发现现在联入的终端B就是自己希望联入的。就好比在机场接人,明明遇到了自己想要接的人却不认识,误认为是其它的人,安排别人给接走了,后来才知道是自己错过了机会,但是无论如何,人已经接到了任务已经完成了。然后,A通过常规的listen()函数和accept()函数得到与B的连接,而由A发起的向B的公网地址的连接会以失败告终。尽管终端AB的连接失败,终端A仍然得到了终端B发起的向A的连接,等效于终端AB之间已经联通,不管中间过程如何,终端AB已经连接起来了,结果是终端AB的基于TCP协议的P2P连接已经建立起来了。

第一种结果适用于基于BSD的操作系统对于TCP的实现,而第二种结果更加普遍一些,多数LinuxWindows系统都会按照第二种结果来处理。

2.4      TCP同时打开

假定各终端的TCP连接启动时间比较巧合,使得他们各自发送的SYN报文,在到达对方的NAT设备之前,对方的SYN报文都已经穿越NAT设备,并在NAT设备上生成TCP映射关系。在这种幸运的情况下,NAT设备不会拒绝SYN报文,双方的SYN报文都能通过终端间的NAT设备,到达对方。此时,终端会发现TCP连接同时打开,每个终端的TCP返回SYN-ACK报文,报文的SYN部分必须与之前发送的SYN报文一样,而ACK部分告知对方到达的SYN信息。

在这种情况下,应用程序的实现依赖于TCP的实现。如果双方终端按照2.3节描述第二种行为执行,可能最终程序所有的异步connect()呼叫都会失败,但程序依然会收到一个新的、有效地P2P TCP流套接字accept()函数。 由于应用程序并不关心是否它最终收到的P2P TCP套接字的connect()accept(),会导致有效地流可在任何依据RFC793标准的TCP上传输。

2.5      有序的Hole Punching

在这种变化的TCP Hole punching流程中,终端倾向于采用异步的方式进行连接,比如:(1)终端A通过服务器S告知终端B,需要建立一个连接,同时终端A并没有侦听本地端口;(2)终端B尝试向A发起连接,并在NAT设备打洞,但是由于超时、接收到终端ANAT设备回应的RST报文或者终端A返回的RST而导致连接失败;(3)终端B关闭与服务器S的连接,并用该TCP端口进行侦听;(4)服务器S依次也关闭与终端A的连接,终端A再尝试用原端口直接与终端B建立连接。

这种序贯程序尤其被用于安装Windows XP SP2操作系统之前的主机,这些主机不能正确处理TCP同时开启,或者TCP套接字不支持SO_REUSEADDR的参数。这种序贯程序存在更多的不确定性,可能会使通信启动更慢或不稳定。

3      P2P友好NAT

本节主要描述了能支持上文涉及的Hole punching技术的关键特性,并不是所有当前的NAT能支持这些特性,虽然大部分可以,同时NAT设备商会因市场的需要,而提供能更好支持P2P的设备。

3.1      一致的地址转换

本文描述的Hole punching技术只有当NAT设备始终将TCP或者UDP私网源endpoint一致的映射到一个相对应的公网endpoint时,才能运行正常。在RFC3489中,称该类NAT设备为cone类型NAT,即NAT设备强制将所有来自同一个私网endpoint映射到一个相同的公网endpoint

考虑如上文图46描述的例子中,当终端A初始连接服务器S时,NAT A分配100.0.0.1:2000来映射A的私网endpoint10.0.0.1:1000,当终端A尝试与终端B建立连接时,采用相同的本地私网endpoint作为源IP地址和端口,终端B的公网endpoint作为目的IP地址和端口。终端A需要依靠NAT A保留私网地址的标识,并需要重新使用存在的公网endpoint100.0.0.1:2000,因为该endpoint也是终端B发报文给终端A的目的endpoint

对于symmetric类型NAT设备,由于NAT上分配用来连接服务器和对端设备的端口不一致,而导致通信失败。大部分symmetric类型NAT设备,会采用公平地、可预期的方式给连续的映射关系分配端口。对于这种情况,可以采用一种变化的Hole punching算法,通过预测NAT设备分配给P2P连接的公网端口,可以解决该类NAT设备的穿越问题。但是由于分配的端口是一个动态的目标,有时候也会有错误发生,比如已经被其他映射关系占用了预期的端口等。因为symmetric类型NAT设备并不比cone类型NAT设备更加安全,设备商为了支持P2P协议而更少采用symmetric类型NAT

3.2      处理主动发起的TCP连接

NAT设备公网侧收到一个SYN报文,而没有对应的映射关系时,NAT设备会悄悄的丢弃SYN报文,这很重要。一些NAT设备会采用返回TCP RST报文,甚至ICMP错误报告报文替代丢弃行为,这样会影响TCP Hole punching流程,如2.2节的第4步所描述,会导致终端花费更长时间完成打洞。

3.3      保持载荷独立

少数NAT设备会去扫描报文载荷,获取4byte类似IP地址的信息,然后用报文IP头中的地址信息去替换这4byte字段。这种不好的处理方式会使得通信不正确,应用程序可以简单的保护他们发送报文中的IP地址信息,比如将IP地址采用二进制反码表示。

3.4      回环转换

一些多层NAT应用中,需要回环转换使得Hole punching能正常工作,如1.3节所描述的NAT C必须支持回环转换。

 

感谢您对本刊物的关注,如果您在阅读时有何感想,请点击反馈。
新华三官网
联系我们