开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Springboot(六) mysql配置及数据库查询

发表于 2021-11-25

👨‍🎓作者:bug菌

✏️博客:CSDN、掘金、infoQ、51CTO等

🎉简介:CSDN|阿里云|华为云|51CTO等社区博客专家,历届博客之星Top30,掘金年度人气作者Top40,51CTO年度博主Top12,掘金 | InfoQ | 51CTO等社区优质创作者,全网粉丝合计15w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板等海量资料。

…

✍️温馨提醒:本文字数:1999字, 阅读完需:约 5 分钟

🏆本文收录于《Spring Boot从入门到精通》,专门攻坚指数提升。

本专栏致力打造最硬核 Spring Boot 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。

一、前言🔥

环境说明:Windows10 + Idea2021.3.2 + Jdk1.8 + SpringBoot 2.3.1.RELEASE

通过前几期的基础教学,想必大家都已经springboot项目创建及启动等基本内容了吧,今个儿我就来整点高级的,跟数据库交互交互。如何连接mysql数据库,创建数据库表,最后再成功查询数据库并打印数据内容?是这期要讲的内容,可能会比较简单,同时也希望大家不要掉以轻心,打好基础,认真听哦。

二、添加mysql依赖

我们先在pom.xml文件中引入mysql数据库依赖;添加如下

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<!--mysql依赖--><dependency>    
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<!--引入jdbc stater-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

三、创建数据库表

我们使用 navicat 数据库管理工具,先连接本地数据库,输入连接名,主机ip,端口(3306),用户名,密码是你当初安装mysql服务时填写的。完事之后,

如果不会操作的,请参考bug菌写的这篇 navicat如何创建数据库及导入数据库文件

1、先创建个数据库,取名这个大家随意啊,命名最好见名之意。

2、选择刚才创建好的数据库下层的表,然后右键打击选择’新建表’

3、随便创建几个字段,然后点击保存。

附上创建表sql:

1
2
3
4
5
6
7
8
sql复制代码CREATE TABLE `user` (
`id` int(11) NOT NULL COMMENT '主键id',
`name` varchar(255) NOT NULL COMMENT '用户名',
`age` int(11) DEFAULT NULL COMMENT '年龄',
`sex` varchar(2) DEFAULT NULL COMMENT '性别',
`address` varchar(255) DEFAULT NULL COMMENT '住址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

4、先随便造一条数据吧,待会儿能用得上。

ok!以上跟着bug菌一步一步来哈,不要急,更不要遗漏任何一步骤,接下来我们就是配置连接数据库信息。

四、yaml文件配置数据库连接

我就以yaml文件为例,.properties文件也是一样的配置。注意,这个时候,前面一期有教大家为何要使用profile文件动态切换环境此刻就用上了,我们先指定开发环境,即:

先在application.yaml 中指定 active = dev;

1
2
3
4
yaml复制代码spring:
profiles:
# 控制使用哪套环境变量
active: dev

配置数据库连接信息如下:

以application-dev.yaml为例,.properties也是一样的。这里就不着重展开讲啦。

1
2
3
4
5
6
7
yaml复制代码#开发环境数据库信息
spring:
datasource:
url: jdbc:mysql://localhost:3306/springboot_db?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8
username: root
password: 123456 #数据库名、用户名和密码改为自己的
driver-class-name: com.mysql.cj.jdbc.Driver

这样就配置好了,启动项目,没有报错!但是就一定能保证项目与数据库之间连通是可以的吗?肯定不是,毕竟你只是配置了连接信息。

所以接下来,我们再写一个controller 进行数据库连接。一探究竟!

我们先来创建个 “UserController”类,然后注入JdbcTemplate jdbc模板类,具体请看如下:目的就是访问数据库并返回用户数据信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
kotlin复制代码package com.example.demo.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;import java.util.Map;

@RestController
@RequestMapping("/user")
public class UserController {

//注入JDBC模板接口
@Autowired
private JdbcTemplate jdbcTemplate;


/**
* 查询所有用户信息
*/
@GetMapping("/get-users")
public List<Map<String, Object>> getUserList(){
//查询sql语句
String sql = "select * from user";
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql);
//返回结果
return list;
}
}

写完之后重启项目,没有报错,ok!接着打开浏览器访问该get请求。看看能否成功返回查询数据?输入访问地址,如下:

http://localhost:8080/user/get-users

输完直接回车,ok!成功将我们刚才插入数据库的那条数据查出了并打印在页面上。证明数据库连接是没有问题的。

接下来,大家可以自由尝试,比如通过接口调用的形式插入一条数据,通过主键id修改一条数据,删除一条数据等等,此处就不一一举例啦。学习过程中,产生任何疑问皆可评论区留言,bug菌一定第一时间给予大家帮助解答困惑。

... ...


  ok,以上就是我这期的全部内容啦,如果还想学习更多,你可以看看我的往期热文推荐哦,每天积累一个奇淫小知识,日积月累下去,你一定能成为令人敬仰的大佬的。好啦,咱们下期见~

热文推荐💭

    为了便于同学快速定位文章学习,熬夜几天终于整理出了【基础篇】及【进阶篇】专栏的文章及有效地址,同学们敬请吩咐bug菌吧。

    目前基础篇已完结100篇+,进阶篇已完结70篇+,两专栏都在持续更新中;生命不息,更新不止,加油卷起来吧,同学们。部分文章总结如下:

7.1 Spring Boot【基础篇】

导读:SpringBoot 学习指南(附思维导图)

Spring Boot入门(01):Spring Boot的奋斗成长史

Spring Boot入门(02):快速开发环境搭建和项目启动

Spring Boot入门(03): yaml、properties配置文件介绍及使用

Spring Boot入门(04):多环境切换,实例演示

Spring Boot入门(05):starter基础入门

Spring Boot入门(06):Spring Boot常用注解大全

Spring Boot入门(07):整合 MySQL 和 Druid数据源(两万字教学)

Spring Boot入门(08):整合Mybatis访问MySQL实现增删改查

Spring Boot入门(09):使用MyBatis的XML配置方式访问MySQL实现增删改查

Spring Boot入门(10): mybatis之xml映射文件>、<=等特殊符号写法

Spring Boot入门(11):Spring Boot 整合 JPA

Spring Boot入门(12):整合Mybatis-Plus mybatis-plus实现接口增删改查

Spring Boot入门(13): Mybatis-Plus之条件构造器使用手册

Spring Boot入门(14): mybatis-plus之如何自定义sql

Spring Boot入门(15):Spring Boot 整合 MyBatis-Plus AutoGenerator 自动生成项目骨架代码

Spring Boot入门(16):Spring Boot整合Swagger-UI实现在线API文档

Spring Boot入门(17):Spring Boot整合Knife4j,美化强化丑陋的Swagger

Spring Boot入门(18):Spring Boot静态资源映射

Spring Boot入门(19):Spring Boot 整合 Thymeleaf 模板引擎,开发Web页面

Spring Boot入门(20):实现多数据源配置,开箱即用

Spring Boot入门(21):整合Log4j2以及配置详解

Spring Boot入门(22):整合LogBack 实现日志文件本地保存

Spring Boot入门(23):Spring Boot基于AOP拦截日志

Spring Boot入门(24):Spring Boot事务

Spring Boot入门(25):过滤器、拦截器、监听器对比及使用场景

Spring Boot入门(26):实现邮件发送简单邮件、附件邮件、嵌入资源(图片)邮件、模板邮件等

Spring Boot入门(27):war包部

Spring Boot入门(28):jar包部署

Spring Boot入门(29):如何实现热部署

Spring Boot入门(30):Windows安装Redis客户端?你玩过么

… …

若想学习更多,这边请👉👉👉《滚雪球学Spring Boot》👈👈👈

7.2 Spring Boot【进阶篇】

Spring Boot进阶(01):Spring Boot 集成 Redis,实现缓存自由

Spring Boot进阶(02):使用Validation进行参数校验

Spring Boot进阶(03):如何使用MyBatis-Plus实现字段的自动填充

Spring Boot进阶(04):如何使用MyBatis-Plus快速实现自定义sql分页

Spring Boot进阶(05):Spring Boot 整合RabbitMq,实现消息队列服务

Spring Boot进阶(06):Windows10系统搭建 RabbitMq Server 服务端

Spring Boot进阶(07):集成EasyPoi,实现Excel/Word的导入导出

Spring Boot进阶(08):集成EasyPoi,实现Excel/Word携带图片导出

Spring Boot进阶(09):集成EasyPoi,实现Excel文件多sheet导入导出

Spring Boot进阶(10):集成EasyPoi,实现Excel模板导出成PDF文件

Spring Boot进阶(11):Spring Boot 如何实现纯文本转成.csv格式文件?

Spring Boot进阶(12):Spring Boot 如何获取Excel sheet页的数量?

Spring Boot进阶(13):Spring Boot 如何获取@ApiModelProperty(value = “序列号“, name = “uuid“)中的value值name值?

Spring Boot进阶(14):Spring Boot 如何手动连接库并获取指定表结构?一文教会你

Spring Boot进阶(15):根据数据库连接信息指定分页查询表结构信息

Spring Boot进阶(16):Spring Boot 如何通过Redis实现手机号验证码功能?

Spring Boot进阶(17):Spring Boot如何在swagger2中配置header请求头等参数信息

Spring Boot进阶(18):SpringBoot如何使用@Scheduled创建定时任务?

Spring Boot进阶(19):Spring Boot 整合ElasticSearch

Spring Boot进阶(20):配置Jetty容器

Spring Boot进阶(21):配置Undertow容器

Spring Boot进阶(22):Tomcat与Undertow容器性能对比分析

