最近在写一个关于物联网的小工具,用linux工控小主机做一个串口服务器,将串口数据与指定的tcp服务器做数据双向透传,使用spring-integration和jssc的方案实现,把主要过程记录下来,以备查询
整个工程是基于jssc和spring-integration-ip在Spring boot上开发,便于后期集成管理界面,总体思路是用jssc接收发和转发串口数据,再用spirng integration将串口数据转发到tcp服务端,大体架构如下图所示:
工程中需要用的几个关键依赖包如下:
<!-- spring integration ip 依赖包,这里用的是5.3.1 -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-ip</artifactId>
<version>5.3.1.RELEASE</version>
</dependency>
<!-- apache开源的处理进制转换的工具类 -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.14</version>
</dependency>
<!-- jssc 串口工具依赖包 -->
<dependency>
<groupId>org.scream3r</groupId>
<artifactId>jssc</artifactId>
<version>2.8.0</version>
</dependency>
对于spring integration的配置还是比较容易的,这里有几个概念对于第一次接触spring integration的来说可有点晕,主要涉及到消息的入站、出站,管道,我个人理解就是入站从外部输入数据,出站是从本地输出数据,管道就是数据的通道,对于出站和入站也分出站管道和入站管道,具体配置如下:
package org.noka.serialservice.config;
import org.apache.commons.codec.binary.Hex;
import org.noka.serialservice.Serializer.NByteArrayCrLfSerializer;
import org.noka.serialservice.service.SerialService;
import org.noka.serialservice.service.TcpGateway;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.ip.tcp.TcpReceivingChannelAdapter;
import org.springframework.integration.ip.tcp.TcpSendingMessageHandler;
import org.springframework.integration.ip.tcp.connection.AbstractClientConnectionFactory;
import org.springframework.integration.ip.tcp.connection.TcpConnectionOpenEvent;
import org.springframework.integration.ip.tcp.connection.TcpNetClientConnectionFactory;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.support.MessageBuilder;
/** ----------------------------------------------------------------------------------
* TCP数据转发服务配置
* @author xiefangjian@163.com
* @version 1.0.0
*----------------------------------------------------------------------------------*/
@EnableIntegration
@Configuration
public class TcpConfig implements ApplicationListener<ApplicationEvent> {
private static Logger logger = LoggerFactory.getLogger(TcpConfig.class);
//TCP服务器地址,可以用域名或IP
@Value("${socket.client.ip}")
private String host;
//TCP服务器端口
@Value("${socket.client.port}")
private int port;
@Value("${socket.client.RetryInterval:60}")
private long RetryInterval;//连接断开时,多长时间重新连接,以秒秒为单位,默认为1分钟
//串口数据转发服务对象
private final SerialService serialService;
//TCP端数据转发网关
private final TcpGateway tcpGateway;
/**
* 配置构造方法
* @param serialService 串口数据转发服务对象
* @param tcpGateway TCP端数据转发网关
*/
public TcpConfig(SerialService serialService, TcpGateway tcpGateway) {
this.serialService = serialService;
this.tcpGateway = tcpGateway;
}
/**
* 创建TCP连接
* @return tcp clinet连接工厂对象 AbstractClientConnectionFactory
*/
@Bean
public AbstractClientConnectionFactory clientCF() {
TcpNetClientConnectionFactory tc = new TcpNetClientConnectionFactory(this.host, this.port);//创建连接
tc.setDeserializer(new NByteArrayCrLfSerializer());//设置自定义反序列化对象,对数据转发做分析处理
tc.setSerializer(new NByteArrayCrLfSerializer());
return tc;
}
/**
* TCP服务下发数据接收管道配置,spring integration称之为入站管道配置
* @param connectionFactory 连接工厂
* @return TcpReceivingChannelAdapter 入站管道对象
*/
@Bean
public TcpReceivingChannelAdapter tcpInAdapter(AbstractClientConnectionFactory connectionFactory) {
TcpReceivingChannelAdapter inGate = new TcpReceivingChannelAdapter();//新建一个TCP入站管道
inGate.setConnectionFactory(connectionFactory);//绑定到当前的连接工厂上
inGate.setClientMode(true);//设置连接为客户端模式
inGate.setOutputChannelName("clientIn");//入站管道名称,后面数据接收的地方需要该名称进行匹配
inGate.setRetryInterval(RetryInterval*1000);//连接断开时,多长时间重新连接,以毫秒为单位
return inGate;
}
/**
* 服务器有数据下发
* @param in 服务器有数据下发时,序列化后的对象,这里使用byte数组
*/
@ServiceActivator(inputChannel = "clientIn")
public void upCase(Message<byte[]> in) {
logger.info("[net service data]========================================");
logger.info("[net dow data]"+new String(in.getPayload()));//字符串方式打印服务器下发的数据
logger.info("[net dow hex]"+Hex.encodeHexString(in.getPayload(),false));//16进制方式打印服务器下发的数据
serialService.send(in.getPayload());//将服务器下发的数据转发给串口
}
/**
* 向服务器发送数据管道绑定
* @param connectionFactory tcp连接工厂类
* @return 消息管道对象
*/
@Bean
@ServiceActivator(inputChannel = "clientOut")
public MessageHandler tcpOutAdapter(AbstractClientConnectionFactory connectionFactory) {
TcpSendingMessageHandler outGate = new TcpSendingMessageHandler();//创建一个新的出站管道
outGate.setConnectionFactory(connectionFactory);//绑定到连接工厂
outGate.setClientMode(true);//设置为客户端连接模式
return outGate;
}
/**
* 连接成功时调用的方法
* @param event 响应事件
*/
@Override
public void onApplicationEvent(ApplicationEvent event) { //监听连接打开事件
if (event instanceof TcpConnectionOpenEvent) {
/**---------连接时如果需要发送认证类消息时可以写在这里--------------------**/
byte[] snc="OK".getBytes();//这里在连接时,简单的向服务器发送一个OK字符串
tcpGateway.send(MessageBuilder.withPayload(snc).build());//发送消息
}
}
}
TcpGateway只是一个接口,用于在其它地方调用该接口向TCP服务器发送消息,具体实现如下:
package org.noka.serialservice.service;
import org.springframework.integration.annotation.MessagingGateway;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
/**----------------------------------------------------------------
* TCP发送消息网关,其它需要发向TCP服务器发送消息时,调用该接口
**--------------------------------------------------------------**/
@MessagingGateway(defaultRequestChannel = "clientOut")
@Component
public interface TcpGateway {
void send(Message<byte[]> out);//发送消息方法
}
自定义序列列和反序列化对象,主要是处理byte类型的数据,在测试的时候开始使用默认的序列化对象,TCP服务端每次发送数据都需要在结束时加入"\r\n",否则收不到数据,在使用byte类型传输时,显示很不友好,而且这样对TCP服务端造成了特殊要求,不能通用,改造后的序列化对象如下:
package org.noka.serialservice.Serializer;
import org.springframework.integration.ip.tcp.serializer.AbstractPooledBufferByteArraySerializer;
import org.springframework.integration.ip.tcp.serializer.SoftEndOfStreamException;
import java.io.*;
/**----------------------------------------------------------------------------------
* 自定义序列化工具类
* @author xiefangjian@163.com
* @version 1.0.0
**--------------------------------------------------------------------------------**/
public class NByteArrayCrLfSerializer extends AbstractPooledBufferByteArraySerializer{
/**
* 单例模式
*/
public static final NByteArrayCrLfSerializer INSTANCE = new NByteArrayCrLfSerializer();
/**
* 数据转换输出,当前服务器有数据下发时,读取成byte数组后调用输出方法,传输给出站管道
* @param inputStream 输入流
* @param buffer 缓存对象
* @return 读取之后的bytes
* @throws IOException
*/
@Override
public byte[] doDeserialize(InputStream inputStream, byte[] buffer) throws IOException {
int n = this.fillToCrLf(inputStream, buffer);
return this.copyToSizedArray(buffer, n);
}
/**
* 数据校验及数据处理,原对象是逐个byte读取的,然后校验是否为结束符
* 这里做了修改,直接读取到指定长度的bytes中,然后返回,
* @param inputStream
* @param buffer
* @return
* @throws IOException
*/
public int fillToCrLf(InputStream inputStream, byte[] buffer) throws IOException {
int n=0; //读到到的数据长度,这里指byte数组长度
if (logger.isDebugEnabled()) {
logger.debug("Available to read: " + inputStream.available());
}
try {
return inputStream.read(buffer);//读取数据,buffer默认为2048个byte
} catch (SoftEndOfStreamException e) {
throw e;
} catch (IOException e) {
publishEvent(e, buffer, n);
throw e;
} catch (RuntimeException e) {
publishEvent(e, buffer, n);
throw e;
}
}
/**
* 写入数据,去掉了原对象的结束符自动补齐,不需要特定结束符
* @param bytes
* @param outputStream
* @throws IOException
*/
@Override
public void serialize(byte[] bytes, OutputStream outputStream) throws IOException {
outputStream.write(bytes);//直接输出
}
}
串口部分采用一个服务类加一个工具类完成,一个串口一个监听,有数据就进行TCP转发,同样TCP有数据下发就向串口转发,服务类实现如下:
package org.noka.serialservice.service;
import jssc.SerialPort;
import jssc.SerialPortList;
import org.apache.commons.codec.binary.Hex;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
/**---------------------------------------------------
* 串口服务类,主要对串口进行操作
* @author xiefangjian@163.com
* @version 1.0.0
**-------------------------------------------------**/
@Component
public class SerialService{
private static Logger logger = LoggerFactory.getLogger(SerialService.class);
private final TcpGateway tcpGateway;//TCP数据发送网关,自动注入,调用该接口发送数据到TCP服务端
private List<SerialUtils> COM_LIST = new ArrayList<>();//缓存串口列表
/**
* 构造方法,注入TCP网关对象
* @param tcpGateway TCP网关对象
*/
public SerialService(TcpGateway tcpGateway){
this.tcpGateway = tcpGateway;
initComs();//初始化串口
}
/**
* 打开所有串口
*/
public void initComs(){
String[] com_lists= SerialPortList.getPortNames();//获取串口列表
for(String com:com_lists){
logger.info("init com:"+com);
SerialPort serialPort = new SerialPort(com);//设置串口
COM_LIST.add(new SerialUtils(serialPort,tcpGateway));//缓存串口列表
}
}
/**
* 发送数据到所有打开的串口
* @param str 需要发送的数据
*/
public void send(byte[] str){
for(SerialUtils s:COM_LIST){
s.sendData(str);//发送数据到串口
logger.info("[send "+s.getName()+" data]"+new String(str));//字符串方式日志打印
logger.info("[send "+s.getName()+" hex]"+Hex.encodeHexString(str,false));//16进制方式打印日志
}
}
/**
* 关闭串口
*/
@PreDestroy
public void destory(){
for(SerialUtils s:COM_LIST){
s.close();//关闭串口
}
}
}
串口工具类如下:
package org.noka.serialservice.service;
import jssc.SerialPort;
import jssc.SerialPortEvent;
import jssc.SerialPortEventListener;
import jssc.SerialPortException;
import org.apache.commons.codec.binary.Hex;
import org.noka.serialservice.config.SerialParams;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.integration.support.MessageBuilder;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**--------------------------------------------------------------------
* 串口工具类
* @author xiefangjian@163.com
* @version 1.0.0
**------------------------------------------------------------------**/
public class SerialUtils implements SerialPortEventListener {
private static Logger logger = LoggerFactory.getLogger(SerialUtils.class);
private SerialPort serialPort=null;//串口对象
private TcpGateway tcpGateway; //网关对象
/**
* 构造方法,初始化串口对象和网关对象
* @param serialPort 串口对象
* @param tcpGateway 网关对象
*/
public SerialUtils(SerialPort serialPort, TcpGateway tcpGateway) {
if(null!=serialPort){
this.serialPort=serialPort; //串口对象
openCom();//初始化串口
}
this.tcpGateway = tcpGateway;//网关对象
}
/**
* 获取本串口名称
* @return 串口名称
*/
public String getName(){
if(null!=serialPort) {
return serialPort.getPortName();
}
return null;
}
/**
* 打开串口
*/
public void openCom(){
try {
serialPort.openPort();//打开串口
/*
串口波特率,默认为115200
串口数据位,默认为8位
串口停止位,默认为1位
串口奇偶校验,默认无奇偶校验
串口发送请求,默认为true
串口发送允许,默认为true
serial.baud=115200
serial.dataBits=8
serial.stopBits=1
serial.parity=0
serial.rts=true
serial.dtr=true
*/
serialPort.setParams(
SerialParams.baudRate, //串口波特率,默认为115200
SerialParams.dataBits, //串口数据位,默认为8位
SerialParams.stopBits, //串口停止位,默认为1位
SerialParams.parity, //串口奇偶校验,默认无奇偶校验
SerialParams.rts, //串口发送请求,默认为true
SerialParams.dtr //串口发送允许,默认为true
);
int a = serialPort.getFlowControlMode();
serialPort.setFlowControlMode(SerialParams.flow);//无硬件流控
logger.info("----------------------------------------------------------------------");
logger.info("Serial Port:"+serialPort.getPortName()+" baudRate:"+SerialParams.baudRate);
logger.info("Serial Port:"+serialPort.getPortName()+" dataBits:"+SerialParams.dataBits);
logger.info("Serial Port:"+serialPort.getPortName()+" stopBits:"+SerialParams.stopBits);
logger.info("Serial Port:"+serialPort.getPortName()+" parity:"+SerialParams.parity);
logger.info("Serial Port:"+serialPort.getPortName()+" rts:"+SerialParams.rts);
logger.info("Serial Port:"+serialPort.getPortName()+" dtr:"+SerialParams.dtr);
logger.info("Serial Port:"+serialPort.getPortName()+" FlowControlMode:"+SerialParams.flow);
logger.info("-----------------------------------------------------------------------");
serialPort.addEventListener(this);//开启数据接收监听
}catch (SerialPortException ux) {
logger.error(ux.getMessage());
}
}
/**
* 发送数据
* @param str 需要写入的数据
*/
public void sendData(byte[] str){
if(null!=serialPort && serialPort.isOpened()){//如果串口已经打开
try {
serialPort.writeBytes(str);//向串口写入数据
}catch (SerialPortException ex){
logger.error(ex.getMessage());
}
}
}
/**
* 发送数据
* @param str 需要写入的数据
*/
public void sendData(String str){
if(null!=serialPort && serialPort.isOpened()){//如果串口已经打开
try {
serialPort.writeString(str);//向串口写入数据
}catch (SerialPortException ex){
logger.error(ex.getMessage());
}
}
}
/**
* 关闭串口
*/
public void close(){
try {
serialPort.closePort();
}catch (Exception ex){
logger.error(ex.getMessage());
}
}
/**
* 串口有数据上来,转发数据到服务器上
* @param serialPortEvent 事件类型
*/
@Override
public void serialEvent(SerialPortEvent serialPortEvent) {
if (serialPortEvent.isRXCHAR()) {//有数据到达事件发生
ByteArrayOutputStream xs = new ByteArrayOutputStream();//数据缓存对象
try {
if(serialPort.getInputBufferBytesCount()>0){//数据池有数据
Thread.sleep(10);//等待10毫秒,以便更多的数据进入数据池,以确保数据传输完成
while(serialPort.getInputBufferBytesCount()>0) {//循环读取数据
Thread.sleep(3);//等待10毫秒
byte[] sx = serialPort.readBytes();//读取数据
if (null != sx) {
xs.write(sx);//放入数据缓存池
} else {
break;//为空时,说明读取完成,需要跳出循环
}
}
}
} catch (SerialPortException | InterruptedException | IOException e) {
logger.error(e.getMessage());
}
byte[] xbs = xs.toByteArray();//获取所有缓存数据
try {
tcpGateway.send(MessageBuilder.withPayload(xbs).build());//发送串口数据到服务器
logger.info("[com up data start for " + getName() + "]----------------------------------------");
logger.info("[com up data]" + new String(xbs));//字符串方式打印日志
logger.info("[com up hex]" + Hex.encodeHexString(xbs, false));//16进制方式打印日志
logger.info("[com up data end for" + getName() + "]-----------------------------------------");
}catch (Exception ex){
logger.error("[net client is closed]");
logger.info("[com up data start for " + getName() + "]----------------------------------------");
logger.info("[com up data]" + new String(xbs));//字符串方式打印日志
logger.info("[com up hex]" + Hex.encodeHexString(xbs, false));//16进制方式打印日志
logger.info("[com up data end for" + getName() + "]-----------------------------------------");
}
try{xs.close();}catch (Exception es){}//关闭数据缓存池
}
}
}
SerialParams是自定义的一个参数配置类,主要用于在配置文件中配置串口的各项参数
package org.noka.serialservice.config;
import jssc.SerialPort;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**-------------------------------------------------
* 串口参数配置
* @author xiefangjian@163.com
* @version 1.0.0
**-----------------------------------------------**/
/**-------------------------------------------------
*
* serial.baud=115200 串口波特率,默认为115200
* serial.dataBits=8 串口数据位,默认为8位
* serial.stopBits=1 串口停止位,默认为1位
* serial.parity=0 串口奇偶校验,默认无奇偶校验
* serial.rts=true 串口发送请求,默认为true
* serial.dtr=true 串口发送允许,默认为true
*
**---------------------------------------------**/
@Component
public class SerialParams {
public static Integer baudRate= SerialPort.BAUDRATE_115200;//串口波特率
public static Integer dataBits=SerialPort.DATABITS_8;//8位数据位
public static Integer stopBits=SerialPort.STOPBITS_1;//1位停止位
public static Integer parity=SerialPort.PARITY_NONE;//无奇偶校验
public static boolean rts=true;//发送请求
public static boolean dtr=true;//发送允许
public static Integer flow=SerialPort.FLOWCONTROL_NONE;//无硬件流控制
@Value("${serial.baud:115200}")
public void setBaudRate(Integer baudRate) {
this.baudRate = baudRate;
}
@Value("${serial.dataBits:8}")
public void setDataBits(Integer dataBits) {
this.dataBits = dataBits;
}
@Value("${serial.stopBits:1}")
public void setStopBits(Integer stopBits) {
this.stopBits = stopBits;
}
@Value("${serial.parity:0}")
public void setParity(Integer parity) {
this.parity = parity;
}
@Value("${serial.rts:true}")
public void setRts(boolean rts) { this.rts = rts; }
@Value("${serial.dtr:true}")
public void setDtr(boolean dtr) {
this.dtr = dtr;
}
@Value("${serial.parity:0}")
public void setFlow(Integer flow){
this.flow=flow;
}
}