前两个博客磕磕绊绊的写完了客户端和服务端的例子,今天就来研究一点更深入的东西。
TCP的粘包和拆包
这个是我在网上找到的解释粘包和拆包的博客,看着还可以:解释拆包粘包。
下面咱们介绍一下书上咋说的,TCP是个流协议,什么是流,就是一大串没有边界的数据,大家可以想象一下河里的水。TCP底层不了解咱们上层业务数据的具体含义,它会根据TCP缓冲区的实际情况对数据进行划分,所以在业务上可以认为,一个完整的业务数据可能会被TCP拆分成多个包进行发送,也可能把多个小包合成一个包进行发送,这就是TCP的拆包和粘包。
图示TCP粘包拆包
书上是下面这样解释上图的
TCP拆包粘包发生的原因
- 应用程序写入的大小大于套接字缓冲区的大小
- 进行MSS大小的分片
- 以太网帧的payload大于MTU进行ip分片
图示:
如何解决粘包拆包
- 消息定长
- 消息末尾添加换行符或指定标识
- 将消息分为消息头和消息体消息头中含有消息体的长度
- 更复杂的应用层协议
下面咱们在代码里看看(还是写代码舒服)
TimeServerHandler:加了一个请求计数器
package NettyTestTCP;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
private int counter = 0;
/*** 服务端读取到网络数据后的处理*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
String body = new String(bytes,"UTF-8");
System.out.println("服务端得到的请求次数:"+ ++counter);
String currentTime = "获取当前时间".equals(body)?new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()):"指令有误";
ByteBuf buf = Unpooled.copiedBuffer(currentTime.getBytes());
//将消息写到缓冲数组中
ctx.write(buf);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//将消息推到SocketChannel中发送给客户端
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
TimeClientHandler:加了一个返回计数器
package NettyTestTCP;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class TimeClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
private final ByteBuf sendMsg;
private int counter = 0;
public TimeClientHandler() {
byte[] bytes = "获取当前时间".getBytes();
sendMsg = Unpooled.buffer(bytes.length);
sendMsg.writeBytes(bytes);
}
//读取到服务器端的数据后的操作
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
String body = new String(bytes,"UTF-8");
System.out.println("当前时间是:"+body +",-------"+ ++counter);
System.out.println();
}
/*客户端被通知channel活跃以后,做事*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//往服务器写数据
for (int i = 0;i<100;i++){
byte[] bytes = "获取当前时间".getBytes();
ByteBuf sendMsg = Unpooled.buffer(bytes.length);
sendMsg.writeBytes(bytes);
ctx.writeAndFlush(sendMsg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
结果如下所示
服务端应该接收到100次请求但是只有30多次
客户端应该得到100次响应但是只有三次
利用LineBasedFrameDecoder解决TCP粘包问题
为了解决TCP 的粘包和拆包问题Netty提供了许多种编解码器用来处理,只要咱们掌握了这些类库的使用,粘包拆包问题就贼简单,甚至咱们都不需要知道它是咋搞的这也是其他NIO框架无法比拟的。下面开始搞咱们上边的问题还是以时间服务器来说
TimeClient:
package NettyTestTCPNew;
import LineBase.LineBaseClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
public class TimeClient {
public void connect(int port, String host) {
//配置客户端的NIO线程
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
//服务器端是ServerBootstrap
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(eventLoopGroup)
.channel(NioSocketChannel.class)
//.option(ChannelOption.TCP_NODELAY,true)
.remoteAddress("127.0.0.1",9999)
.handler(new ChannelInitializerImp());
//发起异步连接
ChannelFuture channelFuture = bootstrap.connect().sync();
//等待客户端的连接关闭
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
finally {
//释放IO线程组
eventLoopGroup.shutdownGracefully();
}
}
private static class ChannelInitializerImp extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
//跟服务端一样加了这个编解码器
ch.pipeline().addLast(new LineBasedFrameDecoder(1024*100000));
// socketChannel.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new TimeClientHandler());
}
}
public static void main(String[] args) {
new TimeClient().connect(8989,"127.0.0.1");
}
}
TimeClientHandler:
package NettyTestTCPNew;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
import java.util.concurrent.atomic.AtomicInteger;
public class TimeClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
private AtomicInteger counter = new AtomicInteger(0);
//读取到服务器端的数据后的操作
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
// byte[] bytes = new byte[byteBuf.readableBytes()];
// byteBuf.readBytes(bytes);
// String body = new String(bytes,"UTF-8");
System.out.println("当前时间是:"+byteBuf.toString(CharsetUtil.UTF_8) +",-------"+ counter.incrementAndGet());
}
/*客户端被通知channel活跃以后,做事*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//往服务器写数据
ByteBuf sendMsg = null;
String msg = "获取当前时间";
for (int i = 0;i<100;i++){
sendMsg = Unpooled.buffer(msg.length());
sendMsg.writeBytes(msg.getBytes());
ctx.writeAndFlush(sendMsg);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
}
TimeServer:
package NettyTestTCPNew;
import LineBase.LineBaseEchoServer;
import LineBase.LineBaseServerHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import java.net.InetSocketAddress;
public class TimeServer {
public void bind() throws Exception {
//两个线程组,各包含一组NIO线程
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
//ServerBootstrap是Netty的启动NIO的辅助类,目的降低服务端开发的难度
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)/*指定使用NIO进行网络传输*/
.localAddress(new InetSocketAddress(9999))/*指定服务器监听端口*/
/*服务端每接收到一个连接请求,就会新启一个socket通信,也就是channel,
所以下面这段代码的作用就是为这个子channel增加handle*/
//绑定一个处理请求的类 我这里用一个匿名内部类写了
.childHandler(new ChannelInitializerImp());
//绑定端口 ChannelFuture异步回调 一直阻塞直到连接完成
ChannelFuture future = bootstrap.bind().sync();
//等待服务器监听端口关闭
System.out.println("服务启动了");
future.channel().closeFuture().sync();
}finally {
//关系线程池
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
private static class ChannelInitializerImp extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new TimeServerHandler());
}
}
public static void main(String[] args) throws Exception {
int port = 8989;
new TimeServer().bind();
}
}
TimeServerHandler:
package NettyTestTCPNew;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.text.SimpleDateFormat;
import java.util.Date;
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
private int counter = 0;
/*** 服务端读取到网络数据后的处理*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
byte[] bytes = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(bytes);
String body = new String(bytes,"UTF-8");
System.out.println("服务端得到的请求:"+ body+" ,===== "+ ++counter);
String currentTime = "获取当前时间".equals(body)?new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()):"指令有误";
ByteBuf buf = Unpooled.copiedBuffer(currentTime.getBytes());
//将消息发给客户端
ctx.writeAndFlush(buf);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
具体的实现方法如下所示:
LineBasedFrameDecoder 的工作原理是它依次遍历 ByteBuf 中的可读字节,判断看是否有 "\n” 或者 "\r\n”,如果有,就以此位置为结束位置,从可读索引到结束位置区间的字节就组成了一行。它是以回车换行符为结束标记的解码器,支持配置单行的最大长度,如果连续读取到最大长度后仍然没有发现换行符,会抛出异常,同时忽略掉之前读取到的异常码流。
StringDecoder 解码器工作原理将接收到的对象转换成字符串,然后继续调用后面的 Hander。
LineBasedFrameDecoder + StringDecoder 组合就是按行切换的文本解码器,它被设计用来支持 TCP 的粘包与拆包。
1)LineBasedFrameDecoder(final int maxLength)
2)LineBasedFrameDecoder(final int maxLength, final boolean stripDelimiter, final boolean failFast)