前一篇文章我以經實現了基於java原生的socket來實現TCP服務器,並可以解析數據,可以說是一個比較簡單的結構。後來我通過研究Netty發出,Netty是一個很好的框架,比較穩定。
1,還是那個拓撲結構
2.後臺流程圖
3.代碼
1)MainPrl.java
main函數實現了設備動態加載,可以通過配置文件很靈活的設置設備的增減。
package qx.drc.main;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import qx.drc.ser.DataRecvServer;
import qx.drc.utils.CommTool;
import qx.drc.utils.MyPath;
public class MainPrl {
private static final Logger logger = LoggerFactory.getLogger(MainPrl.class);
private static DataReceiveConfig cfg=DataReceiveConfig.getInstance();
public static void main(String[] args) {
// TODO 自動生成的方法存根
List<DrcProInfo> list=cfg.listDrcPros;
for (DrcProInfo drcProInfo : list) {
try {
logger.info("初始化程序:"+drcProInfo.getParseDataName());
CommTool.printInfo("初始化程序:"+drcProInfo.getParseDataName());
CommTool.printInfo( drcProInfo.getJarPath());
URL url=new URL("file:"+ drcProInfo.getJarPath());
URLClassLoader myClassLoader=new URLClassLoader(new URL[]{url},
Thread.currentThread().getContextClassLoader());
Class myClass=null;
ParseData parseData=null;
try {
myClass = myClassLoader.loadClass(drcProInfo.getJarClassFullName());
parseData=(ParseData)myClass.newInstance();
parseData.setDrcProInfo(drcProInfo);
} catch (ClassNotFoundException e) {
e.printStackTrace();
continue;
}
catch (InstantiationException e) {
e.printStackTrace();
continue;
} catch (IllegalAccessException e) {
e.printStackTrace();
continue;
}
DataRecvServer server = new DataRecvServer(drcProInfo.getPort(),
drcProInfo.getParseDataName(),parseData);
try {
server.start();
} catch (Exception e) {
// TODO 自動生成的 catch 塊
e.printStackTrace();
}
myClassLoader.close();
} catch (IOException e) {
logger.error(e.getMessage());
e.printStackTrace();
}
}
}
}
2) DataRecvServer.java
DataRecvServer類主要實現了新建一個服務器,並初始化服務器設置
import java.io.IOException;
import javax.xml.stream.events.StartDocument;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 qx.drc.main.ParseData;
public class DataRecvServer {
private static final Logger logger = LoggerFactory.getLogger(DataRecvServer.class);
private int port; //接收數據端口
private String proName; // 接收數據名稱
private ParseData parseData; //解析數據對象
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
public DataRecvServer(int port,String proName,ParseData parseData){
this.port=port;
this.proName=proName;
this.parseData=parseData;
}
public void start() throws Exception{
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
//創建ServerBootstrap實例
ServerBootstrap serverBootstrap=new ServerBootstrap();
//初始化ServerBootstrap的線程組
serverBootstrap.group(bossGroup,workerGroup);
//設置將要被實例化的ServerChannel類
serverBootstrap.channel(NioServerSocketChannel.class);
//serverBootstrap.handler(new LoggingHandler(LogLevel.ERROR));
//在ServerChannelInitializer中初始化ChannelPipeline責任鏈,並添加到serverBootstrap中
serverBootstrap.childHandler(new ServerChannelInitializer(this.parseData));
//標識當服務器請求處理線程全滿時,用於臨時存放已完成三次握手的請求的隊列的最大長度
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024);
// 是否啓用心跳保活機機制
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
//綁定端口後,開啓監聽
/*ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
if(channelFuture.isSuccess()){
System.out.println("TCP服務啓動 成功---------------");
}*/
ChannelFuture f = serverBootstrap.bind(port);
f.addListener(future -> {
if (future.isSuccess()) {
System.out.printf("%s 開啓端口 %s 成功\n",proName, port);
} else {
System.out.printf("%s 開啓端口 %s 失敗\n", proName,port);
}
});
}
public void setPort(int port) {
this.port = port;
}
public void setProName(String proName) {
this.proName = proName;
}
public void setParseData(ParseData parseData) {
this.parseData = parseData;
}
}
4)ServerChannelInitializer.java
ServerChannelInitializer類主要實現了,初化服務器通道的,即加載編碼器,解碼器及數據解析
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.bytes.ByteArrayEncoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.DefaultEventExecutorGroup;
import io.netty.util.concurrent.EventExecutorGroup;
import qx.drc.main.ParseData;
public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
private static final Logger logger = LoggerFactory.getLogger(ServerChannelInitializer.class);
static final EventExecutorGroup group = new DefaultEventExecutorGroup(2);
private ParseData parseData;
public ServerChannelInitializer(ParseData parseData) throws InterruptedException {
this.parseData = parseData;
}
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//IdleStateHandler心跳機制,如果超時觸發Handle中userEventTrigger()方法
pipeline.addLast("idleStateHandler",new IdleStateHandler(15, 0, 0, TimeUnit.MINUTES));
pipeline.addLast(new ByteArrayEncoder());
pipeline.addLast(new DataDecoder(this.parseData));
pipeline.addLast(new DataServerHandler(this.parseData));
}
}
5)DataDecoder.java
DataDecoder是一個公共解碼類,解碼方式傳遞接口的方式結出。
import java.util.List;
import org.apache.log4j.lf5.PassingLogRecordFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import qx.drc.main.ParseData;
import qx.drc.utils.ByteUtils;
import qx.drc.utils.CommTool;
/**
* 解碼器
* @author 70910
*
*/
public class DataDecoder extends ByteToMessageDecoder{
protected final Logger log = LoggerFactory.getLogger(getClass());
private ParseData parseData;
public DataDecoder(ParseData parseData) {
this.parseData = parseData;
}
@Override
protected void decode(ChannelHandlerContext arg0, ByteBuf in, List<Object> list) throws Exception {
// TODO 自動生成的方法存根
//System.err.println(parseData.getDrcProInfo().getParseDataName() + "--------解碼器正在解碼----------");
try {
in.retain();
parseData.decoder(in,list);
} catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
cause.printStackTrace();
ctx.close();
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
}
6)DataServerHandler.java
公共解析類,通過傳遞的接口類,實現數據解析與存儲
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import qx.drc.main.ParseData;
import qx.drc.utils.ByteUtils;
import qx.drc.utils.CommTool;
public class DataServerHandler extends ChannelInboundHandlerAdapter{
protected final Logger log = LoggerFactory.getLogger(getClass());
private ParseData parseData;
public DataServerHandler(ParseData parseData) {
this.parseData = parseData;
}
/**
* 當我們通道進行激活的時候 觸發的監聽方法
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.err.println(parseData.getDrcProInfo().getParseDataName() + "--------通道激活------------");
}
/**
* 當我們的通道里有數據進行讀取的時候 觸發的監聽方法
*/
@Override
public void channelRead(ChannelHandlerContext ctx /*NETTY服務上下文*/, Object msg /*實際的傳輸數據*/) throws Exception {
if(msg instanceof byte[]){
byte[] bytes = (byte[]) msg;
CommTool.printInfo(parseData.getDrcProInfo().getParseDataName()+",接收到數據 " + bytes.length + " bytes");
//解析 ,需要判定是否是心跳包
if(!parseData.parse(bytes)){
log.info(parseData.getDrcProInfo().getParseDataName()+",接收數據解析失敗");
CommTool.printInfo(parseData.getDrcProInfo().getParseDataName()+",數據解析失敗");
return;
}
//存儲
if(!parseData.save()){
log.info(parseData.getDrcProInfo().getParseDataName()+",數據存儲失敗");
CommTool.printInfo(parseData.getDrcProInfo().getParseDataName()+",數據存儲失敗");
return;
}
if(!parseData.save2Txt()){
log.info(parseData.getDrcProInfo().getParseDataName()+",數據存儲到TXT失敗");
CommTool.printInfo(parseData.getDrcProInfo().getParseDataName()+",數據存儲到TXT失敗");
return;
}
CommTool.printInfo(parseData.getDrcProInfo().getParseDataName()+",數據解析存儲成功");
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.err.println(parseData.getDrcProInfo().getParseDataName() + "--------數據讀取完畢----------");
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
System.err.println(parseData.getDrcProInfo().getParseDataName() + "--------數據讀異常----------: ");
cause.printStackTrace();
ctx.close();
}
}
7) ParseData.java
解碼,解析接口類,通過繼承可以實現具體的解碼和解析功能
import java.util.Date;
import java.util.List;
import io.netty.buffer.ByteBuf;
/**
* 解析接口
* @author yangze
*
*/
public interface ParseData {
//解析
boolean parse(byte[] bytes);
//解碼
boolean decoder(ByteBuf in,List<Object> list);
//保存
boolean save();
//保存文本
public boolean save2Txt();
//設置項目信息
void setDrcProInfo(DrcProInfo drcProInfo);
//獲取項目信息
DrcProInfo getDrcProInfo();
//是否是心跳包
boolean isHeartPack();
//獲取最後心跳時間
Date getLastHeartPackDate();
}
8)實現解碼與解析類,關注decoder與parse兩個方法即可
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.log4j.Logger;
import io.netty.buffer.ByteBuf;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.sql.Connection;
import java.sql.PreparedStatement;
import qx.drc.main.DeviceInfo;
import qx.drc.main.DrcProInfo;
import qx.drc.main.ParamInfo;
import qx.drc.main.ParseData;
import qx.drc.utils.ByteUtils;
import qx.drc.utils.CRC16_Modbus;
import qx.drc.utils.CommTool;
import qx.drc.utils.DateUtils;
import qx.drc.utils.MyPath;
/**
* 解析負氧離子數據
* @author yangze
* 2019-08-21
*
*/
public class AnionSensorDataParse implements ParseData {
private static Logger logger = Logger.getLogger(AnionSensorDataParse.class);
public DrcProInfo drcProInfo;
private int deviAddr; //設備地址
private int funcCode; //功能碼
private int dataLength; //數據長度
private int anionData;
private String typeOfPk;
private byte[] srcData;
private DeviceInfo deviceInfo = new DeviceInfo();
private Map<String, ParamInfo> name2ParamInfoMap= new HashMap<String, ParamInfo>();
private static Connection conn = null;
private Date lastDataTime = new Date();
private boolean isHeartPk = false;
public AnionSensorDataParse(){
}
// 初始化數據庫
private boolean iniDB() {
if (conn != null){
try {
if(conn.isClosed()){
conn = DriverManager.getConnection(drcProInfo.getDbConnUrl());
}
return true;
} catch (SQLException ex) {
logger.error(drcProInfo.getParseDataName()+","+ex.getMessage());
CommTool.printInfo(drcProInfo.getParseDataName()+",數據庫鏈接失敗");
ex.printStackTrace();
}
}
try {
Class.forName("com.mysql.jdbc.Driver").newInstance();
} catch (Exception ex) {
logger.error(drcProInfo.getParseDataName()+","+ex.getMessage());
CommTool.printInfo(drcProInfo.getParseDataName()+",數據庫驅動載入失敗");
return false;
}
try {
conn = DriverManager.getConnection(drcProInfo.getDbConnUrl());
return true;
} catch (SQLException ex) {
logger.error(drcProInfo.getParseDataName()+","+ex.getMessage());
CommTool.printInfo(drcProInfo.getParseDataName()+","+ex.toString());
return false;
}
}
/**
* 獲取設備信息
* @return
*/
private boolean getDeviceInfo() {
if(iniDB()){
try{
Statement stmt = conn.createStatement(); //創建Statement對象
String sql = "select devi_id,devi_code,devi_name from device_info where devi_code='"+ drcProInfo.getDeviceCode()+"'" ;
ResultSet rs = stmt.executeQuery(sql);//創建數據對象
ResultSetMetaData rsmd = rs.getMetaData() ;
int columnCount = rsmd.getColumnCount();
if(columnCount <= 0) return false;
while (rs.next()){
deviceInfo.setDeviId(rs.getString(1));
deviceInfo.setDeviCode(rs.getString(2));
deviceInfo.setDeviName(rs.getString(3));
}
rs.close();
stmt.close();
return true;
}catch(Exception e){
logger.error(drcProInfo.getParseDataName()+",error:"+e.getMessage());
CommTool.printInfo(drcProInfo.getParseDataName()+",獲取設備信息錯誤");
return false;
}
}
else
return false;
}
/**
* 獲取設備監測指標信息
* @return
*/
private boolean getDeviceParamsInfo() {
if(iniDB()){
try{
Statement stmt = conn.createStatement(); //創建Statement對象
String sql = "SELECT p.para_id,p.para_name,p.para_unit FROM devi_para_rel d " +
"LEFT JOIN param_info p ON p.para_id = d.para_id " +
"where d.devi_id='" + deviceInfo.getDeviId() + "'";
ResultSet rs = stmt.executeQuery(sql);//創建數據對象
ResultSetMetaData rsmd = rs.getMetaData() ;
int columnCount = rsmd.getColumnCount();
if(columnCount <= 0) return false;
while (rs.next()){
ParamInfo paramInfo = new ParamInfo();
paramInfo.setParaId(rs.getString(1));
paramInfo.setParaName(rs.getString(2));
paramInfo.setParaUnit(rs.getString(3));
name2ParamInfoMap.put(paramInfo.getParaName(), paramInfo);
}
rs.close();
stmt.close();
return true;
}catch(Exception e){
logger.error(drcProInfo.getParseDataName()+",error:"+e.getMessage());
CommTool.printInfo(drcProInfo.getParseDataName()+",獲取設備信息錯誤");
return false;
}
}
else
return false;
}
@Override
public boolean decoder(ByteBuf in,List<Object> list) {
if(in.readableBytes() < 9) {
//數據包長度不夠,繼續接收再處理
System.err.println(drcProInfo.getParseDataName() + "-----------數據包不完整,繼續讀取");
return false;
}
else{
int headIndex = in.readerIndex();
while(in.readableBytes() > 1) {
//讀兩個字節
byte[] headArray = new byte[2];
in.readBytes(headArray);
//重置讀取位置
if(headArray[0] == 0x01 && headArray[1] == 0x03) {
//如果爲頭,跳出循環
break;
}
headIndex += 2;
}
in.readerIndex(headIndex);
if(in.readableBytes() < 9) {
//餘下的數據是否足夠一個包
System.err.println(drcProInfo.getParseDataName() + "-----------餘下的數據不足夠一個包,繼續接收......");
return false;
}
else {
byte[] data = new byte[9];
in.readBytes(data);
list.add(data);
System.err.println(drcProInfo.getParseDataName() + "------------解碼器解碼成功");
}
}
return true;
}
@Override
//01 03 04 A1 20 00 07 99 C7
//報文解釋說明:“01”設備地址;“03”讀功能碼;“04”數據長度;“A1 20 00 07”爲寄存器值;“99 C7”校驗碼。計算負氧離子: 0x0007A120=500000個
public boolean parse(byte[] data){
isHeartPk = false;
srcData = Arrays.copyOfRange(data, 0, 9);
String preStr=drcProInfo.getParseDataName();
int indexNow=0;
//設備地址
byte[] arr= Arrays.copyOfRange(srcData, indexNow, indexNow+1);
deviAddr=bytes2Byte255(arr[0]);
indexNow+=1;
if(deviAddr != 0x01)
{
logger.error(preStr+",數據包:包頭錯誤");
CommTool.printInfo(preStr+"數據包:包頭錯誤");
return false;
}
//功能碼
arr=Arrays.copyOfRange(srcData, indexNow,indexNow+ 1);
funcCode=bytes2Byte255(arr[0]);
indexNow+=1;
//System.out.println("功能碼:"+ CommTool.getHexString(arr));
if(funcCode == 0x03)
{
typeOfPk = "讀取負氧離子個數";
arr=Arrays.copyOfRange(srcData, indexNow,indexNow+ 1);
dataLength=bytes2Byte255(arr[0]);
indexNow++;
byte[] anionDataArr = new byte[4];
anionDataArr[1] = (Arrays.copyOfRange(srcData, indexNow, indexNow+1))[0];
indexNow++;
anionDataArr[0] = (Arrays.copyOfRange(srcData, indexNow, indexNow+1))[0];
indexNow++;
anionDataArr[3] = (Arrays.copyOfRange(srcData, indexNow, indexNow+1))[0];
indexNow++;
anionDataArr[2] = (Arrays.copyOfRange(srcData, indexNow, indexNow+1))[0];
indexNow++;
anionData = ByteUtils.getInt(anionDataArr);
//System.out.println("負氧離子16進制:" + CommTool.getHexString(CommTool.intToByteArray(anionData)));
//System.out.println("負氧離子個數:" + anionData);
if(!crc16Validate(srcData, 7)){
byte[] arr_temp=Arrays.copyOfRange(srcData,0,dataLength+2);
String str=CommTool.getHexString(arr_temp);
logger.error(preStr+"CRC16校驗錯誤.["+str+"]");
CommTool.printInfo(preStr+"CRC16校驗錯誤");
return false;
}
indexNow+=2;
//打印輸出
if(drcProInfo.isOutputConsole()){
System.out.println("**************************");
CommTool.printHexString(srcData);
System.out.println(" package size (byte): "+String.valueOf(indexNow));
System.out.println(" package type: "+typeOfPk);
byte[] funcCodeByte = {CommTool.intToByteArray(funcCode)[0]};
System.out.println(" function code: "+CommTool.getHexString(funcCodeByte));
byte[] deviAddrByte = {CommTool.intToByteArray(deviAddr)[0]};
System.out.println(" address of anion sensor: " + CommTool.getHexString(deviAddrByte));
System.out.println(" data size: "+ dataLength);
System.out.println(" anion num: "+ anionData);
System.out.println("**************************");
}
return true;
}
else
{
CommTool.printInfo(preStr+",數據包類型不正確");
logger.error(preStr+",數據包類型不正確");
return false;
}
}
@Override
public boolean isHeartPack() {
return this.isHeartPk;
}
@Override
public Date getLastHeartPackDate() {
return this.lastDataTime;
}
private boolean crc16Validate(byte[] byteArr,int datalength){
byte[] dataArr = new byte[datalength];
byte[] crc16Arr=new byte[2];
System.arraycopy(byteArr, 0, dataArr, 0, datalength);
System.arraycopy(byteArr, datalength, crc16Arr, 0, 2);
int crc16=CRC16_Modbus.calcCrc16(dataArr,0,dataArr.length);
int crc16_=(int)ByteUtils.getInt(new byte[]{crc16Arr[0],crc16Arr[1],0,0});
if(crc16 == crc16_) return true;
else return false;
}
private int bytes2Byte255(byte b) {
int num = 0;
num |= (b & 0xff);
return num;
}
@Override
public boolean save2Txt() {
if(drcProInfo.getSave2Txt()) {
String txtPath = drcProInfo.getSave2TxtPath() + "/" + DateUtils.getDate("yyyyMMdd") + ".txt";
String content = CommTool.getHexString(srcData);
return saveContent2Txt(content,txtPath);
}
return true;
}
public boolean saveContent2Txt(String content,String txtPath) {
File file = new File(txtPath);
try {
String parentDir =file.getParent();
if (!MyPath.check2CreatePath(parentDir))
{
logger.error(drcProInfo.getParseDataName()+",創建路徑失敗:" + parentDir);
CommTool.printInfo(drcProInfo.getParseDataName()+",創建路徑失敗:" + parentDir);
return false;
}
if(!file.exists()) {
file.createNewFile();
}
OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(file,true),"GB2312");
BufferedWriter bw = new BufferedWriter(osw);
bw.write(DateUtils.getDate("yyyy-MM-dd HH:mm:ss") + " " + content+"\r\n");
bw.close();
return true;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
logger.error(drcProInfo.getParseDataName()+",將數據保存到TXT失敗:" + e.getMessage());
CommTool.printInfo(drcProInfo.getParseDataName()+",將數據保存到TXT失敗:" + e.getMessage());
return false;
}
}
@Override
public boolean save() {
if(!getDeviceInfo()) return false;
if(!getDeviceParamsInfo()) return false;
if(iniDB()){
String sql = "insert into data_real(dare_id,devi_id,para_id,dare_value,dare_datetime)"
+ "values (?,?,?,?,?)";
PreparedStatement pstmt;
try {
pstmt = (PreparedStatement) conn.prepareStatement(sql);
Date da=new Date();
pstmt.setString(1, CommTool.uuid());
pstmt.setString(2, deviceInfo.getDeviId());
pstmt.setString(3, name2ParamInfoMap.get("負氧離子").getParaId());
pstmt.setFloat(4, anionData);
pstmt.setTimestamp(5, new java.sql.Timestamp(da.getTime()));
int res = pstmt.executeUpdate();
pstmt.close();
} catch (SQLException e) {
logger.error(drcProInfo.getParseDataName()+",error:"+e.getMessage());
CommTool.printInfo(drcProInfo.getParseDataName()+",存儲錯誤");
return false;
}
return true;
}
else
return false;
}
@Override
public void setDrcProInfo(DrcProInfo drcProInfo) {
this.drcProInfo=drcProInfo;
}
@Override
public DrcProInfo getDrcProInfo() {
return this.drcProInfo;
}
}