1、TCP粘包和拆包
- 熟悉TCP编程的可能都知道,无论是服务端还是客户端,当我们读取或者是发送消息的时候,都需要考虑TCP底层的粘包和拆包机制。
- TCP是个“流”协议,所谓流,就是没有界限的一串数据。大家可以想想河里的流水,它们是连成一片的,其间并没有分界线。再加上网络上MTU的往往小于在应用处理的消息数据,所以就会引发一次接收的数据无法满足消息的需要,导致粘包的存在。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实现情况进行包的拆分,所以在业务上认为,一个完整的包可能会被TCP拆成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
- 处理粘包的唯一方法就是制定应用层的数据通讯协议,通过协议来规范现有接收的数据是否满足消息数据的需要。
2、解决办法
2.1、消息定长,报文大小固定长度,不够空格补全,发送和接收方遵循相同的约定,这样即使粘包了通过接收方编程实现获取定长报文也能区分。
2.2、包尾添加特殊分隔符,例如每条报文结束都添加回车换行符(例如FTP协议)或者指定特殊字符作为报文分隔符,接收方通过特殊分隔符切分报文区分。
2.3、将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段,通常设计思路为消息头的第一个字段使用int来表示消息的总长度
3、Netty ByteBuf
当我们进行数据传输的时候,往往需要使用到缓冲区,常用的缓冲区就是JDK NIO类库提供的java.nio.Buffer。从功能角度而言,ByteBuffer完全可以满足NIO编程的需要,但是NIO编程过于复杂,也存在局限性。ByteBuffer长度固定,一旦分配完成,不可动态修改。
JDK ByteBuffer由于只有一个位置指针用于处理读写操作,因此每次读写的时候都需要额外调用flip(),否则功能将出错。
Netty ByteBuf提供了两个指针用于支持顺序读取和写入操作:readerIndex用于标识读取索引,writerIndex用于标识写入索引。两个位置指针将ByteBuf缓冲区分割成三个区域。
(1) readerIndex到writerIndex之间的空间为可读的字节缓冲区
(2) writerIndex到capacity之间为可写的字节缓冲区
(3) 0到readerIndex之间是已经读取过的缓冲区
下面是Netty ByteBuf的常用的几个方法。
isReadable: 缓冲区是否可读
markReaderIndex: 记录当前缓冲区读指针的位置
resetReaderIndex: 重置缓冲区至markReaderIndex的位置处(markReaderIndex的初始位置为0)
readerIndex: 当前缓冲区读指针的位置
readableBytes: 当前缓冲区可读的字节数
read*: 读取数据
写方法和上面类似
备注: 当一开始使用resetReaderIndex重置缓冲区读指针的位置时候,读指针会被重置0;
当使用markReaderIndex记录过当前缓冲区读指针的位置时,再使用resetReaderIndex重置缓冲区读指针的位置,读指针会被重置到markReaderIndex记录的位置处。
3、自定义协议封装类.
/**
* User: yzc
* Date: 2017/12/25 18:48
* Comment: 自定义协议封装
* +------------+------------+------------+------------+
* |协议开始标志 | 长度 | 数据 | 校验位(异或)|
* +------------+------------+------------+------------+
* | 0x7F | 2字节 | 数据 | 1字节 |
* +------------+------------+------------+------------+
*/
public class SmartProtocol {
/**
* 自定义协议的开始位置
*/
private static final byte HEAD_DATA = 0x7F;
/**
* 消息的长度
*/
private short contentLength;
/**
* 消息的内容
*/
private byte[] content;
/**
* 校验位
*/
private byte checkBit;
public SmartProtocol(short contentLength, byte[] content, byte checkBit) {
this.contentLength = contentLength;
this.content = content;
this.checkBit = checkBit;
}
}
4、自定义协议的编码器
/**
* User: yzc
* Date: 2017/12/25 19:01
* Comment: 自定义编码器
*/
public class SmartEncoder extends MessageToByteEncoder<SmartProtocol> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, SmartProtocol smartProtocol, ByteBuf byteBuf) throws Exception {
//1、写入消息头开始标志
byteBuf.writeByte(SmartProtocol.getHeadData());
//2、写入消息长度
byteBuf.writeShort(smartProtocol.getContentLength());
//3、写入数据
byteBuf.writeBytes(smartProtocol.getContent());
//4、写入校验位数据
byteBuf.writeByte(smartProtocol.getCheckBit());
}
}
5、自定义协议的解码器
/**
* User: yzc
* Date: 2017/12/25 19:07
* Comment: 自定义解码器
*/
public class SmartDecoder extends ByteToMessageDecoder {
private static Logger logger = LogManager.getLogger();
/**
* 协议开始的标准HEAD_DATA,byte类型,占据1个字节.
* 表示数据的长度contentLength,short类型,占据2个字节.
*/
public final int BASE_LENGTH = 1 + 2;
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
// 可读长度必须大于基本长度
if (byteBuf.readableBytes() >= BASE_LENGTH) {
// 防止socket字节流攻击/客户端传来的数据过大
if (byteBuf.readableBytes() > 128) {
byteBuf.skipBytes(byteBuf.readableBytes());
}
// 记录包头开始的index
int beginReader;
while (true) {
// 获取包头开始的index
beginReader = byteBuf.readerIndex();
// 标记包头开始的index
byteBuf.markReaderIndex();
// 读到了协议的开始标志,结束while循环
if (byteBuf.readByte() == SmartProtocol.getHeadData()) {
break;
}
// 未读到包头,略过一个字节
// 每次略过,一个字节,去读取,包头信息的开始标记
byteBuf.resetReaderIndex();
byteBuf.readByte();
// 当略过一个字节之后,数据包的长度可能又变得不满足
// 此时应该结束。等待后面的数据到达
if (byteBuf.readableBytes() < BASE_LENGTH) {
return;
}
}
// 消息的长度
short contentLength = byteBuf.readShort();
// 判断请求数据包数据是否到齐
if (byteBuf.readableBytes() < contentLength) {
// 还原读指针
byteBuf.readerIndex(beginReader);
return;
}
// 读取data数据
byte[] data = new byte[contentLength];
byteBuf.readBytes(data);
// 判断校验位请求数据包数据是否到齐
if (byteBuf.readableBytes() < 1) {
// 还原读指针
byteBuf.readerIndex(beginReader);
return;
}
// 读取校验位数据
byte checkBit = byteBuf.readByte();
//校验判断数据是否出错
byte checkResult = CheckUtil.getXorCheck(contentLength, data);
logger.debug("receive checkBit:" + checkBit + " calculate checkResult:" + checkResult);
if (checkBit == checkResult) {
SmartProtocol protocol = new SmartProtocol(contentLength, data, checkBit);
list.add(protocol);
}
}
}
}
6、服务端加入协议的编/解码器
@Service("neoChannelInitializer")
public class NeoChannelInitializer extends ChannelInitializer<SocketChannel> implements ApplicationContextAware {
private static ApplicationContext context;
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//tcpServerHandler必须是每次请求都重新创建一个,底层pipiline不是可共享的,否则多次请求下将报错
TcpServerHandler tcpServerHandler = ((TcpServerHandler) context.getBean("tcpServerHandler"));
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("smartDecoder", new SmartDecoder());
pipeline.addLast("smartEncoder", new SmartEncoder());
// //心跳监测0表示关闭 读超时:5s内没有数据接收,写超时:没有数据发送 全部空闲时间:没有数据接收或者发送
pipeline.addLast("ping", new IdleStateHandler(5, 0, 0));
pipeline.addLast(tcpServerHandler);
}
}
7、自定义Handler
/**
* User: yzc
* Date: 2017/12/24 15:37
* Comment: Tcp服务器实际的业务判断处理
*/
//@Shareable
public class TcpServerHandler extends ChannelHandlerAdapter {
private static Logger logger = LogManager.getLogger();
@Autowired
private AgpsUserServiceI agpsUserServiceImpl;
@Autowired
private TechtotopServiceI techtotopServiceImpl;
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
logger.debug("CLIENT" + ctx.channel().remoteAddress().toString() + " 接入连接");
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.close();
logger.debug("CLIENT" + ctx.channel().remoteAddress().toString() + " 断开连接");
super.channelInactive(ctx);
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
super.userEventTriggered(ctx, evt);
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
if (e.state() == IdleState.READER_IDLE) {
logger.debug("读数据超时");
ctx.close();
} else if (e.state() == IdleState.WRITER_IDLE) {
logger.debug("写数据超时");
ctx.close();
} else {
logger.debug("总读写数据超时");
ctx.close();
}
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
SmartProtocol reqSmartProtocol = (SmartProtocol) msg;
logger.debug("receive client : [" + reqSmartProtocol.toString() + "]");
ByteBuf reqBuffer = Unpooled.copiedBuffer(reqSmartProtocol.getContent());
//判断请求字符串是不是指定的格式
...
具体逻辑
...
ReferenceCountUtil.release(reqBuffer); //reqBuffer使用完后手动回收,避免内存泄漏。
....
SmartProtocol smartProtocol = techtotopServiceImpl.getTcpEphemeris();
ctx.writeAndFlush(smartProtocol).addListener(ChannelFutureListener.CLOSE);
}
/**
* 发送响应发回客户端
* @param ctx
* @param resp
*/
private void sendResponse(ChannelHandlerContext ctx, String resp) {
if (resp == null) {
resp = "";
}
short contentLength = Short.parseShort(String.valueOf(resp.length()));
byte checkBit = CheckUtil.getXorCheck(contentLength, resp.getBytes());
SmartProtocol smartProtocol = new SmartProtocol(contentLength, resp.getBytes(), checkBit);
ctx.writeAndFlush(smartProtocol).addListener(ChannelFutureListener.CLOSE); //注册监听,服务端数据发送完毕后,主动关闭链路
}
8、Server类
//用于分配处理业务线程的线程组个数
protected static final int BIZGROUPSIZE = Runtime.getRuntime().availableProcessors() * 2; //默认
//业务出现线程大小
protected static final int BIZTHREADSIZE = 8;
public void bind(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(BIZGROUPSIZE);
EventLoopGroup workerGroup = new NioEventLoopGroup(BIZTHREADSIZE);
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.option(ChannelOption.SO_KEEPALIVE, false)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000)
.option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 64 * 1024) //水位高值
.option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 32 * 1024) //水位低值
.option(ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT) //容量可自动动态调整的接收缓冲区分配器,减少内存使用
.childHandler(neoChannelInitializer);
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
logger.info("TcpServer 已关闭");
}
}
9、优化
ChannelOption.SO_BACKLOG --等待队列的大小
ChannelOption.SO_KEEPALIVE, false --是否保持长连接
ChannelOption.CONNECT_TIMEOUT_MILLIS --客户端连接超时时间
ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 64 * 1024 //水位高值
ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 32 * 1024 //水位低值 --设置后可判断buffer的isWritable是false还是true,从而控制读写,防止buffer不断地增长占用太多系统资源
true。所以应用应该判断isWritable
ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT //容量可自动动态调整的接收缓冲区分配器,减少内存使用
//心跳监测0表示关闭 读超时:5s内没有数据接收,写超时:没有数据发送 全部空闲时间:没有数据接收或者发送
pipeline.addLast("ping", new IdleStateHandler(5, 0, 0));
//发送完数据后,立刻关闭链接
ctx.writeAndFlush(smartProtocol).addListener(ChannelFutureListener.CLOSE);
10、netty和spring整合
要点:对于每一个客户端,我们都是new一个handler处理,所以在整合Spring的时候一定要注意,每次新的处理请求,一定要重新创建一个handler实例:tcpServerHandler必须是每次请求都重新创建一个,底层pipiline不是可共享的,否则多次请求下将报错。
在Spring中有一个设置bean的scope为prototype表示多实例模式。这里一定要主要Spring的prototype模式的坑,解决办法参考:prototype作用域的“坑”
我的实现:
<bean id="tcpServerHandler" class="com.neo.agps.tcp.server.TcpServerHandler" scope="prototype"/>
获取handler时,实现spring的ApplicationContextAware接口的setApplicationContext方法;
每次需要handler都这样:context.getBean("tcpServerHandler"));
11、server的自启动
这里还是使用Spring的扩展,tcpserver实现ApplicationListener接口
然后实现onApplicationEvent方法:
@Override
public void onApplicationEvent(ContextRefreshedEvent evt) {
if (evt.getApplicationContext().getParent() == null) {
...
}
}
注意一定要判断父容器,否则可能会启动两次
参考文章:
Netty之解决TCP粘包拆包(自定义协议)