Java nio服务器端对于客户端连接状态的判断

Java nio服务器端对于客户端连接状态的判断

本文将介绍一个基于Java NIO开发的TCP通讯服务器端实现,用于实时监控客户端的连接状态。因为本人第一次用Java写服务器端,甚至是第一次写服务器端程序,因此过程并不是十分顺利,也是一步一个坑在走。之前遇到一个非常棘手的问题,即如果客户端因为断电或异常终止程序时,并不会像服务器发送一个读end-stream的标记,这时,我通过在服务器端给每个客户端连接加心跳监控的方式,主动检查连接状态,当心跳超时未收到客户端回应时,即调用相应客户端的SocketChannel.Close()方法。但是当同一个客户端再次请求建立连接时,会导致服务器通讯线程崩溃。

下面先介绍如何实现主动监听客户端连接状态,再注明解决上述问题的办法。
因本人算是服务器端的初学者,这套方法经测试确实可用,如有不妥之处还望轻喷~也希望能看到各位更完美的解决方案。

如果客户端异常关闭,关闭前没有主动断开TCP连接,这样服务器端无法被动获知客户端的状态。因此通过不断给客户端发送心跳包的方式来主动检测连接情况。

  • 以下为监控线程的代码实现:
/**
 * TCP连接监控线程
 * @author Quintus
 *
 */
public class SocketMonitor implements Runnable
{
    /**
     * 通讯线程发送消息方法
     */
    private ISend sendFunc;
    /**
     * 通讯线程的关闭连接方法
     */
    private IShutDown shutDownFunc;

    public SocketMonitor(ISend _sendFunc, IShutDown _shutDownFunc)
    {
        sendFunc = _sendFunc;
        shutDownFunc = _shutDownFunc;
    }

    public void run()
    {
        //启一个Timer来监控客户端状态
        Timer time = new Timer();

        //每隔固定时间执行一次,例如我这里每10秒发送一次心跳包
        time.schedule(new ScheduleSend(), 0,NetworkConfig.CheckTCPInterval * 1000);
    }

    class ScheduleSend extends TimerTask
    {
        public void run() 
        {
            //GlobalObj.LinkMap存储所有客户端连接数据
            if(GlobalObj.LinkMap != null)
            {
                Iterator iter = GlobalObj.LinkMap.entrySet().iterator();
                ArrayList<SocketChannel> shutDownList = new ArrayList<SocketChannel>();

                //遍历所有客户端连接
                while (iter.hasNext()) 
                {
                    Map.Entry entry = (Map.Entry) iter.next();
                    SocketChannel key = (SocketChannel)entry.getKey();

                    //更新心跳包用时,当收到客户端回应的心跳包时再将此时间重置为0
                    //【注意】客户端为了回应服务器端的心跳检查,也要实现一套心跳包机制,在此略过
                    entry.setValue((Integer)entry.getValue() + NetworkConfig.CheckTCPInterval);

                    //判断如果心跳包用时超过某个时限,则断开连接。例如我这里的超时时限为30秒,
                    //也就是服务器发送三次心跳包客户端都未回应,则判断客户端已失去响应,服务器断开连接。
                    if((Integer)entry.getValue() >= NetworkConfig.ShutDownTime)
                    {
                        shutDownList.add(key);
                    }
                    else
                    {
                        S100 sendData = new S100();
                        sendData.value = (int)entry.getValue();
                        sendFunc.Send(key, sendData);
                    }
                }
                if(shutDownList.size() > 0)
                {
                    shutDownFunc.ShutDownClient(shutDownList);
                }
            }
        }
    }
}

下面是需要特殊注意的问题点:

如果服务器因超时而要关闭与客户端的连接时,直接调用以下代码,当被服务器关闭连接的客户端再次请求建立连接时,会导致服务器的通讯线程崩溃。

public void CloseClient(SelectionKey _key)
    {
        try 
        {
            if(_key != null)
            {
                _key.cancel();
                _key.channel().close();
                GlobalObj.LinkMap.remove(_key.channel());
                System.out.println("断开连接:"+((SocketChannel)_key.channel()).socket().getPort());
            }

        }
        catch (Exception e) 
        {
            e.printStackTrace();
        }
    }

正确的做法是当服务器判断超时而要断开与客户端的连接时,调用以下代码:

public void ShutDownClient(SocketChannel _clientSocket)
    {
        if(_clientSocket.isOpen())
        {
            try 
            {
                //将连接的输入输出都关闭,而不是直接Close连接
                _clientSocket.shutdownInput();
                _clientSocket.shutdownOutput();
                GlobalObj.LinkMap.remove(_clientSocket);
                System.out.println("客户端无响应:" + _clientSocket.socket().getPort());
            }
            catch (Exception e) 
            {
                e.printStackTrace();
            }
        }
    }

这样,当客户端再次请求建立连接时,服务器端会收到之前产生异常的客户端端口发来一个读end标志,此时会通过以下代码来彻底关闭已经废弃的连接:

...
if(key.isReadable())
{
    SocketChannel clientSocket = (SocketChannel)key.channel();
    if(clientSocket.isOpen())
    {
        ByteBuffer buffer = (ByteBuffer)key.attachment();
        int result = clientSocket.read(buffer);
        //收到读end-stream标记
        if(result == -1)
        {
            CloseClient(key);
        }
        else if(result != 0)
        {
            //读取数据操作
        }
    }
}
...

public void CloseClient(SelectionKey _key)
    {
        try 
        {
            if(_key != null)
            {
                _key.cancel();
                _key.channel().close();
                GlobalObj.LinkMap.remove(_key.channel());
                System.out.println("断开连接:"+((SocketChannel)_key.channel()).socket().getPort());
            }

        }
        catch (Exception e) 
        {
            e.printStackTrace();
        }
    }

至于上述错误做法导致线程崩溃的原因,目前还不清楚,还望高人指教。

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值