前言部分

说到长链接的技术,我们首先都会想到netty这个框架,也是目前使用最广泛的长链接框架,由于该框架使用简单性能稳定,自然也是项目的首先,一般来说长链接的话,可能是做即时通讯会用到的比较多,因为要随时监控是否有新的信息发送过来。其实长链接的应用范围是很广泛的,我们平时也是一直都在使用,不过我们没有很留意而已。常见的推送就是通过长链接来实现实时的接受后台的消息。起初我的项目中也是使用的推送来完成和客户端的即时通讯的,后期由于对消息的实时性要求提高,和消息种类的增多,所以决定自己集成长链接进来,并且自己封装了协议来处理不同类型的通知。

我们的项目长链接作用是通知同步消息、订单信息、商品信息,还有一个重要的功能是将同账号下其他设备上的产生的数据变化,实时的同步到其他的同账号的设备上。当然也不是全部的同步,同步一些共享的消息,比如我说的商品信息,但是订单的信息都是根据机械走的,这也我们做连锁设备的必要需求。

内容部分

其实内容没有多少,网上的实例也很多了,我这里只是将以前写的demo做一个整理出来。最后我会把demo上传,供大家参考。

主要步骤,分为服务端喝客户端两部分。

  1. 首先我们实现长链接通讯,需要两个端即客户端和服务端,然后这需要两个handle来接受对方发过来消息。
  2. 下面先看客户端的代码,下面代码主要配置引导程序,编码,解码器,接受消息的处理器。如下:
mWorkerGroup = new NioEventLoopGroup();
            mBootstrap = new Bootstrap();
            mDispatcher = new Dispatcher();
            mBootstrap.group(mWorkerGroup)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            pipeline.addLast(new IdleStateHandler(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds));
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            pipeline.addLast("handler", mDispatcher);
                        }
                    });
        }
        future = mBootstrap.connect(mServerAddress);

几个比较核心的类,NioEventLoopGroup内部线程池初始化;Bootstrap引导程序;Dispatcher就是接受消息返回的handler了。这里的编解码器我只是使用了简单的string。实际上多数是会使用protobuf的,因为这种协议的优势比一半的json和xml更有优势。占用更小的体积,更安全的数据传输。上面初始化客户端的代码很简单了,深入内容并没有去探究,因为主要还是实现目前的需求为主。

  1. 然后是客户端的另一个关键的类handler,这里我起的名字是Dispatcher,不过该类还是继承自SimpleChannelInboundHandler。我们主要关注两个方法一个是userEventTriggered,一个是channelRead0。

初始化的时候添加IdleStateHandler是为了监听通道的状态,主要分为三种状态,WRITER_IDLE:写超时;READER_IDLE:读超时;ALL_IDLE:读写超时。不同的状态会回调到userEventTriggered方法中,我们可以根据不同的状态进行不同的操作,比如读超时(很久没收到服务端的联系)的时候可能是客户端掉线了,这时候客户端可以主动去连接服务端。很久没写入数据的时候,我们也可以发个心跳到服务端,告诉一下客户端还在线。

  1. 代码也差不多吧,这里我没有写入业务代码,只是做了一下演示,如下:
@Override
    public void channelRead0(ChannelHandlerContext cxt, String msg) throws Exception {
        Log.d("Dispatcher收到服务端的消息", msg);
    }
    
 
 @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent e = (IdleStateEvent) evt;
            switch (e.state()) {
                case WRITER_IDLE:
                    Log.d("Dispatcher", "client WRITER_IDLE over.");
                    //发送心跳
                    sendHeart();
                    break;
                case READER_IDLE:
                    //重连
                    Log.d("Dispatcher", "client READER_IDLE over.");
                    NettyClient.getInstance().reConnect();
                    break;
                case ALL_IDLE:
                    Log.d("Dispatcher", "client ALL_IDLE over.");
                    //发送心跳
                    sendHeart();
                    break;
                default:
                    break;
            }
        }
    }
  1. 下面看一下服务端的代码,其实很客户端类似一致,也是一个service里面做初始化,然后在用一个handler来接受来自客户端的消息就可以了。这里要注意一下handler和childHandler的区别,我在网上搜索了一下,都是如下解释。

