SpringBoot + Netty 实现 TCP拆包粘包处理、TCP恶意连接拦截
================ 代码实现过程 ================
NettyServer:创建TCP服务
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
/**
* @author: Owen
* @date: 2020/11/26
* @description:TCP服务
*/
@Slf4j
public class NettyServer {
private void startServer() {
//初始化Netty线程池
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workGroup);
b.channel(NioServerSocketChannel.class); //NIO非阻塞
b.option(ChannelOption.SO_BACKLOG, 1024); //连接缓冲池的大小
b.childOption(ChannelOption.TCP_NODELAY, true); //关闭延迟发送
b.childOption(ChannelOption.SO_KEEPALIVE, true); //维持链接的活跃,清除死链接
b.childHandler(new DoorInitChannel()); //连接通道处理器
//绑定端口,调用sync()方法来执行同步阻塞,直到绑定完成
ChannelFuture sync = b.bind(9701).sync();
//获取该Channel的CloseFuture,并且阻塞当前线程直到绑定的端口关闭才会执行关闭通道
sync.channel().closeFuture().sync();
} catch (Exception e) {
log.error("TCP server init faild: "+e.getMessage();
e.printStackTrace();
} finally {
cleanUp(bossGroup, workGroup);
}
}
/**
* @author: Owen
* @date: 2020/11/26
* @description:清理Netty线程池
*/
private void cleanUp(EventLoopGroup bossGroup, EventLoopGroup workGroup) {
bossGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
/**
* @author: Owen
* @date: 2020/11/26
* @description:TCP服务初始化
*/
public void init() {
new Thread(() -> {
startServer();
}).start();
}
}
ChannelInit:通道连接事件
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.timeout.IdleStateHandler;
/**
* @author: Owen
* @date: 2020/11/26
* @description:连接通道初始化事件
*/
public class ChannelInit extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel channel){
//TCP消息解码器 处理(拆包\粘包)
channel.pipeline().addLast("decoder", new BytePacketDecoder());
//TCP连接活跃检测 60秒无活跃操作则 触发该事件
channel.pipeline().addLast(new IdleStateHandler(60, 0, 0));
//TCP事件监听、业务消息处理、连接\断开监听
channel.pipeline().addLast("handler", new MessageHandler());
}
}
NettyEvent :Netty连接事件 实体类
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author: Owen
* @date: 2021/9/27
* @description:Netty设备连接事件
*/
@Data
public class NettyEvent {
//TCP建立连接时间
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date connectDate;
//连接次数统计 原子整数类型 保证对数字的操作是线程安全
private AtomicInteger connectCount;
public NettyEvent() {
}
public NettyEvent(Date connectDate, AtomicInteger connectCount) {
this.connectDate = connectDate;
this.connectCount = connectCount;
}
}
ClientEventManage :客户端事件管理
import com.google.common.cache.*;
import io.netty.channel.Channel;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author: Owen
* @date: 2020/11/2
* @description:TCP客户端事件管理
*/
@Slf4j
public class ClientEventManage {
//TCP连接标识
public static String CONNECT_CODE = "TCP_CONNECT_";
//Netty连接事件缓存(记录TCP连接)
private static LoadingCache<String, NettyEvent> connectEvent;
/**
* @Author: Owen
* @Date: 2022/7/29
* @Description:Netty连接校验(防止恶意连接)
*/
public static void connectCheck(Channel ctx) {
NettyEvent cahche = null;
try {
//客户端请求IP地址
String clientIP = locationInfo(ctx);
ExceptionUtil.isBlank(clientIP, "TCP client ip get is null !");
//构建缓存Key
String key = (CONNECT_CODE + clientIP);
//查看当前IP 是否存在连接缓存
cahche = queryConnectCache(key);
//存在缓存
if (!Objects.isNull(cahche)) {
//连接统计 增量+1
int connectCount = cahche.getConnectCount().incrementAndGet();
log.info("TCP client iP:[" + getClientIp(ctx) + "] connect count:[" + connectCount + "], first connect time: " + DateUtil.getFormatTime(cahche.getConnectDate()));
//60秒钟缓存失效之前,TCP请求连接限制30次
if (30 <= connectCount) {
//连接统计 减量-1
cahche.getConnectCount().decrementAndGet();
log.error("TCP client iP:[" + getClientIp(ctx) + "] connect number exceed the 30 limit!");
//关闭本次 TCP连接请求
ctx.close();
}
}
//不存在缓存
else {
//构建 连接缓存数据 记录当前时间,统计次数为1
cahche = new NettyEvent(new Date(), new AtomicInteger(0));
//设置连接缓存(默认60秒失效)
connectEvent().put(key, cahche);
}
} catch (Exception e) {
e.printStackTrace();
throw new BusinessException("TCP connect check faild: " + e.getMessage());
}
}
/**
* @author: Owen
* @date: 2021/9/25
* @description:获取请求IP
*/
public static String getClientIp(Channel ctx) {
try {
InetSocketAddress ipSocket = (InetSocketAddress) ctx.remoteAddress();
InetAddress address = ipSocket.getAddress();
StringBuffer value = new StringBuffer(address.getHostAddress());
value.append(":");
value.append(ipSocket.getPort());
return value.toString();
} catch (Exception e) {
e.printStackTrace();
log.error("Get tcp request location Faild: " + e.getMessage());
}
return null;
}
/**
* @author: Owen
* @date: 2021/9/25
* @description:获取请求地址详情
*/
public static String locationInfo(Channel ctx) {
try {
InetSocketAddress ipSocket = (InetSocketAddress) ctx.remoteAddress();
InetAddress address = ipSocket.getAddress();
StringBuffer value = new StringBuffer(address.getHostAddress());
return value.toString();
} catch (Exception e) {
e.printStackTrace();
log.error("Get tcp request location Faild: " + e.getMessage());
}
return null;
}
/**
* @Author: Owen
* @Date: 2022/7/29
* @Description:查询连接缓存
*/
public static NettyEvent queryConnectCache(String key) {
NettyEvent cahceValue = null;
try {
cahceValue = connectEvent().get(key);
} catch (Exception e) {
}
return cahceValue;
}
/**
* @Author: Owen
* @Date: 2022/7/29
* @Description:TCP连接缓存(单例模式 懒加载)
*/
public static LoadingCache<String, NettyEvent> connectEvent() {
try {
//第一次判空
if (Objects.isNull(connectEvent)) {
//保证线程安全
synchronized (LoadingCache.class) {
//第二次判空,保证单例对象的唯一性,防止第一次有多个线程进入第一个if判断
if (Objects.isNull(connectEvent)) {
try {
//构建定时缓存
connectEvent = buildCache(new CacheLoader<String, NettyEvent>() {
@Override
public NettyEvent load(String key) {
//值为null触发该事件
return null;
}
//超过60秒 没有(读\写)操作,则自动清除
}, 60, 60);
} catch (Exception e) {
log.error("TCP connect cache, build faild: " + e.getMessage());
}
}
}
}
} catch (Exception e) {
log.error("TCP connect cache exception" + e.getMessage());
}
return connectEvent;
}
/**
* @author: Owen
* @date: 2020/12/4
* @description:构建定时缓存
*/
private static LoadingCache<String, NettyEvent> buildCache(CacheLoader<String, NettyEvent> cacheLoader, long expireAfterAccess, long expireAfterWrite) {
try {
LoadingCache<String, NettyEvent> cache = CacheBuilder.newBuilder()
//10W容量大小,在缓存项接近该大小时, Guava开始回收旧的缓存项
.maximumSize(100000)
//活跃时间 设置时间对象没有被(读/写)访问,超过时间则从中删除(在另外的线程里面不定期维护)
.expireAfterAccess(expireAfterAccess, TimeUnit.SECONDS)
//失效时间 设置缓存在写入之后 缓存数据过期时间
.expireAfterWrite(expireAfterWrite, TimeUnit.SECONDS)
//移除监听器,缓存项 失效|| 被移除 会触发
.removalListener(new RemovalListener<String, NettyEvent>() {
@Override
public void onRemoval(RemovalNotification<String, NettyEvent> rn) {
//逻辑操作
// log.error("TCP connect cache :[" + rn.getKey() + "] timeout to remove!");
}
})
//开启Guava Cache的统计功能
.recordStats()
.build(cacheLoader);
return cache;
} catch (Exception e) {
log.error("Loading cache build exception: " + e.getMessage());
return null;
}
}
/**
* @author: Owen
* @date: 2020/12/4
* @description:时间转换
*/
public static String getFormatTime(Date date) {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return simpleDateFormat.format(date);
}
}
MessageHandler:消息处理器
/**
* @author: Owen
* @date: 2020/11/26
* @description: TCP事件监听处理器
*/
@Slf4j
public class MessageHandler extends SimpleChannelInboundHandler<Object> {
/**
* @author: Owen
* @date: 2021/9/25
* @description: TCP连接注册事件(拦截连接请求)
*/
@Override
public void channelRegistered(ChannelHandlerContext ctx) {
log.info("TCP client:" + ctx.channel() + " =========》》》》》request!");
//TCP请求验证
ClientEventManage.connectCheck(ctx.channel());
}
/**
* @author: Owen
* @date: 2021/9/25
* @description: TCP连接事件(拦截连接成功)
*/
@Override
public void channelActive(ChannelHandlerContext ctx){
log.info("TCP client:" + ctx.channel() + " connect success!");
}
/**
* @author: Owen
* @date: 2021/9/25
* @description: TCP断开连接(拦截连接断开)
*/
@Override
public void channelInactive(ChannelHandlerContext ctx){
log.error("TCP client:" + ctx.channel() + " connect close!");
}
/**
* @author: Owen
* @date: 2021/9/24
* @description: 消息监听(业务处理入口)
*/
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
try {
log.info("TCP messge :"+msg);
} catch (Exception e) {
log.error("TCP message read exception: " + e.getMessage());
} finally {
//清理消息
ReferenceCountUtil.release(msg);
}
}
/**
* @author: Owen
* @date: 2021/9/24
* @description: 连接活跃检测
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof IdleStateEvent) {
IdleStateEvent e = (IdleStateEvent) evt;
switch (e.state()) {
//TCP连接 空闲检测(60秒无活跃操作则)
case READER_IDLE:
log.error("TCP client:" + ctx.channel() + " connect lose efficacy!");
ctx.channel().close();
break;
case WRITER_IDLE:
//handleWriterIdle(ctx);
break;
case ALL_IDLE:
//handleAllIdle(ctx);
break;
default:
break;
}
}
}
/**
* @author: Owen
* @date: 2021/9/25
* @description: 消息结束之后时调用
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
/**
* @author: Owen
* @date: 2021/9/25
* @description: 业务异常捕获
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("TCP client:" + ctx.channel() + " business exceptions: "+ cause.getMessage());
}
}
MessageDecoder :消息解码器
TCP数据 拆包 粘包 详情图
import com.za.edu.bean.DataPacket;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
/**
*@Author Owen
*@Date 2020/8/31
*@Description TCP消息解码器(处理粘包、拆包)
*/
public class MessageDecoder extends ByteToMessageDecoder {
/**
*@Author Owen
*@Date 2020/8/31
*@Description TCP消息解码
*/
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> out) throws Exception {
byte[] head = null; //(数据包(头)有效数据)
Integer theLength = null; //客户端 推送数据包的 总长度
Integer bodyLength = null; //客户端 推送数据的 内容长度
Integer practicaLength = null; //服务端 实际接收长度
//数据解码(处理拆包、粘包)
try {
//标记当前指针位置
byteBuf.markReaderIndex();
//服务端 实际接收总长度
practicaLength = byteBuf.readableBytes();
//若服务端 实际接收总长度小于 数据头默认长度,则抛出异常
ExceptionUtil.isTrue(practicaLength < ((数据包(头)有效长度)), "Total data length error");
//遍历数据 验证当前可读数据中 数据头标识 是否存在
boolean flag = false;
//遍历所有字节
while (byteBuf.isReadable()) {
//判读是否是为 数据头标识(例如 数据包规则 以02开始 03结束)
if (byteBuf.readByte() == (数据包(头)标识 02)) {
//表示已经找到 数据头02的字节
flag = true;
//若服务端 实际接收总长度 小于 默认 (头+尾)固定长度,则抛出异常
ExceptionUtil.isTrue(practicaLength < ((数据包(头)有效长度)+(数据包(尾)有效长度)) - 1,
"Actual data length error!");
//找到开始位置 02,跳出循环
int index = (byteBuf.readerIndex() - 1);
//从开始位读取数据
byteBuf.readerIndex(index);
break;
}
//防止CPU飙高,执行线程睡眠10毫秒 释放当前线程资源
Thread.sleep(10);
}
//是否获取到完整的数据包
ExceptionUtil.isTrue(!flag, "Not found data head ! ");
//数据头
head = new byte[(数据包(头)默认有效长度)];
//读取数据头
byteBuf.readBytes(head);
//获取消息的实际长度(业务不同 规则也不同)
theLength = 根据TCP客户端 推送的消息,并解析获取到 当前数据包的实际长度;
bodyLength= 根据TCP客户端 推送的消息,减去头尾长度 得到有效数据内容的长度;
//服务端实际接收总长度 是否小于 客户端推送的数据的总长度,满足则抛出异常
ExceptionUtil.isTrue(服务端 实际接收总长度 < theLength, "Total length is less than actual length ! ");
} catch (Exception e) {
//================= 数据包 出现拆包 =================
//打印异常
e.printStackTrace();
//重置byteBuf读指针,等待后面的数据到达重组后 重新获取
byteBuf.resetReaderIndex();
return;
}
//(数据包(内容)有效数据)
byte[] body = null;
//(数据包(尾)有效数据)
byte[] foot = null;
//服务端 当前接收数据的总长度 是否大于 客户端 推送的数据的总长度 (true 则表示数据出现 粘包)
boolean ifAdhesion = (practicaLength > theLength);
//数据内容 业务校验
try {
//=========== 取出 实际有效长度的数据,如果出现粘包数据,多出部分的数据也不会被取出
//(数据包(内容)有效数据)
body = new byte[bodyLength];
//读取 内容
byteBuf.readBytes(body);
//(数据包(尾)有效数据)
foot = new byte[数据包(尾)默认长度];
//读取 内容
byteBuf.readBytes(foot);
//根据(数据包(内容)有效数据)解析出业务数据
Object message= 根据客户端消息业务规则,解析出对应的数据内容;
ExceptionUtil.isNull(message, "TCP message get is null!");
//写入数据 交给 (MessageHandler事件监听器) 处理业务逻辑
out.add(tcpEvent);
} catch (Exception e) {
//================= 数据包出现(粘包) 或 业务数据验证不通过 =================
//异常打印
e.printStackTrace();
} finally {
//是否为粘包数据,若满足粘包,由于当前并未取出粘包部分的数据,如果清理byteBuf则会丢失下一个数据包
if (!ifAdhesion) {
//非粘包 则清除当前这次接收的 数据缓存
byteBuf.clear();
}
}
}
}
ExceptionUtil :异常工具类
import lombok.extern.slf4j.Slf4j;
import org.junit.platform.commons.util.StringUtils;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.regex.Pattern;
/**
* @Author: Owen
* @Date: 2022/7/25
* @Description:异常工具类
*/
@Slf4j
public class ExceptionUtil {
public static void isTrue(Boolean boole, String msg) {
if (boole) {
log.debug(msg);
new ExceptionManage(msg);
}
}
public static void isNotTrue(Boolean boole, String msg) {
if (!boole) {
log.debug(msg);
new ExceptionManage(msg);
}
}
public static void isNull(Object obj, String msg) {
if (obj == null) {
log.debug(msg);
new ExceptionManage(msg);
}
}
public static void isNotNull(Object obj, String msg) {
if (obj != null) {
log.debug(msg);
new ExceptionManage(msg);
}
}
public static void isMatcher(String regex, String str, String msg) {
if (!Pattern.matches(regex, str)) {
log.debug(msg);
new ExceptionManage(msg);
}
}
public static void isBlank(String obj, String msg) {
if (StringUtils.isBlank(obj)) {
log.debug(msg);
new Exception(msg);
}
}
public static void isEmpty(Collection value, String msg) {
if (CollectionUtils.isEmpty(value)) {
log.debug(msg);
new ExceptionManage(msg);
}
}
public static void isEmpty(String str, String msg) {
if(StringUtils.isBlank(str)) {
log.debug(msg);
new ExceptionManage(msg);
}
}
public static void isNotEmpty(Collection value, String msg) {
if (!CollectionUtils.isEmpty(value)) {
log.debug(msg);
new Exception(msg);
}
}
public static void isNotBlank(String value, String msg){
if(StringUtils.isNotBlank(value)){
log.debug(msg);
new ExceptionManage(msg);
}
}
public static void isNonZero(Integer value, String msg) {
if (! ((null == value) || (value == 0))) {
log.debug(msg);
new ExceptionManage(msg);
}
}
public static void isNonZero(Long value, String msg) {
if (!((null == value) || (value == 0))) {
log.debug(msg);
new ExceptionManage(msg);
}
}
public static void isNonZero(Double value, String msg) {
if (!((null == value) || (value == 0))) {
log.debug(msg);
new ExceptionManage(msg);
}
}
public static void isNullOrZero(Integer value, String msg) {
if ((null == value) || (value == 0)) {
log.debug(msg);
new ExceptionManage(msg);
}
}
public static void isNullOrZero(Long value, String msg) {
if ((null == value || value == 0)) {
log.debug(msg);
new ExceptionManage(msg);
}
}
public static void isNullOrZero(Double value, String msg) {
if ((null == value) || (value == 0)) {
log.debug(msg);
new ExceptionManage(msg);
}
}
/**
* @Author: Owen
* @Date: 2022/7/25
* @Description:异常管理内部类
*/
static class ExceptionManage extends RuntimeException {
protected int code = 500;
protected String msg;
public ExceptionManage(int code,String msg) {
super(msg);
this.msg = msg;
this.code = code;
}
public ExceptionManage(String msg) {
super(msg);
this.msg = msg;
}
public ExceptionManage() {
this("服务器出了点意外...");
this.msg = "服务器出了点意外...";
}
public ExceptionManage(Exception cause) {
super(cause);
}
public ExceptionManage(String msg,Exception cause) {
super(msg,cause);
this.msg = msg;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
@Override
public String toString() {
if(StringUtils.isNotBlank(msg)) {
return "code:" + getCode() + ",msg:" + msg + ";";
}
return super.toString() + ";code:" + getCode();
}
}
}
模拟TCP恶意连接 测试结果
Jmeter模拟TCP连接,向我们TCP服务异步发起120个连接
测试结果:
只根据IP来做校验 不带端口号,因为客户端发起的 每一次TCP连接端口号都不同!