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分别控制用户发送消息、上线、下线):
 
 
相关文章:
springboot整合tio-websocket方案实现简易聊天
写在最前: 常用的http协议是无状态的,且不能主动响应到客户端。最初想实现状态动态跟踪只能用轮询或者其他效率低下的方式,所以引入了websocket协议,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务…...
《TCP IP网络编程》第十三章
第 13 章 多种 I/O 函数 13.1 send & recv 函数 Linux 中的 send & recv: send 函数定义: #include <sys/socket.h> ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags); /* 成功时返回发送的字节数,失败…...
驱动开发 day8 (设备树驱动,按键中断实现led亮灭)
//编译驱动 (注意Makefile的编译到移植到开发板的内核) make archarm //清除编译生成文件 make clean ****************************************** //安装驱动 insmod mycdev.ko //卸载驱动 rmmod mycdev 需要在<内核路径>/arch/arm/boot/dts/ 修改 stm32mp157a-fsm…...
DataX将MySQL数据同步到HDFS中时,空值不处理可以吗
DataX将MySQL数据同步到HDFS中时,空值存到HDFS中时,默认是存储为\N,这样会有两个缺点: 会产生歧义,如果MySQL业务数据中有\N数据,那么存储到HDFS上是\N,null值存储也是\N,当用Hive查…...
P3373 【模板】线段树 2(乘法与加法)(内附封面)
【模板】线段树 2 题目描述 如题,已知一个数列,你需要进行下面三种操作: 将某区间每一个数乘上 x x x;将某区间每一个数加上 x x x;求出某区间每一个数的和。 输入格式 第一行包含三个整数 n , q , m n,q,m n,…...
实现langchain-ChatGLM API调用客户端(及未解决的问题)
langchain-ChatGLM是一个基于本地知识库的LLM对话库。其基于text2vec-large-Chinese为Embedding模型,ChatGLM-6B为对话大模型。原项目地址:https://github.com/chatchat-space/langchain-ChatGLM 对于如何本地部署ChatGLM模型,可以参考我之前…...
【AltWalker】模型驱动:轻松实现自动化测试用例的生成和组织执行
目录 模型驱动的自动化测试 优势 操作步骤 什么是AltWalker? 安装AltWalker 检查是否安装了正确的版本 牛刀小试 创建一个测试项目 运行测试 运行效果 在线模型编辑器 VScode扩展 本地部署 包含登录、选择产品、支付、退出登录的模型编写 模型效果 1…...
大数据课程E3——Flume的Sink
文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 掌握Sink的HDFS Sink; ⚪ 掌握Sink的Logger Sink; ⚪ 掌握Sink的File Roll Sink; ⚪ 掌握Sink的Null Sink; ⚪ 掌握Sink的AVRO Sink; ⚪ 掌握Sink的Custom Sink; 一、HDFS Sink …...
如何快速做单元测试?
首先写unit test之前,要确认自己的测试遵循两个原则: 1、尽量不要干涉原来的代码。从阅读代码的体验来说,不要让你的测试(哪怕是一小段if..else...的代码)出现在你准备测试的代码中。 2、代码要只是测试某个class里面…...
不同对象的集合转换
https://blog.csdn.net/qq_42483473/article/details/128984514 import com.alibaba.fastjson.JSON;import java.util.ArrayList; import java.util.List;/*** author */ public class ObjectConversion {/*** 从List<A> copy到List<B>* param list List<B>…...
【机器学习】Gradient Descent
Gradient Descent for Linear Regression 1、梯度下降2、梯度下降算法的实现(1) 计算梯度(2) 梯度下降(3) 梯度下降的cost与迭代次数(4) 预测 3、绘图4、学习率 首先导入所需的库: import math, copy import numpy as np import matplotlib.pyplot as plt plt.styl…...
直播读弹幕机器人:直播弹幕采集+文字转语音(附完整代码)
目录 前言代码实现请求数据解析数据文字转语音完整代码 高级点的tk界面版 前言 直播读弹幕机器人是指能够实时读取直播平台上观众发送的弹幕,并将其转化为语音进行播放的机器人。这种机器人通常会使用文字转语音技术,将接收到的弹幕文本转为语音&#x…...
K3s vs K8s:轻量级对决 - 探索替代方案
在当今云原生应用的领域中,Kubernetes(简称K8s)已经成为了无可争议的领导者。然而,随着应用规模的不断增长,一些开发者和运维人员开始感受到了K8s的重量级特性所带来的挑战。为了解决这一问题,一个名为K3s的…...
dev控件gridControl,gridview中添加合计
需求:在合并结账查询中,双击每一条结账出现这次结账对应的结算明细: 弹出的页面包括:结算日期,ID,姓名,费别,预交金收入,结算金额,收据号,合计&a…...
SpringBoot基础认识
创建SpringBoot模块 首先需要引设置maven并引用maven环境 1.打开项目结构,new module,选择Spring Initializr,URL选默认: group填写分组如com.kdy , Artifact起个模块名如springboot_quickstart,Type选择M…...
二十三种设计模式第十九篇--命令模式
命令模式是一种行为设计模式,它将请求封装成一个独立的对象,从而允许您以参数化的方式将客户端代码与具体实现解耦。在命令模式中,命令对象充当调用者和接收者之间的中介。这使您能够根据需要将请求排队、记录请求日志、撤销操作等。 命令模…...
STM32基础入门学习笔记:基础知识和理论 开发环境建立
文件目录: 一:基础知识和理论 1.ARM简介 2.STM32简介 3.STM32命名规范 4.STM32内部功能* 5.STM32接口定义 二:开发环境建立 1.开发板简介 2.ISP程序下载 3.最小系统电路 4.KEIL的安装 5.工程简介与调试流程 6.固件库的安装 7.编…...
Qt应用开发(基础篇)——数值微调输入框QAbstractSpinBox、QSpinBox、QDoubleSpinBox
目录 一、前言 二、QAbstractSpinBox类 1、accelerated 2、acceptableInput 3、alignment 4、buttonSymbols 5、correctionMode 6、frame 7、keyboardTracking 8、readOnly 9、showGroupSeparator 10、specialValueText 11、text 12、wrapping 13、信号 二、Q…...
html | 无js二级菜单
1. 效果图 2. 代码 <meta charset"utf-8"><style> .hiddentitle{display:none;}nav ul{list-style-type: none;background-color: #001f3f;overflow:hidden; /* 父标签加这个,防止有浮动子元素时,该标签失去高度*/margin: 0;padd…...
appium的基本使用
appium的基本使用 一、appium的基本使用appium环境安装1、安装Android SDK 2、安装Appium3、安装手机模拟器4、Pycharm安装 appium-python-alicent5、连接appium和模拟器6、Python代码调用appium软件,appium软件在通过adb命令调用android操作系统(模拟器…...
iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...
大话软工笔记—需求分析概述
需求分析,就是要对需求调研收集到的资料信息逐个地进行拆分、研究,从大量的不确定“需求”中确定出哪些需求最终要转换为确定的“功能需求”。 需求分析的作用非常重要,后续设计的依据主要来自于需求分析的成果,包括: 项目的目的…...
Prompt Tuning、P-Tuning、Prefix Tuning的区别
一、Prompt Tuning、P-Tuning、Prefix Tuning的区别 1. Prompt Tuning(提示调优) 核心思想:固定预训练模型参数,仅学习额外的连续提示向量(通常是嵌入层的一部分)。实现方式:在输入文本前添加可训练的连续向量(软提示),模型只更新这些提示参数。优势:参数量少(仅提…...
JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...
3.3.1_1 检错编码(奇偶校验码)
从这节课开始,我们会探讨数据链路层的差错控制功能,差错控制功能的主要目标是要发现并且解决一个帧内部的位错误,我们需要使用特殊的编码技术去发现帧内部的位错误,当我们发现位错误之后,通常来说有两种解决方案。第一…...
如何在看板中体现优先级变化
在看板中有效体现优先级变化的关键措施包括:采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中,设置任务排序规则尤其重要,因为它让看板视觉上直观地体…...
oracle与MySQL数据库之间数据同步的技术要点
Oracle与MySQL数据库之间的数据同步是一个涉及多个技术要点的复杂任务。由于Oracle和MySQL的架构差异,它们的数据同步要求既要保持数据的准确性和一致性,又要处理好性能问题。以下是一些主要的技术要点: 数据结构差异 数据类型差异ÿ…...
04-初识css
一、css样式引入 1.1.内部样式 <div style"width: 100px;"></div>1.2.外部样式 1.2.1.外部样式1 <style>.aa {width: 100px;} </style> <div class"aa"></div>1.2.2.外部样式2 <!-- rel内表面引入的是style样…...
Mac下Android Studio扫描根目录卡死问题记录
环境信息 操作系统: macOS 15.5 (Apple M2芯片)Android Studio版本: Meerkat Feature Drop | 2024.3.2 Patch 1 (Build #AI-243.26053.27.2432.13536105, 2025年5月22日构建) 问题现象 在项目开发过程中,提示一个依赖外部头文件的cpp源文件需要同步,点…...
Java毕业设计:WML信息查询与后端信息发布系统开发
JAVAWML信息查询与后端信息发布系统实现 一、系统概述 本系统基于Java和WML(无线标记语言)技术开发,实现了移动设备上的信息查询与后端信息发布功能。系统采用B/S架构,服务器端使用Java Servlet处理请求,数据库采用MySQL存储信息࿰…...
