当前位置: 首页 > news >正文

Netty+SpringBoot 打造一个 TCP 长连接通讯方案

项目背景

最近公司某物联网项目需要使用socket长连接进行消息通讯,捣鼓了一版代码上线,结果BUG不断,本猿寝食难安,于是求助度娘,数日未眠项目终于平稳运行了,本着开源共享的精神,本猿把项目代码提炼成了一个demo项目,尽量摒弃了其中丑陋的业务部分,希望与同学们共同学习进步。

一、项目架构

本项目使用了netty、redis以及springboot2.2.0

二、项目模块

本项目目录结构如下图:
在这里插入图片描述
netty-tcp-core是公共模块,主要是工具类。netty-tcp-server是netty服务端,服务端仅作测试使用,实际项目中我们只使用了客户端。netty-tcp-client是客户端,也是本文的重点。

三、业务流程

我们实际项目中使用RocketMQ作为消息队列,本项目由于是demo项目于是改为了BlockingQueue。数据流为:

生产者->消息队列->消费者(客户端)->tcp通道->服务端->tcp通道->客户端。

当消费者接收到某设备发送的消息后,将判断缓存中是否存在该设备与服务端的连接,如果存在并且通道活跃则使用该通道发送消息,如果不存在则创建通道并在通道激活后立即发送消息,当客户端收到来自服务端的消息时进行响应的业务处理。

四、代码详解

1.消息队列

由于本demo项目移除了消息中间件,于是需要自己创建一个本地队列模拟真实使用场景

package org.example.client;import org.example.client.model.NettyMsgModel;import java.util.concurrent.ArrayBlockingQueue;/*** 本项目为演示使用本地队列 实际生产中应该使用消息中间件代替(rocketmq或rabbitmq)** @author ReWind00* @date 2023/2/15 11:20*/
public class QueueHolder {private static final ArrayBlockingQueue<NettyMsgModel> queue = new ArrayBlockingQueue<>(100);public static ArrayBlockingQueue<NettyMsgModel> get() {return queue;}
}

使用一个类保存队列的静态实例以便在任何类中都可以快速引用。接下来我们需要启动一个线程去监听队列中的消息,一但消息投递到队列中,我们就取出消息然后异步多线程处理该消息。

