springboot integrates tio-websocket solution to realize simple chat

Write first:

The commonly used http protocol is stateless and cannot actively respond to the client. Initially, polling or other low-efficiency methods were the only way to achieve dynamic status tracking, so the websocket protocol was introduced to allow the server to actively push data to the client. In the WebSocket API, the browser and the server only need to complete a handshake, and a persistent connection can be created directly between the two, and two-way data transmission can be performed. To put it simply, two or more clients cannot communicate with each other. In order to realize a function similar to one-to-one chat, the essence is that client A sends information to the socket server, and then the socket server actively pushes it to client B. Or multiple clients, realizing information transfer between two or more clients.

Tucao: t-io is an excellent socket framework, but there are very few documents, and the documents written by the author are not clear and unfriendly to novices (except for spending money). Other documents written either cost money or are terrible. This technical environment is really rubbish.

1. Import package (import two dependencies of TIO, other necessary dependencies will not be described in detail)

        <dependency>
            <groupId>org.t-io</groupId>
            <artifactId>tio-websocket-spring-boot-starter</artifactId>
            <version>3.6.0.v20200315-RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.t-io</groupId>
            <artifactId>tio-core-spring-boot-starter</artifactId>
            <version>3.6.0.v20200315-RELEASE</version>
        </dependency>

2. yml configuration

server:
  port: 8652

tio:
  websocket:
    server:
      port: 8078
      heartbeat-timeout: 12000
    cluster:
      enabled: false
  customPort: 4768 //自定义socket服务端监听端口,其实也可以用上面server.port做监听端口

3. Configuration parameters

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.tio.utils.time.Time;

/**
 * @Author 955
 * @Date 2023-07-26 17:25
 * @Description
 */
@Component
public class CaseServerConfig {
    
    

    /**
     * 协议名字(可以随便取,主要用于开发人员辨识)
     */
    public static final String PROTOCOL_NAME = "xxxxxxx";
    public static final String CHARSET = "utf-8";
    /**
     * 监听的ip
     */
    public static final String SERVER_IP = null;//null表示监听所有,并不指定ip
    /**
     * 监听端口
     */
    public static int PORT;
    /**
     * 心跳超时时间,单位:毫秒
     */
    public static final int HEARTBEAT_TIMEOUT = 1000 * 60;

    /**
     * 服务器地址
     */
    public static final String SERVER = "127.0.0.1";

    /**
     * ip数据监控统计,时间段
     *
     * @author tanyaowu
     */
    public static interface IpStatDuration {
    
    
        public static final Long DURATION_1 = Time.MINUTE_1 * 5;
        public static final Long[] IPSTAT_DURATIONS = new Long[]{
    
    DURATION_1};
    }
    
    /**
     * 用于群聊的group id(自定义)
     */
    public static final String GROUP_ID = "showcase-websocket";

    @Value("${tio.customPort}")
    public void setPort(int port) {
    
    
        PORT = port;
    }

}

Fourth, implement some monitoring classes

1. ServerAioListener listens

import org.tio.core.ChannelContext;
import org.tio.core.intf.Packet;
import org.tio.server.intf.ServerAioListener;

/**
 * @Author 955
 * @Date 2023-07-26 17:24
 * @Description
 */
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;
    }

}

2. IpStatListener monitoring (this is optional)


import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tio.core.ChannelContext;
import org.tio.core.TioConfig;
import org.tio.core.intf.Packet;
import org.tio.core.stat.IpStat;
import org.tio.core.stat.IpStatListener;

/**
 * @Author 955
 * @Date 2023-07-27 12:03
 * @Description
 */
public class ShowcaseIpStatListener implements IpStatListener {
    
    

    @SuppressWarnings("unused")
    private static Logger log = LoggerFactory.getLogger(ShowcaseIpStatListener.class);
    public static final ShowcaseIpStatListener me = new ShowcaseIpStatListener();

    /**
     *
     */
    private ShowcaseIpStatListener() {
    
    
    }

    @Override
    public void onExpired(TioConfig tioConfig, IpStat ipStat) {
    
    
        //在这里把统计数据入库中或日志
//        if (log.isInfoEnabled()) {
    
    
//            log.info("可以把统计数据入库\r\n{}", Json.toFormatedJson(ipStat));
//        }
    }

    @Override
    public void onAfterConnected(ChannelContext channelContext, boolean isConnected, boolean isReconnect, IpStat ipStat) throws Exception {
    
    
//        if (log.isInfoEnabled()) {
    
    
//            log.info("onAfterConnected\r\n{}", Json.toFormatedJson(ipStat));
//        }
    }