Spring Boot进阶(23):实现文件上传

Spring Boot进阶(24):如何快速实现多文件上传?

Spring Boot进阶(25):文件上传的单元测试怎么写?

Spring Boot进阶(26):Mybatis 中 resultType、resultMap详解及实战教学

Spring Boot进阶(27):Spring Boot 整合 kafka(环境搭建+演示)

Spring Boot进阶(28):Jar包Linux后台启动部署及滚动日志查看,日志输出至实体文件保存

Spring Boot进阶(29):如何正确使用@PathVariable,@RequestParam、@RequestBody等注解?不会我教你,结合Postman演示

Spring Boot进阶(30):@RestController和@Controller 注解使用区别,实战演示

… …

若想学习更多,这边请👉👉👉《SpringBoot 进阶实战》 👈👈👈

若想系统完整的从0到1的学习,可以参考这篇专栏总结《2023最新首发,全网最全 Spring Boot 学习宝典(附思维导图)》,本专栏致力打造最硬核 Spring Boot 进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中。欢迎大家订阅持续学习。

在入门及进阶之途,我必助你一臂之力,系统性学习,从入门到精通,带你不走弯路,直奔终点;投资自己,永远性价比最高,都这么说了,你还不赶紧来学??

本文涉及所有源代码,均已上传至github开源,供同学们一对一参考 GitHub传送门,

同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

  1. 文末💭

我是bug菌,CSDN | 阿里云 | 华为云 | 51CTO 等社区博客专家,历届博客之星Top30,掘金年度人气作者Top40,51CTO年度博主Top12,掘金 | InfoQ | 51CTO等社区优质创作者,全网粉丝合计15w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板等海量资料。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

netty(十四)Netty提升 - 粘包与半包 一、现象分

发表于 2021-11-25

「这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战」

一、现象分析

1.1 粘包

通过代码的方式演示下粘包的现象:

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
scss复制代码public class HalfPackageServer {

public static void main(String[] args) {

NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
//设置服务器接收端缓冲区大小为10
serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//此处打印输出,看看收到的内容是10次16字节,还是一次160字节
printBuf((ByteBuf) msg);
super.channelRead(ctx, msg);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080);
//阻塞等待连接
channelFuture.sync();
//阻塞等待释放连接
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("server error:" + e);
} finally {
// 释放EventLoopGroup
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}

static void printBuf(ByteBuf byteBuf){
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i< byteBuf.writerIndex();i++) {
stringBuilder.append(byteBuf.getByte(i));
stringBuilder.append(" ");
}

stringBuilder.append("| 长度:");
stringBuilder.append(byteBuf.writerIndex());
stringBuilder.append("字节");
System.out.println(stringBuilder);
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
java复制代码public class HalfPackageClient {
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//建立连接成功后,会触发active事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//循环发送,每次16字节,共10次
for (int i = 0; i < 10; i++) {
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeBytes(new byte[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15});
ctx.writeAndFlush(buffer);
}
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();
//释放连接
channelFuture.channel().closeFuture().sync();

} catch (InterruptedException e) {
System.out.println("client error :" + e);
} finally {
//释放EventLoopGroup
worker.shutdownGracefully();
}
}
}

结果:

1
复制代码0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 长度:160字节

如上所示,发送了10次的16个字节,接收到了一个160字节,而不是10个16字节。这就是粘包现象。

1.2 半包

半包现象我们仍然使用前面的代码,但是服务端需要多设置一个属性,即修改服务端接收缓冲区的buffer大小,我们这里修改为10个字节。

serverBootstrap.option(ChannelOption.SO_RCVBUF,10);

这行代码起初运行完我有点不理解,明明设置是10,但是接收到的内容确是40,可见这个设置的值,不是10个字节,通过源码跟踪,我发现这个数值ChannelOption的泛型是个Ingteger,估计是进行了类型的计算,一个Integer是4个字节,所以有了设置是10,最终却接收40个字节。

如果同学们觉得这个问题说的不对的话,可以帮我指正一下。感谢!!

public static final ChannelOption SO_RCVBUF = valueOf(“SO_RCVBUF”);

服务端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
scss复制代码public class HalfPackageServer {

public static void main(String[] args) {

NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
//设置服务器接收端缓冲区大小为10
serverBootstrap.option(ChannelOption.SO_RCVBUF,10);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//此处打印输出,看看收到的内容是10次16字节,还是一次160字节
printBuf((ByteBuf) msg);
super.channelRead(ctx, msg);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080);
//阻塞等待连接
channelFuture.sync();
//阻塞等待释放连接
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("server error:" + e);
} finally {
// 释放EventLoopGroup
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}

static void printBuf(ByteBuf byteBuf){
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i< byteBuf.writerIndex();i++) {
stringBuilder.append(byteBuf.getByte(i));
stringBuilder.append(" ");
}

stringBuilder.append("| 长度:");
stringBuilder.append(byteBuf.writerIndex());
stringBuilder.append("字节");
System.out.println(stringBuilder);
}
}

客户端与前面的粘包相同。

结果:

1
2
3
4
5
复制代码0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 | 长度:36字节
4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 | 长度:40字节
12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 | 长度:40字节
4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 1 2 3 4 5 6 7 8 9 10 11 | 长度:40字节
12 13 14 15 | 长度:4字节

如上所示,总共160字节,总共发送了五次,每一次都含有不完整16字节的数据。这就是半包,其实里面也包含的粘包。

至于为什么第一个只有36,这里没有具体分析,但是我们可以猜想下,每次连接的首次应该是有表示占用了4个字节。

二、粘包、半包分析

产生粘包和半包的本质:TCP是流式协议,消息是无边界的。

2.1 滑动窗口

TCP是一种可靠地传出协议,每发送一个段就需要进行一次确认应答(ack)处理。如何没收到ack,则会再次发送。

但是如果对于一个客户端来说,每次发送一个请求,都要等到另一个客户端应该的话,才能发送下一个请求,那么整个构成就成了串行化的过程,大大降低了连接间的传输效率。

TCP如何解决效率地下的问题?

引入滑动窗口。窗口大小即决定了无需等待应答而可以继续发送的数据最大值。

简易滑动过程如下所示:

image.png

窗口实际就起到一个缓冲区的作用,同时也能起到流量控制的作用。

  • 只有窗口内的数据才允许被发送(绿色),当应答(蓝色)未到达前,窗口必须停止滑动
  • 如果 0-100 这个段的数据 ack 回来了,窗口就可以向下滑动,新的数据段会被加入进来进行发送。
  • 接收方也会维护一个窗口,只有落在窗口内的数据才能允许接收

2.2 粘包现象分析

  • 现象,发送了10次的16个字节,接收到了一个160字节,而不是10个16字节。
  • 产生原因:
+ 应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
+ 滑动窗口(TCP):假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当接收方滑动窗口中缓冲了多个报文就会粘包。
+ [Nagle 算法(TCP)](https://baike.baidu.com/item/Nagle%E7%AE%97%E6%B3%95/5645172?fr=aladdin):会造成粘包

2.3 半包现象分析

  • 现象,前面的半包示例代码发送了10次的16个字节,接收到的数据有部分是被阶段的,不是完整的16字节。
  • 产生原因
+ 应用层:接收方 ByteBuf 小于实际发送数据量
+ 滑动窗口(TCP):假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
+ MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包

2.4 引申:Nagle 算法 和 MSS限制

2.4.1 Nagle算法

TCP中为了提高网络的利用率,经常使用一个叫做Nagle的算法。

该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送的一种处理机制。

如果以下两个条件都不满足时,则进行一段时间延迟后,才进行发送:

  • 已发送的数据都已经收到确认应答时
  • 可以发送最大段长度(MSS)的数据时

根据这个算法虽然网络利用率可以提高,但是可能会发生某种程度的延迟。对于时间要求准确的系统,往往需要关闭该算法。

在Netty中如下的条件:

  • 如果 SO_SNDBUF 的数据达到 MSS(maximum segment size),则需要发送。
  • 如果 SO_SNDBUF 中含有 FIN(表示需要连接关闭)这时将剩余数据发送,再关闭。
  • 如果 TCP_NODELAY = true,则直接发送。
  • 已发送的数据都收到 ack 时,则需要发送。
  • 上述条件不满足,但发生超时(一般为 200ms)则需要发送。
  • 除上述情况,延迟发送

2.4.2 MSS限制

MSS 限制

数据链路层对一次能够发送的最大数据有限制,每种数据链路的最大传输单元(MTU)都不尽相同。

  • 以太网的 MTU 是 1500
  • FDDI(光纤分布式数据接口)的 MTU 是 4352
  • 本地回环地址的 MTU 是 65535 - 本地测试不走网卡

MSS 是最大段长度(maximum segment size),它是 MTU 刨去 tcp 头和 ip 头后剩余能够作为数据传输的字节数

  • ipv4 tcp 头占用 20 bytes,ip 头占用 20 bytes,因此以太网 MSS 的值为 1500 - 40 = 1460
  • TCP 在传递大量数据时,会按照 MSS 大小将数据进行分割发送
  • MSS 的值在三次握手时通知对方自己 MSS 的值,然后在两者之间选择一个小值作为 MSS

三、粘包和半包的解决方案

3.1 短连接

每当客户端。发送一条消息后,就与服务器断开连接。

我们需要对客户端的代码进行改造,其实就是,将内部发送10次消息的循环抽出来,一次连接只发送一个消息,需要建立10次连接,这个我就不演示了,不用想都能得到结果:

  • 客户端发送一定不会产生粘包问题。
  • 服务端只要接收缓冲区足够大,一定不会产生半包的问题,但是不能完全保证。

此方案的缺点:

1)建立大量的链接,效率低。

