前言部分
说到长链接的技术,我们首先都会想到netty这个框架,也是目前使用最广泛的长链接框架,由于该框架使用简单性能稳定,自然也是项目的首先,一般来说长链接的话,可能是做即时通讯会用到的比较多,因为要随时监控是否有新的信息发送过来。其实长链接的应用范围是很广泛的,我们平时也是一直都在使用,不过我们没有很留意而已。常见的推送就是通过长链接来实现实时的接受后台的消息。起初我的项目中也是使用的推送来完成和客户端的即时通讯的,后期由于对消息的实时性要求提高,和消息种类的增多,所以决定自己集成长链接进来,并且自己封装了协议来处理不同类型的通知。
我们的项目长链接作用是通知同步消息、订单信息、商品信息,还有一个重要的功能是将同账号下其他设备上的产生的数据变化,实时的同步到其他的同账号的设备上。当然也不是全部的同步,同步一些共享的消息,比如我说的商品信息,但是订单的信息都是根据机械走的,这也我们做连锁设备的必要需求。
内容部分
其实内容没有多少,网上的实例也很多了,我这里只是将以前写的demo做一个整理出来。最后我会把demo上传,供大家参考。
主要步骤,分为服务端喝客户端两部分。
-
首先我们实现长链接通讯,需要两个端即客户端和服务端,然后这需要两个handle来接受对方发过来消息。
-
下面先看客户端的代码,下面代码主要配置引导程序,编码,解码器,接受消息的处理器。如下:
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更有优势。占用更小的体积,更安全的数据传输。上面初始化客户端的代码很简单了,深入内容并没有去探究,因为主要还是实现目前的需求为主。
-
然后是客户端的另一个关键的类handler,这里我起的名字是Dispatcher,不过该类还是继承自SimpleChannelInboundHandler。我们主要关注两个方法一个是userEventTriggered,一个是channelRead0。
初始化的时候添加IdleStateHandler是为了监听通道的状态,主要分为三种状态,WRITER_IDLE:写超时;READER_IDLE:读超时;ALL_IDLE:读写超时。不同的状态会回调到userEventTriggered方法中,我们可以根据不同的状态进行不同的操作,比如读超时(很久没收到服务端的联系)的时候可能是客户端掉线了,这时候客户端可以主动去连接服务端。很久没写入数据的时候,我们也可以发个心跳到服务端,告诉一下客户端还在线。
代码也差不多吧,这里我没有写入业务代码,只是做了一下演示,如下:
@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; } } }
-
下面看一下服务端的代码,其实很客户端类似一致,也是一个service里面做初始化,然后在用一个handler来接受来自客户端的消息就可以了。这里要注意一下handler和childHandler的区别,我在网上搜索了一下,都是如下解释。
handler()和childHandler()的主要区别是,handler()是发生在初始化的时候,childHandler()是发生在客户端连接之后,我简单的验证了。
其他的区别并不明显,这里我们业务上是,如果后台数据变化,服务端主动发送更新数据的消息到客户端,来保证多端和前后端数据统一。
//创建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);
-
上面介绍完初始化服务端的代码,这里来介绍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协议来处理,但是阅读了很多的博客,大家还是统一推荐使用。毕竟长连接的数据交互频率 比较高,也要考虑用户流量的问题。
项目中使用度不高,所以也没有太多比较深入使用。
如果没积分的同学,就留言邮箱吧