    @Override
    public void onDecodeError(ChannelContext channelContext, IpStat ipStat) {
    
    
//        if (log.isInfoEnabled()) {
    
    
//            log.info("onDecodeError\r\n{}", Json.toFormatedJson(ipStat));
//        }
    }

    @Override
    public void onAfterSent(ChannelContext channelContext, Packet packet, boolean isSentSuccess, IpStat ipStat) throws Exception {
    
    
//        if (log.isInfoEnabled()) {
    
    
//            log.info("onAfterSent\r\n{}\r\n{}", packet.logstr(), Json.toFormatedJson(ipStat));
//        }
    }

    @Override
    public void onAfterDecoded(ChannelContext channelContext, Packet packet, int packetSize, IpStat ipStat) throws Exception {
    
    
//        if (log.isInfoEnabled()) {
    
    
//            log.info("onAfterDecoded\r\n{}\r\n{}", packet.logstr(), Json.toFormatedJson(ipStat));
//        }
    }

    @Override
    public void onAfterReceivedBytes(ChannelContext channelContext, int receivedBytes, IpStat ipStat) throws Exception {
    
    
//        if (log.isInfoEnabled()) {
    
    
//            log.info("onAfterReceivedBytes\r\n{}", Json.toFormatedJson(ipStat));
//        }
    }

    @Override
    public void onAfterHandled(ChannelContext channelContext, Packet packet, IpStat ipStat, long cost) throws Exception {
    
    
//        if (log.isInfoEnabled()) {
    
    
//            log.info("onAfterHandled\r\n{}\r\n{}", packet.logstr(), Json.toFormatedJson(ipStat));
//        }
    }

}

3. WsServerAioListener listens


import com.wlj.config.CaseServerConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tio.core.ChannelContext;
import org.tio.core.Tio;
import org.tio.core.intf.Packet;
import org.tio.websocket.common.WsResponse;
import org.tio.websocket.common.WsSessionContext;
import org.tio.websocket.server.WsServerAioListener;

/**
 * @Author 955
 * @Date 2023-07-27 12:01
 * @Description
 */
public class ShowcaseServerAioListener extends WsServerAioListener {
    
    

    private static Logger log = LoggerFactory.getLogger(ShowcaseServerAioListener.class);
    public static final ShowcaseServerAioListener me = new ShowcaseServerAioListener();

    private ShowcaseServerAioListener() {
    
    
    }

    @Override
    public void onAfterConnected(ChannelContext channelContext, boolean isConnected, boolean isReconnect) throws Exception {
    
    
        super.onAfterConnected(channelContext, isConnected, isReconnect);
        if (log.isInfoEnabled()) {
    
    
            log.info("onAfterConnected\r\n{}", channelContext);
        }
    }

    @Override
    public void onAfterSent(ChannelContext channelContext, Packet packet, boolean isSentSuccess) throws Exception {
    
    
        super.onAfterSent(channelContext, packet, isSentSuccess);
        if (log.isInfoEnabled()) {
    
    
            log.info("onAfterSent\r\n{}\r\n{}", packet.logstr(), channelContext);
        }
    }

    @Override
    public void onBeforeClose(ChannelContext channelContext, Throwable throwable, String remark, boolean isRemove) throws Exception {
    
    
        super.onBeforeClose(channelContext, throwable, remark, isRemove);
        if (log.isInfoEnabled()) {
    
    
            log.info("onBeforeClose\r\n{}", channelContext);
        }
        WsSessionContext wsSessionContext = (WsSessionContext) channelContext.getAttribute();
        if (wsSessionContext != null && wsSessionContext.isHandshaked()) {
    
    
            int count = Tio.getAllChannelContexts(channelContext.tioConfig).getObj().size();
            String msg = channelContext.getClientNode().toString() + " 离开了,现在共有【" + count + "】人在线";
            //用tio-websocket,服务器发送到客户端的Packet都是WsResponse
            WsResponse wsResponse = WsResponse.fromText(msg, CaseServerConfig.CHARSET);
            //群发
            Tio.sendToGroup(channelContext.tioConfig, CaseServerConfig.GROUP_ID, wsResponse);
        }
    }

    @Override
    public void onAfterDecoded(ChannelContext channelContext, Packet packet, int packetSize) throws Exception {
    
    
        super.onAfterDecoded(channelContext, packet, packetSize);
        if (log.isInfoEnabled()) {
    
    
            log.info("onAfterDecoded\r\n{}\r\n{}", packet.logstr(), channelContext);
        }
    }

