最近在做java服务端通过netty与手表通信,基于GPRS通信协议,可实现接收手表的信息,也可由服务端主动向手表发送信息。
服务端
@Component
public class NettyServer {
/**
* 日志
*/
private Logger log = LoggerFactory.getLogger(getClass());
/**
* IP地址
*/
@Value("${netty.ip}")
private String ip;
/**
* 端口号
*/
@Value("${netty.port}")
private int port;
//EventLoopGroup是用来处理IO操作的多线程事件循环器
//负责接收客户端连接线程
EventLoopGroup bossGroup = new NioEventLoopGroup();
//负责处理客户端i/o事件、task任务、监听任务组
EventLoopGroup workerGroup = new NioEventLoopGroup();
private Channel channel = null;
/**
* 启动服务器方法
*/
// @PostConstruct 交给启动项统一管理,不使用注解启动
public void init() {
InetSocketAddress address = new InetSocketAddress(ip,port);
log.info("netty服务器启动地址:"+ip);
try {
//启动 NIO 服务的辅助启动类
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup);//绑定线程池
//配置 Channel
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.localAddress(address);
serverBootstrap.childHandler(new NettyServerInitializer());编码解码
//backlog用于构造服务端套接字ServerSocket对象,
// 标识当服务器请求处理线程全满时,用于临时存放已完成三次握手的请求的队列的最大长度
serverBootstrap.option(ChannelOption.SO_BACKLOG, 128);//服务端接受连接的队列长度,如果队列已满,客户端连接将被拒绝
serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
//socketchannel的设置,关闭延迟发送
serverBootstrap.childOption(ChannelOption.TCP_NODELAY, true);
//保持长连接,2小时无数据激活心跳机制
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
serverBootstrap.childHandler(new NettyServerInitializer());
// 绑定端口,开始接收进来的连接
//绑定服务端口监听
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
channel = channelFuture.channel();
System.out.println("netty服务启动: [port:" + port + "]");
log.info("netty服务启动: [port:" + port + "]");
service=newBlockingExecutorsUseCallerRun(Runtime.getRuntime().availableProcessors());
// 等待服务器socket关闭
//服务器关闭监听
/*channel.closeFuture().sync()实际是如何工作:
channel.closeFuture()不做任何操作,只是简单的返回channel对象中的closeFuture对象,对于每个Channel对象,都会有唯一的一个CloseFuture,用来表示关闭的Future,
所有执行channel.closeFuture().sync()就是执行的CloseFuturn的sync方法,从上面的解释可以知道,这步是会将当前线程阻塞在CloseFuture上*/
channelFuture.channel().closeFuture().sync();
} catch (Exception e) {
log.error("netty服务启动异常-" + e.getMessage());
} finally {
//关闭事件流组
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
public static ExecutorService service;
/**
* 阻塞的ExecutorService
*
* @param size
* @return
*/
public ExecutorService newBlockingExecutorsUseCallerRun(int size) {
return new ThreadPoolExecutor(size, size * 2, 10L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(),
new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
try {
executor.getQueue().put(r);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
}
/**
* 停止服务
*/
public void destroy() {
log.info("Shutdown Netty Server...");
if(channel != null) { channel.close();}
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
log.info("Shutdown Netty Server Success!");
}
}
初始化通道
public class NettyServerInitializer extends ChannelInitializer<SocketChannel> {
/**
* 初始化channel
*/
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//指定消息分割符处理数据
// ByteBuf delimiter= Unpooled.copiedBuffer("\n".getBytes());
如果取消了分割符解码,就会出现TCP粘包之类的问题了
// pipeline.addLast(new DelimiterBasedFrameDecoder(1024*1024, delimiter));
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
//读写超时设置
pipeline.addLast(new IdleStateHandler(40,50,70, TimeUnit.SECONDS));
//心跳机制处理器
pipeline.addLast(new HearBeatHandler());
pipeline.addLast(new NettyServerHandler());
}
}
NettyServerHandler
@Component
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 日志
*/
private Logger log = LoggerFactory.getLogger(getClass());
@Autowired
WatchLocationDataMapper watchLocationDataMapper;
@Autowired
private RedisUtil redisUtil;
private static NettyServerHandler nettyServerHandler;
/**
* 初始化
* 由于在自定义类中注入service或者mapper会报空指针异常
* 所以采用这种方式
*/
@PostConstruct
public void init(){
nettyServerHandler = this;
}
/**
* 管理一个全局map,保存连接进服务端的通道数量
*/
private static final ConcurrentHashMap<ChannelId, ChannelHandlerContext> CHANNEL_MAP = new ConcurrentHashMap<>();
/**
* 存储手表设备ID连入时的ChannelId
*/
public static final ConcurrentHashMap<String,ChannelId> WATCHID_MAP = new ConcurrentHashMap<>();
/**
* 对每一个传入的消息都要调用;
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
try {
String tcpString = msg.toString();
System.out.println("tcpString::::"+tcpString);
//[厂商*设备ID*内容长度*内容] 内容长度为16进制
String[] tcpArray = tcpString.substring(1).split("\\*");
//内容数组
String[] contentArray = tcpArray[3].split(",");
//存储手表连接的唯一id
ChannelId channelId = ctx.channel().id();
if(WATCHID_MAP.get(tcpArray[1])==null){
WATCHID_MAP.put(tcpArray[1],channelId);
}else{
if(!WATCHID_MAP.get(tcpArray[1]).equals(channelId.toString())){
WATCHID_MAP.put(tcpArray[1],channelId);
}
}
//内容长度
String length = "";
//回复终端内容
String tcpResult ="";
switch (contentArray[0]){
//链路保持
case "LK":
//[CS*YYYYYYYYYY*LEN*LK,步数,翻滚次数,电量百分数]
//将长度转换为四个字节的ascll
length = toAscll(contentArray[0],1);
tcpResult= "["+tcpArray[0]+"*"+ tcpArray[1]+"*"+length+"*"+contentArray[0]+"]";
System.out.println("tcpResult"+tcpResult);
ctx.writeAndFlush(tcpResult);
break;
//报警数据
case "AL":
//创建手表上传数据类对象,封装数据
WatchLocationData watchLocationData = new WatchLocationData();
watchLocationData.setWatchId(tcpArray[1]);
watchLocationData.setUploadTime(TimeUtil.getDateToString(new Date()));
watchLocationData.setIsLocation(contentArray[3]);
watchLocationData.setLatitude(contentArray[4]);
watchLocationData.setLatitudeLogo(contentArray[5]);
watchLocationData.setLongitude(contentArray[6]);
watchLocationData.setLongitudeLogo(contentArray[7]);
watchLocationData.setSpeed(Double.valueOf(contentArray[8]));
watchLocationData.setOrientation(Double.valueOf(contentArray[9]));
watchLocationData.setElevation(Double.valueOf(contentArray[10]));
watchLocationData.setElectricQuantity(Integer.valueOf(contentArray[13]));
watchLocationData.setStepNumber(Integer.valueOf(contentArray[14]));
watchLocationData.setRollNumber(Integer.valueOf(contentArray[15]));
watchLocationData.setTerminalStatusCode(contentArray[16]);
watchLocationData.setDisposeState(0);
watchLocationData.setLocationDataType(contentArray[0]);
//将终端状态码(16进制)转换成二进制
String terminalState = hexadecimalToBinarySystem(contentArray[16]);
//将字符串倒叙并转换成字符数据,遍历得到值为1的索引,该索引表示手表的状态或报警类型
StringBuffer buffer = new StringBuffer(terminalState);
char[] chars = buffer.reverse().toString().toCharArray();
String watchDataType = "";
for (int i = 0; i < chars.length; i++) {
if(chars[i]=='1'){
watchDataType += i+",";
}
}
watchLocationData.setWatchStateType(watchDataType);
//保存报警数据
boolean save = nettyServerHandler.watchLocationDataMapper.save(watchLocationData);
if(save){
//回复终端
length = toAscll(contentArray[0],1);
tcpResult= "["+tcpArray[0]+"*"+ tcpArray[1]+"*"+length+"*"+contentArray[0]+"]";
System.out.println("tcpResult"+tcpResult);
ctx.writeAndFlush(tcpResult);
}
break;
//设置中心号码终端回复信息
case "CENTER":
//将终端回复的消息存入Redis
// System.out.println("tcpString"+tcpString);
break;
default:
break;
}
}catch (Exception e){
e.printStackTrace();
log.error("channelRead出现系统错误"+e.toString());
}
}
/**
* 16进制转换成二进制
* @param str
* @return
*/
public static String hexadecimalToBinarySystem(String str){
String s = Integer.toBinaryString(Integer.valueOf(str, 16));
int length = s.length();
int residue = 32-length;
for (int i = 0; i < residue; i++) {
s = "0"+s;
}
return s;
}
/**
* 转换为四个字节的ascll(1长度,2电话号码)
* @param str
* @return
*/
public static String toAscll(String str,Integer type){
if(type==1){
String s = (Integer.toHexString(str.length())).toUpperCase();
if(s.length()==1){
s = "000"+s;
}else if(s.length()==2){
s= "00"+s;
}else if(s.length()==3){
s= "0" + s;
}
return s;
}else if(type==2){
String s = (Long.toHexString(Long.parseLong(str))).toUpperCase();
return s;
}
return null;
}
/**
* 将姓名转成unicode
* @param str 待转字符串
* @return unicode字符串
*/
public static String convertUnicode(String str) {
str = (str == null ? "" : str);
String tmp;
StringBuffer sb = new StringBuffer(1000);
char c;
int i, j;
sb.setLength(0);
for (i = 0; i < str.length(); i++) {
c = str.charAt(i);
// sb.append("\\u");
j = (c >>>8); //取出高8位
tmp = Integer.toHexString(j);
if (tmp.length() == 1)
sb.append("0");
sb.append(tmp);
j = (c & 0xFF); //取出低8位
tmp = Integer.toHexString(j);
if (tmp.length() == 1)
sb.append("0");
sb.append(tmp);
}
return (new String(sb));
}
/**
* @param msg 需要发送的消息内容
* @param channelId 连接通道唯一id
* @author xiongchuan on 2019/4/28 16:10
* @DESCRIPTION: 服务端给客户端发送消息
* @return:
*/
public String channelWrite(ChannelId channelId, Object msg) throws Exception {
ChannelHandlerContext ctx = null;
try {
ctx = CHANNEL_MAP.get(channelId);
} catch (Exception e) {
return "设备不在线!";
}
if (ctx == null) {
log.info("通道【" + channelId + "】不存在");
return "设备不在线!";
}
if (msg==null || StringUtils.isEmpty(msg.toString())) {
log.info("服务端响应/发送空的消息");
return "服务端响应/发送空的消息";
}
//将客户端的信息直接返回写入ctx
ctx.writeAndFlush(msg);
return "成功";
}
/**
* 有客户端连接服务器会触发此函数
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIp = insocket.getAddress().getHostAddress();
int clientPort = insocket.getPort();
//获取连接通道唯一标识
ChannelId channelId = ctx.channel().id();
System.out.println();
//如果map中不包含此连接,就保存连接
if (CHANNEL_MAP.containsKey(channelId)) {
log.info("客户端【" + channelId + "】是连接状态,连接通道数量: " + CHANNEL_MAP.size());
} else {
//保存连接
CHANNEL_MAP.put(channelId, ctx);
log.info("客户端【" + channelId + "】连接netty服务器[IP:" + clientIp + "--->PORT:" + clientPort + "]");
log.info("连接通道数量: " + CHANNEL_MAP.size());
}
}
/**
* 有客户端终止连接服务器会触发此函数
* @param ctx
* @throws Exception
*/
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
InetSocketAddress insocket = (InetSocketAddress) ctx.channel().remoteAddress();
String clientIp = insocket.getAddress().getHostAddress();
ChannelId channelId = ctx.channel().id();
//包含此客户端才去删除
if (CHANNEL_MAP.containsKey(channelId)) {
//删除连接
CHANNEL_MAP.remove(channelId);
System.out.println();
log.info("客户端【" + channelId + "】退出netty服务器[IP:" + clientIp + "--->PORT:" + insocket.getPort() + "]");
log.info("连接通道数量: " + CHANNEL_MAP.size());
}
}
/**
* 异常捕获
* @param ctx
* @param cause
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.out.println();
ctx.close();
log.info(ctx.channel().id() + " 发生了错误,此连接被关闭" + "此时连通数量: " + CHANNEL_MAP.size());
cause.printStackTrace();
}
}
HearBeatHandler
public class HearBeatHandler extends ChannelInboundHandlerAdapter {
/**
* 日志
*/
private Logger log = LoggerFactory.getLogger(getClass());
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
String socketString = ctx.channel().remoteAddress().toString();
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.READER_IDLE) {
// log.info("Client: " + socketString + " READER_IDLE 读超时");
// ctx.disconnect();
} else if (event.state() == IdleState.WRITER_IDLE) {
// log.info("Client: " + socketString + " WRITER_IDLE 写超时");
// ctx.disconnect();
} else if (event.state() == IdleState.ALL_IDLE) {
log.info("Client: " + socketString + " ALL_IDLE 总超时");
ctx.channel().close();
}
}
}
}
springboot启动类启动netty
@EnableAsync//用线程池启动Netty,不然会阻塞springboot
public class CommunityApplication implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(CommunityApplication.class, args);
}
@Autowired
private IinitService iinitService;
@Autowired
private HikCloudUtil hikCloudUtil;
@Async//异步启动
@Override
public void run(String... args) throws Exception {
//文件夹维护
iinitService.initFolder();
//获取token
hikCloudUtil.Login();
//资源初始化
iinitService.initConstant();
// 维护数据库
//iinitService.updateMysqlDB();
//启动netty
iinitService.nettyInit();
}
}