一、导入tio相关依赖(tio是一款对socket进行封装了,支持高并发的一款框架)
org.t-io
tio-websocket-spring-boot-starter
3.6.0.v20200315-RELEASE
org.t-io
tio-core-spring-boot-starter
3.6.0.v20200315-RELEASE
二、yml配置文件
自己可以设置连接端口和
websocket端口
tio:
# websocket port default 9876
websocket:
server:
port: 8802
heartbeat-timeout: 120000
cluster:
enabled: false
mobilePhone: 4780
三、导入几个配置类
package com.taipy.ppi.launcher.tcp;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.tio.server.ServerTioConfig;
import org.tio.server.TioServer;
@Component
public class ServerStarter implements ApplicationRunner {
//handler, 包括编码、解码、消息处理
public static ServerAioHandlerImpl aioHandler = new ServerAioHandlerImpl();
//事件监听器
public static ServerAioListenerImpl aioListener = new ServerAioListenerImpl();
//一组连接共用的上下文对象
public static ServerTioConfig serverGroupContext = new ServerTioConfig(aioHandler, aioListener);
//tioServer对象
public static TioServer tioServer = new TioServer(serverGroupContext);
//监听的端口
public static int serverPort = Const.PORT;
@Override
public void run(ApplicationArguments args) throws Exception {
tioServer.start(null,serverPort);
}
}
package com.taipy.ppi.launcher.tcp;
import org.tio.core.ChannelContext;
import org.tio.core.intf.Packet;
import org.tio.server.intf.ServerAioListener;
public class ServerAioListenerImpl implements ServerAioListener {
@Override
public void onAfterConnected(ChannelContext channelContext, boolean b, boolean b1) throws Exception {
}
@Override
public void onAfterDecoded(ChannelContext channelContext, Packet packet, int i) throws Exception {
}
@Override
public void onAfterReceivedBytes(ChannelContext channelContext, int i) throws Exception {
}
@Override
public void onAfterSent(ChannelContext channelContext, Packet packet, boolean b) throws Exception {
}
@Override
public void onAfterHandled(ChannelContext channelContext, Packet packet, long l) throws Exception {
}
@Override
public void onBeforeClose(ChannelContext channelContext, Throwable throwable, String s, boolean b) throws Exception {
}
@Override
public boolean onHeartbeatTimeout(ChannelContext channelContext, Long aLong, int i) {
return false;
}
}
package com.taipy.ppi.launcher.tcp;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class Const {
/**
* 服务器地址
*/
public static final String SERVER = "127.0.0.1";
/**
* 监听端口
*/
public static int PORT ;
/**
* 心跳超时时间
*/
public static final int TIMEOUT = 60000;
@Value("${tio.mobilePhone}")
public void setPort(int port){
PORT = port;
}
}
package com.taipy.ppi.launcher.tcp;
import com.alibaba.fastjson.JSONObject;
import lombok.Getter;
import lombok.Setter;
import org.tio.core.intf.Packet;
import java.util.List;
/**
* @author Lenovo 消息体
*/
@Setter
@Getter
public class MindPackage extends Packet {
private static final long serialVersionUID = -172060606924066412L;
public static final String CHARSET = "utf-8";
private List body;
}
package com.taipy.ppi.launcher.tcp;
import com.alibaba.fastjson.JSONObject;
import lombok.Getter;
import lombok.Setter;
import org.tio.core.intf.Packet;
/**
* @author Lenovo 消息体
*/
@Setter
@Getter
public class ResponsePackage extends Packet {
private static final long serialVersionUID = -172060606924066412L;
public static final String CHARSET = "utf-8";
private JSONObject body;
private String phoneNum;
private Integer type; // 下发指令类型
}
package com.taipy.ppi.launcher.tcp;
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.taipy.ppi.launcher.tcp.vo.ClientDirectivesVo;
import com.taipy.ppi.launcher.tcp.vo.DataDistributionReportVo;
import com.taipy.ppi.launcher.tcp.vo.PositioningDataReportVo;
import com.taipy.ppi.launcher.tcp.vo.ResponseVo;
import jodd.util.ThreadUtil;
import lombok.extern.slf4j.Slf4j;
import org.tio.core.ChannelContext;
import org.tio.core.Tio;
import org.tio.core.TioConfig;
import org.tio.core.exception.AioDecodeException;
import org.tio.core.intf.Packet;
import org.tio.server.intf.ServerAioHandler;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author Lenovo 处理器
*/
@Slf4j
public class ServerAioHandlerImpl implements ServerAioHandler {
private static AtomicInteger counter = new AtomicInteger(0);
private Map channelMaps = new ConcurrentHashMap<>();
private Queue respQueue = new LinkedBlockingQueue<>();
private Queue heartQueue = new LinkedBlockingQueue<>();
public boolean offer2SendQueue(ResponsePackage respPacket) {
return respQueue.offer(respPacket);
}
public Queue getRespQueue() {
return respQueue;
}
public boolean offer2HeartQueue(ResponsePackage respPacket) {
return heartQueue.offer(respPacket);
}
public Map getChannelMaps() {
return channelMaps;
}
/**
* 解码:把接收到的ByteBuffer,解码成应用可以识别的业务消息包
* 总的消息结构:消息体
* 消息体结构: 对象的json串的16进制字符串
*/
@Override
public MindPackage decode(ByteBuffer buffer, int i, int i1, int i2, ChannelContext channelContext) throws AioDecodeException {
MindPackage imPacket = new MindPackage();
try {
List msgList = new ArrayList<>();
Charset charset = Charset.forName("UTF-8");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(buffer);
String str = charBuffer.toString();
if (str.indexOf("{") != 0) {
str = str.substring(str.indexOf("{"));
}
if (str.indexOf("}{") > -1) {
String[] split = str.split("}");
List list = Arrays.asList(split);
list.forEach(item -> {
item += "}";
msgList.add(JSON.parseObject(item));
});
} else {
msgList.add(JSON.parseObject(str));
}
log.info("收到" + msgList.size() + "条消息");
imPacket.setBody(msgList);
return imPacket;
} catch (Exception e) {
return imPacket;
}
}
/**
* 编码:把业务消息包编码为可以发送的ByteBuffer
*/
@Override
public ByteBuffer encode(Packet packet, TioConfig groupContext, ChannelContext channelContext) {
ResponsePackage helloPacket = (ResponsePackage) packet;
JSONObject body = helloPacket.getBody();
//写入消息体
try {
return ByteBuffer.wrap(body.toJSONString().getBytes("GB2312"));
} catch (UnsupportedEncodingException e) {
}
return null;
}
/**
* 处理消息
*/
@Override
public void handler(Packet packet, ChannelContext channelContext) throws Exception {
MindPackage helloPacket = (MindPackage) packet;
List msgList = helloPacket.getBody();
if (CollectionUtil.isNotEmpty(msgList)) {
msgList.forEach(body -> {
if (body != null) {
log.info("收到设备上报信息 " + body);
// 获取指令
Integer type = body.getInteger("type");
if (type != null) {
channelContext.set("type",type);
String phoneNum = body.getString("phoneNum");
Tio.bindToken(channelContext,phoneNum);
ResponsePackage respPacket = new ResponsePackage();
switch (type) {
// 接收下线指令
case ClientDirectivesVo.END_REPORT_RESPONSE:
//保存连接
channelMaps.put(phoneNum, channelContext);
//TODO 更改客户端状态为下线状态
log.info("收到{}客户端下线通知",phoneNum);
// 回执方法
receiptHandler(respPacket,phoneNum,ClientDirectivesVo.END_REPORT_RESPONSE);
break;
case ClientDirectivesVo.HEART_BEET_REQUEST: //接收心跳检查指令
//保存连接
channelMaps.put(phoneNum, channelContext);
log.info("收到{}客户端心跳检查指令",phoneNum);
// 回执方法
receiptHandler(respPacket,phoneNum,ClientDirectivesVo.HEART_BEET_REQUEST);
break;
case ClientDirectivesVo.GPS_START_REPORT_RESPONSE: //开始上报GPS指令
//保存连接
channelMaps.put(phoneNum, channelContext);
PositioningDataReportVo vo =JSONObject.toJavaObject(body, PositioningDataReportVo.class);
log.info("收到{}客户端上报GPS指令,上报数据:{}",phoneNum,vo);
// 回执方法
receiptHandler(respPacket,phoneNum,ClientDirectivesVo.GPS_START_REPORT_RESPONSE);
break;
case ClientDirectivesVo.DATA_DISTRIBUTION: //开始下发数据指令
//保存连接
channelMaps.put(phoneNum, channelContext);
log.info("收到{}客户端下发数据指令",phoneNum);
// 回执方法
DataDistributionReportVo data = new DataDistributionReportVo();
data.setUserId("20");
data.setPhone("147555544444");
data.setPlanId("1222222");
data.setXxxx("预留字段");
// 回复时的设备标志,必填
respPacket.setPhoneNum(phoneNum);
respPacket.setBody((JSONObject) JSON.toJSON(data));
respPacket.setType(ClientDirectivesVo.DATA_DISTRIBUTION);
offer2SendQueue(respPacket);
break;
}
}
}
});
}
return;
}
/**
* 回执信息方法
* @Author: laohuang
* @Date: 2022/11/24 13:53
*/
public void receiptHandler(ResponsePackage respPacket,String phoneNum,Integer clientDirectives) {
// 回执信息
ResponseVo callVo = new ResponseVo();
callVo.setType(clientDirectives);
// 响应结果 1:成功 0:失败
callVo.setValue(1);
// 回复时的设备标志,必填
respPacket.setPhoneNum(phoneNum);
respPacket.setBody((JSONObject) JSON.toJSON(callVo));
respPacket.setType(clientDirectives);
offer2SendQueue(respPacket);
}
private Object locker = new Object();
public ServerAioHandlerImpl() {
try {
new Thread(() -> {
while (true) {
try {
ResponsePackage respPacket = respQueue.poll();
if (respPacket != null) {
synchronized (locker) {
String phoneNum = respPacket.getPhoneNum();
ChannelContext channelContext = channelMaps.get(phoneNum);
if (channelContext != null) {
Boolean send = Tio.send(channelContext, respPacket);
String s = JSON.toJSONString(respPacket);
System.err.println("发送数据"+s);
System.err.println("数据长度"+s.getBytes().length);
log.info("下发设备指令 设备ip" + channelContext + " 设备[" + respPacket.getPhoneNum() + "]" + (send ? "成功" : "失败") + "消息:" + JSON.toJSONString(respPacket.getBody()));
}
}
}
} catch (Exception e) {
log.error(e.getMessage());
} finally {
log.debug("发送队列大小:" + respQueue.size());
ThreadUtil.sleep(10);
}
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 确保只有一个呼叫器响应后修改呼叫记录
* @param recordId 记录id
* @param resCallSn 响应的呼叫器sn
*/
public synchronized void updateCallRecordAndStopResponse(Long recordId, String resCallSn, String sn) {
}
}
这里的数据格式要自己与客户端定数据格式
package com.taipy.ppi.launcher.tcp.vo;
import lombok.Data;
/**
* @program: taipy-ppi-restful
* @description: 客户端接收指令类型
* @author: laohuang
* @create: 2022-11-24 11:51
**/
@Data
public class ClientDirectivesVo {
// 结束上报指令
public static final int END_REPORT_RESPONSE = 0;
// 心跳检查指令
public static final int HEART_BEET_REQUEST = 1;
// GPS开始上报指令
public static final int GPS_START_REPORT_RESPONSE = 2;
// 客户端数据下发
public static final int DATA_DISTRIBUTION = 3;
// 0:结束上报指令,1:心跳检测指令,2:GPS开始上报指令,3:客户端数据下发
private Integer type;
}
可以根据自己不同的业务需求制定实体类
处理消息部分代码仅供参考,需要根据自己的业务进行改造!
最后 启动类上加开启tio注解
@EnableTioWebSocketServer
然后启动程序
在控制台会显示tio成功启动
现在用tcp连接工具测试
这里就可以看到<-----发送给服务器的数据 和------->服务器回执的数据