public class LoopThread implements Runnable {@Overridepublic void run() {for (int i = 0; i < MAIN_THREAD_POOL_SIZE; i++) {executor.execute(() -> {while (true) {//取走BlockingQueue里排在首位的对象,若BlockingQueue为空,阻断进入等待状态直到try {NettyMsgModel nettyMsgModel = QueueHolder.get().take();messageProcessor.process(nettyMsgModel);} catch (InterruptedException e) {log.error(e.getMessage(), e);}}});}}
}

使用take方法会使该线程一直阻塞直到队列收到消息后进入下一次循环。

2.执行类

process方法来自于MessageProcessor类,该类为单例,但是会有多线程同时执行。

public void process(NettyMsgModel nettyMsgModel) {String imei = nettyMsgModel.getImei();try {synchronized (this) { //为避免收到同一台设备多条消息后重复创建客户端,必须加锁if (redisCache.hasKey(NETTY_QUEUE_LOCK + imei)) { //上一条消息处理中log.info("imei={}消息处理中,重新入列", imei);//放回队列重新等待消费 延迟x秒(实际项目中应该使用rocketmq或者rabbitmq实现延迟消费)new Timer().schedule(new TimerTask() {@Overridepublic void run() {QueueHolder.get().offer(nettyMsgModel);}}, 2000);log.info("imei={}消息处理中,重新入列完成", imei);return;} else {//如果没有在连接中的直接加锁redisCache.setCacheObject(NETTY_QUEUE_LOCK + imei, "1", 120, TimeUnit.SECONDS);}}//缓存中存在则发送消息if (NettyClientHolder.get().containsKey(imei)) {NettyClient nettyClient = NettyClientHolder.get().get(imei);if (null != nettyClient.getChannelFuture() && nettyClient.getChannelFuture().channel().isActive()) { //通道活跃直接发送消息if (!nettyClient.getChannelFuture().channel().isWritable()) {log.warn("警告,通道不可写,imei={},channelId={}", nettyClient.getImei(),nettyClient.getChannelFuture().channel().id());}nettyClient.send(nettyMsgModel.getMsg());} else {log.info("client imei={},通道不活跃,主动关闭", nettyClient.getImei());nettyClient.close();//重新创建客户端发送this.createClientAndSend(nettyMsgModel);}} else {  //缓存中不存在则创建新的客户端this.createClientAndSend(nettyMsgModel);}} catch (Exception e) {log.error(e.getMessage(), e);} finally {//执行完后解锁redisCache.deleteObject(NETTY_QUEUE_LOCK + imei);}}

其中imei是我们设备的唯一标识,我们可以用imei作为缓存的key来确认是否已创建过连接。由于我们消息的并发量可能会很大,所以存在当某设备的连接正在创建的过程中,另一个线程收到该设备消息也开始创建连接的情况,所以我们使用synchronized 代码块以及redis分布式锁来避免此情况的发生。当一条消息获得锁后,在锁释放前,后续消息将会被重新放回消息队列并延迟消费。

获取锁的线程会根据imei判断缓存是否存在连接,如果存在直接发送消息,如果不存在则进入创建客户端的方法。

private void createClientAndSend(NettyMsgModel nettyMsgModel) {log.info("创建客户端执行中imei={}", nettyMsgModel.getImei());//此处的DemoClientHandler可以根据自己的业务定义NettyClient nettyClient = SpringUtils.getBean(NettyClient.class, nettyMsgModel.getImei(), nettyMsgModel.getBizData(),this.createDefaultWorkGroup(this.workerThread), DemoClientHandler.class);executor.execute(nettyClient); //执行客户端初始化try {//利用锁等待客户端激活synchronized (nettyClient) {long c1 = System.currentTimeMillis();nettyClient.wait(5000); //最多阻塞5秒 5秒后客户端仍然未激活则自动解锁long c2 = System.currentTimeMillis();log.info("创建客户端wait耗时={}ms", c2 - c1);}if (null != nettyClient.getChannelFuture() && nettyClient.getChannelFuture().channel().isActive()) { //连接成功//存入缓存NettyClientHolder.get().put(nettyMsgModel.getImei(), nettyClient);//客户端激活后发送消息nettyClient.send(nettyMsgModel.getMsg());} else { //连接失败log.warn("客户端创建失败,imei={}", nettyMsgModel.getImei());nettyClient.close();//可以把消息重新入列处理}} catch (Exception e) {log.error("客户端初始化发送消息异常===>{}", e.getMessage(), e);}
}

当netty客户端实例创建后使用线程池执行初始化,由于是异步执行,我们此时立刻发送消息很可能客户端还没有完成连接,因此必须加锁等待。进入synchronized 代码块,使用wait方法等待客户端激活后解锁,参数5000为自动解锁的毫秒数,意思是如果客户端出现异常情况迟迟未能连接成功并激活通道、解锁,则最多5000毫秒后该锁自动解开。

这参数在实际使用时可以视情况调整,在并发量很大的情况下,5秒的阻塞可能会导致线程池耗尽,或内存溢出。待客户端创建成功并激活后则立即发送消息。

3.客户端

package org.example.client;import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.DelimiterBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.CharsetUtil;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.example.client.handler.BaseClientHandler;
import org.example.core.util.SpringUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;/*** @author ReWind00* @date 2023/2/15 9:59*/
@Slf4j
@Component
@Scope("prototype")
@Getter
@NoArgsConstructor
public class NettyClient implements Runnable {@Value("${netty.server.port}")private int port;@Value("${netty.server.host}")private String host;//客户端唯一标识private String imei;//自定义业务数据private Map<String, Object> bizData;private EventLoopGroup workGroup;private Class<BaseClientHandler> clientHandlerClass;private ChannelFuture channelFuture;public NettyClient(String imei, Map<String, Object> bizData, EventLoopGroup workGroup, Class<BaseClientHandler> clientHandlerClass) {this.imei = imei;this.bizData = bizData;this.workGroup = workGroup;this.clientHandlerClass = clientHandlerClass;}@Overridepublic void run() {try {this.init();log.info("客户端启动imei={}", imei);} catch (Exception e) {log.error("客户端启动失败:{}", e.getMessage(), e);}}public void close() {if (null != this.channelFuture) {this.channelFuture.channel().close();}NettyClientHolder.get().remove(this.imei);}public void send(String message) {try {if (!this.channelFuture.channel().isActive()) {log.info("通道不活跃imei={}", this.imei);return;}if (!StringUtils.isEmpty(message)) {log.info("队列消息发送===>{}", message);this.channelFuture.channel().writeAndFlush(message);}} catch (Exception e) {log.error(e.getMessage(), e);}}private void init() throws Exception {//将本实例传递到handlerBaseClientHandler clientHandler = SpringUtils.getBean(clientHandlerClass, this);Bootstrap b = new Bootstrap();//2 通过辅助类去构造server/clientb.group(workGroup).channel(NioSocketChannel.class).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 3000).option(ChannelOption.SO_RCVBUF, 1024 * 32).option(ChannelOption.SO_SNDBUF, 1024 * 32).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024 * 1024, Unpooled.copiedBuffer("\r\n".getBytes())));ch.pipeline().addLast(new StringEncoder(CharsetUtil.UTF_8));// String解码。ch.pipeline().addLast(new StringDecoder(CharsetUtil.UTF_8));// String解码。
//                        // 心跳设置ch.pipeline().addLast(new IdleStateHandler(0, 0, 600, TimeUnit.SECONDS));ch.pipeline().addLast(clientHandler);}});this.connect(b);}private void connect(Bootstrap b) throws InterruptedException {long c1 = System.currentTimeMillis();final int maxRetries = 2; //重连2次final AtomicInteger count = new AtomicInteger();final AtomicBoolean flag = new AtomicBoolean(false);try {this.channelFuture = b.connect(host, port).addListener(new ChannelFutureListener() {public void operationComplete(ChannelFuture future) throws Exception {if (!future.isSuccess()) {if (count.incrementAndGet() > maxRetries) {log.warn("imei={}重连超过{}次", imei, maxRetries);} else {log.info("imei={}重连第{}次", imei, count);b.connect(host, port).addListener(this);}} else {log.info("imei={}连接成功,连接IP:{}连接端口:{}", imei, host, port);flag.set(true);}}}).sync(); //同步连接} catch (Exception e) {log.error(e.getMessage(), e);}log.info("设备imei={},channelId={}连接耗时={}ms", imei, channelFuture.channel().id(), System.currentTimeMillis() - c1);if (flag.get()) {channelFuture.channel().closeFuture().sync(); //连接成功后将持续阻塞该线程}}
}

netty客户端为多实例,每个实例绑定一个线程,持续阻塞到客户端关闭为止,每个客户端中可以保存自己的业务数据,以便在后续与服务端交互时处理业务使用。客户端执行连接时,给了2次重试的机会,如果3次都没连接成功则放弃。后续可以选择将该消息重新入列消费。我们实际项目中,此处还应该预先给服务端发送一条登录消息,待服务端确认后才能执行后续通讯,这需要视实际情况进行调整。

另一个需要注意的点是EventLoopGroup是从构造函数传入的,而不是在客户端中创建的,因为当客户端数量非常多时,每个客户端都创建自己的线程组会极大的消耗服务器资源,因此我们在实际使用中是按业务去创建统一的线程组给该业务下的所有客户端共同使用的,线程组的大小需要根据业务需求灵活配置。

在init方法中,我们给客户端加上了一个handler来处理与服务端的交互,下面来看一下具体实现。

package org.example.client.handler;import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
import org.example.client.NettyClient;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;import java.util.Map;/*** @author ReWind00* @date 2023/2/15 10:09*/
@Slf4j
@Component
@Scope("prototype")
public class DemoClientHandler extends BaseClientHandler {private final String imei;private final Map<String, Object> bizData;private final NettyClient nettyClient;private int allIdleCounter = 0;private static final int MAX_IDLE_TIMES = 3;public DemoClientHandler(NettyClient nettyClient) {this.nettyClient = nettyClient;this.imei = nettyClient.getImei();this.bizData = nettyClient.getBizData();}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {log.info("客户端imei={},通道激活成功", this.imei);synchronized (this.nettyClient) { //当通道激活后解锁队列线程,然后再发送消息this.nettyClient.notify();}}@Overridepublic void channelInactive(ChannelHandlerContext ctx) throws Exception {log.warn("客户端imei={},通道断开连接", this.imei);}@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {log.info("客户端imei={},收到消息:  {}", this.imei, msg);//处理业务...if ("shutdown".equals(msg)) {this.nettyClient.close();}}@Overridepublic void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {if (evt instanceof IdleStateEvent) {IdleStateEvent e = (IdleStateEvent) evt;boolean flag = false;if (e.state() == IdleState.ALL_IDLE) {this.allIdleCounter++;log.info("客户端imei={}触发闲读或写第{}次", this.imei, this.allIdleCounter);if (this.allIdleCounter >= MAX_IDLE_TIMES) {flag = true;}}if (flag) {log.warn("读写超时达到{}次,主动断开连接", MAX_IDLE_TIMES);ctx.channel().close();}}}@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {log.error("客户端imei={},连接异常{}", imei, cause.getMessage(), cause);}
}

DemoClientHandler也是多实例bean,每个实例持有自己的NettyClient引用,以便在后续处理具体业务。在channelActive方法中,我们可以看到执行了客户端实例的notify方法,此处就是在客户端创建成功并且通道激活后解除wait锁的地方。channelRead方法就是我们处理服务端发送过来的消息的方法,我们的具体业务应该在该方法执行,当然不建议长时间阻塞客户端的工作线程,可以考虑异步处理。

最后我们看一下客户端缓存类。

package org.example.client;import java.util.concurrent.ConcurrentHashMap;/*** @author ReWind00* @date 2023/2/15 11:01*/
public class NettyClientHolder {private static final ConcurrentHashMap<String, NettyClient> clientMap = new ConcurrentHashMap<>();public static ConcurrentHashMap<String, NettyClient> get() {return clientMap;}}

由于netty的通道无法序列化,因此不能存入redis,只能缓存在本地内存中,其本质就是一个ConcurrentHashMap。

package org.example.client.controller;import org.example.client.QueueHolder;
import org.example.client.model.NettyMsgModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;/*** @author ReWind00* @date 2023/2/15 13:48*/
@RestController
@RequestMapping("/demo")
public class DemoController {/*** 间隔发送两条消息*/@GetMapping("testOne")public void testOne() {QueueHolder.get().offer(NettyMsgModel.create("87654321", "Hello World!"));try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}QueueHolder.get().offer(NettyMsgModel.create("87654321", "Hello World Too!"));}/*** 任意发送消息** @param imei* @param msg*/@GetMapping("testTwo")public void testTwo(@RequestParam String imei, @RequestParam String msg) {QueueHolder.get().offer(NettyMsgModel.create(imei, msg));}/*** 连续发送两条消息 第二条由于redis锁将会重新放回队列延迟消费*/@GetMapping("testThree")public void testThree() {QueueHolder.get().offer(NettyMsgModel.create("12345678", "Hello World!"));QueueHolder.get().offer(NettyMsgModel.create("12345678", "Hello World Too!"));}
}

测试接口代码如上,调用testOne,日志如下:

在这里插入图片描述
可以看到第一条消息触发了客户端创建流程,创建后发送了消息,而5秒后的第二条消息直接通过已有通道发送了。

测试接口代码如上,调用testTwo,日志如下:

在这里插入图片描述
发送shutdown可以主动断开已有连接。

测试接口代码如上,调用testThree,日志如下:

在这里插入图片描述
可以看到第二条消息重新入列并被延迟消费了。

分享一套开源充电桩云平台(v2.5.1)-- 支持二轮(电动自行车)、四轮(电动汽车)

相关文章:

Netty+SpringBoot 打造一个 TCP 长连接通讯方案

项目背景 最近公司某物联网项目需要使用socket长连接进行消息通讯&#xff0c;捣鼓了一版代码上线&#xff0c;结果BUG不断&#xff0c;本猿寝食难安&#xff0c;于是求助度娘&#xff0c;数日未眠项目终于平稳运行了&#xff0c;本着开源共享的精神&#xff0c;本猿把项目代码…...

2023.11.15 每日一题(AI自生成应用)【C++】【Python】【Java】【Go】 动态路径分析

目录 一、题目 二、解决方法 三、改进 一、题目 背景&#xff1a; 在一个城市中&#xff0c;有数个交通节点&#xff0c;每个节点间有双向道路相连。每条道路具有一个初始权重&#xff0c;代表通行该路段的成本&#xff08;例如时间、费用等&#xff09;。随着时间的变化&am…...

【libGDX】初识libGDX

1 前言 libGDX 是一个开源且跨平台的 Java 游戏开发框架&#xff0c;于 2010 年 3 月 11 日推出 0.1 版本&#xff0c;它通过 OpenGL ES 2.0/3.0 渲染图像&#xff0c;支持 Windows、Linux、macOS、Android、iOS、Web 等平台&#xff0c;提供了统一的 API&#xff0c;用户只需要…...

VIVADO+FPGA调试记录

vivadoFPGA调试记录 vitis编译vivado导出的硬件平台&#xff0c;提示xxxx.h file cant find vitis编译vivado导出的硬件平台&#xff0c;提示’xxxx.h file cant find’ 此硬件平台中&#xff0c;包含有AXI接口类型的ip。在vitis编译硬件平台时&#xff0c;经常会报错&#xf…...

Android——Gradle插件gradle-wrapper.properties

一、Android Studio版本&#xff0c;Android Gradle插件版本&#xff0c;Gradle版本 Android Studio 通过Android Gradle插件 使用 Gradle来构建代码&#xff1b; Android Studio每次升级后&#xff0c; Android Gradle 插件自动更新&#xff0c;对应的Gradle版本也会变动&…...

iOS应用加固方案解析:ipa加固安全技术全面评测

在移动应用开发领域&#xff0c;iOS应用的安全性一直备受关注。ipaguard作为一款专业的iOS应用加固方案&#xff0c;采用混淆加密技术&#xff0c;旨在保护应用免受破解、逆向和篡改等风险。本文将深入探讨ipaguard的产品功能、安全技术及其在iOS应用加固领域中的核心优势和特色…...

过滤器模式 rust和java的实现

文章目录 过滤器模式实现 过滤器模式实现javarustjavarust rust代码仓库 过滤器模式 过滤器模式&#xff08;Filter Pattern&#xff09;或标准模式&#xff08;Criteria Pattern&#xff09;是一种设计模式&#xff0c;这种模式允许开发人员使用不同的标准来过滤一组对象&…...

Feature Pyramid Networks for Object Detection(2017.4)

文章目录 Abstract1. Introduction3. Feature Pyramid NetworksBottom-up pathwayTop-down pathway and lateral connections 7. Conclusion FPN Abstract 特征金字塔是识别系统中检测不同尺度物体的基本组成部分。但最近的深度学习对象检测器避免了金字塔表示&#xff0c;部分…...

Python3基础模块 random

Python3基础模块 random import random #作用&#xff1a;生成随机数使用dir(module)查看模块内容 >>> import random >>> dir(random) [BPF, LOG4, NV_MAGICCONST, RECIP_BPF, Random, SG_MAGICCONST, SystemRandom, TWOPI, _BuiltinMethodType, _MethodT…...

ubuntu安装pgsql16

ubuntu安装postgresSQL 官网地址&#xff1a; https://www.postgresql.org/download/ 1.安装 # 添加源 sudo sh -c echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list # 安装数字签名 w…...

数据管理70个名词解析

数据标准化70个名词解析 1、数据 是指任何以电子或者其他方式对信息的记录。在计算机科学技术中&#xff0c;“数据”是客观事物的符号表示&#xff0c;指所有可被输入到计算机中并可被计算机程序处理的符号的总称;在管理科学技术中&#xff0c;“数据”是描述事件或事物的属性…...

线性代数本质系列(二)矩阵乘法与复合线性变换,行列式,三维空间线性变换

本系列文章将从下面不同角度解析线性代数的本质&#xff0c;本文是本系列第二篇 向量究竟是什么&#xff1f; 向量的线性组合&#xff0c;基与线性相关 矩阵与线性相关 矩阵乘法与复合线性变换 三维空间中的线性变换 行列式 逆矩阵&#xff0c;列空间&#xff0c;秩与零空间 克…...

Linux-CentOS重要模块

软件包管理器&#xff1a;CentOS使用Yum&#xff08;Yellowdog Updater, Modified&#xff09;作为其包管理器。Yum提供了一种方便的方式来安装、更新和删除软件包&#xff0c;并自动解决依赖关系。 RPM&#xff1a;RPM&#xff08;RPM Package Manager&#xff09;是CentOS中…...

posix定时器的使用

POSIX定时器是基于POSIX标准定义的一组函数&#xff0c;用于实现在Linux系统中创建和管理定时器。POSIX定时器提供了一种相对较高的精度&#xff0c;可用于实现毫秒级别的定时功能。 POSIX定时器的主要函数包括&#xff1a; timer_create()&#xff1a;用于创建一个定时器对象…...

安科瑞煤矿电力监控系统的研究与应用

摘要&#xff1a;作为一个巨大的能源消耗国家&#xff0c;我国每年对煤炭的市场需求巨大。煤炭作为我国点力气和供暖企业的重要原材料&#xff0c;煤矿的开采过程存在着难以消除的风险&#xff0c;我国的煤炭安全问题长期困扰着相关企业和监督部门&#xff0c;也受到社会的广泛…...

高教社杯数模竞赛特辑论文篇-2023年A题:基于机理分析法的定日镜场优化设计模型(附获奖论文及MATLAB代码实现)

目录 摘要 一、 问题重述 1 . 1 问题背景 1 . 2 问题要求 二、 问题分析...

缩点+图论路径网络流:1114T4

http://cplusoj.com/d/senior/p/SS231114D 重新梳理一下题目 我们先建图 x → y x\to y x→y&#xff0c;然后对点分类&#xff1a;原串出现点&#xff0c;原串未出现点。 假如我们对一个原串出现点进行了操作&#xff0c;那么它剩余所有出边我们立刻去操作必然没有影响。所…...

Go语言fyne开发桌面应用程序-环境安装

环境安装 参考https://developer.fyne.io/started/#prerequisites网站 之前的文章介绍了如何安装GO语言这里不在叙述 msys2 首先安装msys2&#xff0c;https://www.msys2.org/ 开始菜单打开MSYS2 执行 $ pacman -Syu$ pacman -S git mingw-w64-x86_64-toolchain注意&#…...

JavaWeb——CSS3的使用

目录 1. CSS概述 2. CSS引入方式 3. CSS颜色显示 4. CSS选择器 4.1. 元素&#xff08;标签&#xff09;选择器 4.2. id选择器 4.3. 类选择器 4.4. 三者优先级 5. 盒子模型 1. CSS概述 CSS&#xff0c;全称为“Cascading Style Sheets”&#xff0c;中文译为“层叠样式…...

AR导览小程序开发方案

一、背景介绍 随着科技的不断发展&#xff0c;虚拟现实&#xff08;VR&#xff09;和增强现实&#xff08;AR&#xff09;技术逐渐被应用于各个领域。其中&#xff0c;AR导览小程序作为一种新兴的导览方式&#xff0c;以其独特的视觉体验和互动性受到了广泛的关注。AR导览小程…...

继承、多态

复习 需求&#xff1a; 编写一个抽象类&#xff1a;职员Employee,其中定义showSalary(int s)抽象方法&#xff1b;编写Employee的子类&#xff0c;分别是销售员Sales和经理Manager,分别在子类中实现对父类抽象方法的重写&#xff0c;并编写测试类Test查看输出结果 package cn.…...

贪吃蛇小游戏代码

框架区 package 结果;import java.awt.Color; import java.awt.EventQueue; import java.awt.Font; import java.awt.Frame; import java.awt.Graphics; import java.awt.Image; import java.util.ArrayList; import java.util.List; import java.util.Random;import javax.s…...

Python数据容器(字典)

字典 1.字典的定义2.字典数据的获取3.字典的嵌套4.嵌套字典的内容获取5.字典的常用操作6.常用操作总结7.遍历字典8.练习 1.字典的定义 同样使用{}&#xff0c;不过存储的元素是一个一个的&#xff1a;键值对&#xff0c;语法如下 # 定义字典字面量 {key:value,key:value,...,…...

餐饮展示小程序的作用是什么

餐饮是市场重要的组成部分&#xff0c;尤其是我国八大菜系&#xff0c;各类细分菜数量非常多&#xff0c;并分布在全国&#xff0c;各类大小品牌餐饮商家数量也非常庞大&#xff0c;每个城市的商业街都是一个接一个餐厅&#xff0c;酒类、酒店多样。 餐饮行业经营痛点比较明显…...

33、Flink 的Table API 和 SQL 中的时区

Flink 系列文章 1、Flink 部署、概念介绍、source、transformation、sink使用示例、四大基石介绍和示例等系列综合文章链接 13、Flink 的table api与sql的基本概念、通用api介绍及入门示例 14、Flink 的table api与sql之数据类型: 内置数据类型以及它们的属性 15、Flink 的ta…...

Origin:科研绘图与学术图表绘制从入门到精通

文章目录 一、引言二、安装和启动Origin三、创建和保存图表四、深入学习Origin绘图功能五、应用Origin进行科研绘图和学术图表绘制六、总结与建议《Origin科研绘图与学术图表绘制从入门到精通》亮点内容简介作者简介目录获取方式 一、引言 Origin是一款功能强大的数据分析和科…...

腾讯云标准型SA4服务器AMD处理器性能测评

腾讯云服务器标准型SA4实例CPU采用AMD处理器&#xff0c;新一代腾讯云自研星星海双路服务器&#xff0c;搭配AMD EPYC Genoa处理器&#xff0c;内存采用最新 DDR5&#xff0c;默认网络优化&#xff0c;最高内网收发能力达4500万pps&#xff0c;最高内网带宽可支持100Gbps。阿腾…...

LeetCode 2656. K 个元素的最大和:一次遍历(附Python一行版代码)

【LetMeFly】2656.K 个元素的最大和&#xff1a;一次遍历&#xff08;附Python一行版代码&#xff09; 力扣题目链接&#xff1a;https://leetcode.cn/problems/maximum-sum-with-exactly-k-elements/ 给你一个下标从 0 开始的整数数组 nums 和一个整数 k 。你需要执行以下操…...

element-ui中Form表单使用自定义验证规则

data() {const validatePass (rule, value, callback) > {if (value.length < 3) {callback(new Error("密码不能小于3位"));} else {callback();}};return {rules: {password: [{ required: true, trigger: "blur", validator: validatePass },]}}…...

android源码添加adb host支持

本文开始参考在 android 上使用 adb client-CSDN博客&#xff0c;在shell中已经可以使用。但当我想在app中用 String command "/data/local/tmp/adb -s 307ef90dc8128844 shell ls";StringBuilder output new StringBuilder();try {Process process Runtime.getR…...