ODL Netconf底层连接机制实现

作者简介:陈卓文,国内某游戏公司私有云团队开发者,主要从事SDN/NFV开发。

本文实现分析基于ODL Netconf版本1.4.2;
读者约定:了解netconf协议;熟悉netty pipeline工作流;

背景

Netconf协议底层是通过SSH,TCP/SSL实现连接,本文探究ODL Netconf是如何实现底层连接。在清楚ODL Netconf底层连接实现后,面对当底层网络断开等情况下链接如何保持、ODL Netconf应用层能否感知链接中断等问题时,更加得心应手。

本文仅展开控制器作为client连接底层设备的实现,而不是call home连接的实现。

ODL Netconf与底层设备连接创建过程

NetconfTopology监听发起连接

NetconfTopology监听node yang树,当将node节点写入netconf-topology yang后,最终会触发AbstractNetconfTopology.setupConnection向底层设备发起连接:

向底层设备发起连接,底层相关逻辑封装在NetconfDeviceCommunicator.initializeRemoteConnection方法中:

假设第一次连接,会调用NetconfClientDispatcherImplcreateClient方法。

Netty创建Client及初始化

在NetconfClientDispatcherImpl中会根据不同的协议,使用TCP/SSH建立netconf连接:

在这里,我们仅跟进SSH链接,TCP读者自行分析。SSH client创建过程主要分为两步:

  • 会创建SshClientChannelInitializer对象,用于netty channel的相关初始化设置;
  • 调用AbstractDispatcher.createClient方法,调用netty发起client连接;

AbstractDispatcher.createClient主要完成几个事情:

  • 创建Bootstrap对象/设置workerGrop/设置channelFactory,都是用于netty相关连接动作;
  • Bootstrap对象设置了SO_KEEPALIVE,即channel设置了keepalive;
  • 设置channel的初始化时调用的Initializer:SshClientChannelInitializer
  • 调用p.connect开始连接底层device,实际上也是调用Bootstrap的connect方法,但是Netconf在ProtocolSessionPromise中封装了重试重连等操作;

实际上当执行p.connect方法后,首先会触发
SshClientChannelInitializer.initialize完成对channel的初始化操作,设置相关的netty pipeline:

SSH Session建立

当channel初始化完成后,再调用connect方法。根据netty调用链会在outbound handler中从last开始到first调用各个handler的connect方法。由于几个outbound handler只有在pipeline中第一个(first)的AsyncSshHandler对象override了connect方法,所以会调用其connect方法,其调用startSsh方法开始建立ssh channel

startSsh方法中会调用apache的SshClient.connect向底层设备发起ssh连接,并且异步监听,当连上ssh则调用handleSshSessionCreated方法:

以及后续的在handleSshSessionCreated中会进行ssh的认证,经过一些列交互及调用链,最终当ssh链接建立成功会调用handleSshChanelOpened方法!其做了几个核心工作:

  • 创建AsyncSshHandlerReader对象,其会监听ssh channel。
    • 收到底层设备通过ssh链接发送给控制器消息,让后通过调用ctx.fireChannelRead(msg)触发netty channel pipeline中的handler感知消息;
    • 当ssh链接异常情况,通过调用this.disconnect(ctx, ctx.newPromise())触发netty channel pipeline中的handler感知disconnect;
  • 创建AsyncSshHandlerWriter对象,供上层netconf通过ssh channel发送消息到底层设备
  • 当reader和writer都创建完成,调用ctx.fireChannelActive(),通知netty channel pipeline相关handler channelActive

同时关注一下AsyncSshHandler.write方法,实际上是调用了sshWriteAsyncHandler对象的write方法,而不是调用netty channel的write方法。

这里总结一下AsyncSshHandler做了几件事:

  • 处理到底层设备的链接。在AsyncSshHandler.connect方法中,一些列调用链中并没有调用ctx.connect方法或者super.connect方法,所以并不是通过netty与底层设备建立连接,而是直接如上面提及通过apache的sshClient与底层直接建立链接;
  • 监听ssh channel,读取ssh channel中的消息(设备发送),然后调用ctx.fireChannelRead方法,触发netty pipeline的inboundHandler相关channelRead方法;
  • 实现通过ssh channel发送消息给底层设备。实现netty outboundHandler的write方法,将从pipeline中传递发送的消息通过sshWriteAsyncHandler发送,实际上是做了一层代理/转化;
  • 当ssh channel断开,通知netty pipeline的inboundHandler ChannelInactive;