handler()和childHandler()的主要区别是,handler()是发生在初始化的时候,childHandler()是发生在客户端连接之后,我简单的验证了。

  1. 其他的区别并不明显,这里我们业务上是,如果后台数据变化,服务端主动发送更新数据的消息到客户端,来保证多端和前后端数据统一。
//创建worker线程池,这里只创建了一个线程池,使用的是netty的多线程模型
        mWorkerGroup = new NioEventLoopGroup();
        //服务端启动引导类,负责配置服务端信息
        mServerBootstrap = new ServerBootstrap();
        mServerBootstrap.group(mWorkerGroup)
                .channel(NioServerSocketChannel.class)
                .handler(new ChannelInitializer<NioServerSocketChannel>() {
                    @Override
                    protected void initChannel(NioServerSocketChannel nioServerSocketChannel) throws Exception {
                        ChannelPipeline pipeline = nioServerSocketChannel.pipeline();
                        pipeline.addLast("ServerSocketChannel out", new OutBoundHandler());
                        pipeline.addLast("ServerSocketChannel in", new InBoundHandler());
                    }
                })
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        //为连接上来的客户端设置pipeline
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        pipeline.addLast(new IdleStateHandler(10,0,0));
                        pipeline.addLast("decoder", new StringDecoder());
                        pipeline.addLast("encoder", new StringEncoder());
                        pipeline.addLast("handler", new ServerChannelHandler());
                    }
                });

        channelFuture = mServerBootstrap.bind(PORT_NUMBER);
  1. 上面介绍完初始化服务端的代码,这里来介绍ServerChannelHandler这个类,看这个里面其实和客户端的handler保持高度一致的。代码基本一致,我也不解释了。但是这个只是我本地的测试demo,实际的后台服务应该更复杂。下面贴个代码:
public class ServerChannelHandler extends SimpleChannelInboundHandler<String> {
    private static final String TAG = "ServerChannelHandler";
    @Override
    public void channelRead0(ChannelHandlerContext cxt, String protoTests) throws Exception {
        Log.d(TAG, "收到客户端消息: " + protoTests);
        NettyMessageBean nettyMessageBean = JsonUtils.fromJsonToBean(protoTests, NettyMessageBean.class);
        int command = nettyMessageBean.getCommand();
        switch (command) {
            case 1:
                //心跳
                cxt.writeAndFlush("心跳");
                break;
            case 2:
                //登陆
                cxt.writeAndFlush("登陆成功");
                break;
            default:
                break;
        }
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent e = (IdleStateEvent) evt;
            switch (e.state()) {
                case WRITER_IDLE:
                    System.err.println("writer_idle over.");
                    break;
                case READER_IDLE:
                    System.err.println("reader_idle over.");
                    break;
                case ALL_IDLE:
                    System.err.println("all_idle over.");
                default:
                    break;
            }
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Log.d(TAG, "client inactive: " + ctx.channel().remoteAddress());
        super.channelInactive(ctx);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Log.d(TAG, "client active: " + ctx.channel().remoteAddress());
        super.channelActive(ctx);
    }
}

上面基本完成,但是其实我们还有一个问题没有处理,那就算关于netty的拆包和粘包问题。

  • 网上一般把这个分为几类,我项目中通过特殊的分隔符来进行处理(就是在消息的结尾加入\r\n),其实我们在处理扫码枪的时候也是这么处理的,并且扫码枪和这个netty情况很类似,也会出现上个码和这个码连到一起,或者条码出现不完整的问题。
  • 还有一种是通过消息定长来处理的。推荐链接
// netty提供的自定义长度解码器,解决TCP拆包/粘包问题
   pipeline.addLast("frameEncoder", new LengthFieldPrepender(2));
   pipeline.addLast("frameDecoder", new LengthFieldBasedFrameDecoder(65535,0, 2, 0, 2));

结束啦

上面的内容就是这样了啊,算是对长连接进行了入门了,后续只要结合业务需求,进行深度的code就可以了。因为以前项目中使用也只是通知一些消息类型,然后本地做一些操作,实际上是不传输内容,所以并没有使用protobuf协议来处理,但是阅读了很多的博客,大家还是统一推荐使用。毕竟长连接的数据交互频率 比较高,也要考虑用户流量的问题。

项目中使用度不高,所以也没有太多比较深入使用。

 

如有错误欢迎指出,谢谢了