2)不能解决半包问题。

3.2 固定长度消息

Netty针对固定长度消息提供了一个入站处理器FixedLengthFrameDecoder,能够制定接收消息的固定长度。

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
scss复制代码public class FixedLengthServer {

public static void main(String[] args) {

NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new FixedLengthFrameDecoder(8));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
//此处打印输出,看看收到的内容是10次16字节,还是一次160字节
printBuf((ByteBuf) msg);
super.channelRead(ctx, msg);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080);
//阻塞等待连接
channelFuture.sync();
//阻塞等待释放连接
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("server error:" + e);
} finally {
// 释放EventLoopGroup
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}

static void printBuf(ByteBuf byteBuf) {
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < byteBuf.writerIndex(); i++) {
stringBuilder.append(byteBuf.getByte(i));
stringBuilder.append(" ");
}

stringBuilder.append("| 长度:");
stringBuilder.append(byteBuf.writerIndex());
stringBuilder.append("字节");
System.out.println(stringBuilder);
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
ini复制代码public class FixedLengthClient {
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//建立连接成功后,会触发active事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 发送内容随机的数据包
Random r = new Random();
char c = 1;
ByteBuf buffer = ctx.alloc().buffer();
for (int i = 0; i < 10; i++) {
byte[] bytes = new byte[8];
for (int j = 0; j < r.nextInt(8); j++) {
bytes[j] = (byte) c;
}
c++;
buffer.writeBytes(bytes);
}
ctx.writeAndFlush(buffer);
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();
//释放连接
channelFuture.channel().closeFuture().sync();

} catch (InterruptedException e) {
System.out.println("client error :" + e);
} finally {
//释放EventLoopGroup
worker.shutdownGracefully();
}
}
}

结果:

1
2
3
4
5
6
7
8
9
10
复制代码1 1 0 0 0 0 0 0 | 长度:8字节
2 2 2 2 0 0 0 0 | 长度:8字节
3 0 0 0 0 0 0 0 | 长度:8字节
4 4 4 0 0 0 0 0 | 长度:8字节
5 5 5 5 0 0 0 0 | 长度:8字节
6 0 0 0 0 0 0 0 | 长度:8字节
7 0 0 0 0 0 0 0 | 长度:8字节
8 8 0 0 0 0 0 0 | 长度:8字节
9 9 9 0 0 0 0 0 | 长度:8字节
10 10 10 10 10 0 0 0 | 长度:8字节

使用固定长度也有一定的缺点,消息长度不好确定:

1)过大,造成空间浪费。

2)过小,对于某些数据包可能不够。

3.3 分隔符

1)Netty提供了LineBasedFrameDecoder(换行符帧解码器)。

默认以 \n 或 \r\n 作为分隔符,需要指定最大长度,如果超出指定长度仍未出现分隔符,则抛出异常。

2)Netty提供了DelimiterBasedFrameDecoder(自定义分隔符帧解码器)。
需要自己指定一个ByteBuf类型的分隔符,且需要指定最大长度。

LineBasedFrameDecoder示例代码:

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
java复制代码public class LineBasedServer {

public static void main(String[] args) {

NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println(byteBuf.toString(StandardCharsets.UTF_8));
super.channelRead(ctx, msg);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080);
//阻塞等待连接
channelFuture.sync();
//阻塞等待释放连接
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("server error:" + e);
} finally {
// 释放EventLoopGroup
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码public class LineBasedClient {
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//建立连接成功后,会触发active事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 发送带有分隔符的数据包
ByteBuf buffer = ctx.alloc().buffer();
String str = "hello world\nhello world\n\rhello world\nhello world";
buffer.writeBytes(str.getBytes(StandardCharsets.UTF_8));
ctx.writeAndFlush(buffer);
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();
//释放连接
channelFuture.channel().closeFuture().sync();

} catch (InterruptedException e) {
System.out.println("client error :" + e);
} finally {
//释放EventLoopGroup
worker.shutdownGracefully();
}
}
}

结果:

1
2
3
复制代码hello world
hello world
hello world

DelimiterBasedFrameDecoder代码示例,大体与前一种相同,下面只给出不同位置的代码:

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码protected void initChannel(SocketChannel ch) throws Exception {
ByteBuf buffer = ch.alloc().buffer();
buffer.writeBytes("||".getBytes(StandardCharsets.UTF_8));
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buffer));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println(byteBuf.toString(StandardCharsets.UTF_8));
super.channelRead(ctx, msg);
}
});
}

客户端:

1
ini复制代码String str = "hello world||hello world||hello world||hello world";

使用分隔符的这种方式,也有其缺点:处理字符数据比较合适,但如果内容本身包含了分隔符,那么就会解析错误。逐个字节去比较,相率也不是很好。

3.4 预设长度

LengthFieldBasedFrameDecoder长度字段解码器,允许我们在发送的消息当中,指定消息长度,然后会根据这个长度取读取响应的字节数。

1
2
3
4
5
6
java复制代码public LengthFieldBasedFrameDecoder(
int maxFrameLength,
int lengthFieldOffset,
int lengthFieldLength,
int lengthAdjustment,
int initialBytesToStrip)

主要看这个有5个字段的构造器,下面我们分别看每个字段的含义是什么。

字段含义我先列在这:

maxFrameLength:最大长度。

lengthFieldOffset:长度字段偏移量。

lengthFieldLength:长度字段长度。

lengthAdjustment:以长度字段为基准,还有几个字节是内容。

initialBytesToStrip:从头剥离几个字节。

通过Netty提供的几个例子,解释这几个字段的意思。

例子1:

  • lengthFieldOffset = 0
  • lengthFieldLength = 2
  • lengthAdjustment = 0
  • initialBytesToStrip = 0

则发送前,总共14个字节,其中lengthFieldLength占据两个字节,0x000C表示消息长度是12:

Length Actual Content
0x000C “HELLO, WORLD”

接收解析后,仍然是14个字节:

Length Actual Content
0x000C “HELLO, WORLD”

例子2:

  • lengthFieldOffset = 0
  • lengthFieldLength = 2
  • lengthAdjustment = 0
  • initialBytesToStrip = 2

则发送前,总共14个字节,其中lengthFieldLength占据两个字节,0x000C表示消息长度是12,initialBytesToStrip表示从头开始剥离2个字节,则解析后,将长度的字段剥离掉了:

Length Actual Content
0x000C “HELLO, WORLD”

接收解析后,只有12个字节:

Actual Content
“HELLO, WORLD”

例子3:

  • lengthFieldOffset = 2
  • lengthFieldLength = 3
  • lengthAdjustment = 0
  • initialBytesToStrip = 0

则发送前,总共17个字节,其中lengthFieldLength占据3个字节,长度字段偏移量是2,偏移量用来存放魔数Header 1:

Header 1 Length Actual Content
0xCAFE 0x00000C “HELLO, WORLD”

接收解析后,只有17个字节:

Header 1 Length Actual Content
0xCAFE 0x00000C “HELLO, WORLD”

例子4:

  • lengthFieldOffset = 0
  • lengthFieldLength = 3
  • lengthAdjustment = 2
  • initialBytesToStrip = 0

则发送前,总共17个字节,其中lengthFieldLength占据3个字节,lengthAdjustment 表示从长度字段开始,还有2个字节是内容:

Length Header 1 Actual Content
0x00000C 0xCAFE “HELLO, WORLD”

接收解析后,只有17个字节:

Length Header 1 Actual Content
0x00000C 0xCAFE “HELLO, WORLD”

例子5:

  • lengthFieldOffset = 1
  • lengthFieldLength = 2
  • lengthAdjustment = 1
  • initialBytesToStrip = 3

则发送前,总共17个字节,长度字段便宜1个字节,长度字段为2个字节,从长度字段开始,还有一个字节后是内容,忽略前三个字节。

Header 1 Length Header 2 Actual Content
0xCA 0x000C 0xFE “HELLO, WORLD”

接收解析后,只有13个字节:

Header 2 Actual Content
0xFE “HELLO, WORLD”

模拟例子5,写一段实例代码,其中内容略有不同:

示例代码:

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
java复制代码/**
* @description: TODO
* @author:weirx
* @date:2021/11/12 15:34
* @version:3.0
*/
public class LineBasedServer {

public static void main(String[] args) {

NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.channel(NioServerSocketChannel.class);
serverBootstrap.group(boss, worker);
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024,1,4,1,5));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println(byteBuf.readByte() + "|" + byteBuf.toString(StandardCharsets.UTF_8));
super.channelRead(ctx, msg);
}
});
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080);
//阻塞等待连接
channelFuture.sync();
//阻塞等待释放连接
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("server error:" + e);
} finally {
// 释放EventLoopGroup
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
scss复制代码public class LineBasedClient {
public static void main(String[] args) {
NioEventLoopGroup worker = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.channel(NioSocketChannel.class);
bootstrap.group(worker);
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
//建立连接成功后,会触发active事件
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
send(ctx,"hello, world");
send(ctx,"HI!");
}
});
}
});
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 8080).sync();
//释放连接
channelFuture.channel().closeFuture().sync();

} catch (InterruptedException e) {
System.out.println("client error :" + e);
} finally {
//释放EventLoopGroup
worker.shutdownGracefully();
}
}

static void send(ChannelHandlerContext ctx,String msg){
ByteBuf buffer = ctx.alloc().buffer();
byte[] bytes = msg.getBytes();
int length = bytes.length;
//先写Header 1
buffer.writeByte(1);
//再写长度
buffer.writeInt(length);
//再写Header 2
buffer.writeByte(2);
//最后写内容
buffer.writeBytes(bytes);
ctx.writeAndFlush(buffer);
}
}