这里给出结论及思考:

  • ODL Netconf在ssh情况下,仅使用了netty channel pipeline来传递事件处理(使用inbound/outbound handler),但是实际上与设备建立的链接没有通过netty,而是直接通过apache.sshclient建立链接;
  • AsyncSshHandler作为第一个/first outboundHandler,完成apache sshclient与底层设备交互,并将相应交互(消息,channel down等)转换为netty channel事件,从而触发相关一些列handler处理;
  • 在ODL Netconf使用tcp情况下,直接使用netty channel与底层设备建立链接,并且是与ssh情况一样复用相关的netty channel pipeline来传递事件处理;
  • 所以ssh情况下,这样的实现也是为了更好的复用;

Netconf Session协商过程

netty channel建立成功

在channel初始化过程中(SshClientChannelInitializer.initialize),会创建NetconfClientSessionNegotiator对象并将其设置到pipeline:

NetconfClientSessionNegotiator作为inboundHandler,当上一步中AsyncSshHandler与底层设备建立好连接,最终调用了ctx.fireChannelActive()方法,这样会触发NetconfClientSessionNegotiator.channelActive方法,然后开始协商Negotiation!

netconf session开始协商

在startNegotiation中,会判断Netconf链接底层使用SSH还是TCP/SSL。

  • 如果使用TCP/SSL,首先会监听SSL的handshakeFuture!当SSL握手完成后,才开始Netconf session协商。这里我们可以看出底层链接的三层建立连接顺序关系:底层的netty channel active, ssl以及netconf session。
  • 如果是使用SSH,AsyncSshHandler已经建立连接下,才会触发chanelActive,所以可以直接开始Netconf session协商。

调用的start方法开始Netconf协商,协商过程主要分为两步:

  • 控制器作为client主动发送hello消息;
  • 底层设备回复hello消息;

1、ODL Netconf发送netconf hello消息
ODL netconf作为client主动发起hello包协商过程,主要逻辑实现在AbstractNetconfSessionNegotiator.start方法中:

  • 发送NetconfHelloMessage消息给底层设备,修改netconf session状态为OPEN_WAIT;
  • 发送helloMessage后,pipeline中NETCONF_MESSAGE_ENCODER将NetconfHelloMessageToXMLEncoder替换为NetconfMessageToXMLEncoder;
    • 在初始化channel过程,encoder设置的是NetconfHelloMessageToXMLEncoder
  • 等待一段时间,检查session状态是否为ESTABLISHED做相关处理,注意状态修改为ESTABLISHED在另外的异步过程处理;

2、收到底层设备恢复hello包
当channel收到底层回复消息,触发NetconfClientSessionNegotiator.channelRead读取channel中消息并将其转型为NetconfHelloMessage,然后调用handleMessage方法处理。

NetconfClientSessionNegotiator.handleMessage中处理收到底层的helloMessage:

  • 1.收到底层回复helloMessage说明协商完成,修改状态EST, 并创建NetconfClientSession对象;
  • 2.如果支持exi则再进行exi协商;
  • 3.无论如何最终协商完成调用negotiationSuccessful方法;

netconf session协商完成

收到底层设备回应NetconfHelloMessage(会带有capability)说明协商基本完成,如果支持扩展exi则再进行exi协商,无论如何都会调用NetconfClientSessionNegotiator.negotiationSuccessful方法:将自身在pipeline中替换为NetconfClientSession对象。

最终pipeline的handler情况如下:

netconf session建立触发上层

当netconf session协商完成,将NetconfClientSession加入到netty pipeline中,会触发NetconfClientSession.handlerAdded方法。意思是当前对象加入到pipeline说明session建立完成,调用sessionUp方法。

最终sessionUp方法会调用NetconfDeviceCommunicator.sessionUp方法,进而在ODL Netconf上层逻辑层感知。

值得关注的是NetconfClientSession作为channel的SimpleChannelInboundHandler,其感知底层设备发送给控制器的消息,同时其也感知channel对象可以直接发送消息给底层设备。所以,NetconfClientSession在ODL Netconf中封装与底层设备之间的channel。

NetconfClientSession封装channel操作

既然NetconfClientSession那么重要,我们着重分析一下关于channel的几个行为。

一、Netconf session up
如上述,当NetconfClientSession添加到pipeline,触发其handlerAdded方法,最终调用NetconfDeviceCommunicator.sessionUp方法。

