1 粘包拆包基本概念
TPC是一个面向流的协议。所谓流就是没有边界的一串数据,如同河水般连成一片,其中并没有分界线。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的具体情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆成多个包发送,也有可能把多个小包封装成一个包发送。这就是拆包和粘包的概念。
比如向对方发送信息:Good Morning Sit down please。先向对方问早,再请对方坐下。但实际情况有可能这样:第一次接收:Good Morning Sit
第二次接收:down please
2 解决粘包拆包的途径
TCP是面向流的协议,消息中间没有明显的界限。那为了对消息进行区分,只能依靠上层的应用协议,往往采取如下方式:
[1] 消息长度固定。累计读取到长度总和为定长的LEN的报文后,就认为读取到一个完整的消息。将计数器置位,重新开始读取下个数据报。[2] 将回车换行符作为消息结束符。如FTP协议,这种方式在文本协议中应用广泛。
[3] 将特殊的分隔符作为消息的结束标志。回车换行符就是一种特殊的结束符。
[4] 通过在消息头中定义长度字段来标识消息的总长度。
Netty提供了对应的解码器:LineBaseFrameDecoder、DelimiterBaseFrameDecoder、FixedLengthFrameDecoder等。具体示例参考《Netty权威指南》。下面分析一个粘包拆包的示例,和自定义解码器。
3 粘包拆包实例
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
public class Server {
public static void main(String[] args) {
// 服务类
ServerBootstrap bootstrap = new ServerBootstrap();
// boss线程监听端口,worker线程负责数据读写
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();
// 设置niosocket工厂
bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
// 设置管道的工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("handler1", new HelloHandler());
return pipeline;
}
});
bootstrap.bind(new InetSocketAddress(10101));
System.out.println("start!!!");
}
}
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
public class HelloHandler extends SimpleChannelHandler {
private int count = 1;
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
throws Exception {
ChannelBuffer buffer = (ChannelBuffer) e.getMessage();
byte[] array = buffer.array();
System.out.println(new String(array) + " " + count);
count++;
}
}
import java.net.Socket;
public class Client {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 10101);
String message = "hello";
for (int i = 0; i < 20; i++) {
socket.getOutputStream().write(message.getBytes());
}
socket.close();
}
}
输出结果1hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello 1
输出结果2
hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohe 1
llohellohellohellohello 2
4 自定义处理器
在本例中采取了固定长度的方式解决粘包拆包问题,有以下几个显著的变化:
[1] 在Client发送时,设置了一个4字节的长度头,该长度头记录了内容的长度。
[2] Server端设置了MyDecoder继承自FrameDecoder。注释非常清晰。
[3] 在MyDecoder中对字节数组做了处理,包装成了String类型。所以下一个handler直接处理String类型即可。
import java.net.Socket;
import java.nio.ByteBuffer;
public class Client {
public static void main(String[] args) throws Exception {
Socket socket = new Socket("127.0.0.1", 10101);
// 消息内容
String message = "hello";
byte[] bytes = message.getBytes();
// 构造字节数组,长度为(4+内容长度)
// 其中4个字节长度字段是int为4个字节
ByteBuffer buffer = ByteBuffer.allocate(4 + bytes.length);
// 设置长度字段(仅仅是内容的长度)
buffer.putInt(bytes.length);
// 设置内容
buffer.put(bytes);
byte[] array = buffer.array();
for (int i = 0; i < 20; i++) {
socket.getOutputStream().write(array);
}
socket.close();
}
}
import java.net.InetSocketAddress;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
public class Server {
public static void main(String[] args) {
//服务类
ServerBootstrap bootstrap = new ServerBootstrap();
//boss线程监听端口,worker线程负责数据读写
ExecutorService boss = Executors.newCachedThreadPool();
ExecutorService worker = Executors.newCachedThreadPool();
//设置niosocket工厂
bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));
//设置管道的工厂
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() throws Exception {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", new MyDecoder());
pipeline.addLast("handler1", new HelloHandler());
return pipeline;
}
});
bootstrap.bind(new InetSocketAddress(10101));
System.out.println("start!!!");
}
}
import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.handler.codec.frame.FrameDecoder;
public class MyDecoder extends FrameDecoder {
@Override
protected Object decode(ChannelHandlerContext ctx, Channel channel,
ChannelBuffer buffer) throws Exception {
// 基本长度(至少要有长度头那么长)
int baseLength = 4;
if (buffer.readableBytes() > baseLength) {
// 防止Socket攻击
if (buffer.readableBytes() > 2048) {
buffer.skipBytes(buffer.readableBytes());
}
// 标记
buffer.markReaderIndex();
// 长读取度头
int length = buffer.readInt();
// 长度不够
if (buffer.readableBytes() < length) {
// 还原到上述标记位置
buffer.resetReaderIndex();
// 缓存当前剩余的buffer数据,等待剩下数据包到来
return null;
}
// 读数据
byte[] bytes = new byte[length];
buffer.readBytes(bytes);
// 往下传递对象
return new String(bytes);
}
// 缓存当前剩余的buffer数据,等待剩下数据包到来
return null;
}
}
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
public class HelloHandler extends SimpleChannelHandler {
private int count = 1;
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e)
throws Exception {
System.out.println(e.getMessage() + " " +count);
count++;
}
}