结果:

1
2
复制代码2|hello, world
2|HI!

关于粘包和半包的介绍就这么多了,有用的话,帮忙点个赞吧~~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数据结构与算法(五)栈的顺序存储结构

发表于 2021-11-25

这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战

栈是限定仅在表尾进行插入和删除操作的线性表。

我们将允许插入和删除的一端称为栈顶,另一端称为栈底。

不含任何元素的栈称为空栈。

栈又被称为先进后出的线性表。


也就是说栈是一个特殊的线性表,其只在线性表的表尾进行添加删除数据操作,也就是说上边提到的栈底是固定的,添加删除操作只在栈顶进行。

栈的写入操作,叫做进栈,也称压栈或入栈。

栈的删除操作,叫做出栈,也称弹出栈。

栈限定了只在线性表的末尾进行数据写入删除操作,但是这并不意味着最先进栈的元素一定要最后出栈,这个要看具体情况,举个书上的小例子

【进栈出栈的变化形式】

如果3个整数1,2,3依次入栈,会有哪些出栈次序?

第一种:1-2-3进栈,即3-2-1出栈。

第二种:1进,1出,2斤,2出,3进,3出,进一个出一个,即1-2-3出栈。

第三种:1进,2进,2出,1出,3进,3出,即2-1-3出栈。

第四种:1进,1出,2进,3进,3出,2出,即1-3-2出栈。

第五种:1进,2进,2出,3进,3出,2出,即2-3-1出栈

下边我们来定义栈顺序存储结构的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
csharp复制代码/// <summary>
    /// 栈接口
    /// </summary>
    /// <typeparam name="T"></typeparam>
    interface IStack<T>
    {
        /// <summary>
        /// 清空栈
        /// </summary>
        /// <returns></returns>
        bool ClearStack();
        /// <summary>
        /// 判断栈是否为空
        /// </summary>
        /// <returns>true:空、false:不空</returns>
        bool IsEmptyStack();
        /// <summary>
        /// 获取栈顶元素
        /// </summary>
        /// <returns></returns>
        T GetTopStack();
        /// <summary>
        /// 入栈
        /// </summary>
        /// <returns></returns>
        bool PushStack(T val);
        /// <summary>
        /// 出栈
        /// </summary>
        /// <returns></returns>
        bool PopStack();
        /// <summary>
        /// 获取栈长度
        /// </summary>
        /// <returns></returns>
        int LengthStack();
        /// <summary>
        /// 判断栈是否已满
        /// </summary>
        /// <returns></returns>
        bool IsFull();
        /// <summary>
        /// 打印栈
        /// </summary>
        void paint();
    }

定义一个类来实现这个接口:

首先定义三个全局变量

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码/// <summary>
        /// 顺序栈的容量(数组最大长度)
        /// </summary>
        private int maxsize;
        /// <summary>
        /// 数组,用于存储顺序栈中的数据元素
        /// </summary>
        private T[] data;
        /// <summary>
        /// 指示顺序栈的栈顶(top = -1 时表示栈为空)
        /// </summary>
        private int top;

这个和线性表(顺序存储结构)是一致的。

这里要说明一下顶栈top这个变量,当栈为空时,其为值-1,因此当想清空栈的时候,不需要将链表循环清空,只需将顶栈置为-1即可。

