前言
最近在做的项目有一个需要对接TCP的功能,网上查了一下,决定用netty来实现。
服务端
这次的需求只需要做一个服务端,话不多说,直接上代码
pom
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.18.Final</version>
</dependency>
Netty.java
public class Netty {
private static Logger log = LoggerFactory.getLogger(Netty.class);
// 请求队列长度
private static Integer TcpWaitSize = 1024;
// 运行系统
private static String system = "linux";
private static NettyServerInit nettyServerChannelInitializer = new NettyServerInit();
private ConcurrentHashMap<Integer, ChannelFuture> futures = new ConcurrentHashMap<Integer, ChannelFuture>();
private ConcurrentHashMap<Integer, List<EventLoopGroup>> eventLoopGroups = new ConcurrentHashMap<Integer, List<EventLoopGroup>>();
private ServerBootstrap serverBootstrap = new ServerBootstrap();
private EventLoopGroup bossGroup = null;
private EventLoopGroup workerGroup = null;
private LinkedList<Integer> ports = new LinkedList<>();
public void addPort(Integer port) {
ports.add(port);
}
/**
* 停止监听指定端口
*/
public int stop(int port) {
if (futures.containsKey(port)) {
ChannelFuture future = futures.get(port);
future.channel().closeFuture();
futures.remove(port);
List<EventLoopGroup> list = this.eventLoopGroups.get(port);
for (int i = 0; i < list.size(); i++) {
list.get(i).shutdownGracefully();
}
eventLoopGroups.remove(port);
log.info(">>>>>>>>>> 关闭对端口<{}>的监听", port);
}
return port;
}
/**
* 停止监听所有端口
*/
public void stopAll() {
ports.removeIf(s -> s == stop(s));
}
/**
* 启动监听
*
* @param ports 端口通过‘,’拼接
*/
public void start(String ports) {
log.info(">>>>>>>>>>>>>>> netty server 启动中......");
String[] temp = ports.split(",");
try {
for (int i = 0; i < temp.length; i++) {
int port = Integer.parseInt(temp[i]);
start(port);
}
} catch (Exception e) {
log.error(">>>>>>>>>> netty server 启动异常{}", e);
} finally {
}
log.info(">>>>>>>>>>>>>>> netty server 启动成功");
}
/**
* 监听某一端口
*/
public void start(int port) {
List<EventLoopGroup> list = baseData();
ChannelFuture future = serverBootstrap.bind(port);
// 关联端口和通道
futures.put(port, future);
// 关联端口和工作组
eventLoopGroups.put(port, list);
future.addListener(f -> {
if (f.isSuccess()) {
log.info(">>>>>>>>>> netty server 监听端口<{}>成功", port);
} else {
log.info(">>>>>>>>>> netty server 监听端口<{}>失败", port);
}
});
addPort(port);
}
public List<EventLoopGroup> baseData() {
//存放NioServerSocketChannel.class 或者 EpollServerSocketChannel.class
List<Object> serverSocketChannelList = new ArrayList();
List<EventLoopGroup> eventLoopGroups;
//Group:群组,Loop:循环,Event:事件
//Netty内部都是通过线程在处理各种数据,EventLoopGroup就是用来管理调度他们的,注册Channel,管理他们的生命周期。
//NioEventLoopGroup是一个处理I/O操作的多线程事件循环
String name = System.getProperties().getProperty("os.name").toLowerCase();
if (name.contains(system)) {
//如果程序在linux上运行,可以使用EpollEventLoopGroup,从而获得更好的性能、更少的GC和更高级的特性,而这些特性只在linux上可用
bossGroup = new EpollEventLoopGroup();
workerGroup = new EpollEventLoopGroup();
serverSocketChannelList.add(EpollServerSocketChannel.class);
eventLoopGroups = Stream.of(bossGroup, workerGroup).collect(Collectors.toList());
log.info(">>>>>>>>>> netty server 使用epoll模式(仅linux系统使用)");
} else {
bossGroup = new NioEventLoopGroup();
workerGroup = new NioEventLoopGroup();
serverSocketChannelList.add(NioServerSocketChannel.class);
eventLoopGroups = Stream.of(bossGroup, workerGroup).collect(Collectors.toList());
log.info(">>>>>>>>>> netty server 使用nio模式");
}
try {
ServerBootstrap sbs = new ServerBootstrap();
this.serverBootstrap = sbs.group(bossGroup, workerGroup) //绑定线程池
.channel((Class<? extends ServerChannel>) serverSocketChannelList.get(0)) // 指定使用的channel
.option(ChannelOption.SO_BACKLOG, TcpWaitSize) //当处理线程都忙碌时候,临时存放已完成三次握手的请求的队列的最大长度
.childOption(ChannelOption.SO_KEEPALIVE, true) //保持长连接
.childOption(ChannelOption.TCP_NODELAY, true) //true防止数据传输延迟 如果false的话会缓冲数据达到一定量在flush,降低系统网络调用(具体场景)
//FixedRecvByteBufAllocator:固定长度的接收缓冲区分配器,由它分配的ByteBuf长度都是固定大小的,并不会根据实际数据报的大小动态收缩。但是,如果容量不足,支持动态扩展。动态扩展是Netty ByteBuf的一项基本功能,与ByteBuf分配器的实现没有关系;
//AdaptiveRecvByteBufAllocator:容量动态调整的接收缓冲区分配器,它会根据之前Channel接收到的数据报大小进行计算,如果连续填充满接收缓冲区的可写空间,则动态扩展容量。如果连续2次接收到的数据报都小于指定值,则收缩当前的容量,以节约内存。
.childOption(ChannelOption.RCVBUF_ALLOCATOR, new AdaptiveRecvByteBufAllocator(64, 1024, 65535))
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) //使用内存池
.childHandler(nettyServerChannelInitializer); //channel初始化
} catch (Exception e) {
e.printStackTrace();
}
return eventLoopGroups;
}
}
NettyChannelUtil.java
public class NettyChannelUtil {
//活跃通道列表 channelId:channel
public static ConcurrentHashMap<String, Channel> sessionChannelMap = new ConcurrentHashMap<String, Channel>();
//桩编号和通道id映射 pileCode:channelId
public static ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>();
//保存连接时间 channelId:time
public static ConcurrentHashMap<String, String> mapTime = new ConcurrentHashMap<String, String>();
/**
* 获取所有连接
*/
public static Map<String, Channel> channelAll() {
return sessionChannelMap;
}
/**
* 获取所有连接的时间
*/
public static Map<String, String> channelConnectTimeAll() {
return mapTime;
}
/**
* 获取所有映射的桩编号
*/
public static Map<String, String> channelPileCodeAll() {
return map;
}
/**
* 获取连接时间
*/
public static String getTime(String channelId) {
return mapTime.get(channelId);
}
/**
* 获取连接对应的桩编号
*/
public static String getPileCode(String channelId) {
return map.get(channelId);
}
/**
* 获取连接数
*/
public static Integer getConnectNumber() {
return sessionChannelMap.size();
}
/**
* 添加连接
*/
public static boolean addConnect(Channel channel) {
String channelId = channel.id().asLongText();
//作为主动请求的依据,若是重连会覆盖
channelAll().put(channelId, channel);
channelConnectTimeAll().put(channelId, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
return true;
}
/**
* 移除连接
*/
public static boolean deleteConnect(Channel channel) {
String channelId = channel.id().asLongText();
channelAll().remove(channelId);
channelConnectTimeAll().remove(channelId);
channelPileCodeAll().remove(channelId);
return true;
}
/**
* @return java.util.List<java.lang.String>
* @Author xiongchuan
* @Description 根据value获取值
* @Date 2020/5/7 23:44
* @Param [value]
**/
public static List<String> getByValue(String value) {
List<String> list = new ArrayList<>();
for (String key : map.keySet()) {
if (map.get(key).equals(value)) {
list.add(key);
}
}
return list;
}
}
NettyHandler.java
@ChannelHandler.Sharable
public class NettyHandler extends ChannelInboundHandlerAdapter {
private static Logger log = LoggerFactory.getLogger(NettyHandler.class);
/**
* channelAction
* channel 通道 action 活跃的
* 当客户端主动链接服务端的链接后,这个通道就是活跃的了。也就是客户端与服务端建立了通信通道并且可以传输数据
*
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
log.info("================通道活跃中....========================hashcode值:{}", this.hashCode());
try {
NettyChannelUtil.addConnect(ctx.channel());
} catch (Exception e) {
log.info("未知错误!{}", e);
}
}
/**
* channelInactive
* channel 通道 Inactive 不活跃的
* 当客户端主动断开服务端的链接后,这个通道就是不活跃的。也就是说客户端与服务端的关闭了通信通道并且不可以传输数据
*
* @param ctx
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) {
log.warn("--------Netty Disconnect Client IP is :{} {} --------", ctx.channel().id().asShortText(), ctx.channel().remoteAddress());
removeChannel(ctx.channel());
ctx.close();
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
log.info("================Netty读取信息已经完成!========================");
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
log.error("--------Netty Exception ExceptionCaught :{} {} =======================\n", ctx.channel().id().asShortText(), cause.getMessage());
cause.printStackTrace();
ctx.close();
}
/**
* 检测指定时间内无读写操作时触发,此设置在pipeline链路中设置
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
//判断事件是不是IdleStateEvent事件,然后再判断是否为读空闲or写空闲or读写空闲,是就做相应处理,不是就把事件透传给下一个处理类
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
ctx.close();
log.info("================服务器检测到未在指定时间内接收到客户端【{}】的数据,关闭此通道!\r\n", ctx.channel().id().asShortText());
}
} else {
super.userEventTriggered(ctx, evt);
}
}
/**
* 移除通道
*
* @param channel
*/
public void removeChannel(Channel channel) {
try {
//去除不活跃的通道映射
NettyChannelUtil.deleteConnect(channel);
log.info("--------当前活跃通道总数:{}", NettyChannelUtil.getConnectNumber());
} catch (Exception e) {
log.info("未知错误!error:{}", e);
} finally {
}
}
/**
* 功能:读取服务器发送过来的信息
*
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
log.info("================通道读取服务器发送过来的信息========================");
try {
//String data = (String) msg;
log.info("原始报文:{}", msg);
byte req[]=(byte[]) msg;
String message=ConversionUtil.bytesToHexString(req);
String clientId = ctx.channel().id().asLongText();
/**
* 使用不同于nioEventLoopGroup的线程池去处理耗时操作,避免阻塞nioEventLoopGroup线程池
*/
ThreadUtil.execAsync(() -> {
handleMessageStart(ctx, req,message, clientId);
});
} catch (Exception e) {
log.info("================通道读取消息异常:{}========================", e.getMessage());
} finally {
ReferenceCountUtil.release(msg);
}
}
//处理业务逻辑之前的判断
private void handleMessageStart(ChannelHandlerContext ctx, byte[] req,String data, String clientId) {
//do something
}
}
注:这里是因为是十六进制的byte数组所以这么转换,应用的时候需要根据实际需求来
NettyServerInit.java
public class NettyServerInit extends ChannelInitializer<SocketChannel> {
private static final Logger LOGGER = LoggerFactory.getLogger(NettyServerInit.class);
private static final NettyHandler serverHandler = new NettyHandler();
// redis设置过期时间
public static Integer redisTimeout = 100;
// 连接断开时间
public static Integer readerIdleTime = 300;
private static Integer writerIdleTime = 0;
private static Integer allIdleTime = 0;
// 解决粘包分隔符
public static String delimiter = "_$";
@Override
protected void initChannel(SocketChannel channel) {
InetSocketAddress socketAdd = channel.remoteAddress();
LOGGER.info("================检测到socket客户端链接到本服务器, IP为:" + socketAdd.getAddress().getHostAddress() + ", Port为:" + socketAdd.getPort() + " hashCode:" + this.hashCode() + "========================");
ChannelPipeline pipeline = channel.pipeline();
//心跳设置
pipeline.addLast(new IdleStateHandler(readerIdleTime, writerIdleTime, allIdleTime, TimeUnit.SECONDS));
//编码
pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
//解码
//pipeline.addLast(new DelimiterBasedFrameDecoder(1024*1024, Unpooled.wrappedBuffer(delimiter.getBytes())));
pipeline.addLast(new NettyDecoder());
//添加处理类
pipeline.addLast(serverHandler);
}
}
注:NettyDecoder是自己实现的解码校验类,继承自ByteToMessageDecoder就可以
启动方式
NETTY=new Netty();
NETTY.start(8080);