    @Override
    public void onAfterReceivedBytes(ChannelContext channelContext, int receivedBytes) throws Exception {
    
    
        super.onAfterReceivedBytes(channelContext, receivedBytes);
        if (log.isInfoEnabled()) {
    
    
            log.info("onAfterReceivedBytes\r\n{}", channelContext);
        }
    }

    @Override
    public void onAfterHandled(ChannelContext channelContext, Packet packet, long cost) throws Exception {
    
    
        super.onAfterHandled(channelContext, packet, cost);
        if (log.isInfoEnabled()) {
    
    
            log.info("onAfterHandled\r\n{}\r\n{}", packet.logstr(), channelContext);
        }
    }


}

4. IWsMsgHandler interception (the logic inside is based on the specific business, but this must be implemented, otherwise the startup will report an error)

package com.wlj.im;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.tio.core.ChannelContext;
import org.tio.core.Tio;
import org.tio.core.TioConfig;
import org.tio.http.common.HttpRequest;
import org.tio.http.common.HttpResponse;
import org.tio.websocket.common.WsRequest;
import org.tio.websocket.common.WsResponse;
import org.tio.websocket.server.handler.IWsMsgHandler;

/**
 * @Author 955
 * @Date 2023-07-31 18:26
 * @Description
 */
@Slf4j
@Component
public class TioWsMsgHandler implements IWsMsgHandler {
    
    
    /**
     * TIO-WEBSOCKET 配置信息
     */
    public static TioConfig serverTioConfig;

    @Override
    public HttpResponse handshake(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {
    
    
        serverTioConfig = channelContext.tioConfig;
        return httpResponse;
    }

    @Override
    public void onAfterHandshaked(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {
    
    
        // 拿到用户id
        String id = httpRequest.getParam("id");
        // 绑定用户
        Tio.bindUser(channelContext, id);

        // 绑定业务类型(根据业务类型判定处理相关业务)
        String bsId = httpRequest.getParam("bsId");
        if (StringUtils.isNotBlank(bsId)) {
    
    
            Tio.bindBsId(channelContext, bsId);
        }
        // 给用户发送消息
        WsResponse wsResponse = WsResponse.fromText("您已成功连接 WebSocket 服务器", "UTF-8");
        Tio.sendToUser(channelContext.tioConfig, id, wsResponse);
    }

    @Override
    public Object onBytes(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
    
    
        return null;
    }

    @Override
    public Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {
    
    
        // 关闭连接
        Tio.remove(channelContext, "WebSocket Close");
        return null;
    }

    @Override
    public Object onText(WsRequest wsRequest, String s, ChannelContext channelContext) throws Exception {
    
    
        WsResponse wsResponse = WsResponse.fromText("服务器已收到消息:" + s, "UTF-8");
        Tio.sendToUser(channelContext.tioConfig, userid, wsResponse);
        return null;
    }
}

5. Some message bodies (according to business requirements)

import com.alibaba.fastjson.JSONObject;
import lombok.Getter;
import lombok.Setter;
import org.tio.core.intf.Packet;

import java.util.List;

/**
 * @Author 955
 * @Date 2023-07-26 17:26
 * @Description  消息体
 */
@Setter
@Getter
public class MindPackage extends Packet {
    
    

    private static final long serialVersionUID = -172060606924066412L;
    public static final String CHARSET = "utf-8";
    private List<JSONObject> body;

}



import com.alibaba.fastjson.JSONObject;
import lombok.Getter;
import lombok.Setter;
import org.tio.core.intf.Packet;

import java.io.Serializable;

/**
 * @Author 955
 * @Date 2023-07-26 17:27
 * @Description  响应消息体
 */
@Getter
@Setter
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;
}

Six, some vo (according to the actual business)

import lombok.Data;

import java.io.Serializable;

/**
 * @Author 955
 * @Date 2023-07-26 17:28
 * @Description 客户端接收指令类型
 */
@Data
public class ClientDirectivesVo implements Serializable {
    
    

    // 结束上报指令
    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;

}


import lombok.Data;

import java.io.Serializable;

/**
 * @Author 955
 * @Date 2023-07-26 17:29
 * @Description 业务实体vo,根据自己业务来
 */
@Data
public class PositioningDataReportVo implements Serializable {
    
    

    private String userId;

    private String name;

    private String phone;