下方是我整个类的代码:具体调用的实例在文末,可下载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
csharp复制代码public class StackList<T> : IStack<T>
    {
        /// <summary>
        /// 顺序栈的容量(数组最大长度)
        /// </summary>
        private int maxsize;
        /// <summary>
        /// 数组,用于存储顺序栈中的数据元素
        /// </summary>
        private T[] data;
        /// <summary>
        /// 指示顺序栈的栈顶(top = -1 时表示栈为空)
        /// </summary>
        private int top;
        /// <summary>
        /// 构造函数
        /// </summary>
        public StackList(int size)
        {
            data = new T[size];
            maxsize = size;
            top = -1;
        }
        /// <summary>
        /// 清空栈(这里不需要将数组清空,只要将顶栈赋值为-1就好)
        /// </summary>
        /// <returns></returns>
        public bool ClearStack()
        {
            top = -1;
            return true;
        }
        /// <summary>
        /// 获取栈顶
        /// </summary>
        /// <returns></returns>
        public T GetTopStack()
        {
            T res = data[top];
            return res;
        }
        /// <summary>
        /// 是否是空栈
        /// </summary>
        /// <returns></returns>
        public bool IsEmptyStack()
        {
            if (top == -1)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        /// <summary>
        /// 获取栈的长度
        /// </summary>
        /// <returns></returns>
        public int LengthStack()
        {
            return top + 1;
        }
        /// <summary>
        /// 出栈
        /// </summary>
        /// <returns></returns>
        public bool PopStack()
        {
            if (IsEmptyStack())
            {
                Console.WriteLine("栈为空,无法删除!");
                return false;
            }
            T temp = default(T);
            data[top] = temp;
            top--;
            return true;
        }
        /// <summary>
        /// 入栈
        /// </summary>
        /// <returns></returns>
        public bool PushStack(T val)
        {
            if (IsFull())
            {
                Console.WriteLine("栈已满,写入失败!");
                return false;
            }
            top++;
            data[top] = val;
            return true;
        }
        /// <summary>
        /// 判断栈是否已满
        /// </summary>
        /// <returns></returns>
        public bool IsFull()
        {
            if (top == maxsize - 1)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        /// <summary>
        /// 打印栈
        /// </summary>
        public void paint()
        {
            Console.WriteLine("栈顶:"+top);
            for (int i = 0; i < data.Length; i++)
            {
                Console.WriteLine("index:"+i + " | data:"+data[i]);
            }
        }
    }

顺序栈的操作和线性表顺序存储结构相比其实要简单,它的写入和删除操作只发生在表尾,因此在使用它的时候,大概参照线性表的顺序存储结构的用法就好了。

下图是具体调用的实例:

11111.png

两栈共享空间

栈还有一个在我看来比较高级的用法叫两栈共享空间。

大概是一个栈在表首,一个栈在表尾,每个栈都有一个顶栈。

当两个顶栈彼此相遇的时候,就证明两栈已满,无法再继续操作。

具体代码不在这里进行展示,文末有实例,可下载。

共享栈最后效果如下图所示

22222.png

有好的建议,请在下方输入你的评论。

欢迎访问个人博客
guanchao.site

欢迎访问小程序:

在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

leetcode 1611 Minimum One Bit

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

描述

Given an integer n, you must transform it into 0 using the following operations any number of times:

  • Change the rightmost (0th) bit in the binary representation of n.
  • Change the ith bit in the binary representation of n if the (i-1)th bit is set to 1 and the (i-2)th through 0th bits are set to 0.

Return the minimum number of operations to transform n into 0.

Example 1:

1
2
ini复制代码Input: n = 0
Output: 0

Example 2:

1
2
3
4
5
vbnet复制代码Input: n = 3
Output: 2
Explanation: The binary representation of 3 is "11".
"11" -> "01" with the 2nd operation since the 0th bit is 1.
"01" -> "00" with the 1st operation.

Example 3:

1
2
3
4
5
6
7
vbnet复制代码Input: n = 6
Output: 4
Explanation: The binary representation of 6 is "110".
"110" -> "010" with the 2nd operation since the 1st bit is 1 and 0th through 0th bits are 0.
"010" -> "011" with the 1st operation.
"011" -> "001" with the 2nd operation since the 0th bit is 1.
"001" -> "000" with the 1st operation.

Example 4:

1
2
ini复制代码Input: n = 9
Output: 14

Example 5:

1
2
ini复制代码Input: n = 333
Output: 393

Note:

1
复制代码0 <= n <= 109

解析

根据题意,给定一个整数 n,必须使用任意次数的以下操作将其转换为 0:

  • 更改 n 的二进制表示最右边的位。可以 0 变为 1 ,也可以 1 变为 0 。
  • 如果第 (i-1) 位设置为 1 并且第 (i-2) 到第 0 位设置为 0,则更改 n 的二进制表示中的第 i 位。可以 0 变为 1 ,也可以 1 变为 0 。

注意题目中的二进制位的索引都是从右向左的,返回将 n 转换为 0 的最小操作数。因为方法一只是将最右边的 0 和 1 互换,无法对前面的字符进行操作,所以关键就是巧用方法二进行变化,假如我们举例,将 101011 变为 000000 ,其最简单的思路就是递归:

  • (1)101011 第一位为 1 ,想要将其变为 100000 ,就调用自定义的 convert 函数,该函数的功能就是找出将 01011 变为 10000 的最少次数
  • (2)应用方法二将变化之后的 110000 变为 010000 进行了 1 次操作,然后计算将 10000 变为 00000 的次数,和上面同样的方法,将 0000 通过 convert 函数变为 1000 ,在进行相同的操作,直到最后变为 000000
  • (3)所以定义递归函数 dfs ,表示对输入二进制的最少次数操作,将上面的过程表示出来就是 dfs(101011) = convert(01011) + 1 + dfs(10000)

但是 convert 有两种情况:

  • 第一种情况是二进制的第一个数字是 1 ,如 1110 。那直接调用 dfs(110) 即可
  • 第二种情况是二进制的第一个数字是 0 ,如 0111 ,又是需要递归 :convert(0111) = convert(111) + 1 + dfs(100)

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
python复制代码class Solution(object):

def __init__(self):
self.d = {}
self.t = {}

def minimumOneBitOperations(self, n):
"""
:type n: int
:rtype: int
"""
return(self.dfs(bin(n)[2:]))

def dfs(self, s):
if s == '0' : return 0
if s == '1' : return 1
if s in self.d : return self.d[s]
if s[0] == '0': return self.dfs(s[1:])
m = s[1:]
n = list(s[1:])
n[0] = '1'
for i in range(1, len(n)):
n[i] = '0'
n = ''.join(n)
self.d[s] = self.convert(m) + 1 + self.dfs(n)
return self.d[s]

def convert(self, s):
if s == '0' : return 1
if s == '1' : return 0
if s in self.t : return self.t[s]
if s[0] == '1':
self.t[s] = self.dfs(s[1:])
else:
m = s[1:]
n = list(s[1:])
n[0] = '1'
for i in range(1, len(n)):
n[i] = '0'
n = ''.join(n)
self.t[s] = self.convert(m) + 1 + self.dfs(n)
return self.t[s]

运行结果

1
2
erlang复制代码Runtime: 44 ms, faster than 5.17% of Python online submissions for Minimum One Bit Operations to Make Integers Zero.
Memory Usage: 13.3 MB, less than 77.59% of Python online submissions for Minimum One Bit Operations to Make Integers Zero.

原题链接:leetcode.com/problems/mi…

您的支持是我最大的动力

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go语言搬砖 flag官方标准库

发表于 2021-11-25

「这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战」。

前言

flag是官方出的命令行参数解析标准库, 让编写命令行工具更加简单

特点

  • 支持参数类型string、bool、int、int64、uint、uint64、float float64、duration等
  • 支持自定义参数key名
  • 支持参数简单判断(参数长度)
  • 支持返回指针类型或数据类型
  • 自带帮助命令(-h)

例子

博主通常情况用flag.TypeVar的方式比较多,因为flag.Type返回指针类型,使用时需要带着星号(*),容易忘记,且代码编辑器也不提示语法错误

flag.TypeVar例子

TypeVar格式:类型指针,参数key名(长或短都行),默认值(int,bool不能为空)String可以为空,帮助信息

NFlag方法进行了简单判断,如果传入参数过少,会直接中断程度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
js复制代码package main

import (
"flag"
"log"
)

func main() {
var file string
var name string
var ssl bool
flag.StringVar(&file, "f", "", "需要上传的文件")
flag.StringVar(&name, "n", "", "上传后名称")
flag.BoolVar(&ssl, "s", false, "开关,默认关闭")
flag.Parse()

if flag.NFlag() < 2 {
log.Println(flag.NFlag())
log.Fatalln("最少传2个参数!!!")
}
log.Println(file,name,ssl)
}

使用方式: ./main -f etc.conf -n nginx

使用方式: ./main -f etc.conf -n nginx -s true

flag.Type例子

Type格式:参数key名(长或短都行),默认值(int,bool,duration不能为空)String可以为空,帮助信息

这里利用了os.Args来进行参数个数判断。。其实NFlag方法源码也是基于的os.Args

Type方法会返回指针类型,所以需要变量接收,变量在使用时也带个星号*(*name)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
js复制代码package main

import (
"flag"
"log"
"os"
)

func main() {

message := flag.String("message", "", "消息通知")
number := flag.Int("number",0,"发送数量,默认0条")
status := flag.Bool("status",false,"通知开关,默认关闭")
delay := flag.Duration("delay",0,"是否延迟,默认关闭")
flag.Parse()

if len(os.Args) < 4 {
log.Println(len(os.Args))
log.Fatalln("最少传4个参数!!!")
}
log.Println(*message,*number,*status,delay)
}

使用方式: ./main -message hello -number 1

使用方式: ./main -message hello -number 1 -status true -delay 5

小结

flag用于编写简单的命令行工具完全能满足需求,且是官方出品,打包后体积很小

当然还有大佬出的增强型flag工具(github.com/jessevdk/go…) 支持长短别名,函数回调,结构嵌套等,感兴趣的小伙伴可以看看

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

聊聊Pulsar,一款非常优秀的消息中间件!!!

发表于 2021-11-25

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」。

有一个爱学习的研发老总,总是爱尝试不同的技术,这不,又让我来研究研究Pulsar,我之前用的消息中间件基本上都是RabbitMQ和kafka,这次的项目老总说,用Pulsar。之前都没用过,心里没底,既然要用,那就肯定要好好研究了。

直接扔给我一份英文文档,哎,太瞧得起我了,想要学习Pulsar的小伙伴,文档百度云盘链接自行下载。

image-20211124193918701

链接:pan.baidu.com/s/1H2qcygZO…

提取码:pbzj

现在项目中基本上用起来了,简单聊聊Pulsar这框架吧?我目前所知道的基本都是网上获取的,基本知识点,还没深入进去,很多坑还没踩。最近项目中工作也很多,基本上没事时间写文章了(都是借口,哈哈)。

首先还是来了解一下什么是Pulsar吧,有那么多的消息中间件,为什么还要搞一个Pulsar呢?

Pulsar是什么?

先去官网看看,学习新的技术,官网肯定是第一选择。

官网:pulsar.apache.org/docs/zh-CN/…

Pulsar 是一个用于服务器到服务器的消息系统,具有多租户、高性能等优势。

Pulsar 源自Yahoo,于2016年开源并捐献给Apache基金会,并在2018年9月升级成为Apache顶级项目。

Pulsar 这个消息平台近两年非常火,被称为下一代的消息流平台,大有取代Kafka的势头。

Pulsar基本架构

  • Pulsar采用存储计算分离的架构,pulsar使用了bookkeeper做消息的存储,bookkeeper保证了消息存储的可靠性和高效性,bookkeeper为pulsar提供了存储的扩展能力。
  • Pulsar使用zk做元数据存储。
  • 多租户,pulsar最初的设计就是支持多租户的。
  • 命名空间:一个租户可以有多个命名空间,一个topic属于一个命名空间,pulsar中的配置都是以命名空间为单位配置的。

  • Pulsar的broker用于处理消息的读写,broker中会有消息的本地缓存,因为多数场景下,消息被写入后会立刻被消费,因此broker中持有的新消息的缓存能非常有效的提高性能和MQ的整体吞吐。

相比kafka、rocketmq等MQ,pulsar基于bookkeeper的存储计算分离架构,使得pulsar的消息存储可以独立于broker而扩展。

多租户

多租户也是一个刚需功能,可以在同一个集群中对不同业务、团队的数据进行隔离。

1
java复制代码persistent://core/order/create-order

以这个 topic 名称为例,在 core 这个租户下有一个 order 的 namespace,最终才是 create-order 的 topic 名称。

在实际使用中租户一般是按照业务团队进行划分,namespace 则是当前团队下的不同业务;这样便可以很清晰的对 topic 进行管理。

通常有对比才会有伤害,在没有多租户的消息中间件中是如何处理这类问题的呢:

  1. 干脆不分这么细,所有业务线混着用,当团队较小时可能问题不大;一旦业务增加,管理起来会非常麻烦。
  2. 自己在 topic 之前做一层抽象,但其实本质上也是在实现多租户。
  3. 各个业务团队各自维护自己的集群,这样当然也能解决问题,但运维复杂度自然也就提高了。

以上就很直观的看出多租户的重要性了。

应用

除此之外的上层应用,比如生产者、消费者这类概念与使用大家都差不多。

比如 Pulsar 支持四种消费模式:

  • Exclusive:独占模式,同时只有一个消费者可以启动并消费数据;通过 SubscriptionName 标明是同一个消费者),适用范围较小。
  • Failover 故障转移模式:在独占模式基础之上可以同时启动多个 consumer,一旦一个 consumer 挂掉之后其余的可以快速顶上,但也只有一个 consumer 可以消费;部分场景可用。
  • Shared 共享模式:可以有 N 个消费者同时运行,消息按照 round-robin 轮询投递到每个 consumer 中;当某个 consumer 宕机没有 ack 时,该消息将会被投递给其他消费者。这种消费模式可以提高消费能力,但消息无法做到有序。
  • KeyShared 共享模式:基于共享模式;相当于对同一个topic中的消息进行分组,同一分组内的消息只能被同一个消费者有序消费。

第三种共享消费模式应该是使用最多的,当对消息有顺序要求时可以使用 KeyShared 模式。

关于 pulsar 基本知识点,我这里就不一一介绍了,介绍的话我也是拷贝过来的,我觉得看别人写的头头是道,你还不如自己去实践一遍,这样才能加深自己的印象,目前 pulsar 只能在Linux系统下安装。使用起来也很简单,几行代码就搞定了。多尝试,软件行业技术更新就是这么快,坚持学习,才能不被淘汰。

结语

看了很多网上一些人对于pulsar的前景看法,基本上都是比较看好的,Pulsar 社区一直在不断发展壮大,Pulsar 技术的发展和用例数量的增加已经形成良性循环,Pulsar 生态也在壮大。

Pulsar 也具有许多优势,因此能够在统一的消息和事件流平台脱颖而出,并成为更多人的选择。相比于 Kafka,Pulsar 更具有弹性,在运维和扩展上更为简单。

感兴趣的可以去研究研究,相比于摸鱼一天,不如利用时间多学一些感兴趣的知识,其实不仅是技术文章,天文地理都可以去了解(我就喜欢看宇宙方面的知识)。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java期末作业——王者荣耀的洛克王国版游戏 👺导读 👺项目

发表于 2021-11-25

「这是我参与11月更文挑战的第24天,活动详情查看:2021最后一次更文挑战」

在这里插入图片描述

👺导读

最近肥学在刷Java毕竟学业将至不得不为后面的道路考虑啊,做后端的不了解Java基础和学几个主流框架怎么也说不过去吧。然后我就试着写了我这个Java新手都能完成的王者荣耀洛克王国版模拟游戏暂时还没加上前端,等后面刷完框架在加。好了我们屁话不多说来看看吧

👺项目介绍

该项目使用MVC+DAO模式,这也是目前比较流行的模式吧。
MVC的意思是

字母 表示含义
M model,模型层,Javabean
V view 视图层 ,servlet和jsp前端页面
C controller 控制层

DAO 就是数据持久化层。
整个项目没有什么特点,它唯一的特点就是简单代码量较大附含视频特别适合新手练而且内容也涵盖了放射,正则表达式等一些技术。
在这里插入图片描述

每一个包里面又有很多的Java类

在这里插入图片描述

👺功能介绍

特别强调介绍里面出现的人名绝无恶意,只是我个人在创作期间在《觉醒年代》中被他们深深吸引导致。

1
2
3
4
5
6
7
8
9
10
java复制代码王者荣耀洛克王国版模拟游戏
1、系统需求:
用户方:
1、登录 输入用户名 密码 进行登录
2、选择英雄进行游戏
包括: 英雄的类型、英雄的攻击值、攻击命中率
目前类型为:妲己和貂蝉
3、游戏开始后,英雄发起攻击,根据英雄的攻击值和命中率,计算分数
4、游戏结束后可将分数记录下来
5、可以查看以前的游戏分数
1
2
3
4
5
6
7
8
9
10
java复制代码管理员方:
1、登录 默认 用户名:admin 密码:123 ,后期从XML文件中读出
用户名密码输入三次错误,则退出游戏
2、新增玩家
3、修改玩家
4、删除玩家
5、查询玩家
6、查询游戏
7、分数统计
8、参数设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码2、系统界面:

1、首页

~~~~~~~~~~~~~~~菜单界面~~~~~~~~~~~~~~~~~~

王者荣耀的洛克王国版

1.玩家登陆

2.管理员登录

3.退出

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
请输入您想选择模式前的数值:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码
2、选择“1、玩家登录”
******************************************************

玩 家 登 录

请输入用户名:
请输入密码:

******************************************************
如果用户名和密码输入正确(需要管理员添加玩家)
则出现界面如下:

恭喜登陆成功!
1
2
3
4
5
6
7
8
9
java复制代码
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
王者荣耀洛克王国版

1·开始游戏
2·查看成绩
0·返回上级
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
请选择:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码	2.1开始游戏
 您以选择成功!
 开始游戏!
 欢迎来到王者荣耀洛克王国版虚拟画面
 妲己:主法术攻击技能有三
 技能一:月刃 攻击值100
 技能二:魅惑 攻击值150
 技能三:狐之殇 攻击值300
 请输入您要使用的技能:
 1
 技能:月刃
 描述:对敌方造成法术伤害

 太棒了教对面做人!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码

 妲己:主法术攻击技能有三
 技能一:月刃 攻击值100
 技能二:魅惑 攻击值150
 技能三:狐之殇 攻击值300
 请输入您要使用的技能:
 2

 哎呀,打偏了我的我的!


 妲己:主法术攻击技能有三
 技能一:月刃 攻击值100
 技能二:魅惑 攻击值150
 技能三:狐之殇 攻击值300
 请输入您要使用的技能:
 3
 技能:狐之殇
 描述:对地方造成法术伤害+加减速效果

 成功超神!

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码	攻击结束鲁迅您的得分:400
 是否继续?(是或否)


 2.2查看成绩
游戏时间 分数
2021-08-07 00:00:00.0 600
2021-08-08 07:56:37.0 450
2021-08-08 16:38:03.0 400
1、返 回 上 级
 2.3返回上级

返回首页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码
3、选择“2、管理员登录”
******************************************************

管 理 员 登 录

请输入用户名:
请输入密码:

******************************************************
如果用户名和密码输入正确(默认都为admin)
则出现界面如下:
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1·新增玩家
2·修改玩家
3·删除玩家
4·查询玩家
5·查询游戏英雄
6·分数统计
7·参数设置
0·返回上级
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
如果密码输错三次则退出游戏。系统关闭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码

 3.1 新增玩家
新增玩家:
请输入玩家的登录名:(必须要有字母,且在系统中唯一)
请输入玩家的密码:(必须要有字母和数字,且最少不能少于6位)
请输入玩家的昵称:
请输入玩家的性别:(只可输入男或女)
请输入玩家的年龄:(只可输入正整数,且最大不可大于99)


新增玩家成功!
1、继续新增玩家
0、返 回 上 级
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码
 3.2 修改玩家
请选择要修改的玩家:
1、xx1 2、xx2 3、xx3 4、XX4
选“1”修改xx1
选择要修改的属性:
1、玩家的密码:(必须要有字母和数字,且最少不能少于6位)
2、玩家的昵称:
3、玩家的性别:(只可输入男或女)
4、玩家的年龄:(只可输入正整数,且最大不可大于100)

0、返回上级
1
2
3
4
5
6
7
8
9
java复制代码

 3.3 删除玩家
请选择要删除的玩家:
1、xx1 2、xx2 3、xx3 4、XX4
选“1”删除xx1
全部删除后不可再删除

0、返回上级
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码
 3.4 查询玩家
 1·查询全部玩家
 2·根据登陆名查找
 0·回退
 loginname nickname sex age
 周树人 鲁迅 男 18
 阿秀 领导人 男 20
 李大钊 守常 男 19
 123a 肥学 男 23


0、返 回 上 级
********************************************************************
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码

 3.5 查询游戏英雄
*******************************************************************************
英雄名:妲己
一技能:月刃 攻击力:100 命中率:90
二技能:魅惑 攻击力:150 命中率:80
三技能:狐之殇 攻击力:300 命中率:60
英雄名:貂蝉
一技能:独舞 攻击力:90 命中率:90
二技能:谗诱 攻击力:150 命中率:80
三技能:拜月 攻击力:200 命中率:80
1
2
3
4
java复制代码
 3.6分数统计
玩家 总分 战力指数
 周树人 1450 ¤¤¤¤¤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码
 3.7参数设置
 1·管理员的登陆名
 2·管理员密码
 3·管理员的最大登录次数

 以下仅作英雄详情展示
 英雄ID:1 英雄名:妲己
 技能一 伤害值:100: 命中率:90
 技能二 伤害值:150: 命中率:80
 技能三 伤害值:300: 命中率:60
 ~~~
 英雄ID:2 英雄名:貂蝉
 技能一 伤害值:90: 命中率:90
 技能二 伤害值:150: 命中率:80
 技能三 伤害值:200: 命中率:80
 ~~~
4、选择“0、退出”
系统提示“游戏结束。。。。。。”系统关闭

👺所建立的数据表

在这里插入图片描述

部分代码展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码public class AdminManager {
/**
* 登录
* @throws SQLException
*/
//
DBUtil db=new DBUtil();
SysManager sm=new SysManager();
PlayerDao pd=new PlayerDao();
GameDao gd=new GameDao();
public boolean AdminOperate() throws SQLException {
boolean b=false;
for(int i=0;i<DataInit.login.getLogintime();i++) {
Login login=Menu.getLoginUI();
b=this.checkoutLogin(login);
if(b) {
System.out.println("恭喜登录成功!!!\n\n");
//循环这个管理员界面使不按0推出就不会返回上一层
boolean feixue=true;
while(feixue) {
int c=Menu.getAdminUI();

feixue=this.adminOperate2(c);

}
}
else {
System.out.println("请重新登陆!");
if(DataInit.login.getLogintime()-i>0) {
System.out.println("还可以输入"+(DataInit.login.getLogintime()-i)+"次");
}
}
}
return b;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
java复制代码//游戏过程
public void play() throws SQLException {
System.out.println("欢迎来到王者荣耀洛克王国版虚拟画面");
int score=0;
//选择英雄
hero h=heros;
for(int i=0;i<3;i++){
score=this.selectSkill(h)+score;
System.out.println("\n");
}
System.out.println("攻击结束"+player.get("nickname")+"您的得分:"+score);
//保存游戏记录
Game game=new Game();
game.setPid(Integer.parseInt(player.get("id")));
game.setScore(score);
gd.insertGame(game);
}

//技能展示
public int selectSkill(hero h) {
//生成随机数
double ran=Math.random()*10;
//展示每个英雄的技能
h.display();
System.out.println("请输入您要使用的技能:");
int s=InputHelper.getInt();

switch(s) {
case 1:

int pro1 =h.probability1/10;
if(ran<=pro1) {
h.skill1();
System.out.println("\n太棒了教对面做人!");
return h.skill1;
}else {
System.out.println("\n哎呀,打偏了!");
}
break;
case 2:

int pro2 =h.probability2/10;
if(ran<=pro2) {
h.skill2();
System.out.println("\n666");
return h.skill2;
}
else {
System.out.println("\n哎呀,打偏了我的我的!");
}
break;
case 3:
int pro3 =h.probability3/10;
if(ran<=pro3) {
h.skill3();
System.out.println("\n成功超神!");
return h.skill3;
}else {
System.out.println("\n大哥你技术不行呀!");
}
break;
default:
System.out.println("请输入正确的技能!");

}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
java复制代码public class GameDao {
DBUtil db=new DBUtil();
//新增游戏即重新开始
public int insertGame(Game game) throws SQLException {
//now()自带函数获取档当前时间
String sql="insert into wzry_game(pid,playtime,score) values(?,now(),?)";
Object[] obj= {game.getPid(),game.getScore()};
return db.upDate(sql, obj);
}
//玩家查询自己的游戏记录
public List<Map<String,String>> queryGameByPid(int id) throws SQLException{
String sql="select b.*,a.loginname from wzry_player a,wzry_game b where a.id=b.pid and pid=?";
Object[] obj= {id};
List<Map<String,String>> list=db.query(sql, obj);
return list;
}
//查询
public List<Map<String,String>> queryAllGame() throws SQLException{
String sql="select b.*,a.loginname from wzry_player a,wzry_game b where a.id=b.pid";
List<Map<String,String>>list=db.query(sql, null);

return list;

}
//分数统计

public List<Map<String,String>> queryGameScore() throws SQLException {
String sql="select a.loginname as 玩家,sum(b.score) as 总分,case "
+" when sum(b.score)>=500 then '¤¤¤¤¤' "
+" when sum(b.score)>=400 and sum(b.score)<500 then '¤¤¤¤' else '¤¤¤' end 'lucky'"
+" from wzry_player as a inner join wzry_game as b on a.id=b.pid"
+" group by a.loginname";

return db.query(sql, null);
}
//查询所有英雄
public List<Map<String,String>> queryAllHeros() throws SQLException{
String sql="select * from heros";
List<Map<String,String>> heros=db.query(sql, null);
return heros;
}


}

👺特别注意

另外如果大家想获取MySQL基础知识和资源的话可以到主页我的信息里面找找,源码也在里面

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

记一次 MySql sql_mode 引发的线上问题

发表于 2021-11-25

前言

事发

最近有个版本要求修改评论长度限制,由64改为500,在调试过程中,评论始终只能显示出来200字,经过查库,发现字段长度被定为200,插入时被截断。超出长度不是因该报错吗?嗯,应该是sql_mode的问题。

验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Sql复制代码## 创建表 username 长度为8
CREATE TABLE `test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(8) DEFAULT NULL,
`pwd` varchar(8) DEFAULT NULL,
`seq` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=168 DEFAULT CHARSET=utf8;


## 插入 username 长度为9的值
INSERT INTO `test`(`username` , `pwd` , `seq`)
VALUES
(
'123456789' ,
'133aaa' ,
NULL
);

## 插入成功
No errors;1 row affected,taking 3.2 ms

## 查询发现值被截断
select username from test;
> 12345678


## 改变sql_mode
set SESSION sql_mode = 'STRICT_ALL_TABLES';

## 再次插入长度相同的值
INSERT INTO `test`(`username` , `pwd` , `seq`)
VALUES
(
'123456789' ,
'133aaa' ,
NULL
);

## 报错
Data too long for column 'username' at row 1

SQL_MODE

MySQL服务器可以在不同的模式设置(sql_mode)下运行,并可以对不同的客户端进行不同的设置,通过sql_mode变量设置。

影响点:

  • 语法
  • 数据校验

设置SQL_MODE

默认的sql_mode值为NO_ENGINE_SUBSTITUTION,可在启动时及运行时设置该变量。

启动时设置:

  • 启动时命令行添加 —sql-mode=”A,B,C……”(多值使用“,”隔开)
  • 在my.cnf配置文件中添加sql-mode=”A,B,C……”(多值使用“,”隔开)

运行时设置:

1
2
Sql复制代码SET GLOBAL sql_mode = 'A,B,C……'; ## 全局 重连生效
SET SESSION sql_mode = 'A,B,C……';## 当前会话 即时生效

查看:

1
2
sql复制代码- SELECT @@GLOBAL.sql_mode 
- SELECT @@SESSION.sql_mode

注意:

  • 在表使用分区后,最好不要改变sql_mode,因为不同的sql_mode会影响一些计算结果,对分区策略产生影响
  • 主从服务器,使用相同的sql_mode,以实现数据同步

SQL_MODE可选值

其它几个影响语法及数据校验的值

  • ALLOW_INCALID_DATES

不校验时间的有效性,只会校验月在1-12之间,日在1-31之间,只针对DATE和DATETIME类型有效,对TIMESTAMP无效。

1
2
3
Sql复制代码insert into temp values('2019-02-31 01:01:01');

Incorrect datetime value: '2019-02-31 01:01:01' for column 'dt' at row 1
1
2
3
4
Sql复制代码set SESSION sql_mode  = 'ALLOW_INVALID_DATES';
insert into temp values('2019-02-31 01:01:01');

No errors;1 row affected,taking 3.2 ms
  • ANSI_QUOTES

双引号(“)被用作标识符引用字符,和“`”相同。

1
2
3
Sql复制代码truncate table "temp";

You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '"temp"' at line 1
1
2
3
4
Sql复制代码set SESSION sql_mode  = 'ANSI_QUOTES';
truncate table "temp";

No errors;0 row affected,taking 0.2 ms
  • ERROR_FOR_DEVISION_BY_ZERO

控制/0及mod(n,0)的验证,该模式开启后可/0及mod(n,0)操作会生成警告,如果此时开启了严格SQL模式(STRICT_TRANS_TABLES 或 STRICT_ALL_TABLES),会阻止语句执行,报错。不影响select语句。

  • HIGH_NOT_PRECEDENCE

提高逻辑运算NOT优先级

1
2
3
4
5
6
7
8
9
10
11
12
13
Sql复制代码select not 1;
> 0

select 1 between -1 and 2;
> 1

select not 1 between -1 and 2;
> 0

## 设置sql_mode
set SESSION sql_mode = 'HIGH_NOT_PRECEDENCE';
select not 1 between -1 and 2;
> 0
  • ONLY_FULL_GROUP_BY

影响group by语法

1
2
3
4
5
6
7
8
9
10
11
Sql复制代码select count(0) ct,username from test group by pwd;
> 2 | 12345678




## 设置sql_mode
set SESSION sql_mode = 'ONLY_FULL_GROUP_BY';

select count(0) ct,username from test group by pwd;
Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'test.test.username' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

更多

以上列举了几个对编码影响较大的选项,更多选项见官网说明。

总结

以后关于SQL的写法,和某人抬杠时,除了要确定MySQL版本之外,还要问一句,您是啥sql_mode…….O(∩_∩)O哈!。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

HarmonyOS(鸿蒙)——双击事件

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

一、简介

1.1 什么是组件

组件就是文本、按钮、图片等元素的统称

1.2 什么是事件

事件就是可以被组件识别的操作,常见的事件有单击、双击、长按和滑动等。

1.3 什么是双击事件

双击事件就是我们对屏幕连续点击两次,比如刷抖音双击屏幕的点赞等……

1.4 实现步骤

实现HarmonyOS(鸿蒙)的双击事件主要分为四个步骤:

  1. 定义组件,给组件分配唯一ID,之后通过ID定位组件
  2. 给定义的组件绑定双击事件
  1. 实现DoubleClickedListener接口并重写onDoubleClick方法
  2. 实现onDoubleClick方法中的具体逻辑,以此完成点击事件的相关业务操作

二、案例

2.1 创建项目

File -> New -> New Project

选择Empty Ability(Java),单击Next;

填写项目相关配置信息,点击Next;

项目创建完成后的效果如下

\

2.2 定义组件

这一步会定义一个按钮(按钮也是一个组件)和一个文本组件,并且给按钮和文本组件分配唯一ID,之后通过ID定位按钮和文本组件,然后我们会通过双击事件改变文本框中的内容。在这里可能需要首先了解一下Ability相关技术,这样可以更好的了解Ability框架以及页面之间的包含关系,如果有完全不了解的可以查阅这篇文章,做个简单入门《HarmonyOS(鸿蒙)—— Ability与页面》。

找到MainAbilitySlice.java文件,然后按住ctrl键+点击ResourceTable.Layout_ability_main,进入ability_main.xml文件

也可以直接定位ability_main.xml文件

\

组件代码开发,首先编写ability_main.xml文件内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ini复制代码<?xml version="1.0" encoding="utf-8"?>
<DirectionalLayout
xmlns:ohos="http://schemas.huawei.com/res/ohos"
ohos:height="match_parent"
ohos:width="match_parent"
ohos:alignment="center"
ohos:orientation="vertical">

<!-- ohos:id定义组件的id,注意格式固定$+id:xxxx -->
<!--match_content 表示包裹内容,按钮的大小与按钮内的文字大小一致-->
<Text
ohos:id="$+id:text"
ohos:height="match_content"
ohos:width="match_content"
ohos:text="各位看官大人,万福金安!"
ohos:text_size="15fp"
/>

<Button
ohos:id="$+id:button"
ohos:width="match_content"
ohos:height="match_content"
ohos:text="请双击"
ohos:text_size="19fp"
ohos:text_color="#FFFFFF"
ohos:top_padding="8vp"
ohos:bottom_padding="8vp"
ohos:right_padding="70vp"
ohos:left_padding="70vp"
ohos:center_in_parent="true"
ohos:margin="10vp"
ohos:background_element="#007DFF"
/>

</DirectionalLayout>

2.3 定义的组件绑定单击事件

Component findComponentById(int resID)方法返回的是Component,Component是HarmonyOS中所有组件的父类。我们首先找到MainAbilitySlice.java文件,在onStart方法中进行事件的绑定。

2.4 实现Component.DoubleClickedListener接口并重写onDoubleClick方法

关于这一步的写法一共有四种,大家可以根据自己的需求去进行选择,我这里选用的是方法引用的方式,我觉得这样比较简洁,又能使用到类的成员变量。四种写法的详细介绍文章地址如下:

❤️HarmonyOS(鸿蒙)❤️——单击事件的四种写法详述

2.5 实现onDoubleClick方法中的具体逻辑,以此完成点击事件的相关业务操作

在onDoubleClick方法中,处理具体点击操作的逻辑,这里通过双击按钮修改文本组件中的文字来实现。2.3-2.5步骤的代码比较简单,全在下面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
java复制代码package com.liziba.demo.slice;

import com.liziba.demo.ResourceTable;
import ohos.aafwk.ability.AbilitySlice;
import ohos.aafwk.content.Intent;
import ohos.agp.components.Button;
import ohos.agp.components.Component;
import ohos.agp.components.Text;

public class MainAbilitySlice extends AbilitySlice{

/** 提取Text组件为成员变量,后续方法需要使用 */
Text text = null;

@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setUIContent(ResourceTable.Layout_ability_main);

// 1、找到组件
// button - 按钮组件
Button button = (Button) this.findComponentById(ResourceTable.Id_button);
// 文本组件
text = (Text) this.findComponentById(ResourceTable.Id_text);

// 2、绑定双击事件(给需要点击的组件添加双击事件)
button.setDoubleClickedListener(this::onDoubleClick);
}

@Override
public void onActive() {
super.onActive();
}

@Override
public void onForeground(Intent intent) {
super.onForeground(intent);
}

public void onDoubleClick(Component component) {

// component表示的是被双击的组件对象
int id = component.getId();
if (ResourceTable.Id_button == id) {
text.setText("大家好,我是李子捌!");
}
}
}

三、测试

3.1 登录远程模拟器

点击Tools -> Device Manager

点击Login进行登录

登录华为账户,点击允许,如果没有注册账户的,请先注册一个华为账户

选择P40作为远程模拟器

启动成功后会看到如下调试手机界面

3.2 运行项目

点击右上角的三角形直接运行,或者点击甲壳虫进入调试模式

运行效果,双击前

\

双击前后

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

VisualVM及模拟简单场景分析使用

发表于 2021-11-25

介绍

Visual VM是一个功能强大的多合一故障诊断和性能监控可视化工具(文件过大可能会有些问题),在JDK6升级到JDK7以后作为JDK的一部分发布(jdk路径bin下的jvisualvm可以启动进行监控),完全免费的。

Visual VM也可以作为独立的软件安装,当前可在🔗下载进行安装。之所以强大,那是因为Visual VM支持插件安装,提供离线下载插件安装和在线安装插件。

本文也模拟了死锁和内存泄漏OOM场景通过该工具进行了简单分析,希望能对实际应用有所帮助。

安装

VisualVM作为Java VisualVM发布在Oracle JDK 6~8中,则我们可以在jdk的path路径下的bin目录运行jvisualvm命令来打开。如果需要独立安装,介绍中也提供了下载地址,安装比较简单就不作介绍了(同意协议直接安装即可)。另外我们来说下插件的安装。

插件在线安装

在线安装我们选择Tools->Downloaded->Available Plugins选择或搜索需要安装的插件进行install。
image.png

插件离线下载安装

在下载页面中我们可以点击如下所示的离线插件(*.nbm插件),选择自己想要的插件进行下载:

屏幕快照 2021-11-23 上午8.09.30.png

上面提供了指向所有VisualVM和Java VisualVM版本的插件手动下载的链接。单击插件中心URL并下载所需插件,我们点击VisualVM的2.1.1版本,如下所示:

屏幕快照 2021-11-23 上午8.13.12.png
就会进入到插件下载的目录:

屏幕快照 2021-11-23 上午8.15.47.png

选择我们需要的插件下载即可。安装的时候我们选择Tools->Downloaded->Add Plugins选择已下载的插件安装即可。

屏幕快照 2021-11-23 上午8.18.32.png

功能

我们可以看到主要的分析功能包含以下几个部分,其中后面三个是我们安装的插件。我们选择一些进行简单介绍,然后通过模拟示例来具体分析和发现问题。

image.png

Overview

应用程序的基本概况,如进程ID、Main class、启动参数等

屏幕快照 2021-11-24 下午7.32.08.png

Monitor

监控应用程序的CPU、堆、永久区、类加载和线程数的总体情况,通过“执行垃圾回收”和“Dump”按钮可手动执行Full Gc和Dump当前堆快照

屏幕快照 2021-11-24 下午7.54.24.png

Threads

提供详细的线程信息,单机右上角的“线程Dump”可以导出当前所有线程的堆栈信息(相当于使用jstack命令)。如果Visual VM在当前线程中找到死锁,则会以十分显眼的方式在该页面给予提示。

屏幕快照 2021-11-24 下午7.54.49.png

Sampler

显示了CPU和内存两个性能采样器,用于实时监控程序信息。

CPU采样器可以将CPU占用时间定为到方法,可以发现占用CPU时间最长的方法以及线程占用CPU的时间

屏幕快照 2021-11-24 下午8.01.58.png

内存采样器可以查看当前程序的堆信息和每个线程单独占用的情况。

屏幕快照 2021-11-24 下午8.03.35.png

简单示例分析

模拟死锁分析

首先我们先模拟一个简单的java死锁示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码
public class DeadLock implements Runnable{
private static final Logger logger = LoggerFactory.getLogger(DeadLock.class);

private static Object object1 = new Object();
private static Object object2 = new Object();

private int flag;

public DeadLock(int flag) {
super();
this.flag = flag;
}

@Override
public void run() {
if(flag == 1){
synchronized (object1){
logger.info("Thread-1-1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object2){
logger.info("Thread-1-2");
}
}
} else {
synchronized (object2){
logger.info("Thread-2-1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (object1){
logger.info("Thread-2-2");
}
}
}
}
}
1
2
3
4
csharp复制代码public  void test(){
new Thread(new DeadLock(1)).start();
new Thread(new DeadLock(2)).start();
}

通过执行上面的test方法就会产生互相等待的死锁,这时如果我们本地已经打开了visualVM工具我们可以看到非常显眼的提示。

屏幕快照 2021-11-25 上午12.00.19.png

我们进行Thread Dump会发现有很详细的关于死锁的信息供我们分析,我们看到Thread Dump文件的最底部很展示了死锁的信息,这样我们很容易定位到问题。

屏幕快照 2021-11-25 上午12.17.08.png

上面的话我们是通过本地启动VisualVM工具直接进行监控Thread Dump的。我们也可以通过命令行的方式进行Thread Dump。我们来简单演示下。

首先我们通过如下命令查找到应用进程(展示所有java进程):

1
perl复制代码ps -ef|grep java

或者

1
复制代码jps -l

在进行dump之前我们也可以通过top 查看这个进程目前使用资源的情况。

然后通过jstack命令进行Thread Dump,如下命令所示(例如进程号是32430):

1
bash复制代码jstack -l > /Users/wukong/Desktop/tool/tdump.tdump  32430

然后将上面下载的tdump.tdump文件加载到VisualVM同样可以定位到死锁详细信息。

模拟内存泄漏分析

我们在本地idea启动的jvm参数简单添加如下参数:

1
ruby复制代码-Xms50m -Xmx50m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/Users/wukong/Desktop/tool/heapdump.hprof

其中

-Xms表示堆内存初始值

-Xmx表示堆内存最大值

-XX:+HeapDumpOnOutOfMemoryError 设置首次内存溢出时导出当时堆中相关信息

-XX:HeapDumpPath=/tmp/heapdump.hprof 指定dump堆信息时的路径

然后启动程序执行如下所示方法:

1
2
3
4
5
6
7
ini复制代码public static void test(){
List<Student> list = new ArrayList<>();
for (int i = 0; i < 1000000; i++){
Student student = new Student(i, "张寿臣、马三立、常宝堃、侯宝林、刘宝瑞、李伯祥、高英培、马季、唐杰忠、李文华、侯耀文、石富宽、苏文茂、李金斗、冯巩、郭德纲、于谦、姜昆、岳云鹏、孙越等.");
list.add(student);
}
}

由于我们设置的堆内存最小和最大值都只有50M,则启动程序运行请求相关接口就会出现OOM,我们会看到打印的日志出现:

1
2
3
bash复制代码java.lang.OutOfMemoryError: GC overhead limit exceeded
Dumping heap to /Users/wukong/Desktop/tool/heapdump.hprof ...
Heap dump file created [27428405 bytes in 0.147 secs]

现在我们就把生成的dump文件load到visualVM工具中分析,在概要中我们可以清晰看到OOM的红色警示的线程。

屏幕快照 2021-11-24 下午10.35.51.png

右键选择Select in Threads,

屏幕快照 2021-11-24 下午10.40.09.png

进入到如下界面:

屏幕快照 2021-11-24 下午10.42.30.png
我们定位到具体的方法:at com.wk.manage.web.service.impl.TestService.test(TestService.java:15),然后点击上面红色箭头标识的ArrayList。

屏幕快照 2021-11-24 下午10.46.49.png

elementData展开可以看到具体对象,

屏幕快照 2021-11-24 下午10.48.00.png

通过上面简单分析我们可以发现通过这个工具可以很明显的定位到导致OOM的线程以及集合对象。对我们分析问题起到很大帮助。

总结

本文简单介绍了可视化性能监控工具VisualVm的基本功能以及本地模拟了一些示例分析。在线上如果出现内存泄露或者死锁导致系统运行很慢的话,我们首先会生成dump文件然后立即重启保证当前线上应用,然后分析dump文件定位问题和解决问题。

另外VisualVM的很多插件的功能也是很强大的,例如Btrace,VisualGC等插件,大家有兴趣可以研究下。
还有就是VisualVM是支持远程JMX连接的,但是不建议线上使用,所以在此没有介绍,有兴趣的也可以在测试环境自行研究下。

参考书籍:《实战Java虚拟机》

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…203204205…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%