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

news2025/1/11 20:51:41

写在最前:

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

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

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

        <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配置

server:
  port: 8652

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

三、配置参数

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监听

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监听(这个可选)


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监听


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

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;
    }
}

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

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(根据实际业务来)

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;

}

七、具体业务方法


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服务端)

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分别控制用户发送消息、上线、下线):
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/819403.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

富士通“Actlyzer”提供基于AI的基于视频的行为分析

想象一下这样的场景&#xff1a;一个男人走近一个家的前门&#xff0c;蹲下并检查钥匙孔。这是丢失房屋钥匙的居民还是客人&#xff1f;还是寻找入口点的窃贼&#xff1f;“Actlyzer”是一种新的人工智能安全系统&#xff0c;旨在区分这种情况。富士通实验室和研发中心的行为分…

经典文献阅读之--Online Extrinsic Calibration(激光雷达,视觉和惯导外参在线标定)

0. 简介 为了实现精确定位&#xff0c;自动驾驶车辆通常依靠围绕移动平台的多传感器感知系统。校准是一个耗时的过程&#xff0c;机械畸变会导致外部校准误差。因此&#xff0c;《Lidar-Visual-Inertial Odometry with Online Extrinsic Calibration》提出了一种激光雷达-视觉…

安卓手机怎样录屏?手机录屏小技巧分享

如果你需要在安卓手机上录制一段视频或者教程&#xff0c;录屏功能是一个非常有用的工具。在这篇文章中&#xff0c;我们将会介绍安卓手机如何录屏&#xff0c;并分享一些小技巧。 在安卓手机上录屏的方法有很多种&#xff0c;这里我们介绍两种比较常用的方法。 方法一&#x…

Nodejs 爬虫 第二章(通过掘金 userId 抓取到所有文章)!HTML to Markdown

前言 就在昨天晚上&#xff0c;做了一个通过掘金文章链接进行解析html转化为markdown文档&#xff0c;并且把图片进行下载和替换&#xff0c;但是最后也留了个伏笔&#xff08;我一定会回来的&#xff5e;&#xff09;就是通过用户id&#xff0c;把所有的文章都转化出来&#x…

安卓嵌入h5页面方法笔记

<?xml version"1.0" encoding"utf-8"?> <manifest xmlns:android"http://schemas.android.com/apk/res/android"xmlns:tools"http://schemas.android.com/tools"><uses-featureandroid:name"android.hardware.t…

2023年的深度学习入门指南(24) - 处理音频的大模型 OpenAI Whisper

2023年的深度学习入门指南(24) - 处理音频的大模型 OpenAI Whisper 在这一讲之前&#xff0c;我们所用的大模型都是针对文本的。这一讲我们增加一个新的领域&#xff0c;即音频。我们将介绍OpenAI的Whisper模型&#xff0c;它是一个处理音频的大模型。 Whisper模型的用法 Wh…

万界星空/推出生产制造执行MES系统/开源MES/免费下载

免费MES系统介绍 什么是MES系统呢&#xff1f;MES系统主要功能就是解决“如何生产”的问题。通过实施MES系统&#xff0c;一站式解决您所困扰的所有生产制作流程问题。 普通的免费MES系统只提供简单的基本功能让客户体验&#xff0c;而万界星空MES系统运用低代码的形式开发&a…

Python初刷题笔记

目录 保留小数的方法&#xff1a;​编辑 进制问题如何转换&#xff1a; 大小写如何转换&#xff1a; 删除空格问题&#xff1a; 循环输入的简便方法&#xff1a; 截取某一部分所需要的函数&#xff1a; 字符串处理常用函数小汇总&#xff1a; sort和sorted函数的区别&am…

【Python】数据分析+数据挖掘——掌握Python和Pandas中的单元格替换操作

1. 前言 数据处理和清洗是数据分析和机器学习中至关重要的步骤。在数据处理过程中&#xff0c;我们经常需要对数据集进行清洗和转换&#xff0c;其中单元格替换是一个常用的技术。Python作为一种功能强大且灵活的编程语言&#xff0c;为数据处理提供了丰富的工具和库。Pandas库…

《你不知道的 Chrome 调试技巧》-- 学习笔记

