前言部分
说到长链接的技术,我们首先都会想到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协议来处理,但是阅读了很多的博客,大家还是统一推荐使用。毕竟长连接的数据交互频率 比较高,也要考虑用户流量的问题。
项目中使用度不高,所以也没有太多比较深入使用。
如有错误欢迎指出,谢谢了