前面的一篇文章分析了ZeroMQ中最为简单Socket类型,Dealer。。不过觉得这种具体的Socket类型的分析可以留到以后,或者等以后什么时候会用到了再分析再不迟。。。。
但是作为一个消息通信的框架,最重要的还是通信的可靠性,而这其中最最重要的就是连接断开之后的重连接机制。。。
在看具体的重连接机制之前,先来看看ZeroMQ中如何主动的建立于远程的连接吧,先来看看SocketBase中定义的connect方法:
//与远程地址建立连接 public boolean connect (String addr_) { if (ctx_terminated) { throw new ZError.CtxTerminatedException(); } // Process pending commands, if any. boolean brc = process_commands (0, false); if (!brc) return false; // Parse addr_ string. URI uri; try { uri = new URI(addr_); //构建URI对象 } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } String protocol = uri.getScheme(); //获取协议类型 String address = uri.getAuthority(); String path = uri.getPath(); if (address == null) address = path; check_protocol (protocol); //检查是否是合格的协议类型 if (protocol.equals("inproc")) { //如果是进程内部的通信 // TODO: inproc connect is specific with respect to creating pipes // as there's no 'reconnect' functionality implemented. Once that // is in place we should follow generic pipe creation algorithm. // Find the peer endpoint. Ctx.Endpoint peer = find_endpoint (addr_); if (peer.socket == null) return false; // The total HWM for an inproc connection should be the sum of // the binder's HWM and the connector's HWM. int sndhwm = 0; if (options.sndhwm != 0 && peer.options.rcvhwm != 0) sndhwm = options.sndhwm + peer.options.rcvhwm; int rcvhwm = 0; if (options.rcvhwm != 0 && peer.options.sndhwm != 0) rcvhwm = options.rcvhwm + peer.options.sndhwm; // Create a bi-directional pipe to connect the peers. ZObject[] parents = {this, peer.socket}; Pipe[] pipes = {null, null}; int[] hwms = {sndhwm, rcvhwm}; boolean[] delays = {options.delay_on_disconnect, options.delay_on_close}; Pipe.pipepair (parents, pipes, hwms, delays); // Attach local end of the pipe to this socket object. attach_pipe (pipes [0]); // If required, send the identity of the peer to the local socket. if (peer.options.recv_identity) { Msg id = new Msg (options.identity_size); id.put (options.identity, 0 , options.identity_size); id.set_flags (Msg.identity); boolean written = pipes [0].write (id); assert (written); pipes [0].flush (); } // If required, send the identity of the local socket to the peer. if (options.recv_identity) { Msg id = new Msg (peer.options.identity_size); id.put (peer.options.identity, 0 , peer.options.identity_size); id.set_flags (Msg.identity); boolean written = pipes [1].write (id); assert (written); pipes [1].flush (); } // Attach remote end of the pipe to the peer socket. Note that peer's // seqnum was incremented in find_endpoint function. We don't need it // increased here. send_bind (peer.socket, pipes [1], false); // Save last endpoint URI options.last_endpoint = addr_; // remember inproc connections for disconnect inprocs.put(addr_, pipes[0]); return true; } //选择一个比较IO线程,用于部署待会将会创建爱的session IOThread io_thread = choose_io_thread (options.affinity); if (io_thread == null) { throw new IllegalStateException("Empty IO Thread"); } //创建address对象 Address paddr = new Address (protocol, address); if (protocol.equals("tcp")) { //如果是tcp的话 paddr.resolved( new TcpAddress () ); paddr.resolved().resolve ( address, options.ipv4only != 0 ? true : false); } else if(protocol.equals("ipc")) { //进程间通信 paddr.resolved( new IpcAddress () ); paddr.resolved().resolve (address, true); } // Create session. //创建session,第一参数是当前session将会依附的IO线程,第二个参数表示需要主动建立连接 SessionBase session = SessionBase.create (io_thread, true, this, options, paddr); assert (session != null); // PGM does not support subscription forwarding; ask for all data to be // sent to this pipe. boolean icanhasall = false; if (protocol.equals("pgm") || protocol.equals("epgm")) icanhasall = true; //创建pipe的关联,连接session与当前的socket if (options.delay_attach_on_connect != 1 || icanhasall) { // Create a bi-directional pipe. ZObject[] parents = {this, session}; Pipe[] pipes = {null, null}; int[] hwms = {options.sndhwm, options.rcvhwm}; boolean[] delays = {options.delay_on_disconnect, options.delay_on_close}; Pipe.pipepair (parents, pipes, hwms, delays); // Attach local end of the pipe to the socket object. //将第一个pipe与当前socket关联 attach_pipe (pipes [0], icanhasall); // Attach remote end of the pipe to the session object later on. //将另外一个pipe与session关联起来,这样session与socket就能够通过pipe通信了 session.attach_pipe (pipes [1]); } // Save last endpoint URI options.last_endpoint = paddr.toString (); add_endpoint (addr_, session); //将这个session与这个地址关联起来 return true; }
这里就主要关注TCP连接的建立部分吧,毕竟在分布式的环境下还是再用TCP,通过前面的文章,我们知道一个Socket下面可能对应了多个连接,而每一个连接其实对应的是一个StreamEngine对象,而每一个StreamEngine对象又都关联了一个Session对象,用于与上层的Socket之间的交互,那么这里其实可以看到代码最主要要做的事情就是创建Session对象,以及Pipe对象啥的。。。。接着再调用add_endpoint方法,用于部署这个session,那么接下来来看看这个方法吧:
//这里管理地址与session,其实也就记录当前所有的建立连接的地址,以及相对的session private void add_endpoint (String addr_, Own endpoint_) { // Activate the session. Make it a child of this socket. launch_child (endpoint_); //部署这个endpoint,这里主要的是要将这个endpoint加入到IO线程 endpoints.put (addr_, endpoint_); }
这里其实用于不是session对象,那么对于这个session对象,将会执行process_plug方法,那么来看看这个方法的定义:
//执行plug命令,如果需要连接的话,那么要开始进行连接 protected void process_plug () { io_object.set_handler(this); //设置io对象的handler,用于响应io事件 if (connect) { //如果这里需要主动与远程建立连接的话,那么启动连接 start_connecting (false); //启动连接,false表示不等待 } }
这里首先会设置当前io对象的事件回调,connect属性,在创建session的时候设置的,如果是主动创建的连接那么将会是true,如果是listener接收到的连接,那么将会是false,这里来看看这个方法的定义:
//如果是connect的话,那么需要调用这个方法来建立连接 private void start_connecting (boolean wait_) { assert (connect); // Choose I/O thread to run connecter in. Given that we are already // running in an I/O thread, there must be at least one available. IOThread io_thread = choose_io_thread (options.affinity); //挑选一个io线程,用于部署待会的TCPConnector assert (io_thread != null); // Create the connecter object. if (addr.protocol().equals("tcp")) { TcpConnecter connecter = new TcpConnecter ( io_thread, this, options, addr, wait_); //alloc_assert (connecter); launch_child (connecter); //部署这个TCPconnector return; } if (addr.protocol().equals("ipc")) { IpcConnecter connecter = new IpcConnecter ( io_thread, this, options, addr, wait_); //alloc_assert (connecter); launch_child (connecter); return; } assert (false); }
这里传进来了一个参数,这个参数在构建TCPConnector的时候将会被用到,用于表示这个连接的建立是否是延迟的。。这里刚开始建立连接的时候,是false,表示不要延迟,待会看重连接的时候会发现,在重连接中将会使用延迟的连接。。。
这里也可以看到对于具体连接的建立,其实是委托给了TCPConnector对象来做的,它其实是一个工具类。。。
具体它是怎么建立连接的就不详细的列出来了,大概的说一下过程吧:
(1)创建一个socketchannel对象,并将其设置为非阻塞的,然后调用connect方法来建立于远程地址的连接
(2)将socketchannel注册到IO线程的poller上去,并要设置connect事件
(3)对于connect事件的回调要做的事情,其实是在poller对象上解除这个socketchannel的注册,然后创建一个新的streamengine对象来包装这个socketchannel,然后再将这个streamEngine对象与刚刚的session对象关联起来。。
这里我们可以来看看这个connect的事件回调方法做了什么事情吧:
//连接建立的事件回调,其实也有可能是连接超时 public void connect_event (){ boolean err = false; SocketChannel fd = null; try { fd = connect (); //获取已经建立好连接的channel } catch (ConnectException e) { err = true; } catch (SocketException e) { err = true; } catch (SocketTimeoutException e) { err = true; } catch (IOException e) { throw new ZError.IOException(e); } io_object.rm_fd (handle); //可以将当前的IOObject从poller上面移除了,同时代表这个TCPConnector也就失效了, handle_valid = false; if (err) { // Handle the error condition by attempt to reconnect. close (); add_reconnect_timer(); //尝试重新建立连接 return; } handle = null; try { Utils.tune_tcp_socket (fd); Utils.tune_tcp_keepalives (fd, options.tcp_keepalive, options.tcp_keepalive_cnt, options.tcp_keepalive_idle, options.tcp_keepalive_intvl); } catch (SocketException e) { throw new RuntimeException(e); } // Create the engine object for this connection. //创建streamEngine对象,重新封装建立好连接的channel StreamEngine engine = null; try { engine = new StreamEngine (fd, options, endpoint); } catch (ZError.InstantiationException e) { socket.event_connect_delayed (endpoint, -1); return; } // Attach the engine to the corresponding session object. send_attach (session, engine); //将这个engine与session绑定起来,然后同时还会将当前streamEngine绑定到IO线程上,也就是在poller上面注册 // Shut the connecter down. terminate (); //关闭当前的connector socket.event_connected (endpoint, fd); //向上层的socket通知连接建立的消息 }
具体干了什么代码很直白的就能看出来吧,这里还可以看到对于建立连接超时也会进行尝试重连接的。。。
好了,到这里如何建立连接就算是比较的清楚了。。那么接下来看看在连接断开之后将会如何进行重连接吧,先来看看连接断开之后会执行啥操作,
这里首先总得知道如何判断底层的channel的连接是不是已经断开了吧,如何来判断呢,嗯,这个有点基础的就应该知道,如果连接已经断开了,那么在channel上read将会返回-1,好了那么我们就知道代码应该从哪里开始看了,嗯,来看streamEngine的in_event方法,看它在read返回-1之后会做啥:
//当底层的chanel有数据可以读取的时候的回调方法 public void in_event () { if (handshaking) if (!handshake ()) return; assert (decoder != null); boolean disconnection = false; // If there's no data to process in the buffer... if (insize == 0) { //如果inbuf里面没有数据需要处理 // Retrieve the buffer and read as much data as possible. // Note that buffer can be arbitrarily large. However, we assume // the underlying TCP layer has fixed buffer size and thus the // number of bytes read will be always limited. inbuf = decoder.get_buffer (); //从解码器里面获取buf,用于写入读取的数据,因为在已经设置了底层socket的TCP接收缓冲区的大小 insize = read (inbuf); //用于将发送过来的数据写到buf中去,并记录大小 inbuf.flip(); //这里准备从buf里面读取数据了 // Check whether the peer has closed the connection. if (insize == -1) { //如果是-1的话,表示底层的socket连接已经出现了问题 insize = 0; disconnection = true; //设置标志位 } } // Push the data to the decoder. int processed = decoder.process_buffer (inbuf, insize); //解析这些读取到的数据 if (processed == -1) { disconnection = true; } else { // Stop polling for input if we got stuck. if (processed < insize) //如果处理的数据居然还没有读到的数据多,那么取消读取事件的注册 io_object.reset_pollin (handle); // Adjust the buffer. insize -= processed; //还剩下没有处理的数据的大小 } // Flush all messages the decoder may have produced. session.flush (); //将decoder解析出来的数据交给session // An input error has occurred. If the last decoded message // has already been accepted, we terminate the engine immediately. // Otherwise, we stop waiting for socket events and postpone // the termination until after the message is accepted. if (disconnection) { //表示已经断开了连接,那么需要处理一下 if (decoder.stalled ()) { io_object.rm_fd (handle); io_enabled = false; } else { error (); } } }
嗯,这里可以看到,如果返回-1之后,会设置disconnection标志位,然后还会调用error方法来报错,那么接下来来看看这个error方法做了啥吧:
//报错,那么让高层的ZMQ的socket关闭当前连接 private void error () { assert (session != null); socket.event_disconnected (endpoint, handle); //这里可以理解为通知上层的socket, session.detach (); //这个主要是用于session清理与socket的pipe ,然后还会尝试进行重连接 unplug (); //取消在poller上面的注册 destroy (); //关闭底层的channel,关闭当前 }
其实,如果底层的链接断开了,那么当前这个channel也就无效了,那么当前的streamEngine对象也就无效了,那么要做的事情就是销毁当前的对象,然后还要解除在poller上面的注册,然后还要通知上层的socket,当前的这个链接地址的连接已经断开了。。。当然还要告诉session对象,让其进行一些处理,session的处理就包括重连接了,那么来看看他做了啥:
//相当于是要移除底层的Engine的关联 public void detach() { // Engine is dead. Let's forget about it. engine = null; //这里相当于就会释放当前的engine对象 // Remove any half-done messages from the pipes. clean_pipes (); //清除那些没有接受完的msg // Send the event to the derived class. detached (); //取消pipe,然后重连接 // Just in case there's only a delimiter in the pipe. if (pipe != null) pipe.check_read (); }
这里还看不到进行重连接的代码,接下来继续看detached方法:
private void detached() { // Transient session self-destructs after peer disconnects. if (!connect) { //如果不是主动建立连接的话,那么就直接终止就好了否则的话还进行重连接的尝试 terminate (); return; } // For delayed connect situations, terminate the pipe // and reestablish later on if (pipe != null && options.delay_attach_on_connect == 1 && addr.protocol () != "pgm" && addr.protocol () != "epgm") { pipe.hiccup (); pipe.terminate (false); terminating_pipes.add (pipe); pipe = null; } reset (); // 复位标志位 //这里主动进行重连接的尝试 if (options.reconnect_ivl != -1) { start_connecting (true); //进行重连接尝试,这里也就是需要进行一些延迟 } // For subscriber sockets we hiccup the inbound pipe, which will cause // the socket object to resend all the subscriptions. if (pipe != null && (options.type == ZMQ.ZMQ_SUB || options.type == ZMQ.ZMQ_XSUB)) pipe.hiccup (); }
这里可以看到调用了start_conneting方法,不过这里传进去的参数是true,具体的执行流程与上面建立连接差不多,只不过这里是延迟进行连接的。。。
也就是会在IO线程上面设置定时,当超时之后才会进行连接。。。这样也就使得重连接在一定的频率内进行。。。
具体的定时就不细讲了,蛮简单的。。。
通过上面的代码可以知道ZeroMQ在连接断开之后,如果这个连接时自己主动建立的,而不是listener获取的,那么会自动的去尝试进行重连接。。嗯,做的还不错。。