项目中用SpringBoot集成Tio

一、导入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) {


    }
}

这里可以根据自己的业务指定不同的指令类型,项目中用SpringBoot集成Tio_第1张图片

 这里的数据格式要自己与客户端定数据格式

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

项目中用SpringBoot集成Tio_第2张图片

 然后启动程序

在控制台会显示tio成功启动

现在用tcp连接工具测试

项目中用SpringBoot集成Tio_第3张图片

 这里就可以看到<-----发送给服务器的数据  和------->服务器回执的数据

你可能感兴趣的:(spring,boot,java,后端)