    private String type;

}

import lombok.Data;

import java.io.Serializable;

/**
 * @Author 955
 * @Date 2023-07-26 17:30
 * @Description 回执方法vo
 */
@Data
public class ReceiptDataVo implements Serializable {
    
    

    //所属用户id
    private String userId;

    //所属用户电话号码
    private String phone;

    //xxx具体业务字段
    private String yl;

}

import lombok.Data;

import java.io.Serializable;

/**
 * @Author 955
 * @Date 2023-07-26 17:31
 * @Description 响应vo
 */
@Data
public class ResponseVo implements Serializable {
    
    

    //响应类型
    private Integer type;

    //响应值
    private Integer value;

}

7. Specific business methods


import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.wlj.tcp.MindPackage;
import com.wlj.tcp.ResponsePackage;
import com.wlj.vo.ClientDirectivesVo;
import com.wlj.vo.PositioningDataReportVo;
import com.wlj.vo.ReceiptDataVo;
import com.wlj.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 org.tio.utils.hutool.CollUtil;

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 955
 * @Date 2023-07-26 17:27
 * @Description 具体业务方法
 */
@Slf4j
public class ServerAioHandlerImpl implements ServerAioHandler {
    
    


    private static AtomicInteger counter = new AtomicInteger(0);

    private Map<String, ChannelContext> channelMaps = new ConcurrentHashMap<>();

    private Queue<ResponsePackage> respQueue = new LinkedBlockingQueue<>();

    private Queue<ResponsePackage> heartQueue = new LinkedBlockingQueue<>();

    public boolean offer2SendQueue(ResponsePackage respPacket) {
    
    
        return respQueue.offer(respPacket);
    }

    public Queue<ResponsePackage> getRespQueue() {
    
    
        return respQueue;
    }

    public boolean offer2HeartQueue(ResponsePackage respPacket) {
    
    
        return heartQueue.offer(respPacket);
    }

    public Map<String, ChannelContext> 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<JSONObject> msgList = new ArrayList<>();
            //Charset charset = Charset.forName("UTF-8");
            //这里使用UTF-8收中文时会报错
            Charset charset = Charset.forName("GBK");
            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<String> 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<JSONObject> 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");
                        String content = body.getString("content");
                        Tio.bindToken(channelContext, phoneNum);
                        ResponsePackage respPacket = new ResponsePackage();
                        switch (type) {
    
    
                            // 接收下线指令
                            case ClientDirectivesVo.END_REPORT_RESPONSE:
                                //保存连接
                                channelMaps.put(phoneNum, channelContext);
                                //TODO 更改客户端状态为下线状态
                                log.info("收到{}客户端下线通知", phoneNum);
                                Tio.unbindUser(channelContext.tioConfig, phoneNum);
                                respPacket.setPhoneNum("您已下线");
                                // 回执方法
                                receiptHandler(respPacket, phoneNum, ClientDirectivesVo.END_REPORT_RESPONSE);
                                break;
                            case ClientDirectivesVo.HEART_BEET_REQUEST:  //接收心跳检查指令
                                //保存连接
                                channelMaps.put(phoneNum, channelContext);
                                Tio.bindUser(channelContext, phoneNum);
                                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);
                                SetWithLock<ChannelContext> obj = Tio.getByUserid(channelContext.tioConfig, phoneNum);
                                if (ObjectUtil.isEmpty(obj)) {
    
    
                                    // 回执方法
                                    respPacket.setBody(JSONObject.parseObject("{\"type\":\"该用户不在线\"}"));
                                    receiptHandler(respPacket, phoneNum, ClientDirectivesVo.GPS_START_REPORT_RESPONSE);
                                } else {
    
    
                                    // 回执方法
                                    DataDistributionReportVo data = new DataDistributionReportVo();
                                    data.setPhone(phoneNum);
                                    data.setServiceInfo(content);
                                    // 回复时的设备标志,必填
                                    respPacket.setPhoneNum(phoneNum);
                                    respPacket.setBody((JSONObject) JSON.toJSON(data));
                                    respPacket.setType(ClientDirectivesVo.DATA_DISTRIBUTION);
                                    Tio.sendToUser(channelContext.tioConfig, phoneNum, 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) {
    
    


    }
}

8. Tio configuration

public abstract class TioServerConfig {
    
    
    /**
     * 协议名字(可以随便取,主要用于开发人员辨识)
     */
    public static final String PROTOCOL_NAME = "IM";

    public static final String CHARSET = "utf-8";


