TCP是个“流”协议,就是没有界限的一串数据。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包拆分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
为了解决TCP粘包/拆包导致的半包读写问题,netty默认提供了多种解码器用于处理半包,只要能熟练掌握这些类库使用,TCP粘包问题就会非常容易。
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
importio.netty.channel.nio.NioEventLoopGroup;
importio.netty.channel.socket.SocketChannel;
importio.netty.channel.socket.nio.NioServerSocketChannel;
importio.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
/*
* 利用LineBasedFrameDecoder解决TCP粘包问题
*/
public class TimeServerNoStick {
publicvoid bind(int port) throws Exception{
//配置服务端的NIO线程组
EventLoopGroupbossGroup =new NioEventLoopGroup();
EventLoopGroupworkerGroup =new NioEventLoopGroup();
try{
//用于启动NIO服务器的辅助启动类,降低服务端的开发复杂度
ServerBootstrapb=new ServerBootstrap();
b.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,1024)
.childHandler(newChildChannelHandler());
//绑定端口,同步等待成功
ChannelFuturef=b.bind(port).sync();
//等待服务端监听端口关闭
f.channel().closeFuture().sync();
}catch (Exception e) {
//TODO: handle exception
}finally{
//优雅退出,释放线程池资源
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
privateclass ChildChannelHandler extends ChannelInitializer
@Override
protectedvoid initChannel(SocketChannel arg0) throws Exception{
// 增加2个解码器:LineBasedFrameDecoder和StringDecoder
arg0.pipeline().addLast(newLineBasedFrameDecoder(1024));
arg0.pipeline().addLast(newStringDecoder());
arg0.pipeline().addLast(newTimeServerHandlerNoStick());
}
}
publicstatic void main(String[] args) throws Exception{
intport=8080;
if(args!=null&&args.length>0){
try{
port=Integer.valueOf(args[0]);
}catch (NumberFormatException e) {
//TODO: handle exception
}
}
newTimeServerNoStick().bind(port);
}
}
/*
* 利用LineBasedFrameDecoder解决TCP粘包问题
*/
public class TimeServerHandlerNoStickextends ChannelHandlerAdapter{
privateint counter;
@Override
publicvoid channelRead(ChannelHandlerContext ctx,Object msg) throws Exception{
// 可以发现接收到的msg就是删除回车换行符后的请求消息,
//不需要额外考虑处理读半包问题,也不需要对请求消息进行编码
Stringbody=(String) msg;
System.out.println("The time server receiveorder:"+body+";the counter is"+ ++counter);
StringcurrentTime="QUERY TIME ORDER".equalsIgnoreCase(body)?
newjava.util.Date(System.currentTimeMillis()).toString():"BAD ORDER";
ByteBufresp=Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
}
@Override
publicvoid exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
ctx.close();
}
}
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
importio.netty.channel.socket.SocketChannel;
importio.netty.channel.socket.nio.NioSocketChannel;
importio.netty.handler.codec.LineBasedFrameDecoder;
importio.netty.handler.codec.string.StringDecoder;
/*
* 利用LineBasedFrameDecoder解决TCP粘包问题
*/
public class TimeClientNoStick {
publicvoid connect(int port,String host) throws Exception{
//配置客户端NIO线程组
EventLoopGroupgroup=new NioEventLoopGroup();
try{
Bootstrapb=new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(newChannelInitializer
@Override
publicvoid initChannel(SocketChannel ch) throws Exception{
ch.pipeline().addLast(newLineBasedFrameDecoder(1024));
ch.pipeline().addLast(newStringDecoder());
ch.pipeline().addLast(newTimeClientHandlerNoStick(1024));
}
});
//发起异步连接操作
ChannelFuturef=b.connect(host,port).sync();
//等待客户端链路关闭
f.channel().closeFuture().sync();
}catch (Exception e) {
//TODO: handle exception
}finally{
//优雅退出,释放NIO线程组
group.shutdownGracefully();
}
}
publicstatic void main(String[] args) throws Exception{
intport=8080;
if(args!=null&&args.length>0){
try{
port=Integer.valueOf(args[0]);
}catch (NumberFormatException e) {
//TODO: handle exception
}
newTimeClient().connect(port,"127.0.0.1");
}
}
}
import java.util.logging.Logger;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
importio.netty.channel.ChannelHandlerAdapter;
importio.netty.channel.ChannelHandlerContext;
/*
* 利用LineBasedFrameDecoder和StringDecoder解决TCP粘包问题
*/
public class TimeClientHandlerNoStickextends ChannelHandlerAdapter{
privatestatic final Logger logger=Logger.getLogger(TimeClientHandler.class.getName());
privateint counter;
privatebyte[] req;
publicTimeClientHandlerNoStick(){
req=("QUERYTIME ORDER"+System.getProperty("line.separator")).getBytes();
}
@Override
publicvoid channelActive(ChannelHandlerContext ctx){
ByteBufmessage=null;
for(inti=0;i<100;i++){
message=Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}
}
@Override
publicvoid channelRead(ChannelHandlerContext ctx,Object msg) throws Exception{
//此时msg已经是解码成字符串之后的应答消息
Stringbody=(String) msg;
System.out.println("Nowis:"+body+";the counter is"+ ++counter);
}
@Override
publicvoid exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
//释放资源
logger.warning("Unexpectedexception from downstream:"+cause.getMessage());
ctx.close();
}
}
为了尽量模拟TCP粘包的半包场景,采用简单的压力测试,链路建立成功之后,客户端连续发送100条消息给服务端,然后查看服务端和客户端运行结果,分别运行TimeServer和TimeClient,服务端执行结果为:
。。。
客户端执行结果为:
。。。
运行结果完全符合预期,说明通过使用LinedBasedFrameDecoder和StringDecoder成功解决了TCP粘包导致的读半包问题。