处理流数据的传输
SocketBuffer的警告
在如tcp/ip的以流为基础传输数据中,数据被接收后,被保存在一个socket接收缓冲区中。不幸的是,这个以流为基础的缓冲区buffer不是一个包packet的队列,而是一个字节byte队列。这意味着,即使你发送两个消息message作为2个独立的包,操作以系统不会把他们作为两个消息message,而是仅仅当做一堆字节。因此,无法保证你读到的数据就是对方写的数据,这就是tcp、ip中常见的拆包、粘包问题。例如,假设操作系统已经接收到了三个包,如下:
由于流传输的这个普通属性,在读取他们的时候将会存在很大的几率,这些数据会被分段成下面的几部分:
因此,作为一个接收方,不管它是服务端还是客户端,都需要把接收到的数据整理成一个或多个有意义的并且能够被应用程序容易理解的数据。以上面这个例子为例,被接收的数据应该被分段为一下几部分:
要想通过应用程序的方式解决上面提到的,拆分粘包的问题,可以通过以下几个部分来处理:
第一种解决方案
现在,让我回想一下time客户端的示例,我们会遇到相同的问题。一个32位字节的整形数据是一个非常小的数据,并且它也不见得会经常被分段。然而,它确实也会被拆分到不同的数据段中,并且被拆分的可能性会随着传输的增加而增加。
一种简单的解决方式是创建一个内部积累的缓冲区,并且等待知道4个字节都被接收到这个内部的缓冲区中。下面为修改后的客户端TimeClientHandler实现类代码来修复这个问题:
package com.zzj.nio.netty;
import java.util.Date;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* Time客户端处理器类
* @author zzj
* @date Oct 19, 2016 5:48:56 PM
*/
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private ByteBuf buf;
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
buf.release();
buf = null;
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
buf = ctx.alloc().buffer(1);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf m = (ByteBuf) msg; // (1)
buf.writeBytes(m);
System.out.println("ssss");
try {
if (buf.readableBytes()>=4) {
long currentTimeMillis = (buf.readUnsignedInt()-2208988800l)*1000l;
System.out.println(new Date(currentTimeMillis));
ctx.close();
}
} finally {
m.release();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
1.ChannelHandler有两个生命周期监听方法:handlerAdded和handlerRemoved方法,你可以完成任意初始化任务,只要它不会被阻塞很长的时间。
2.首先,所有被接收到的数据会被积累到buf对象中。
3.然后,handler处理器必须检测buf是搜包含了足够多的数据,这里个例子中是4个字节,然后去处理实际的业务逻辑。否咋,netty将会重复调用channelRead方法当数据被调用,并且实际上所有的4个字节都会被积累。
第二种解决方案
尽管第一个解决方案能够解决timeclient可能发生半包粘包的问题,但是修改的那个handler看起来不够干净优雅。想一下,如果存在一个复杂的具有多个字段的协议,比如包含很多很长的字段,ChannelHandler的实现方式将会变得维护可困难。
可能你已经知道,我们可以为ChannelPipeline添加一个ChannelHandler处理器。因此,我们可以将一个复杂的ChannelHandler拆分成多个单独的处理器来减少系统的复杂性。例如,你可以将TimeClientHandler拆分成两个处理器:
1.TimeDecoder用来处理半包拆包的问题。
2.timeClientHandler原始版本的实现.
Netty提供了一个可扩展的类,帮你完成TimeDecoder的开发:
/**
* TimeClient的编码器
* @author zzj
* @date Oct 20, 2016 1:39:38 PM
*/
public class TimeDecoder extends ByteToMessageDecoder {
/* (non-Javadoc)
* @see io.netty.handler.codec.ByteToMessageDecoder#decode(io.netty.channel.ChannelHandlerContext, io.netty.buffer.ByteBuf, java.util.List)
*/
@Override
protected void decode(ChannelHandlerContext arg0, ByteBuf arg1, List<Object> arg2) throws Exception {
if (arg1.readableBytes()<4) {
return;
}
arg2.add(arg1.readBytes(4));
}
}
1.ByteToMessageDecoder是ChannelHandler的一种实现类,它能够很容易的处理半包粘包问题。
2.无论何时,当新数据接收到时,ByteToMessageDecoder会调用一个内部可维护的decode方法来处理内部积累的buffer缓冲区。
3.decode方法可以决定当没有足够的数据时,不添加到out对象中。当有更多的数据接收到后,ByteToMessageDecoder会再次调用decode方法。
4.如果decode方法添加一个对象到out列表对象中,这意味着解码器成功的解码了一个消息。ByteToMessageDecoder会释放掉累计缓冲区已经读取的部分。需要注意的是,我们没有必要去解码多条message消息,因为By特ToMessageDecoder会一直调用decode方法直到没有数据添加到out列表对象中。
下面为TimeClient中TimeDecoder后的代码:
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
//添加业务处理器
channel.pipeline().addLast(new TimeDecoder(),new TimeClientHandler());
}
});
第三种解决方案:使用POJO代替ByteBuf
前面的几个例子中我们都是用ByteBuf作为协议消息的原始数据结构,这一部分我们将改善time协议的客户端和服务端通过pojo来代替ByteBuf对象.
使用POJO在ChannelHandler种的优势很明显: 通过从ChannelHandler中提取出ByteBuf的代码,你的handler处理器变得更好的可维护、可重用。在time客户端和服务端的例子中,我们仅仅读取一个32字节的整形数据,并且使用ByteBuf不是一个主要的直接的问题。然而,你会发现当你需要实现一个真实的协议,分离代码变得非常的必要。首先,让我们定义一个新的类型叫做UnixTime。
package com.zzj.nio.netty.time;
import java.util.Date;
/**
* POJO类表示time
* @author zzj
* @date Oct 20, 2016 2:27:45 PM
*/
public class UnixTime {
private final long value;
public UnixTime(){
this(System.currentTimeMillis() / 1000L + 2208988800L);
}
public UnixTime(long value) {
this.value = value;
}
public long value() {
return value;
}
@Override
public String toString() {
return new Date((value() - 2208988800L) * 1000L).toString();
}
}
现在我们修改TimeDecoder的代码,可以使用UnixTime来替代ByteBuf:
import java.util.List;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
/**
* TimeClient的编码器
* @author zzj
* @date Oct 20, 2016 1:39:38 PM
*/
public class TimeDecoder extends ByteToMessageDecoder {
/* (non-Javadoc)
* @see io.netty.handler.codec.ByteToMessageDecoder#decode(io.netty.channel.ChannelHandlerContext, io.netty.buffer.ByteBuf, java.util.List)
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
if (msg.readableBytes()<4) {
return;
}
out.add(new UnixTime(msg.readUnsignedInt()));
}
}
修改了解码器后,TimeClientHandler处理器中读取消息时,同样需要修改,这里不再使用ByteBuf对象。
package com.zzj.nio.netty.time;
import java.util.Date;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* Time客户端处理器类
*
* @author zzj
* @date Oct 19, 2016 5:48:56 PM
*/
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
UnixTime m = (UnixTime) msg; // (1)
System.out.println(new Date(m.value()));
ctx.close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
是不是代码变得更简单、优雅了。同样的,服务端也可以这样修改。下面为修改TimeServerHandler的代码:
package com.zzj.nio.netty.time;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* @author zzj
* @date Oct 19, 2016 5:00:50 PM
*/
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(final ChannelHandlerContext ctx) throws Exception {
ChannelFuture f = ctx.writeAndFlush(new UnixTime());
f.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}
现在,还没有做的就只有编码器了,通过试下你ChannelHandler来将ByteBuf对象转换为UnixTIme对象。不过这已经是非常简单了,因为当你对一个消息编码的时候,你不需要再处理拆包和组装的过程。
package com.zzj.nio.netty.time;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
/**
* TimeClient的编码器
* @author zzj
* @date Oct 20, 2016 1:39:38 PM
*/
public class TimeEncoder extends ChannelOutboundHandlerAdapter {
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
UnixTime unixTime = (UnixTime)msg;
ByteBuf encoded = ctx.alloc().buffer(4);
encoded.writeInt((int)unixTime.value());
ctx.write(encoded, promise); // (1)
}
}
第一, 通过ChannelPromise,当编码后的数据被写到了通道上Netty可以通过这个对象标记是成功还是失败。
第二, 我们不需要调用cxt.flush()。因为处理器已经单独分离出了一个方法void flush(ChannelHandlerContext cxt),如果像自己实现flush方法内容可以自行覆盖这个方法。
最后的任务就是在TimeServerHandler之前把TimeEncoder插入到ChannelPipeline。但这是不那么重要的工作。
最后的任务就是在TimeServerHandler之前把TimeEncoder插入到ChannelPipeline。但这是不那么重要的工作。
优雅的关闭EveltLoopGroup
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
workerGroup.shutdownGracefully();