/ 闲敲棋子 / 关于socket异常那些事儿

关于socket异常那些事儿

2014-05-05 posted in [学习笔记]

最近发现Openfire服务端隔段时间就会报这样的错:

image

有时候是这样:

image

决定处理一下这个“黑盒”,从异常信息看,这个异常有关socket连接超时或者是连接被重置。查了一下相关问题出现原因:(引)

常出现的Connection reset by peer: 原因可能是多方面的,不过更常见的原因是:

  1. 服务器的并发连接数超过了其承载量,服务器会将其中一些连接Down掉;
  2. 客户端断掉连接,而服务器还在给客户端发送数据;

该异常在客户端和服务器端均有可能发生,引起该异常的原因有两个,一个是如果一端的Socket被关闭(或主动关闭或者因为异常退出而引起的关闭),另一端仍发送数据,发送的第一个数据包引发该异常(Connect reset by peer)。另一个是一端退出,但退出时并未关闭该连接,另一端如果在从连接中读数据则抛出该异常(Connection reset)。简单的说就是由连接断开后的读和写操作引起的。

如果频繁出现,就表示很多客户端连接到Apache服务器的响应时间太长了,可能是网络的问题或者服务器性能问题。

Connection timed out:

连接超时,其实还是找不到已连接的客户端。可能出现原因:客户端网络问题导致连接没断开。

由于Openfire是基于mina做的socket连接,mina本身封装了socket的各种异常,只剩下 ProtocolCodecException、ProtocolEncoderException、ProtocolDecoderException、RecoverableProtocolDecoderException 四种异常,其他都作为运行时异常抛出。

Openfire的连接管理类是ConnectionHandler,继承了mina的IoHandlerAdapter。通过exceptionCaught方法实现连接过程中的异常处理。

源码如下(Openfire 3.7.2版本):

@Override
public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
      if (cause instanceof IOException) {
          // TODO Verify if there were packets pending to be sent and decide what to do with them
          Log.info("ConnectionHandler reports IOException for session: " + session, cause);
      }
      else if (cause instanceof ProtocolDecoderException) {
          Log.warn("Closing session due to exception: " + session, cause);
          
          // PIO-524: Determine stream:error message.
          final StreamError error;
          if (cause.getCause() != null && cause.getCause() instanceof XMLNotWellFormedException) {
          	error = new StreamError(StreamError.Condition.xml_not_well_formed);
          } else {
          	error = new StreamError(StreamError.Condition.internal_server_error);
          }
          
          final Connection connection = (Connection) session.getAttribute(CONNECTION);
          connection.deliverRawText(error.toXML());
          session.close();
      }
      else {
          Log.error("ConnectionHandler reports unexpected exception for session: " + session, cause);
      }
  }

可以看出,openfire本身没有处理IO异常,只是打了一下log。这会导致断开的链接发生没断干净的情况。这种情况下面再讨论。

编写网络程序时需要注意的问题:(引)

  1. 是要正确区分长、短连接。所谓的长连接是指一经建立就永久保持。短连接的情况是,准备数据—>建立连接—>发送数据—>关闭连接。很多的程序员写了多年的网络程序,居然不知道什么是长连接,什么是短连接。

  2. 是对长连接的维护。所谓维护包括两个方面,首先是检测对方的主动断连(即调用 Socket的close方法),其次是检测对方的宕机、异常退出及网络不通。这是一个健壮的通信程序必须具备的。检测对方的主动断连很简单,主要一方主动断连,另一方如果在进行读操作,则此时的返回值只-1,一旦检测到对方断连,则应该主动关闭本端的连接(调用Socket的close方法)。而检测对方的宕机、异常退出及网络不通,常用方法是用“心跳”,也就是双方周期性的发送数据给对方,同时也从对方接收“心跳”,如果连续几个周期都没有收到对方心跳,则可以判断对方宕机、异常退出或者网络不通,此时也需要主动关闭本端连接,如果是客户端可在延迟一定时间后重新发起连接。虽然Socket有一个keep alive选项来维护连接,如果用该选项,一般需要两个小时才能发现对方的宕机、异常退出及网络不通。

  3. 是处理效率问题。不管是客户端还是服务器,如果是长连接一个程序至少需要两个线程,一个用于接收数据,一个用于发送心跳,写数据不需要专门的线程,当然另外还需要一类线程(俗称Worker线程)用于进行消息的处理,也就是说接收线程仅仅负责接收数据,然后再分发给Worker进行数据的处理。如果是短连接,则不需要发送心跳的线程,如果是服务器还需要一个专门的线程负责进行连接请求的监听。这些是一个通信程序的整体要求,具体到你的程序中,就看你如何对程序进行优化了。

openfire自带了心跳机制,是通过在服务端设置Client Connections –> Idle Connections Policy中的:

Disconnect  client after they have been idle for [***] seconds

mina框架提供了空闲检测功能,这项功能可检测客户端口建立了TCP/IP连接、却不发送任何消息的情况。

当我们设置了选项时长,openfire会调用mina的session.setIdleTime()方法,在客户端口连接经过指定时长未发送任何消息的情况下触发sessionIdle事件,由sessionIdle()方法处理。

Openfire can send an XMPP Ping request to clients that are idle, before they are disconnected. Clients must respond to such a request, which allows Openfire to determine if the client connection has indeed been lost. The XMPP specification requires all clients to respond to request. If a client does not support the XMPP Ping request, it must return an error (which in itself is a response too).

选项中 Send an XMPP Ping request to idle clients对ConnectionHandler进行了再一次封装,在第一次触发sessionIdle时发送一次ping 消息,迫使客户端进行响应。

在ConnectionHandler 的sessionIdle()方法中判断当前的idle次数大于1次时将关闭客户端连接。我们设置了idle Time 之后这个idle的检测发生在达到一半时间和达到指定时间,每次检测都会将idle 的次数加1 。 例如我们设置了120s,则mina会在这个时长的一半时间内,60s,发送一个ping包,等待客户端响应,然后在到达指定时长后,客户端仍未发送消息时再发一次。如果两次都没有收到回复,则认为连接断掉了,触发sessionIdle()事件,关闭连接。

但是两次心跳中间的消息包就会丢失,并且不会抛出异常,因此选择通过消息回执的方式,保证消息的稳定性XEP-0184

考虑解决方案:

  1. 在发生Exception后主动关闭连接。
  2. 心跳机制的设置与优化

由于服务端没有断开异常连接,会产生大量的CLOSE_WAITFIN_WAIT2状态。可以通过

netstat -na|grep 9090|grep FIN_WAIT2|wc -l

或者:

netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'

命令查看。

返回结果:

TIME_WAIT 1
CLOSE_WAIT 87
FIN_WAIT1 4
ESTABLISHED 377
FIN_WAIT2 3305

解决方案:

  1. 为 FIN_WAIT2 增加超时机制
  2. CLOSE_WAIT缩短keepAlive时间

修改linux系统内核参数:

vi /etc/sysctl.conf

加入以下数据:

net.ipv4.tcp_keepalive_time = 30
net.ipv4.tcp_keepalive_probes = 2
net.ipv4.tcp_keepalive_intvl = 2
net.ipv4.tcp_fin_timeout = 30

设置fin的连接超时时间为30s

PS: 想详细了解这些TCP的状态迁移,以及异常信息,需要用到计算机网络的知识。相关连接