二、Netconf session down/channel inactive
当channelInactive时,触发其channelInactive方法,最终调用NetconfDeviceCommunicator.sessionDown方法。

三、收到底层设备消息
当收到底层设备发送给控制器消息,触发其channelRead0方法,最终调用NetconfDeviceCommunicator.onMessage方法。

四、发送消息给设备
当我们请求底层设备,发起rpc请求,是通过NetconfDeviceRpc.invokeRpc方法,其实际上调用NetconfDeviceCommunicator.sendRequest方法,在NetconfDeviceCommunicator中会有相应的锁等操作并最终调用NetconfClientSession对象的sendMessage方法。

NetconfClientSession.sendMessage中,会调用channel EventLoop并向chanel写入并flush消息。所以,上层应用发起rpc请求最终都是通过NetconfClientSession对象将消息通过channel发送给底层设备。

总结:
所以说NetconfClientSession封装了与底层设备channel相关的操作。

关闭连接回收相关资源过程

连接是双向的,以控制器主动关闭连接为主动过程,反过来底层设备断开连接触发控制器回收资源为被动过程。

被动关闭过程

ssh channel底层异常处理主要体现在AsyncSshHandlerReader.operationComplete方法中:

当有两种情况会触发AsyncSshHandlerReader.operationComplete方法

  • 监听底层设备通过ssh channel发往控制器的消息
    底层设备发消息给控制器,这里监听了read方法,会触发operationComplete方法;
  • 当控制器通过ssh channel写入消息发送给设备,底层链接异常情况下610s后会触发,并且是future.getException() != null

当触发AsyncSshHandlerReader.operationComplete方法且带有异常情况下,最终会调用AsyncSshHandler.disconnect方法:

  • 这里回收ssh channel相关资源;
  • 触发netty channel inactive事件,触发回收ODL Netconf逻辑层相关资源;

当触发netty channel inactive事件,经过netty扭转会触发
NetconfClientSession.channelInactive方法:

然后调用endOfInput方法:在此方法中会触发NetconfDeviceCommunicator.onSessionDown方法

NetconfDeviceCommunicator.onSessionDown中会回收逻辑层相关资源:

主动关闭过程

当调用NetconfDeviceCommunicator.disconnect方法主动关闭连接(其调用NetconfClientSession.close方法,或者直接调用NetconfClientSession.close方法主动关闭连接),具体过程如下:

NetconfClientSession.close方法执行逻辑:

  • 调用netty channel close方法,其触发outboundhandler的close方法回收底层连接相关资源对象;
  • 设置session up状态为false;
  • 调用NetconfDeviceCommunicator.onSessionTerminated方法,回收逻辑层相关资源对象;

调用netty channel close方法,最终会触发AsyncSshHandlerclose方法:其调用disconnect方法,与被动触发断开连接过程一致。

同时调用NetconfDeviceCommunicator.onSessionTerminated方法,从方法名也能看出是主动关闭连接,其调用tearDown方法回收上层对象。

TL;DR

上文给出大量实现细节,这里做出总结:

  • ODL Netconf底层为连接实现了两套SSH, TCP/SSL,都通过netty pipeline的线程模型,处理流来实现事件处理及扭转,但是SSH情况下连接直接通过apache sshclient跟底层交互,而不是使用netty与底层连接,其实现了AsyncSshHandler类来完成SSH与netty channle pipeline的转换;
  • 连接的创建(TCP, SSH)、netconf session的协商、最底层消息读取/写入都是通过netty pipeline的inbound/outbound handler来实现,通过NetconfClientSession类封装与底层设备之间的channel的交互(可能是SSH channel,可能是TCP/SSL channel);
  • NetconfClientSession之上再封装了NetconfDeviceCommunicator对象,其实现了请求排队/锁等机制,在ODL Netconf逻辑代码之中直接面对的是NetconfDeviceCommunicator对象;


  • 本站原创文章仅代表作者观点,不代表SDNLAB立场。所有原创内容版权均属SDNLAB,欢迎大家转发分享。但未经授权,严禁任何媒体(平面媒体、网络媒体、自媒体等)以及微信公众号复制、转载、摘编或以其他方式进行使用,转载须注明来自 SDNLAB并附上本文链接。 本站中所有编译类文章仅用于学习和交流目的,编译工作遵照 CC 协议,如果有侵犯到您权益的地方,请及时联系我们。
  • 本文链接https://www.sdnlab.com/22997.html
分享到:
相关文章
条评论

登录后才可以评论

陈卓文 发表于19-02-26
3