springboot整合tio-websocket方案实现简易聊天

写在最前:

常用的http协议是无状态的,且不能主动响应到客户端。最初想实现状态动态跟踪只能用轮询或者其他效率低下的方式,所以引入了websocket协议,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。简单来说就是两个或多个客户端之间不能相互交流,要想实现类似一对一聊天的功能,实质上就是A客户端发送信息到socket服务器,再由socket服务器主动推送到B客户端或者多个客户端,实现两个或多个客户端之间的信息传递。

吐槽:t-io是个很优秀的socket框架,但是文档很少,作者写的文档也不明不白的对新手很不友好(花钱除外),其他写的文档不是要钱就是写的巨烂,这技术环境真心垃圾。

一、导包(导入TIO的两个依赖,其他必要依赖不赘述)

java 复制代码
        <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>

二、yml配置

java 复制代码
server:
  port: 8652

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

三、配置参数

java 复制代码
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;
    }

}

四、实现一些监听类

1.ServerAioListener监听

java 复制代码
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监听(这个可选)

java 复制代码
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监听

java 复制代码
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拦截(里面逻辑根据具体业务,但是必须实现这个,不然启动报错)

java 复制代码
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 WebSocketMessageHandler 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;
    }
}

五、一些消息体(根据业务需求)

java 复制代码
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;
}

六、一些vo(根据实际业务来)

java 复制代码
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;

}

七、具体业务方法

java 复制代码
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) {


    }
}

八、启动类(加上@EnableTioWebSocketServer,表明作为Socket服务端)

java 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.tio.websocket.starter.EnableTioWebSocketServer;

@SpringBootApplication
@EnableTioWebSocketServer
public class PartApplication {
    public static void main(String[] args) {
        SpringApplication.run(PartApplication.class, args);
    }
}

九、使用NetAssist测试工具测试效果(0积分下载即可)
https://download.csdn.net/download/m0_49605579/88106789?spm=1001.2014.3001.5503

注:这里远程主机端口为yml内配置的tioPort,即为项目启动时控制台打印的监听端口,连接上就可以发送数据到服务器,工具可以打开多个模拟多个客户端。

写在最后:

这里说一下主要业务这个handler的逻辑:

第一步:

A用户发送{"type":1,"phoneNum":"用户A"}对应type:HEART_BEET_REQUEST,使用Tio.bindUser(channelContext, userId);绑定该用户。

B用户按上述同样操作{"type":1,"phoneNum":"用户A"}

第二步:

A用户发送{"type":3,"content":"发送消息到用户B","phoneNum":"用户B"}对应type:DATA_DISTRIBUTION通过服务器下发指令,服务器这里先判断是否在线,如果在线就把A用户发的消息推送给手机号是用户B的B用户,此时B用户实时收到消息。

效果图(type分别控制用户发送消息、上线、下线):

相关推荐
橙子家6 小时前
Serilog 日志库简单实践(二):控制台与调试 Sinks(.net8)
后端
想不明白的过度思考者6 小时前
Rust——异步递归深度指南:从问题到解决方案
开发语言·后端·rust
陈果然DeepVersion7 小时前
Java大厂面试真题:Spring Boot+Kafka+AI智能客服场景全流程解析(五)
java·spring boot·kafka·向量数据库·大厂面试·rag·ai智能客服
FAFU_kyp7 小时前
Spring Boot 邮件发送系统 - 从零到精通教程
java·网络·spring boot
ConardLi7 小时前
Easy Dataset 已经突破 11.5K Star,这次又带来多项功能更新!
前端·javascript·后端
芒克芒克7 小时前
ssm框架之Spring(上)
java·后端·spring
晨晖27 小时前
SpringBoot的yaml配置文件,热部署
java·spring boot·spring
冒泡的肥皂7 小时前
MVCC初学demo(二
数据库·后端·mysql
追逐时光者7 小时前
一款基于 .NET WinForm 开源、轻量且功能强大的节点编辑器,采用纯 GDI+ 绘制无任何依赖库仅仅100+Kb
后端·.net
鬼火儿7 小时前
1.2 redis7.0.4安装与配置开机自启动
java·后端