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

写在最前:

常用的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: 8652tio:websocket:server:port: 8078heartbeat-timeout: 12000cluster:enabled: falsecustomPort: 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 {@Overridepublic void onAfterConnected(ChannelContext channelContext, boolean b, boolean b1) throws Exception {}@Overridepublic void onAfterDecoded(ChannelContext channelContext, Packet packet, int i) throws Exception {}@Overridepublic void onAfterReceivedBytes(ChannelContext channelContext, int i) throws Exception {}@Overridepublic void onAfterSent(ChannelContext channelContext, Packet packet, boolean b) throws Exception {}@Overridepublic void onAfterHandled(ChannelContext channelContext, Packet packet, long l) throws Exception {}@Overridepublic void onBeforeClose(ChannelContext channelContext, Throwable throwable, String s, boolean b) throws Exception {}@Overridepublic 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() {}@Overridepublic void onExpired(TioConfig tioConfig, IpStat ipStat) {//在这里把统计数据入库中或日志
//        if (log.isInfoEnabled()) {
//            log.info("可以把统计数据入库\r\n{}", Json.toFormatedJson(ipStat));
//        }}@Overridepublic void onAfterConnected(ChannelContext channelContext, boolean isConnected, boolean isReconnect, IpStat ipStat) throws Exception {
//        if (log.isInfoEnabled()) {
//            log.info("onAfterConnected\r\n{}", Json.toFormatedJson(ipStat));
//        }}@Overridepublic void onDecodeError(ChannelContext channelContext, IpStat ipStat) {
//        if (log.isInfoEnabled()) {
//            log.info("onDecodeError\r\n{}", Json.toFormatedJson(ipStat));
//        }}@Overridepublic 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));
//        }}@Overridepublic 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));
//        }}@Overridepublic void onAfterReceivedBytes(ChannelContext channelContext, int receivedBytes, IpStat ipStat) throws Exception {
//        if (log.isInfoEnabled()) {
//            log.info("onAfterReceivedBytes\r\n{}", Json.toFormatedJson(ipStat));
//        }}@Overridepublic 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() {}@Overridepublic void onAfterConnected(ChannelContext channelContext, boolean isConnected, boolean isReconnect) throws Exception {super.onAfterConnected(channelContext, isConnected, isReconnect);if (log.isInfoEnabled()) {log.info("onAfterConnected\r\n{}", channelContext);}}@Overridepublic 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);}}@Overridepublic 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都是WsResponseWsResponse wsResponse = WsResponse.fromText(msg, CaseServerConfig.CHARSET);//群发Tio.sendToGroup(channelContext.tioConfig, CaseServerConfig.GROUP_ID, wsResponse);}}@Overridepublic 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);}}@Overridepublic void onAfterReceivedBytes(ChannelContext channelContext, int receivedBytes) throws Exception {super.onAfterReceivedBytes(channelContext, receivedBytes);if (log.isInfoEnabled()) {log.info("onAfterReceivedBytes\r\n{}", channelContext);}}@Overridepublic 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;@Overridepublic HttpResponse handshake(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {serverTioConfig = channelContext.tioConfig;return httpResponse;}@Overridepublic void onAfterHandshaked(HttpRequest httpRequest, HttpResponse httpResponse, ChannelContext channelContext) throws Exception {// 拿到用户idString 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);}@Overridepublic Object onBytes(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {return null;}@Overridepublic Object onClose(WsRequest wsRequest, byte[] bytes, ChannelContext channelContext) throws Exception {// 关闭连接Tio.remove(channelContext, "WebSocket Close");return null;}@Overridepublic 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 {//所属用户idprivate 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进制字符串*/@Overridepublic 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*/@Overridepublic 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;}/*** 处理消息(最核心的方法)*/@Overridepublic 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.rhkb.cn/news/76389.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ThinkPHP 6 添加跳转提示扩展 liliuwei/thinkphp-jump

liliuwei/thinkphp-jump 是 TP5 中经典跳转提示&#xff0c;在 TP6 中已经取消&#xff0c;通过 composer 下载该扩展可以在 TP6 中使用 TP5 的跳转提示操作。 安装扩展 在应用根目录执行: composer require liliuwei/thinkphp-jump引入扩展 在全局配置目录生成 jump.php 文件…

Eeny Meeny Moo

Eeny Meeny Moo 题目描述输入输出格式输入格式输出格式 输入输出样例输入样例输出样例 正确解法A C 代码 题目描述 你肯定有过这样的经验&#xff0c;那就是当很多一起使用网络的时候&#xff0c;网速变得很慢很慢。为了解决这个问题&#xff0c;德国的Ulm大学开发了一份意外事…

【GEMM预备工作】行主序和列主序矩阵的内存中的连续性,解决理解问题

在内存存储中&#xff0c;默认矩阵是按照行优先储存的&#xff0c;即矩阵的每一列在内存中是连续的。行优先矩阵储存中行数据是不连续的。 而对于列主序的矩阵&#xff0c;是按照列优先储存的&#xff0c;即矩阵的每一行在内存中是连续的。列优先矩阵储存中列数据是不连续的&am…

命令模式(Command)

命令模式是一种行为设计模式&#xff0c;可将一个请求封装为一个对象&#xff0c;用不同的请求将方法参数化&#xff0c;从而实现延迟请求执行或将其放入队列中或记录请求日志&#xff0c;以及支持可撤销操作。其别名为动作(Action)模式或事务(Transaction)模式。 Command is …

redis入门3-在java中操作redis

Redis的java客户端 Jedis、Lettuce、Redisson、以及spring提供的spring data redis Jedis操作redis //添加依赖 <dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.8.0</version> </dep…

从价值的角度看,为何 POSE 通证值得长期看好

PoseSwap 是 Nautilus Chain 上的首个 DEX&#xff0c;基于 Nautilus Chain 也让其成为了首个以模块化构建的 Layer3 架构的 DEX。该 DEX 本身能够以 Dapp 层&#xff08;Rollup&#xff09;的形态&#xff0c;与其他应用层并行化运行。基于 Zk-Rollup PoseiSwap 具备隐私特性&…

智能文件批量改名工具,自定义重命名,格式转换一步到位!

每当你需要将大量视频文件进行重命名&#xff0c;改变格式时&#xff0c;是不是总感觉手动操作费时费力&#xff0c;让你抓狂不已&#xff1f;别担心&#xff01;我们的文件批量改名高手将会解决你的困扰 首先&#xff0c;我们要打开文件批量改名高手&#xff0c;在“文件批量重…

【设备树笔记整理3】设备树的规范(dts和dtb)

1 dts 文件格式 1.1 DTS文件布局(layout): /dts-v1/; [memory reservations] / {[property definitions][child nodes] }; &#xff08;1&#xff09;第一行表示当前设备树文件的版本 &#xff08;2&#xff09;第二行用来定义保留的内存区域&#xff0c;该区域不给内核使…

GIS在地质灾害危险性评估与灾后重建中的应用教程

详情点击链接&#xff1a;GIS在地质灾害危险性评估与灾后重建中的实践技术应用 前言 地质灾害是指全球地壳自然地质演化过程中&#xff0c;由于地球内动力、外动力或者人为地质动力作用下导致的自然地质和人类的自然灾害突发事件。由于降水、地震等自然作用下&#xff0c;地质…

R-Meta分析教程

详情点击链接&#xff1a;R-Meta模型教程 一&#xff1a;Meta分析的选题与文献计量分析CiteSpace应用 1、Meta分析的选题与文献检索 1)什么是Meta分析&#xff1f; 2)Meta分析的选题策略 3)文献检索数据库 4)精确检索策略&#xff0c;如何检索全、检索准 5)文献的管理与…

5个免费PPT模板、素材网站,赶紧马住了~

推荐几个做PPT必备的免费素材网站&#xff0c;直接套用模板&#xff0c;赶紧收藏起来~ 菜鸟图库 ppt模板免费下载|ppt背景图片 - 菜鸟图库 菜鸟图库网有非常丰富的免费素材&#xff0c;像设计类、办公类、自媒体类等素材都很丰富。PPT模板种类很多&#xff0c;全部都统一做好…

Stable Diffusion教程(6) - 图片高清放大

放大后细节 修复图片损坏 显存占用 速度 批量放大 文生图放大 好 是 高 慢 否 附加功能放大 一般 否 中 快 是 图生图放大 好 是 低 慢 是 tile模型放大 非常好 是 高 快 是 通过文生图页面的高清修复 优点&#xff1a;放大时能添加更多细节&am…

AutoSAR系列讲解(实践篇)11.6-服务映射(自顶向下)

目录 一、配置Service Needs 二、配置Cfg同步 我们在下一节的实验课中讲解这里的具体配置流程,本节主要讲一下这些配置的大致流程和配置项的作用。NvBlockSwComponents是一个可选项, 我们这里开始不使用NvBlockSwComponents,将我们的Application SWC直接和NvM通过C/S连接起…

【Linux】权限

1、shell命令以及运行原理 Linux 严格意义上说的是一个操作系统&#xff0c;我们称之为“核心&#xff08;kernel&#xff09;“ &#xff0c;但我们一般用户&#xff0c;不能直接使用 kernel。而是通过 kernel 的“外壳”程序&#xff0c;也就是所谓的shell&#xff0c;来与 k…

SpringMVC视图

SpringMVC视图 视图的作用是渲染数据&#xff0c;将模型Model中的数据展示给客户&#xff0c;SpringMVC中视图的种类有很多&#xff0c;默认有转发视图(InternalResourceView)和重定向视图(RedirectView)。 当工程引入jstl的依赖&#xff0c;转发视图会自动跳转jstlView,若使用…

【Spring Cloud 六】Hystrix熔断

这里写目录标题 系列文章目录背景一、Hystrix是什么服务雪崩服务容错的相关概念熔断器降级超时控制限流 二、会什么要有Hystrix三、如何使用Hystrix进行熔断处理整体项目代码服务提供者pom文件yml配置文件启动类controller 服务消费者pom文件yml配置文件启动类feignhystrixcont…

js省市区下拉框联动——前端笔记

问题&#xff1a; 我们常常要用到下拉框联动的功能&#xff0c;比如最常用的是选择地址的 省 市 区 的联动。思路&#xff1a; 先填充第一个下拉框&#xff0c;然后写一个第一个下拉框的change事件来加载第二个下拉框&#xff0c;再写第二个下拉框的change事件来加载第三个下…

Jmeter远程服务模式运行时引用csv文件的路径配置

问题 在使用jmeter过程中&#xff0c;本机的内存等配置不足&#xff0c;启动较多的线程时&#xff0c;可以采用分布式运行。 在分布式运行的时候&#xff0c;jmeter会自动将脚本从master主机发送到remote主机上&#xff0c;所以不需要考虑将脚本拷贝到remote主机。但是jmeter…

Mr. Cappuccino的第55杯咖啡——Mybatis一级缓存二级缓存

Mybatis一级缓存&二级缓存 概述一级缓存特点演示前准备效果演示在同一个SqlSession中在不同的SqlSession中 源代码怎么禁止使用一级缓存一级缓存在什么情况下会被清除 二级缓存特点演示前准备效果演示在不同的SqlSession中 源代码怎么关闭二级缓存 一级缓存&#xff08;Spr…

抖音seo矩阵系统源码搭建开发详解

抖音SEO矩阵系统是一个用于提高抖音视频在搜索引擎排名的工具。如果你想开发自己的抖音SEO矩阵系统&#xff0c;以下是详细的步骤&#xff1a; 开发步骤详解&#xff1a; 确定你需要的功能和算法 抖音SEO矩阵系统包含很多功能&#xff0c;比如关键词研究、内容优化、链接建设、…