TCP是一个流协议,即TCP的数据时没有界限的一串数据。而这样的数据方式必然会导致数据粘包。为了解析TCP数据,我们相对应的也要对数据进行拆包。
粘包的原因:
1. 应用程序write的字节大于套接口发送缓冲区大小;
2. 进行MSS大小的TCP分段;
3. 以太网帧的payload大于MTU进行IP分片;
服务器端:
TimeServer
public class TimeServer {
public void bind(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap();
try {
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChildChannelHandler());
//绑定端口, 同步等待成功;
ChannelFuture future = bootstrap.bind(port).sync();
//等待服务端监听端口关闭
future.channel().closeFuture().sync();
} finally {
//优雅关闭 线程组
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
private class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
// ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast("timeServerHandler",new TimeServerHandler());
}
}
public static void main(String[] args) throws Exception {
int port = 443;
new TimeServer().bind(port);
}
}
TimeServerHandler
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
private int count;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
System.out.println("Server start read");
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length());
// String body = (String) msg;
System.out.println("The time server receive order : " + body + "; the count is : " + ++count);
String currentTime = "Query Time Order".equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()).toString() : "Bad Order";
currentTime = currentTime + System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
// ctx.writeAndFlush(resp);
ctx.write(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
ctx.close();
}
}
客户端:
TimeClient
public class TimeClient {
public void connect(int port, String host) throws Exception{
//配置客户端NIO 线程组
EventLoopGroup group = new NioEventLoopGroup();
Bootstrap client = new Bootstrap();
try {
client.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
// ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast("timeServerHandler",new TimeClientHandler());
}
});
//绑定端口, 异步连接操作
ChannelFuture future = client.connect(host, port).sync();
//等待客户端连接端口关闭
future.channel().closeFuture().sync();
} finally {
//优雅关闭 线程组
group.shutdownGracefully();
}
}
public static void main(String[] args) {
int port = 443;
TimeClient client = new TimeClient();
try {
client.connect(port, "127.0.0.1");
} catch (Exception e) {
e.printStackTrace();
}
}
}
TimeClientHandler
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private int count;
private byte[] req;
public TimeClientHandler() {
req = ("QUERY TIME ORDER" + System.getProperty("line.separator"))
.getBytes();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf message = null;
for (int i = 0; i < 100; i++) {
message = Unpooled.buffer(req.length);
message.writeBytes(req);
ctx.writeAndFlush(message);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
// String body = (String) msg;
System.out.println("NOW is: " + body + "; the counter is " + ++count);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
ctx.close();
}
}
服务端输出:
Server start read The time server receive order : QUERY TIME ORDER QUERY TIME ORDER 。。。 QUERY TIME ORDER QUERY TIME ORD; the count is : 1
Server start read The time server receive order : QUERY TIME ORDER 。。。 QUERY TIME ORDER QUERY TIME ORDER; the count is : 2
客户端输出:
NOW is: Bad Order
Bad Order
; the counter is 1
很明显,由于粘包拆包导致半包读写问题,致使得到的结果不是目标结果。Netty提供多种编码器用于处理半包问题,接下来使用LineBasedFrameDecoder来解决半包读写问题。
服务端:
TimeServer类需要修改initChannel方法,加入LineBasedFrameDecoder解码器,修改后方法如下:
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast("timeServerHandler",new TimeServerHandler());
}
TimeServerHandler类需要修改channelRead方法,由于使用解码器之后,获取的msg已经解码成字符串了,具体代码如下:
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
// System.out.println("Server start read");
// ByteBuf buf = (ByteBuf) msg;
// byte[] req = new byte[buf.readableBytes()];
// buf.readBytes(req);
// String body = new String(req, "UTF-8").substring(0, req.length - System.getProperty("line.separator").length());
String body = (String) msg;
System.out.println("The time server receive order : " + body + "; the count is : " + ++count);
String currentTime = "Query Time Order".equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()).toString() : "Bad Order";
currentTime = currentTime + System.getProperty("line.separator");
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(resp);
// ctx.write(resp);
}
客户端代码:
TimeClient与服务端对应修改initChannel方法,加入LineBasedFrameDecoder解码器,修改后代码如下:
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast("timeServerHandler",new TimeClientHandler());
}
TimeClientHandler类需要修改channelRead方法,修改如下:
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// ByteBuf buf = (ByteBuf)msg;
// byte[] req = new byte[buf.readableBytes()];
// buf.readBytes(req);
// String body = new String(req, "UTF-8");
String body = (String) msg;
System.out.println("NOW is: " + body + "; the counter is " + ++count);
}
运行之后,服务端输出为:
The time server receive order : QUERY TIME ORDER; the count is : 1
The time server receive order : QUERY TIME ORDER; the count is : 2
The time server receive order : QUERY TIME ORDER; the count is : 3
The time server receive order : QUERY TIME ORDER; the count is : 4
The time server receive order : QUERY TIME ORDER; the count is : 5
The time server receive order : QUERY TIME ORDER; the count is : 6
The time server receive order : QUERY TIME ORDER; the count is : 7
。。。。。。
The time server receive order : QUERY TIME ORDER; the count is : 98
The time server receive order : QUERY TIME ORDER; the count is : 99
The time server receive order : QUERY TIME ORDER; the count is : 100
客户端输出为:
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 1
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 2
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 3
。。。。。。
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 94
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 95
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 96
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 97
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 98
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 99
NOW is: Thu Jun 08 17:47:06 CST 2017; the counter is 100
程序运行结果完全符合预期,说明使用LineBasedFrameDecoder和StringDecoder可以解决TCP粘包导致的读半包问题,不需要写额外代码,使用起来比较方便。
LineBasedFrameDecoder是依次遍历ByteBuf中的可读字节,判断看是否有”\n”或者”\r\n”,如果有,则此位置为结束位置,从可读索引到结束位置区间的字节组成一行。它是以换行符为结束标志的解码器。
StringDecoder的功能非常简单,就是接收到的对象转换为字符串,然后调用后面的Handler。
使用LineBasedFrameDecoder+StringDecoder组合就是按照行切换的文本解码器,它被设计用来支持TCP的粘包和拆包!!