截全屏 ctrl shift p &#xff1a;调出command 然后输入screen 效果 引用上次结果 在console中&#xff0c;想要引用上次的计算结果 使用$_ 添加有条件的断点 右击行号&#xff0c;选择 Add conditional breakpoint...(添加条件断点) 或者右击一个已经设置的断点并且选择…

GC垃圾回收器【学习笔记】

GC&#xff1a;Garbage Collectors 垃圾回收器 C/C&#xff0c;手动回收内存&#xff1b;难调试、门槛高。忘记回收、多次回收等问题 Java、Golang等&#xff0c;有垃圾回收器&#xff1a;自动回收&#xff0c;技术门槛降低 一、如何定位垃圾&#xff1f; https://www.infoq.c…

24考研数据结构-串的定义和基本实现

目录 第四章&#xff1a;串概论数据结构之串&#xff1a;定义、实现与存储结构1. 串的定义2. 串的实现方式2.1 顺序存储2.2 链式存储 3. 串的基本操作4. 串的应用结论 4.1串的定义和实现4.1.1串的定义4.1.2串的基本操作字符集编码 4.1.3串的存储结构1. 定长顺序存储表示基本操作…

海外ASO优化之通过应用商店优化来获得自然用户

应用商店优化需要我们跟踪关键词的数据&#xff0c;从而评估我们的ASO策略有效性。跟踪我们选择的时间段和国家/地区的重要搜索查询的历史排名和应用程序的最佳位置。根据我们关注的每个应用的关键词&#xff0c;位置&#xff0c;特色&#xff0c;评论和更新&#xff0c;立即了…

性能压力测试的重要性与实施方法

性能压力测试是在软件开发过程中评估系统在不同负载条件下的表现和稳定性的关键步骤。这种测试是为了确定系统在正常和峰值负载下的性能表现&#xff0c;以验证系统是否能够满足用户需求&#xff0c;同时发现潜在的性能问题并加以解决。 首先&#xff0c;性能压力测试对于确保系…

【HMS Core】融合搜索无法打开搜索服务连接问题

【关键字】 HMS、搜索服务、融合搜索 【问题描述】 有开发者反馈在集成搜索服务-融合搜索时&#xff0c;出现了无法打开搜索服务连接的问题。 严格按照官方文档集成&#xff0c;配置&#xff0c;权限已添加 searchServiceAbility .connect(new ServiceConnectCallback() { …

牛客网Verilog刷题——VL46

牛客网Verilog刷题——VL46 题目解析答案 题目 根据题目提供的双口RAM代码和接口描述&#xff0c;实现同步FIFO&#xff0c;要求FIFO位宽和深度参数化可配置。电路的接口如下图所示。   双口RAM端口说明&#xff1a; 同步FIFO端口说明&#xff1a; 双口RAM代码如下&#xff…

1、Tomcat

java介绍 Java语言和平台由以下几个主要部分组成&#xff1a; 1、Java编程语言(Java Language)&#xff1a;这是Java的核心部分&#xff0c;包括Java语法、关键字、数据类型、运算符、控制结构等。程序员使用Java语言来编写应用程序的源代码。 2、Java开发工具包(Java Developm…

企业可以申请DV https证书吗

DV https证书是有基础认证的数字证书&#xff0c;所以DV https证书也可以叫DV基础型https证书。DV基础型https证书是众多https证书中既支持个人&#xff0c;也支持企事业单位申请的https证书&#xff0c;所以企事业单位都可以申请DV基础型https证书&#xff0c;不论是企业门户网…

价值 1k 嵌入式面试题-计算机网络 OSI

开门见山 请讲下 OSI 各层协议的主要功能&#xff1f; 常见问题 回答不系统回答不确切无法和实际网络协议做关联对应 答题思路 OSI 代表了开放互联系统中信息从一台计算机的一个软件应用流到另一个计算机的另一个软件应用的参考模型 OSI 包含 7 层&#xff0c;每一层负责特…

【JavaSE】- 5min拿下泛型!

泛型 1.1 泛型的定义1.2 泛型细节2.1 泛型类2.2 泛型方法2.3 泛型接口2.4 泛型的通配符 1.1 泛型的定义 泛型的介绍 提供了编译时类型安全检测机制 泛型的好处 把运行时期的问题提前到了编译期间避免了强制类型转换&#xff0c;造成了代码的不优雅 泛型的定义格式 <类型&…