SpringBoot整合Netty
公司是做人工智能人脸识别技术落地的,上周确定要做设备的远程控制,android端推荐使用netty,如是坑开始了~~~
第1天
netty服务的代码网上已经烂大街了,在此不赘述,但是!!!
netty服务搭建后无法保持长连接,客户端发完消息就断开连接
……
第1+n天
android大哥看不下去了
因为我用的netty4,遂推荐我使用5,本地测试没问题,整合至springBoot的时候出现版本冲突,无法解决(ps:最后发现项目用了redis,底层应该用了netty通讯),netty5卒
又回到4上,结果大哥的netty代码毫无问题,就给我用了,整合至本地没问题,废话不多说贴代码
server:
@Component
public class EchoServer implements onChannelOperation {
@Value("${netty.port}")
private int port;
private static final Logger log= LoggerFactory.getLogger(EchoServer.class);
private EventLoopGroup bossGroup = new NioEventLoopGroup();
private EventLoopGroup workerGroup = new NioEventLoopGroup();
//public List socketList = new ArrayList();
private static final ConcurrentHashMap channelMap= new ConcurrentHashMap<>();
public void run() {
/* EventLoopGroup bossGroup = new NioEventLoopGroup(); // 用于处理服务器端接收客户端连接
EventLoopGroup workerGroup = new NioEventLoopGroup(); // 进行网络通信(读写)*/
try {
Thread t=Thread.currentThread();
log.info("run() in EchoServer"+ Calendar.getInstance().getTime()+"___"+t.getName());
ServerBootstrap bootstrap = new ServerBootstrap(); // 辅助工具类,用于服务器通道的一系列配置
bootstrap.group(bossGroup, workerGroup) // 绑定两个线程组, 绑定线程池
.channel(NioServerSocketChannel.class) // 指定NIO的模式
.childHandler(new ChannelInitializer() { // 配置具体的数据处理方式
@Override // 这个方法里,连接一个客户端,进入一次,连接一个客户端进入一次
protected void initChannel(SocketChannel socketChannel) throws Exception {
log.info("有客户端连接了:" + socketChannel);
// 设置超时时间,可选
// socketChannel.pipeline().addLast(new IdleStateHandler(READ_IDEL_TIME_OUT,
// WRITE_IDEL_TIME_OUT, ALL_IDEL_TIME_OUT, TimeUnit.SECONDS));
NettyServerHandler scobj = new NettyServerHandler(EchoServer.this); //设置监听
socketChannel.pipeline().addLast(scobj);
//socketList.add(scobj);
channelMap.put(scobj,socketChannel);
log.info("socket通道数量:" + "--"+channelMap.size());
}
})
.option(ChannelOption.SO_BACKLOG, 32 * 1024) // 设置TCP缓冲区
.option(ChannelOption.SO_SNDBUF, 64 * 1024) // 设置发送数据缓冲大小
.option(ChannelOption.SO_RCVBUF, 64 * 1024) // 设置接受数据缓冲大小
.childOption(ChannelOption.SO_KEEPALIVE, true); // true保持连接, false no
//.childOption(ChannelOption.ALLOW_HALF_CLOSURE, true); // 允许半关闭socket即可。默认为false,客户端shutdownoutput时,SocketChannel.read(..)
ChannelFuture future = bootstrap.bind(port).sync();
log.info("服务器启动成功,监听端口号:" + port);
future.channel().closeFuture().sync();// 关闭服务器通道
} catch (Exception e) {
e.printStackTrace();
log.info("服务器启动失败,监听端口号:" + port);
} finally {
workerGroup.shutdownGracefully();// 释放线程池资源
bossGroup.shutdownGracefully();// 释放线程池资源
log.info("服务器启动失败,监听端口号:" + port);
}
}
@PreDestroy
public void destroy() {
log.info("正在尝试关闭 Netty");
bossGroup.shutdownGracefully().syncUninterruptibly();
workerGroup.shutdownGracefully().syncUninterruptibly();
channelMap.clear();
log.info("关闭成功");
}
@Override
public void onRemoveChannel(NettyServerHandler obj) {
log.info("移除链接!!!!");
channelMap.remove(obj);//移除该链接
}
// 根据设备ID发送数据
public void sendDataAPI(String equipId,String msgType, String sendData) {
NettyServerHandler cnobj = getSocketHandler(equipId);
if (cnobj != null) {
log.info("已匹配通道,准备发送,剩余通道数:"+channelMap.size());
cnobj.sendDataAPI(equipId, sendData);
if(msgType.equals(SocketMsgType.REBOOT)){
log.info("设备重启了,移除该链接");
channelMap.remove(cnobj);
}
}else log.info("没有匹配通道,消息无法发送,剩余通道数:"+channelMap.size());
}
// 获取发送对象socket
public NettyServerHandler getSocketHandler(String equipId) {
/*if (socketList == null || socketList.size() <= 0) return null;
for (NettyServerHandler cnobj : socketList) {
if (cnobj.getEquipId().equals(equipId)) {
return cnobj;
}
}*/
if(channelMap.size()<1)return null;
for (NettyServerHandler key : channelMap.keySet()) {
if(key.getEquipId().equals(equipId)){
return key;
}
}
return null;
}
// 检测选择的机器车是否在线 true 在线,可以发送信息给它
public boolean socketIsActive(String equipId) {
/*for (NettyServerHandler cnobj : socketList) {
if (cnobj.getEquipId().equals(equipId)) {
return true;
}
}*/
for(Map.Entry entry:channelMap.entrySet()){
if (entry.getKey().getEquipId().equals(equipId)){
return true;
}
}
return false;
}
}
接下来是:Handler 包含了部分消息的处理逻辑,业务代码已略去
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
private static final Logger log= LoggerFactory.getLogger(NettyServerHandler.class);
/** 空闲次数 */
private AtomicInteger idle_count = new AtomicInteger(1);
/** 发送次数 */
private AtomicInteger count = new AtomicInteger(1);
//在普通类里获取spring管理的bean,可以借助componet注解类做中继
private ApplicationContext applicationContext= SpringUtils.getApplicationContext();
//注入service层代码
private ***service ***service=applicationContext.getBean(***service.class);
/**
* 建立连接时,发送一条消息
*/
@Autowired
private onChannelOperation mListener;
@Autowired
public NettyServerHandler(onChannelOperation mListener) {
this.mListener = mListener;
}
private ChannelHandlerContext channelHanlder;
private String equipId;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// ctx.writeAndFlush(user);
super.channelActive(ctx);
}
/**
* channel失效处理,客户端下线或者强制退出等任何情况都触发这个方法
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
mListener.onRemoveChannel(this); // 移除通道
log.info("捕获异常,通道为:" + ctx.channel().remoteAddress());
super.channelInactive(ctx);
}
/**
* 超时处理 如果5秒没有接受客户端的心跳,就触发; 如果超过两次,则直接关闭;
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object obj) throws Exception {
if (obj instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) obj;
// 如果读通道处于空闲状态,说明没有接收到心跳命令
if (IdleState.READER_IDLE.equals(event.state())) {
// log.info("已经5秒没有接收到客户端的信息了");
if (idle_count.get() > 1) {
// log.info("关闭这个不活跃的channel");
ctx.channel().close();
}
idle_count.getAndIncrement();
}
} else {
super.userEventTriggered(ctx, obj);
}
}
/**
* 业务逻辑处理
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("接收到客户端数据了: " + msg);
// do something msg
ByteBuf buf = (ByteBuf) msg;
byte[] data = new byte[buf.readableBytes()];
buf.readBytes(data);
//RestTemplate restTemplate=new RestTemplate();
String request = new String(data, "utf-8");
log.info("request:" + request);
//解析json
JSONObject jsonObject=null;
try {
String end=request.substring(request.length()-1,request.length());
if(!end.equals("}")){
log.info("error msg!!!!"+end.toCharArray());
return;
}
jsonObject = new JSONObject(request);
log.info("解析完成:"+jsonObject.toString());
String msgType=jsonObject.get("msgType").toString();
log.info("msgType:"+msgType);
String dataMsg=jsonObject.get("data").toString();
String dataMap="";
//这里可以写业务代码
}catch (Exception e){
log.info("请注意,报文异常!");
e.printStackTrace();
}
dealData(ctx, request);
//count.getAndIncrement();
}
/**
* 异常处理
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
mListener.onRemoveChannel(this);
ctx.close();
}
private void dealData(ChannelHandlerContext ctx, String msg) {
try {
channelHanlder = ctx; // 更新通道信息
JSONObject js = new JSONObject(msg);
String msgType = js.getString("msgType");
equipId = js.getString("equipId");
log.info("收到设备(" + equipId + ")发回的数据:" + msg);
// 模拟发送数据
if (msgType.equals("200002")) // 心跳数据原数据返回去
sendDataAPI(equipId, js.toString());
} catch (JSONException e) {
e.printStackTrace();
}
}
/**
*
* @param equipId 设备ID
* @param sendData 要发送的内容 JSON格式
*/
public void sendDataAPI(String equipId, String sendData) {
if (channelHanlder !=null) {
channelHanlder.writeAndFlush(Unpooled.copiedBuffer(sendData.getBytes()));
log.info("往设备(" + equipId + ")发送了数据:" + sendData);
}
}
public String getEquipId() {
return equipId;
}
public void setEquipId(String equipId) {
this.equipId = equipId;
}
public ChannelHandlerContext getChannelHanlder() {
return channelHanlder;
}
public void setChannelHanlder(ChannelHandlerContext channelHanlder) {
this.channelHanlder = channelHanlder;
}
// 检测再NettyServer是否需要新增此对象
public interface onChannelOperation {
public void onRemoveChannel(NettyServerHandler obj);
}
}
其中组件:SpringUtils 来源自网络,其实注入有很多方式,只做参考
@Component
public class SpringUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if(SpringUtils.applicationContext == null){
SpringUtils.applicationContext = applicationContext;
}
}
//获取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
//通过name获取 Bean.
public static Object getBean(String name){
return getApplicationContext().getBean(name);
}
//通过class获取Bean.
public static T getBean(Class clazz){
return getApplicationContext().getBean(clazz);
}
//通过name,以及Clazz返回指定的Bean
public static T getBean(String name,Class clazz){
return getApplicationContext().getBean(name, clazz);
}
}
最后是springBoot本地启动
public class App extends SpringBootServletInitializer implements CommandLineRunner{
//测试的时候 App需要实现CommandLineRunner 并重写run方法 其他重复代码和注解已省略
@Override
public void run(String... args) throws Exception {
new Thread(()->nettyServer.run()).run();
}
}
但是,重点来了,这么写放到tomcat上不行滴!!!!
又加了监听器,如下
@WebListener
public class NettyServerListener implements //ApplicationListener
ServletContextListener
{
private static final Logger log= LoggerFactory.getLogger(NettyServerListener.class);
private EchoServer echoServer= null;
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("tomcat is going start");
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(sce.getServletContext());
log.info("echoServer is ready");
echoServer = (EchoServer) context.getBean("echoServer");
//echoServer= SpringUtils.getApplicationContext().getBean(EchoServer.class);
Thread t=Thread.currentThread();
log.info("run() of netty"+ Calendar.getInstance().getTime()+"___"+t.getName()+" flag:"+(null==echoServer));
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("tomcat is deading");
log.info("destroy() of netty"+ Calendar.getInstance().getTime()+" flag:"+(null==echoServer));
new Thread(()->echoServer.destroy()).run();
}
}
事实证明new Thread的方式不好使,加上其他错误,在这里我耽误了三天(浪费了我的周末时间),甚至想到给服务器加cpu核心的方式,当然要审批就作废了,最后在网上有位无名大哥点到要用线程池启动,就试了下,幸福来的无比突然,我都已经准备好自裁了……下面是完整的Listener
@WebListener
public class NettyServerListener implements //ApplicationListener
ServletContextListener
{
private static final Logger log= LoggerFactory.getLogger(NettyServerListener.class);
private ExecutorService webSocketSinglePool;
private EchoServer echoServer= null;
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("tomcat is going start");
WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(sce.getServletContext());
log.info("echoServer is ready");
echoServer = (EchoServer) context.getBean("echoServer");
//echoServer= SpringUtils.getApplicationContext().getBean(EchoServer.class);
Thread t=Thread.currentThread();
log.info("run() of netty"+ Calendar.getInstance().getTime()+"___"+t.getName()+" flag:"+(null==echoServer));
webSocketSinglePool.execute(() -> {
try {
log.info("running......");
echoServer.run();
} catch (Exception e) {
log.error("webSocket listen and serve error.", e);
}
//在这儿启动netty会阻塞tomcat
// new Thread(()->echoServer.run()).run();
});
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("tomcat is deading");
log.info("destroy() of netty"+ Calendar.getInstance().getTime()+" flag:"+(null==echoServer));
new Thread(()->echoServer.destroy()).run();
}
@PostConstruct
public void setup() {
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
.setNameFormat("webSocketSinglePool-%d").build();
webSocketSinglePool = new ThreadPoolExecutor(1, 1, 0L,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024),
namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
log.info("webSocketSinglePool init.");
}
/* @Override
public void onApplicationEvent(ContextStartedEvent event) {
log.info("监听到事件了");
runWebSocketServer(event.getApplicationContext());
}*/
private void runWebSocketServer(ApplicationContext applicationContext) {
final EchoServer echoServer = applicationContext.getBean(EchoServer.class);
new Thread(() -> {
try { //开始启动netty服务
log.info("ready to start Netty");
echoServer.run();
} catch (Exception e) {
log.error("webSocket listen and serve error.", e);
}
}).run();
}
@PreDestroy
public void cleanup(){
webSocketSinglePool.shutdown();
log.info("webSocketSinglePool destroyed.");
}
}
至此netty服务成功发布,耗时一周左右,吃一堑长一智