    /**
     * 监听端口
     */
    public static final int SERVER_PORT = 4768;

    /**
     * 心跳超时时间,单位:毫秒
     */
    public static final int HEARTBEAT_TIMEOUT = 1000 * 60;

    /**
     * ip数据监控统计,时间段
     *
     * @author tanyaowu
     */
    public interface IpStatDuration {
    
    
        Long DURATION_1 = Time.MINUTE_1 * 5;
        Long[] IPSTAT_DURATIONS = new Long[]{
    
    DURATION_1};
    }

}



import org.tio.server.ServerTioConfig;
import org.tio.websocket.server.WsServerStarter;

/**
 * TIO 配置文件
 *
 * @author 
 * @since  
 */
public class TioWebsocketStarter {
    
    

    private WsServerStarter wsServerStarter;
    private ServerTioConfig serverGroupContext;


    /**
     * @author tanyaowu
     */
    public TioWebsocketStarter(int port, TioWsMsgHandler wsMsgHandler) throws Exception {
    
    
        wsServerStarter = new WsServerStarter(port, wsMsgHandler);

        serverGroupContext = wsServerStarter.getServerTioConfig();
        serverGroupContext.setName(TioServerConfig.PROTOCOL_NAME);
        serverGroupContext.setServerAioListener(ServerAioListener.me);

        //设置ip监控
        serverGroupContext.setIpStatListener(IpStatListener.me);
        //设置ip统计时间段
        serverGroupContext.ipStats.addDurations(TioServerConfig.IpStatDuration.IPSTAT_DURATIONS);

        //设置心跳超时时间
        serverGroupContext.setHeartbeatTimeout(TioServerConfig.HEARTBEAT_TIMEOUT);
        //如果你希望通过wss来访问,就加上下面的代码吧,不过首先你得有SSL证书(证书必须和域名相匹配,否则可能访问不了ssl)
//		String keyStoreFile = "classpath:config/ssl/keystore.jks";
//		String trustStoreFile = "classpath:config/ssl/keystore.jks";
//		String keyStorePwd = "214323428310224";
//		serverGroupContext.useSsl(keyStoreFile, trustStoreFile, keyStorePwd);

    }

    public WsServerStarter getWsServerStarter() {
    
    
        return wsServerStarter;
    }


}

9. Internal startup class


@Component
public class StartTioRunner implements CommandLineRunner {
    
    

    @Resource
    private TioWsMsgHandler tioWsMsgHandler;

    private TioWebsocketStarter appStarter;

    @Override
    public void run(String... args) throws Exception {
    
    
        appStarter = new TioWebsocketStarter(TioServerConfig.SERVER_PORT, tioWsMsgHandler);
        appStarter.getWsServerStarter().start();
    }

    public TioWebsocketStarter getAppStarter() {
    
    
        return appStarter;
    }
}


9. Use the NetAssist test tool to test the effect (0 point download is enough)
https://download.csdn.net/download/m0_49605579/88106789?spm=1001.2014.3001.5503
Insert image description here

Note: The remote host port here is the tioPort configured in yml, which is the listening port printed on the console when the project is started. Once connected, data can be sent to the server. The tool can open multiple simulated multiple clients.

Insert image description here
Written at the end:
Here is the logic of the handler for the main business:
Step 1:
User A sends {"type": 1,"phoneNum":"User A"} corresponding to type:HEART_BEET_REQUEST, and binds Tio.bindUser(channelContext, userId);the user with the use.
User B performs the same operation as above {"type": 1,"phoneNum":"user A"}
Insert image description here

Step 2:
User A sends {"type": 3, "content": "send message to user B", "phoneNum": "user B"} corresponding to type: DATA_DISTRIBUTION and sends an instruction through the server. The server first judges whether Online, if online, the message sent by user A will be pushed to user B whose mobile phone number is user B, and user B will receive the message in real time.
Insert image description here

Effect diagram (type controls users to send messages, go online, and go offline):
Insert image description here
Insert image description here
Supplement: If you want to get ServerTioConfig in a place other than Handler, you can use this method (the premise is to start it according to the method of implementing CommandLineRunner above)

@Resource
private ApplicationContext applicationContext;
StartTioRunner startTioRunner = applicationContext.getBean(StartTioRunner.class);
ServerTioConfig serverTioConfig = startTioRunner.getAppStarter().getWsServerStarter().getServerTioConfig();

Guess you like

Origin blog.csdn.net/m0_49605579/article/details/131956239