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

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


  • 首页

  • 归档

  • 搜索

函数式编程的Java编码实践:利用惰性写出高性能且抽象的代码

发表于 2021-11-08

简介: 本文会以惰性加载为例一步步介绍函数式编程中各种概念,所以读者不需要任何函数式编程的基础,只需要对 Java 8 有些许了解即可。

作者 | 悬衡

来源 | 阿里技术公众号

本文会以惰性加载为例一步步介绍函数式编程中各种概念,所以读者不需要任何函数式编程的基础,只需要对 Java 8 有些许了解即可。

一 抽象一定会导致代码性能降低?

程序员的梦想就是能写出 “高内聚,低耦合”的代码,但从经验上来看,越抽象的代码往往意味着越低的性能。机器可以直接执行的汇编性能最强,C 语言其次,Java 因为较高的抽象层次导致性能更低。业务系统也受到同样的规律制约,底层的数增删改查接口性能最高,上层业务接口,因为增加了各种业务校验,以及消息发送,导致性能较低。

对性能的顾虑,也制约程序员对于模块更加合理的抽象。

一起来看一个常见的系统抽象,“用户” 是系统中常见的一个实体,为了统一系统中的 “用户” 抽象,我们定义了一个通用领域模型 User,除了用户的 id 外,还含有部门信息,用户的主管等等,这些都是常常在系统中聚合在一起使用的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码public class User {
// 用户 id
private Long uid;
// 用户的部门,为了保持示例简单,这里就用普通的字符串
// 需要远程调用 通讯录系统 获得
private String department;
// 用户的主管,为了保持示例简单,这里就用一个 id 表示
// 需要远程调用 通讯录系统 获得
private Long supervisor;
// 用户所持有的权限
// 需要远程调用 权限系统 获得
private Set< String> permission;
}

这看起来非常棒,“用户“常用的属性全部集中到了一个实体里,只要将这个 User 作为方法的参数,这个方法基本就不再需要查询其他用户信息了。但是一旦实施起来就会发现问题,部门和主管信息需要远程调用通讯录系统获得,权限需要远程调用权限系统获得,每次构造 User 都必须付出这两次远程调用的代价,即使有的信息没有用到。比如下面的方法就展示了这种情况(判断一个用户是否是另一个用户的主管):

1
2
3
typescript复制代码public boolean isSupervisor(User u1, User u2) {
return Objects.equals(u1.getSupervisor(), u2.getUid());
}

为了能在上面这个方法参数中使用通用 User 实体,必须付出额外的代价:远程调用获得完全用不到的权限信息,如果权限系统出现了问题,还会影响无关接口的稳定性。

想到这里我们可能就想要放弃通用实体的方案了,让裸露的 uid 弥漫在系统中,在系统各处散落用户信息查询代码。

其实稍作改进就可以继续使用上面的抽象,只需要将 department, supervisor 和 permission 全部变成惰性加载的字段,在需要的时候才进行外部调用获得,这样做有非常多的好处:

  • 业务建模只需要考虑贴合业务,而不需要考虑底层的性能问题,真正实现业务层和物理层的解耦
  • 业务逻辑与外部调用分离,无论外部接口如何变化,我们总是有一层适配层保证核心逻辑的稳定
  • 业务逻辑看起来就是纯粹的实体操作,易于编写单元测试,保障核心逻辑的正确性

但是在实践的过程中常会遇到一些问题,本文就结合 Java 以及函数式编程的一些技巧,一起来实现一个惰性加载工具类。

二 严格与惰性:Java 8 的 Supplier 的本质

Java 8 引入了全新的函数式接口 Supplier,从老 Java 程序员的角度理解,它不过就是一个可以获取任意值的接口而已,Lambda 不过是这种接口实现类的语法糖。这是站在语言角度而不是计算角度的理解。当你了解了严格(strict)与惰性(lazy)的区别之后,可能会有更加接近计算本质的看法。

因为 Java 和 C 都是严格的编程语言,所以我们习惯了变量在定义的地方就完成了计算。事实上,还有另外一个编程语言流派,它们是在变量使用的时候才进行计算的,比如函数式编程语言 Haskell。

所以 Supplier 的本质是在 Java 语言中引入了惰性计算的机制,为了在 Java 中实现等价的惰性计算,可以这么写:

1
2
ini复制代码Supplier< Integer> a = () -> 10 + 1;
int b = a.get() + 1;

三 Supplier 的进一步优化:Lazy

Supplier 还存在一个问题,就是每次通过 get 获取值时都会重新进行计算,真正的惰性计算应该在第一次 get 后把值缓存下来。只要对 Supplier 稍作包装即可:

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
csharp复制代码/**
* 为了方便与标准的 Java 函数式接口交互,Lazy 也实现了 Supplier
*/
public class Lazy< T> implements Supplier< T> {

private final Supplier< ? extends T> supplier;

// 利用 value 属性缓存 supplier 计算后的值
private T value;

private Lazy(Supplier< ? extends T> supplier) {
this.supplier = supplier;
}

public static < T> Lazy< T> of(Supplier< ? extends T> supplier) {
return new Lazy< >(supplier);
}

public T get() {
if (value == null) {
T newValue = supplier.get();

if (newValue == null) {
throw new IllegalStateException("Lazy value can not be null!");
}

value = newValue;
}

return value;
}
}

通过 Lazy 来写之前的惰性计算代码:

1
2
3
4
ini复制代码Lazy< Integer> a = Lazy.of(() -> 10 + 1);
int b = a.get() + 1;
// get 不会再重新计算, 直接用缓存的值
int c = a.get();

通过这个惰性加载工具类来优化我们之前的通用用户实体:

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
typescript复制代码public class User {
// 用户 id
private Long uid;
// 用户的部门,为了保持示例简单,这里就用普通的字符串
// 需要远程调用 通讯录系统 获得
private Lazy< String> department;
// 用户的主管,为了保持示例简单,这里就用一个 id 表示
// 需要远程调用 通讯录系统 获得
private Lazy< Long> supervisor;
// 用户所含有的权限
// 需要远程调用 权限系统 获得
private Lazy< Set< String>> permission;

public Long getUid() {
return uid;
}

public void setUid(Long uid) {
this.uid = uid;
}

public String getDepartment() {
return department.get();
}

/**
* 因为 department 是一个惰性加载的属性,所以 set 方法必须传入计算函数,而不是具体值
*/
public void setDepartment(Lazy< String> department) {
this.department = department;
}
// ... 后面类似的省略
}

一个简单的构造 User 实体的例子如下:

1
2
3
4
5
6
ini复制代码Long uid = 1L;
User user = new User();
user.setUid(uid);
// departmentService 是一个rpc调用
user.setDepartment(Lazy.of(() -> departmentService.getDepartment(uid)));
// ....

这看起来还不错,但当你继续深入使用时会发现一些问题:用户的两个属性部门和主管是有相关性,需要通过 rpc 接口获得用户部门,然后通过另一个 rpc 接口根据部门获得主管。代码如下:

1
2
ini复制代码String department = departmentService.getDepartment(uid);
Long supervisor = SupervisorService.getSupervisor(department);

但是现在 department 不再是一个计算好的值了,而是一个惰性计算的 Lazy 对象,上面的代码又应该怎么写呢?”函子” 就是用来解决这个问题的

四 Lazy 实现函子(Functor)

快速理解:类似 Java 中的 stream api 或者 Optional 中的 map 方法。函子可以理解为一个接口,而 map 可以理解为接口中的方法。

1 函子的计算对象

Java 中的 Collection< T>,Optional< T>,以及我们刚刚实现 Lazy< T>,都有一个共同特点,就是他们都有且仅有一个泛型参数,我们在这篇文章中暂且称其为盒子,记做 Box< T>,因为他们都好像一个万能的容器,可以任意类型打包进去。

2 函子的定义

函子运算可以将一个 T 映射到 S 的 function 应用到 Box< T> 上,让其成为 Box< S>,一个将 Box 中的数字转换为字符串的例子如下:

在盒子中装的是类型,而不是 1 和 “1” 的原因是,盒子中不一定是单个值,比如集合,甚至是更加复杂的多值映射关系。

需要注意的是,并不是随便定义一个签名满足 Box< S> map(Function< T,S> function) 就能让 Box< T> 成为函子的,下面就是一个反例:

1
2
3
4
typescript复制代码// 反例,不能成为函子,因为这个方法没有在盒子中如实反映 function 的映射关系
public Box< S> map(Function< T,S> function) {
return new Box< >(null);
}

所以函子是比 map 方法更加严格的定义,他还要求 map 满足如下的定律,称为 函子定律(定律的本质就是保障 map 方法能如实反映参数 function 定义的映射关系):

  • 单位元律:Box< T> 在应用了恒等函数后,值不会改变,即 box.equals(box.map(Function.identity()))始终成立(这里的 equals 只是想表达的一个数学上相等的含义)
  • 复合律:假设有两个函数 f1 和 f2,map(x -> f2(f1(x))) 和 map(f1).map(f2) 始终等价

很显然 Lazy 是满足上面两个定律的。

3 Lazy 函子

虽然介绍了这么多理论,实现却非常简单:

1
2
3
typescript复制代码    public < S> Lazy< S> map(Function< ? super T, ? extends S> function) {
return Lazy.of(() -> function.apply(get()));
}

可以很容易地证明它是满足函子定律的。

通过 map 我们很容易解决之前遇到的难题,map 中传入的函数可以在假设部门信息已经获取到的情况下进行运算:

1
2
3
4
ini复制代码Lazy< String> departmentLazy = Lazy.of(() -> departmentService.getDepartment(uid));
Lazy< Long> supervisorLazy = departmentLazy.map(
department -> SupervisorService.getSupervisor(department)
);

4 遇到了更加棘手的情况

我们现在不仅可以构造惰性的值,还可以用一个惰性值计算另一个惰性值,看上去很完美。但是当你进一步深入使用的时候,又发现了更加棘手的问题。

我现在需要部门和主管两个参数来调用权限系统来获得权限,而部门和主管这两个值都是惰性的值。先用嵌套 map 来试一下:

1
2
3
ini复制代码Lazy< Lazy< Set< String>>> permissions = departmentLazy.map(department ->
supervisorLazy.map(supervisor -> getPermissions(department, supervisor))
);

返回值的类型好像有点奇怪,我们期待得到的是 Lazy< Set< String>>,这里得到的却多了一层变成 Lazy< Lazy< Set< String>>>。而且随着你嵌套 map 层数增加,Lazy 的泛型层次也会同样增加,三参数的例子如下:

1
2
3
4
5
6
7
8
ini复制代码Lazy< Long> param1Lazy = Lazy.of(() -> 2L);
Lazy< Long> param2Lazy = Lazy.of(() -> 2L);
Lazy< Long> param3Lazy = Lazy.of(() -> 2L);
Lazy< Lazy< Lazy< Long>>> result = param1Lazy.map(param1 ->
param2Lazy.map(param2 ->
param3Lazy.map(param3 -> param1 + param2 + param3)
)
);

这个就需要下面的单子运算来解决了。

五 Lazy 实现单子 (Monad)

快速理解:和 Java stream api 以及 Optional 中的 flatmap 功能类似

1 单子的定义

单子和函子的重大区别在于接收的函数,函子的函数一般返回的是原生的值,而单子的函数返回却是一个盒装的值。下图中的 function 如果用 map 而不是 flatmap 的话,就会导致结果变成一个俄罗斯套娃–两层盒子。

单子当然也有单子定律,但是比函子定律要复杂些,这里就不做阐释了,他的作用和函子定律也是类似,确保 flatmap 能够如实反映 function 的映射关系。

2 Lazy 单子

实现同样很简单:

1
2
3
typescript复制代码    public < S> Lazy< S> flatMap(Function< ? super T, Lazy< ? extends S>> function) {
return Lazy.of(() -> function.apply(get()).get());
}

利用 flatmap 解决之前遇到的问题:

1
2
3
ini复制代码Lazy< Set< String>> permissions = departmentLazy.flatMap(department ->
supervisorLazy.map(supervisor -> getPermissions(department, supervisor))
);

三参数的情况:

1
2
3
4
5
6
7
8
ini复制代码Lazy< Long> param1Lazy = Lazy.of(() -> 2L);
Lazy< Long> param2Lazy = Lazy.of(() -> 2L);
Lazy< Long> param3Lazy = Lazy.of(() -> 2L);
Lazy< Long> result = param1Lazy.flatMap(param1 ->
param2Lazy.flatMap(param2 ->
param3Lazy.map(param3 -> param1 + param2 + param3)
)
);

其中的规律就是,最后一次取值用 map,其他都用 flatmap。

3 题外话:函数式语言中的单子语法糖

看了上面的例子你一定会觉得惰性计算好麻烦,每次为了取里面的惰性值都要经历多次的 flatmap 与 map。这其实是 Java 没有原生支持函数式编程而做的妥协之举,Haskell 中就支持用 do 记法简化 Monad 的运算,上面三参数的例子如果用 Haskell 则写做:

1
2
3
4
5
6
7
8
kotlin复制代码do
param1 < - param1Lazy
param2 < - param2Lazy
param3 < - param3Lazy
-- 注释: do 记法中 return 的含义和 Java 完全不一样
-- 它表示将值打包进盒子里,
-- 等价的 Java 写法是 Lazy.of(() -> param1 + param2 + param3)
return param1 + param2 + param3

Java 中虽然没有语法糖,但是上帝关了一扇门,就会打开一扇窗。在 Java 中可以清晰地看出每一步在做什么,理解其中的原理,如果你读过了本文之前的内容,肯定能明白这个 do 记法就是不停地在做 flatmap 。

六 Lazy 的最终代码

目前为止,我们写的 Lazy 代码如下:

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
typescript复制代码public class Lazy< T> implements Supplier< T> {

private final Supplier< ? extends T> supplier;

private T value;

private Lazy(Supplier< ? extends T> supplier) {
this.supplier = supplier;
}

public static < T> Lazy< T> of(Supplier< ? extends T> supplier) {
return new Lazy< >(supplier);
}

public T get() {
if (value == null) {
T newValue = supplier.get();

if (newValue == null) {
throw new IllegalStateException("Lazy value can not be null!");
}

value = newValue;
}

return value;
}

public < S> Lazy< S> map(Function< ? super T, ? extends S> function) {
return Lazy.of(() -> function.apply(get()));
}

public < S> Lazy< S> flatMap(Function< ? super T, Lazy< ? extends S>> function) {
return Lazy.of(() -> function.apply(get()).get());
}
}

七 构造一个能够自动优化性能的实体

利用 Lazy 我们写一个构造通用 User 实体的工厂:

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
scss复制代码@Component
public class UserFactory {

// 部门服务, rpc 接口
@Resource
private DepartmentService departmentService;

// 主管服务, rpc 接口
@Resource
private SupervisorService supervisorService;

// 权限服务, rpc 接口
@Resource
private PermissionService permissionService;

public User buildUser(long uid) {
Lazy< String> departmentLazy = Lazy.of(() -> departmentService.getDepartment(uid));
// 通过部门获得主管
// department -> supervisor
Lazy< Long> supervisorLazy = departmentLazy.map(
department -> SupervisorService.getSupervisor(department)
);
// 通过部门和主管获得权限
// department, supervisor -> permission
Lazy< Set< String>> permissionsLazy = departmentLazy.flatMap(department ->
supervisorLazy.map(
supervisor -> permissionService.getPermissions(department, supervisor)
)
);

User user = new User();
user.setUid(uid);
user.setDepartment(departmentLazy);
user.setSupervisor(supervisorLazy);
user.setPermissions(permissionsLazy);
}
}

工厂类就是在构造一颗求值树,通过工厂类可以清晰地看出 User 各个属性间的求值依赖关系,同时 User 对象能够在运行时自动地优化性能,一旦某个节点被求值,路径上的所有属性的值都会被缓存。

八 异常处理

虽然我们通过惰性让 user.getDepartment() 仿佛是一次纯内存操作,但是他实际上还是一次远程调用,所以可能出现各种出乎意料的异常,比如超时等等。

异常处理肯定不能交给业务逻辑,这样会影响业务逻辑的纯粹性,让我们前功尽弃。比较理想的方式是交给惰性值的加载逻辑 Supplier。在 Supllier 的计算逻辑中就充分考虑各种异常情况,重试或者抛出异常。虽然抛出异常可能不是那么“函数式”,但是比较贴近 Java 的编程习惯,而且在关键的值获取不到时就应该通过异常阻断业务逻辑的运行。

九 总结

利用本文方法构造的实体,可以将业务建模上需要的属性全部放置进去,业务建模只需要考虑贴合业务,而不需要考虑底层的性能问题,真正实现业务层和物理层的解耦。

同时 UserFactory 本质上就是一个外部接口的适配层,一旦外部接口发生变化,只需要修改适配层即可,能够保护核心业务代码的稳定。

业务核心代码因为外部调用大大减少,代码更加接近纯粹的运算,因而易于书写单元测试,通过单元测试能够保证核心代码的稳定且不会出错。

十 题外话:Java 中缺失的柯里化与应用函子(Applicative)

仔细想想,刚刚做了这么多,目的就是一个,让签名为 C f(A,B) 的函数可以无需修改地应用到盒装类型 Box< A>和 Box< B> 上,并且产生一个 Box< C>,在函数式语言中有更加方便的方法,那就是应用函子。

应用函子概念上非常简单,就是将盒装的函数应用到盒装的值上,最后得到一个盒装的值,在 Lazy 中可以这么实现:

1
2
3
4
typescript复制代码    // 注意,这里的 function 是装在 lazy 里面的
public < S> Lazy< S> apply(Lazy< Function< ? super T, ? extends S>> function) {
return Lazy.of(() -> function.get().apply(get()));
}

不过在 Java 中实现这个并没有什么用,因为 Java 不支持柯里化。

柯里化允许我们将函数的几个参数固定下来变成一个新的函数,假如函数签名为 f(a,b),支持柯里化的语言允许直接 f(a) 进行调用,此时返回值是一个只接收 b 的函数。

在支持柯里化的情况下,只需要连续的几次应用函子,就可以将普通的函数应用在盒装类型上了,举个 Haskell 的例子如下(< *> 是 Haskell 中应用函子的语法糖, f 是个签名为 c f(a, b) 的函数,语法不完全正确,只是表达个意思):

1
2
rust复制代码-- 注释: 结果为 box c
box f < *> box a < *> box b

参考资料

  • 在 Java 函数式类库 VAVR 中提供了类似的 Lazy 实现,不过如果只是为了用这个一个类的话,引入整个库还是有些重,可以利用本文的思路直接自己实现
  • 函数式编程进阶:应用函子 前端角度的函数式编程文章,本文一定程度上参考了里面盒子的类比方法:juejin.cn/post/689182…
  • 《Haskell函数式编程基础》
  • 《Java函数式编程》

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

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

TCP通讯中发送文件

发表于 2021-11-08

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

正常情况下,谁会用TCP传送一个文件呢?

一开始写我就觉得这个功能特别鸡肋,TCP传送文件,没办法需要这样的功能,只要硬着头皮上了,如果不是有这个需求,我肯定会骂人,真不是多余嘛!

好了言归正传,直接说在TCP通讯中文件发送是如何应用的。

需要的功能:客户端 –>> 服务器 发送文件

开发环境:VS2017 + QT5.14.2

开发语言:C++

实现这个功能,我们会遇到哪些主要难解决的问题呢?

  • 1:文件过大怎么办?
  • 2:传输中断怎么解决?
  • 3:传输过程中,界面卡死怎么处理?
  • 4:文件发送过去,服务端如何处理呢?

以上是我在实现发送文件功能时遇到的问题,在后续文章也会对此一一解答。如果还有其他不懂的问题可以留言告诉我,一同探讨解决。

1:定义通讯协议

TCP通讯中,连接服务器与客户端的桥梁便是通讯协议。定义一个简单明了的通信协议对我们实现功能来说也是事半功倍。

说到了发送文件,那么我们可以假设发送文件的通讯命令字是A1。

首先,在发送文件内容之前,我们需要将文件的名称、大小发给服务器。

我们可以定义一个json结构发送

1
2
3
4
json复制代码{
"fileName":"测试图片.png",
"fileSize":100
}

上述结构体中,fileName存储了需要发送的文件名称,使用string类型表示;fileSize存储了需要发送文件的大小。

其次,后续直接发送带有命令字A1的实际文件内容。为了快速传送,假设一次最多可发送40960个真实内容。

以上,我们就将发送文件的通讯指令建立了。下面开始直接使用

2:给服务器发送文件信息

第一步,读取需要发送文件的大小。

读文件的方式有很多种,例如说:QFile、ifstream、或者是FILE等等。

不知道大家在写程序的时候是个什么规范,就我个人而言,对于一些通讯类来说,绝大多数会采用纯C++标准的方式,这样无论是使用MFC框架以及QT框架,甚至是WIN32程序都是可以兼容的。

1
2
3
ini复制代码FILE* readFile = fopen(spath.c_str(), "rb");
fseek(readFile, 0, SEEK_END);
int nFileSize = ftell(readFile);

第二步,将有效数据组成json结构体字符串数据

json数据序列化以及反序列化的功能,大家应该都不陌生,这里我也写出来了。

在使用json功能之前,记得要添加 #include “json.h”

写法很简单,不用我再过多说明了。

但是需要注意一点的是:通讯过程中必须要使用utf8格式的编号。大家可以用C++方法也可以用QT方法,QT的方法会更快捷一些。毕竟在这里我用的是VS+QT的开发环境。

第三步,给服务器发送信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ini复制代码std::string sJson = m_pDataParsing.AnalysisFileInforToJson("文件名称","文件大小");
int nSendJsonSize = sJson.length();
int nTotalSize = 27 + nSendJsonSize + 1;
char* chSend = new char[nTotalSize];
memset(chSend, 0, nTotalSize);
this->InitHeaderData(chSend);
chSend[21] = 0x00; //服务端编号
chSend[22] = 0x00;
chSend[23] = 0xA1; //命令字,占两个字节
chSend[24] = nSendJsonSize /256/256;
chSend[25] = nSendJsonSize /256;
chSend[26] = nSendJsonSize %256;
//将json数据字符串拷贝到char*数组中
memcpy(chSend+27, sJson.c_str(), nSendJsonSize );
chSend[nTotalSize-1] = 0xEF;
chSend[nTotalSize] = '\0';
//socket通讯发送这里不做说明,就是send发送而已
//销毁new出来的内容
delete[] chSend;
chSend = nullptr;

以上代码是将json数据转成char数据数据进行发送的过程。

InitHeaderData():这里存放的是20位预留字节,是在开放过程中防止后续有添加,根据个人需求设置。

24、25、26 三个字节存放文件的大小,为了防止传送较大的文件。

对于我的理解来说,难点在于如何将json字符串数据赋值到char数组中,其他的都是很简单的。

3:给服务器发送文件内容

这一步骤是比较难理解的,涉及到了页面显示效果。

当我们发送一个很小的文件时,消息很快就发送了,几乎不会造成页面卡顿。但是在发送一个大一些的文件时,就会出现这个问题。

第2阶段实现完成后,我们需要进行以下操作向服务器发送有效文件内容。

第一步,我们需要将文件指针恢复到头位置

rewind(readFile); //指针恢复到头位置

原因:是因为在读取整个文件大小时,文件指针的位置已经到了文件的末尾。发送实际内容时需要从开始发送。\

第二步:定义文件发送时需要的变量

在发送文件时,我们都需要知道哪些呢?

1
2
3
4
ini复制代码int nTotalSize = nFileSize;
char chFileBuffer[g_nMaxSendByte];
int nMoveSize = 0; //移动大小
bool bOK = true;

文件的总体大小nTotalSize、记录每次读取的文件内容chFileBuffer、文件指针移动的位置以及是否成功标识帮助我们跳出循环。

第1阶段已经说明了我们在每次读取时最多取40960个有效内容,这里的g_nMaxSendByte = 40960;

第三步:读取文件并发送

读取文件之前我们需要将chFileBuffer中的旧数据置空,好习惯一定要养成。

memset(chFileBuffer, 0, g_nMaxSendByte];

获取读取当前发送的实际大小。

这是一个三目运算符操作,主要是我太懒了,不想写if、else语句,哈哈。

意思就是说:

当文件的总大小 < 40960个长度时,读取的文件内容是文件的总大小,反之就是40960个长度

int nSendSize = nTotalSize <= g_nMaxSendByte ? nTotalSize : g_nMaxSendByte;

记录需要移动的文件指针大小

nMoveSize += nSendSize;

从文件中读取刚刚计算的内容

fread(chFileBuffer, sizeof(char), nSendSize, readFile);

将读取的文件内容(chFileBuffer)发送给服务端。

发送方法和发送json数据的格式差不多,最主要的差别在于,这里是发送的是char数据。那么我就把如何将char数组赋值到char*数据这部分说明下

memcpy(data + 27, senddata, nsendSize); //发送的json串

发送成功文件后,记得从总文件大小中减去已经发送的大小。当文件大小为负数时,说明文件已经发送完成

nTotalSize -= nSendSize; if (nTotalSize <= 0) { break; //跳出循环 }

将文件的指针进行偏移

fseek(readFile, nMoveSize, SEEK_SET); //将指针偏离到头文件 nSendSize个字节处

在正常情况下,while循环发送的主要内容就这些了。

为什么说是正常情况呢?万一文件传输过程中,与服务器断开连接了呢?这时,我们就需要判断socket是否有有效数据,如果socket == INVALID_SOCKET,也需要跳出循环

接下来展示整体的循环发送文件内容

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
ini复制代码while (true)
{
Sleep(100);
memset(chFileBuffer, 0, g_nMaxSendByte);
//获取当前发送的大小
int nSendSize = nTotalSize <= g_nMaxSendByte ? nTotalSize : g_nMaxSendByte;
nMoveSize += nSendSize;
fread(chFileBuffer, sizeof(char), nSendSize, readFile);
int nTotalSize = 27 + nSendSize + 1;
char* chSend = new char[nTotalSize];
/*
这里是组装发送数据与发送文件信息类似,不做详细说明
*/
if (m_pTcpPanel->m_ClientThreadInfo.serverSocket != INVALID_SOCKET)
{
int nRet = send(serverSocket, chSendData, nsocketSize, 0);
if (nRet == SOCKET_ERROR)
{
//发送失败,停止发送
//每当发送完一次数据时记得要销毁创建的资源
delete[] chSendData;
chSendData = nullptr;
bOK = false;
break;
}
}
else
{
bOK = false;
//每当发送完一次数据时记得要销毁创建的资源
delete[] chSendData;
chSendData = nullptr;
break; //socket异常,直接退出
}
//每当发送完一次数据时记得要销毁创建的资源
delete[] chSendData;
chSendData = nullptr;
nTotalSize -= nSendSize;
if (nTotalSize <= 0)
{
break; //跳出循环
}
fseek(readFile, nMoveSize, SEEK_SET); //将指针偏离到头文件 nSendSize个字节处
}
fclose(readFile); //结束发送后,需要关闭文件

4:界面卡死处理

当我们使用上述方式发送一个较大的文档时,肯定会出现界面卡死的现象。这也是C++在程序处理过程中最头疼的一部分了。

开线程,这是我第一次在写这个功能时第一时间想到的解决方法。

但是,我又想到假设我写的这个程序中有很多加载数据慢的地方,也不一定都是传送文件,可能是其他的功能。我还有线程满天飞吗?

仔细想想我都头疼。后来我采用的是”QtConcurrent::run“的方法,这属于QT中快速建立线程的方式,但是使用这种方式的时候,所调用的函数的返回值一定是True,否则就陷入了死循环中。

在发送文件时,一遍加载等待页面一遍发送文件内容,使用进程的方式处理,比开线程简单多了。

5:服务器接收客户端发送的文件信息

第一步,定义接收数据结构体

首先,我们需要记录文件信息,也就是文件名称、文件大小以及已经接收的文件大小。数据量较多,这里采用的结构体进行存储。

客户端与服务器一般都是多对一的关系,我们在服务端这边记录客户端数据时采用了std::vector m_vetInfo 成员变量。根据实际使用习惯可以定义其他容器比如:map、list

第二步,记录客户端发送的文件信息

首先,从众多的客户端中查询出当前发送消息的客户端

当我们查询到匹配客户端后,如果当前在结构体中记录的总体文件大小 ==0时,说明是第一次存储文件的基本情况,解析数据并进行存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码if(itFind->nTotalFileSize == 0)
{
//说明:该编号的客户端文件属于第一次存储
int nTotalFilesSize = 0;
std::string sFileName = "";
/*
解析客户端发送的内容并获取有效数据,文件名称以及大小
这里不做说明
*/
//将解析的内容进行存储
itFind->sFileName = sFileName;
itFind->nTotalFileSize = nTotalFilesSize;
itFind->nReceivedSize = 0; //未进行数据接收
//创建文件并打开
std::string sSavePath = "这是当前文件的路径,并是已经创建好的";
itFind->writeFile = fopen(sSavePath, "wb");
itFind->sSavePath = sSavePath;
}

第三步,记录实际文件信息

当结构体中的文件总体大小不为0时,说明正在接收实际的有效数据

获取接收真实的文件大小

int nJsonSize = (unsigned char)data[24] * 256 * 256 + (unsigned char)data[25] * 256 + (unsigned char)data[26];

将接收的内容存储到文件指针中存储

fwrite(data + 27, sizeof(char), nJsonSize, itFind->writeFile);

记录到文件指针后记录已经接收的实际长度

itFind->nReceivedSize += nJsonSize;

当服务端记录的文件大小 <= 接收的大小时,说明文件接收完成

1
2
3
4
5
6
7
8
9
ini复制代码if(itFind->nTotalFileSize <= itFind->nReceivedSize)
{
fclose(itFind->writeFile); //关闭当前文件
//文件关闭后清空结构体中的数据
itFind->nTotalFileSize = 0;
itFind->nReceivedSize = -1;
itFind->sSavePath = "";
itFind->sFileName = "";
}

这样就可以实现第一次接收的是文件名称以及大小,以后的接收的数据是实际的文件内容了。

以上便是服务端接收的实际操作。

今天的TCP传送文件操作已经讲解完成。

虽然功能很鸡肋,就当自己提升了一下通讯水平。

本文转载自: 掘金

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

Golang gin-vue-admin框架介绍

发表于 2021-11-08

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

先给大家看看我上个月的礼品呗 嘻嘻

gin-vue-admin基于gin+vue搭建的后台管理系统框架,集成jwt鉴权,权限管理,动态路由,分页封装,多点登录拦截,资源权限,上传下载,代码生成器,表单生成器,通用工作流等基础功能,五分钟一套CURD前后端代码,目前已支持VUE3,欢迎issue和pr~

gitee地址: gitee.com/pixelmax/gi…

github地址: github.com/flipped-aur…

插件仓库以及开发规范:github.com/flipped-aur…

审批流分支: github.com/flipped-aur…

先说下启动代码会遇到的问题

D:\nodejs\node_global\webpack.ps1,因为在此系统上禁止运行脚本…

今天在开发中使用到了 yarn,之前全局安装了yarn,并且在 cmd 中也可以正常使用,但是在VS Code中报出了错误。如下信息: 无

1
2
ini复制代码法加载文件 D:\nodejs\node_global\webpack.ps1,因为在此系统上禁止运行脚本,
有关详细信息,请参阅 https:/go.microsoft.com/fwlink/?LinkID=135170

解决方案:

首先以管理员的身份运行 VS Code .

在终端执行 get-ExecutionPolicy,打印显示出 Restricted,表示禁止状态.

接下来在终端执行 set-executionpolicy remotesigned -s currentuser

在此输入 get-ExecutionPolicy查看,显示 RemoteSigned.

之后再使用yarn install 时,就不会再出现如上所述的问题了,出现其他同样类似情况也是同样的解决方案。

1
2
3
4
5
6
7
8
bash复制代码# 进入web文件夹
cd web

# 安装依赖
cnpm install || npm install

# 启动web项目
npm run serve

好,进入正文啦::::

项目文档

在线文档 : www.gin-vue-admin.com

初始化

从环境到部署教学视频

开发教学 (贡献者: LLemonGreen And Fann)

重要提示

1.本项目从起步到开发到部署均有文档和详细视频教程

2.本项目需要您有一定的golang和vue基础

3.您完全可以通过我们的教程和文档完成一切操作,因此我们不再提供免费的技术服务,如需服务请进行付费支持

4.如果您将此项目用于商业用途,请遵守Apache2.0协议并保留作者技术支持声明。您需保留如下版权声明信息,其余信息功能不做任何限制。如需剔除请联系微信:shouzi_1994

  1. 基本介绍

1.1 项目介绍

Gin-vue-admin是一个基于 vue 和 gin 开发的全栈前后端分离的开发基础平台,集成jwt鉴权,动态路由,动态菜单,casbin鉴权,表单生成器,代码生成器等功能,提供多种示例文件,让您把更多时间专注在业务开发上。

在线预览: demo.gin-vue-admin.com

测试用户名:admin

测试密码:123456

1.2 贡献指南

Hi! 首先感谢你使用 gin-vue-admin。

Gin-vue-admin 是一套为快速研发准备的一整套前后端分离架构式的开源框架,旨在快速搭建中小型项目。

Gin-vue-admin 的成长离不开大家的支持,如果你愿意为 gin-vue-admin 贡献代码或提供建议,请阅读以下内容。

1.2.1 Issue 规范

  • issue 仅用于提交 Bug 或 Feature 以及设计相关的内容,其它内容可能会被直接关闭。如果你在使用时产生了疑问,请到 Slack 或 Gitter 里咨询。
  • 在提交 issue 之前,请搜索相关内容是否已被提出。

1.2.2 Pull Request 规范

  • 请先 fork 一份到自己的项目下,不要直接在仓库下建分支。
  • commit 信息要以[文件名]: 描述信息 的形式填写,例如 README.md: fix xxx bug。
  • 确保 PR 是提交到 develop 分支,而不是 master 分支。
  • 如果是修复 bug,请在 PR 中给出描述信息。
  • 合并代码需要两名维护人员参与:一人进行 review 后 approve,另一人再次 review,通过后即可合并。
  1. 使用说明

1
2
3
4
5
diff复制代码- node版本 > v12.18.3
- golang版本 >= v1.16
- IDE推荐:Goland
- 初始化项目: 不同版本数据库初始化不通 参见 https://www.gin-vue-admin.com/docs/first_master
- 替换掉项目中的七牛云公钥,私钥,仓名和默认url地址,以免发生测试文件数据错乱

2.1 server项目

使用 Goland 等编辑工具,打开server目录,不可以打开 gin-vue-admin 根目录

1
2
3
4
5
6
7
8
9
10
11
12
13
bash复制代码# 克隆项目
git clone https://github.com/flipped-aurora/gin-vue-admin.git
# 进入server文件夹
cd server

# 使用 go mod 并安装go依赖包
go generate

# 编译
go build -o server main.go (windows编译命令为go build -o server.exe main.go )

# 运行二进制
./server (windows运行命令为 server.exe)

2.2 web项目

1
2
3
4
5
6
7
8
bash复制代码# 进入web文件夹
cd web

# 安装依赖
cnpm install || npm install

# 启动web项目
npm run serve

2.3 swagger自动化API文档

2.3.1 安装 swagger

(1)可以访问外国网站
1
arduino复制代码go get -u github.com/swaggo/swag/cmd/swag
(2)无法访问外国网站

由于国内没法安装 go.org/x 包下面的东西,推荐使用 goproxy.cn 或者 goproxy.io

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码# 如果您使用的 Go 版本是 1.13 - 1.15 需要手动设置GO111MODULE=on, 开启方式如下命令, 如果你的 Go 版本 是 1.16 ~ 最新版 可以忽略以下步骤一
# 步骤一、启用 Go Modules 功能
go env -w GO111MODULE=on
# 步骤二、配置 GOPROXY 环境变量
go env -w GOPROXY=https://goproxy.cn,https://goproxy.io,direct

# 如果嫌弃麻烦,可以使用go generate 编译前自动执行代码, 不过这个不能使用 `Goland` 或者 `Vscode` 的 命令行终端
cd server
go generate -run "go env -w .*?"

# 使用如下命令下载swag
go get -u github.com/swaggo/swag/cmd/swag

2.3.2 生成API文档

1
2
bash复制代码cd server
swag init

执行上面的命令后,server目录下会出现docs文件夹里的 docs.go, swagger.json, swagger.yaml 三个文件更新,启动go服务之后, 在浏览器输入 http://localhost:8888/swagger/index.html 即可查看swagger文档

  1. 技术选型

  • 前端:用基于 Vue 的 Element 构建基础页面。
  • 后端:用 Gin 快速搭建基础restful风格API,Gin 是一个go语言编写的Web框架。
  • 数据库:采用MySql(5.6.44)版本,使用 gorm 实现对数据库的基本操作。
  • 缓存:使用Redis实现记录当前活跃用户的jwt令牌并实现多点登录限制。
  • API文档:使用Swagger构建自动化文档。
  • 配置文件:使用 fsnotify 和 viper 实现yaml格式的配置文件。
  • 日志:使用 zap 实现日志记录。
  1. 项目架构

4.1 系统架构图

系统架构图

4.2 前端详细设计图 (提供者:baobeisuper)

前端详细设计图

4.3 目录结构

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
scss复制代码    ├── server
├── api (api层)
│ └── v1 (v1版本接口)
├── config (配置包)
├── core (核心文件)
├── docs (swagger文档目录)
├── global (全局对象)
├── initialize (初始化)
│ └── internal (初始化内部函数)
├── middleware (中间件层)
├── model (模型层)
│ ├── request (入参结构体)
│ └── response (出参结构体)
├── packfile (静态文件打包)
├── resource (静态资源文件夹)
│ ├── excel (excel导入导出默认路径)
│ ├── page (表单生成器)
│ └── template (模板)
├── router (路由层)
├── service (service层)
├── source (source层)
└── utils (工具包)
├── timer (定时器接口封装)
└── upload (oss接口封装)

└─web (前端文件)
├─public (发布模板)
└─src (源码包)
├─api (向后台发送ajax的封装层)
├─assets (静态文件)
├─components(组件)
├─router (前端路由)
├─store (vuex 状态管理仓)
├─style (通用样式文件)
├─utils (前端工具库)
└─view (前端页面)
  1. 主要功能

  • 权限管理:基于jwt和casbin实现的权限管理。
  • 文件上传下载:实现基于七牛云, 阿里云, 腾讯云 的文件上传操作(请开发自己去各个平台的申请对应 token 或者对应key)。
  • 分页封装:前端使用 mixins 封装分页,分页方法调用 mixins 即可。
  • 用户管理:系统管理员分配用户角色和角色权限。
  • 角色管理:创建权限控制的主要对象,可以给角色分配不同api权限和菜单权限。
  • 菜单管理:实现用户动态菜单配置,实现不同角色不同菜单。
  • api管理:不同用户可调用的api接口的权限不同。
  • 配置管理:配置文件可前台修改(在线体验站点不开放此功能)。
  • 条件搜索:增加条件搜索示例。
  • restful示例:可以参考用户管理模块中的示例API。
    • 前端文件参考: web/src/view/superAdmin/api/api.vue
    • 后台文件参考: server/router/sys_api.go
  • 多点登录限制:需要在config.yaml中把system中的use-multipoint修改为true(需要自行配置Redis和Config中的Redis参数,测试阶段,有bug请及时反馈)。
  • 分片长传:提供文件分片上传和大文件分片上传功能示例。
  • 表单生成器:表单生成器借助 @form-generator 。
  • 代码生成器:后台基础逻辑以及简单curd的代码生成器。
  1. 知识库

6.1 团队博客

www.yuque.com/flipped-aur…

内有前端框架教学视频。如果觉得项目对您有所帮助可以添加我的个人微信:shouzi_1994,欢迎您提出宝贵的需求。

6.2 教学视频

(1)手把手教学视频

www.bilibili.com/video/BV1Rg…

(2)后端目录结构调整介绍以及使用方法

www.bilibili.com/video/BV1x4…

(3)golang基础教学视频

bilibili:space.bilibili.com/322210472/c…

(4)gin框架基础教学

bilibili:space.bilibili.com/322210472/c…

(5)gin-vue-admin 版本更新介绍视频

bilibili:space.bilibili.com/322210472/c…

本文转载自: 掘金

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

字符串可以这样加索引,你知吗?《死磕MySQL系列 七》

发表于 2021-11-08

系列文章

三、MySQL强人“锁”难《死磕MySQL系列 三》

四、S 锁与 X 锁的爱恨情仇《死磕MySQL系列 四》

五、如何选择普通索引和唯一索引《死磕MySQL系列 五》

六、五分钟,让你明白MySQL是怎么选择索引《死磕MySQL系列 六》

相信大多数小伙伴跟咔咔一样,给字符串添加索引从未设置过长度,今天就来聊聊如何正确的给字符串加索引。

一、如何建立索引

大多数系统都会存在用户表,并且系统初始设计使用了手机号码登录的。

这是产品提出了一个需求,让系统也可以支持邮箱登录。

肯定知道的是若不给邮箱字段添加索引执行查询是会全表扫描。

此时你心里窃喜这还不简单,给邮箱字段加个索引完事呗!但要做到复杂的需求做好,简单的需求要最好,减轻一切对系统的压力。

此时的你拿起键盘就执行了alter table table_name add index idx_field (field)

有部分小伙伴不喜欢命令行创建索引,喜欢使用phpmyadmin工具来操作MySQL,那么在建立索引时有没有发现后边可以设置大小呢?

通过上边给大家展示的图片知道字符串建立索引是可以定义长度的,那么两者有什么区别。

使用命令行alter table table_name add index idx_field (field)直接创建的索引默认是包含整个字符串。

若这样执行就指定了索引前缀长度alter table table_name add index idx_field (field(6))

一图解千愁,看一下建立的两个索引结构是什么样的。

索引一结构图

索引一结构图

索引一结构图

索引二结构图

索引二结构图

索引二结构图

从图中可以看到,指定了索引长度为6那么就只取邮箱字段的前6个字段,相对索引包含整个字符串来说每个节点存储的数据会更多。

索引那篇文章也给大家说了建立索引在合适的范围内越小越好。

万物皆两面,有坏就有好,第六期文章误选索引的因素之一就是扫描行数。

索引长度减少带来的影响就是索引基数变大,从而增加额外的扫描记录数(执行explain的row字段)。

此时要执行select id,name,email from mac_user where email='1397393964@qq.com';

给整个字符串添加索引执行流程

1、从email索引树找到满足1397393964@qq.com的记录,得到主键ID为1

2、根据ID为1到主键索引树找到这条记录并判断email是否正确,将这行记录假如结果集。

3、重复第一步,直到不满足查询条件,循环结束。

指定索引长度执行流程

1、从email索引树找到满足139739的记录,得到主键ID为1

2、根据ID为1到主键索引树找到这条记录并判断email不正确,丢弃这行记录。

3、在email索引树找刚刚查询的下一条记录,发现还是139739,去除ID2,再到ID的索引树进行判断,当值对后加入结果集。

4、再继续重复上一步,直到不满足查询条件,循环结束。

结论

在模拟执行流程过程中很容易就发现,使用前缀索引会导致读取数据的次数增加,那是不是就代表使用前缀索引会增加查询代价呢?

肯定不是的,试想此时定义的长度是6那么设置为7或者8呢!是不是会好很多,图中的案例为了方便设置了三个一样的数据,但实际情况基本不会出现这样的情况。

建立索引关注的是区分度,只有区分度越高,重复值就越少,查询效率就越高。

所以使用前缀索引,只要定义好长度,就可以坐到既节省空间,又不用额外增加太多的查询代价。

二、创建索引如何确定使用多长的前缀

MySQL中关键词distinct可以返回本列不同的结果集。

例如查询email列有多少个不同的值select count(distinct email) as num from mac_user。

如何计算列不同前缀有多少行

结合MySQL自带的函数left来实现,例如select count(distinct left (email,4)) as num4 from mac_user,截取email的前四个字符串计算有多少行。

再用这个值去除总数得到的就是比例,根据业务情况来判断多少比例可以。

三、使用前缀索引的影响

使用前缀索引会增加扫描行数,同时也会使覆盖索引失效。

为什么会影响覆盖索引?

若执行语句为select id,email from mac_user where email = '1397393964@qq.com'。

使用整个字符串索引结构查询可以使用覆盖索引,从email索引获取到结果就直接返回了,不用再进行回表。

若使用前缀索引在email索引获取到结果后还需要回到id索引在查一下判断查询的email的值是否正确。

哪怕是设置了大于了email的长度也会回表再进行判断,因为MySQL并不知道定义的前缀是否截取了完整信息。

结论

使用前缀索引会增加扫描行数,同样也使用不到覆盖索引。这个因素是你选择是否使用前缀索引要考虑的一个因素。

如果你不知道使用前缀索引还是全字符串索引,本地进行测试选一个合适的方案上到生产环境即可。

四、如何把不可以变为可以使用

假设身份认证系统存储的是身份证号,应该都知道身份证号前6位是地址码,同县的身份证号前6位一般是一样的。

这样使用前缀索引的话区分度会十分低,不但没有起到加速查询的作用,反而会造成索引区分度不大影响查询性能。

若把索引长度越长则每个节点存放的索引值就越少,查询效率也会变的低效。

如果解决这种场景

第一种方案

存储数据时将数据倒叙存储,查询时在正序处理一下即可

第二种方案

在表中新增一个字段,存储数据的hash值,给hash添加前缀索引。

区别

使用这两种方案共同点都不支持范围查询,都只能等值查询。

从占用空间来看:倒叙方式不会增加额外的存储空间,hash会增加一个字段。两者在空间不相上下

从CPU消耗来看:倒叙需要使用函数reverse,hash需要使用crc32 ,reverse消耗会小

从查询效率来看:hash查询更稳定,crc32计算的值虽有冲突但概率非常小,基本每次查询的平均扫描行数接近1。而倒叙使用的前缀索引方式,还会增加扫描行数。

五、总结

直接给字符串创建占用空间。

创建前缀索引,节省空间,会增加扫描行数,无法使用覆盖索引。

倒叙存储,创建前缀索引解决区分度不大的问题。

使用hash方式,查询稳定,不支持范围查询。

“

坚持学习、坚持写作、坚持分享是咔咔从业以来所秉持的信念。愿文章在偌大的互联网上能给你带来一点帮助,我是咔咔,下期见。

”

本文转载自: 掘金

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

浅谈MySQL中客户端与服务端连接方式 一、前言 二、MyS

发表于 2021-11-08

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


  • 💬 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)、有任何问题欢迎私信,看到会及时回复!

一、前言

  • 大家好,我是小诚,前面五篇《从0到1-全面深刻理解MySQL》文章给大家介绍了如何安装数据库到一条SQL在服务端需要经历那些步骤才能够解析完成,相信大家对数据库也有了初步的了解,但俗话说的好”纸上谈兵不如躬行实践”,前面学习的知识都是理论的知识,多少有些枯燥,只有亲自实践才知识是否掌握了知识,所以本篇就给大家介绍客户端和服务端的连接方式,实战完成客户端与服务端的连接,从客户端发送指令的操作并获取服务端响应
  • 安利一下,最近时常会收到一些小伙伴反馈的问题,为了方便交流,所以创建了一个水友交流群,无论你工作、面试有困难想找人请教,还是生活有感悟想要找人倾诉,都可以在交流群和水友交流,群里没有大神和小白区分,只有分享者和倾听者,而且群里会时常发送一些福利,欢迎大家来一起交流,吹水,摸鱼,进步(有兴趣的私我或者留言)
  • 《从0到1-全面深刻理解MySQL系列》系列文章会持续更新,感兴趣的小伙伴可以关注我,,一起加油,一起进步!,如有帮助,不要忘记一键三联哦,ღ( ´・ᴗ・` )比心!

二、MySQL客户端与服务端的“爱恨情仇”

🍅 2.1、MySQL的C/S(客户端/服务器)架构

  说起MySQL架构,即避免不了谈到舔狗和女神之间的关系了,可以将MySQL理解为是由客户端(舔狗)和服务端(女神)两部分组成,舔狗(客户端)可以是多种体现方式,当女神(服务端)”大姨妈”来的时候舔狗(客户端)可以是红糖水(如手机APP),当女神(服务端)生气时舔狗(客户端)可以是名牌包包(如电脑软件/网页)。

  平常我们执行一条SQL的流程,就跟舔狗(客户端)想去找女神(服务端)约会一样,大概要经历以下的流程:

  1、舔狗(客户端)包装好自己的约会的信息并附带上自己的个人介绍然后就发送给女神(服务端)如:女神,我是xx,今晚7点小树林见一起谈心,约不?

  2、女神(服务端)收到舔狗(客户端)的约会请求后,仔细阅读了舔狗(客户端)发来的信息,发现竟然舔狗(客户端)把地点约到小树林,肯定是有什么企图,二话不说直接拒绝了舔狗的约会(就像客户端发送请求时填错了账号或者密码,服务器拒绝连接)。

  3、舔狗(客户端)的约会被女神拒绝后,整天郁郁寡欢,同宿舍的基友看到了给它出了一个建议,让它将约会地点改成某五星级酒店,这样女神一定会答应它的约会,舔狗听后大喜,但是它没有那么多钱,思来想去,最后它走进的男科医院卖了一颗肾换到了5千块钱,舔狗一瘸一拐的从男科医院走出,少了一颗肾的它明显感觉身体不如之前,但一想到晚上就能够和女神一起约会,舔狗脸上绽开了菊花般的微笑。

  4、舔狗(客户端)重新包装了自己的约会信息,然后再次向女神(服务端)发起约会请求,女神看到舔狗的约会地点竟然是豪华的五星级酒店,二话不说就同意了约会(就像客户端发送请求时填写了正确的账号或者密码,服务器接受连接)

  5、舔狗看到女神接受了它的约会请求(建立了连接),非常开心,不断的发送消息给女神(执行SQL),女神服务器也时不时的给舔狗进行回复….

🍑2.2、MySQL客户端和服务端进程

  在现代计算机中,一个计算机可以运行多个程序,每个运行着的程序在计算机中我们称之为一个进程。MySQL的客户端/服务端架构本质上就是两个进程。

  每个进程在一台电脑上都有一个唯一的表示叫做进程ID,英文名称叫PID,这个进程ID是由电脑进行随机分配的,如果进程销毁了,这个ID也会被收回,下次再运行的时候,系统又会重新分配一个PID给进程,确保同一台机器上,某个时间内这个PID是唯一的。

  进程的名称是由程序的设计来定的,MySQL服务端的进程名默认是:mysqld,MySQL客户端的进程名默认是:mysql。

  通俗的理解,女神(服务端)为了显得自己比较有International,给自己起了个英文名叫:mysqld,舔狗(客户端)看到女神有这么好听的名字,为了引入女神的注意,也给自己起了给英文名叫:mysql。

  进程间通信(IPC,InterProcess Communication): 是指在不同进程之间传播或交换信息。

  IPC的方式通常有管道(包括匿名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。

🍓2.3、泡妞大法一: TCP/IP

  自从上次舔狗(客户端)成功约到女神(客户端)后,舔狗(客户端)别提多开心了,恨不得时刻和女神粘在一起,但是,舔狗(客户端)也知道,如果每次都直接和女神说要约她,次数多了女神肯定会厌倦,而且自己只有两颗肾,不可能每次想约女神的时候都去卖肾,这样女神没到手自己人没了,于是它想出了一个方法,每次想要约女神的时候,不直接就表明自己的目的,而是先用小号去试探女神,等到时机成熟了再亮明自己的身份,大概步骤如下:

  👶舔狗(客户端): 切换到小号1,给女神发送消息:”在吗?”,来试探女神(服务端)是否有空,如果女神回复了,说明女神有空,则再进行第二步,如果没有,则重复发送。

  👶舔狗(客户端): “在的,怎么了?”,舔狗的刚发出消息不久便收到女神(服务端)的回复,看到消息后,舔狗很开心,它知道计划的第一步已经成功,于是它开始了第二步,又给女神发送一则消息: “听说,今晚《爱的供养》电影首映,刚舍友抢到两张票(无中生友..),但是临时有事去不了就把票给我了,我一个人也用不了两张票,剩下的一张票不知道干嘛了”,舔狗知道女神最喜欢杨幂了,这不电影上映她一定会去看的,女神看到消息后一定会马上给他回复的。

  👩女神(服务端): 果不其然,女神看到这个消息马上就给舔狗(客户端)回复: “其实我超喜欢杨幂的,不知道剩下的一张票能不能给我呢?”,女神发出后就在静静等待舔狗(客户端)的答复。

  👶舔狗(客户端): 舔狗看到女神的回复后,脸上绽开了菊花般的微笑,但是他清楚,不能表现的太积极,于是故作镇定隔了几分钟后给女神回复: “好呀,那今晚校门口见咯。”

  👶舔狗(客户端): 终于舔狗通过自己的方式成功约到了女神,看完电影舔狗心情愉悦,高兴的买了一袋”橘子”给舍友。

  舔狗(客户端)又成功的约到了(连接)女神(服务端),舍友都感到非常惊讶,纷纷向他请教,于是他将自己约女神的方法写成了一本书分享给宿舍的单身狗看,书名叫做:《泡妞大法一之-三次”试探”》。据说,卡恩与瑟夫后来的提出的TCP、IP协议和TCP协议中建立连接的”三次挥手”机制就是受到这本书籍的影响。

  下面来通过图片来看看舔狗是如何约到女神的吧:

💅小结

  TCP/IP方式连接MySQL服务端程序格式: mysql -h 主机地址 -P 端口号(MySQL默认是3306) -u 用户名 -p 回车(然后输入密码)。

  在真实的业务场景中,我们的应用程序和实际的数据库一般是部署在不同的服务器中的,它们之间要创建连接,就必须通过网络进行。在公网的网络环境下,每台计算机都有唯一的IP地址,MySQL通过TCP/IP网络体系结构采用:IP地址 + 端口号的方式实现和网络中其他的主机进程进行通信。

  小贴士: TCP/IP网络体系结构是现在互联网中最常用的一种体系结构,它是一个协议簇,TCP/IP协议包括TCP、IP、UDP、ICMP等协议组成,而不是单纯的指TCP协议和IP协议,因为TCP和IP两种协议在这个体系结构中占据着非常重要的作用,故以此命名。

🍉2.4、泡妞方法二: 共享内存

   舔狗(客户端)发现使用TCP/IP方式和女神(服务端)发起在前几次发起约会请求都是屡试不爽,但是,渐渐地,女神又开始不搭理他了,他知道使用同一种方式久了女神已经厌倦了,于是他和女神的闺蜜多方打听,最终发现女神有一个特殊的习惯,就是每天早上4点钟会去晨跑,晨跑完后她会去一家早餐店吃早餐,这个习惯是雷打不动,女神坚持了两年。

  舔狗知道自己的机会又来了,于是他每天3点半就起床,然后去到操场等待女神来晨跑,装作”偶遇”的样子,这样就有和女神单独相处的机会了,果不其然,女神看到舔狗后觉的非常惊讶竟然有人和自己有相同的习惯,于是非常有兴致的和舔狗聊了起来,晨跑完后还跟舔狗一起吃了早餐。

  甜蜜的时间总是短暂的,跟女神吃完早餐后就回到了宿舍,因为舔狗平常很少锻炼的原因,3点半起床和跑步是个非常大的挑战,他感觉身体都快要散架了,但躺在床上回忆起了今天和女神相处的点滴,他觉的是值得的,他拿起笔记本,总结了今天的经验,笔记名叫:《泡妞大法二之-创建两人共享空间》

  下面来通过步骤和图片来看看舔狗是如何通过”共享内存”约到女神的吧:

  🐤1、MySQL服务器程序启动时指定–shared-memory参数支持共享内存方式。

  🐹2、客户端连接时指定–protocol=memory参数来显式的指定使⽤共享内存进⾏通信。

💅小结

  共享内存实际上就是允许多个进程共享一块存储空间,任何一个进程对这个空间进行了修改,其他的进程都察觉到,使用这种方式的通信,避免了数据在程序之间的复制流程,数据直接写入到内存,减少了拷贝的次数,提高了通信的速度,所以共享内存也被称作IPC中最快的一种通信方式。

  共享内存的生命周期是和内核一样的,即使所有访问共享内存的进程都完成了,共享内存的区域对象仍然还是存在的(除非进行显示删除),在内核进行重新加载引导之前,对该共享内存区域对象的任何改写操作都将一直保留。

  但是,共享内存中没有任何同步或者互斥的机制,多个进程可以同时对共享内存进行修改操作这样就可能导致数据安全问题,所以为了保证共享内存区域的数据安全,可以使用信号量的方式来实现对共享内存的同步访问机制。

🍊2.5、泡妞方法三: 命名管道

  正所谓”人间不如意事十之八九”,舔狗刚想出使用”共享内存”的方式和女神建立专有的私有空间,但没想到女神没过几天就生病了,没办法继续晨跑,舔狗非常想见女神了解具体的情况,但是联系女神有不回消息,又没法到女神宿舍去,思来想去,最后他想到了一个好的方法,那就是重金收买女神的闺蜜,让闺蜜帮她和女神建立专属的连接。

  心动不如心动,舔狗立马去医院将自己剩下的一个肾也卖了,得到了5千块,他将3千块给了女神的闺蜜,请求她帮忙让自己和女神建立”交流通道”,每天都将自己的小纸条带给女神,就这样,舔狗日复一日的让女神闺蜜传递着小纸条给女神,等到了第二个月,终于收到了女神的回复,内容如下: “谢谢你在我生病的这段时间一直关心我,你真是个好人。“

  虽然字数不多,但舔狗依然很开心,自己的付出终于收到了回报,于是他又拿起了笔记本,记录了自己这段时间和女神的”约会心得”,笔名为:《泡妞大法三之-专属通道》

  下面来通过步骤和图片来看看舔狗是如何通过”命名管道”约到女神的吧:

  🐤1、MySQL服务器程序启动时指定–enable-named-pipe参数支持命名管道方式。

  🐹2、客户端连接时指定–protocol=pipe参数来显式的指定使⽤共享内存进⾏通信。

💅小结

  命名管道也被称为FIFO文件,它是一种特殊类型的文件,在创建的时候给它指定一个名字,任何进程都可以通过该名字打开管道的另一端,根据给定的权限和服务器进程通信。FIFO(first input first output)是遵循先进先出的原则,即第一个进来的数据会第一个被读走。

三、系列文章推荐

  《从0到1-全面深刻理解MySQL系列》

写在最后

  在最后,我们还不知道故事中舔狗的结局会如何,但是我们能够在过程中看到舔狗为了约到女神做出的努力。这不是正如现实中挣扎在生活和工作中的我们吗?虽然艰难,但依然在坚持,致敬在生活中每个平凡的我们!

   坚持用故事讲知识,如果觉得文章对你有帮助,不要忘记扫码关注和一键三连哦,你的支持是我创作更加优质文章的动力。

本文转载自: 掘金

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

vivo 全球商城:商品系统架构设计与实践 一、前言 二、商

发表于 2021-11-08

一、前言

随着用户量级的快速增长,vivo官方商城v1.0的单体架构逐渐暴露出弊端:模块愈发臃肿、开发效率低下、性能出现瓶颈、系统维护困难。

从2017年开始启动的v2.0架构升级,基于业务模块进行垂直的系统物理拆分,拆分出来业务线各司其职,提供服务化的能力,共同支撑主站业务。

商品模块是整个链路的核心,模块的增多严重影响系统的性能,服务化改造势在必行。

本文将介绍vivo商城商品系统建设的过程中遇到的问题和解决方案,分享架构设计经验。

二、商品系统演进

将商品模块从商城拆分出来,独立为商品系统,逐渐向底层发展,为商城,搜索,会员、营销等提供基础标准化服务。

商品系统架构图如下:

前期商品系统比较杂乱,包含业务模块比较多,如商品活动业务、秒杀业务,库存管理,随着业务的不断发展,商品系统承载更多的业务不利于系统扩展和维护。

故思考逐渐将商品业务逐渐下沉并作为最底层、最基础的业务系统,并为众多调用方提供高性能的服务,下面介绍商品系统的升级历史。

2.1 商品活动、赠品剥离

随着商品活动的不断增多,玩法多样,同时与活动相关的额外属性也相应增加,这些都并不是与商品信息强关联,更偏向于用户营销,不应该与核心商品业务耦合在一起,故将其合并入商城促销系统。

赠品不仅仅是手机、配件,有可能会是积分、会员等,这些放在商品系统都不合适,也不属于商品模块的内容,故同步将其合并入商城促销系统。

2.2 秒杀独立

众所周知,秒杀活动的特点是:

  • 限时:时间范围很短,超过设置的时间就结束了
  • 限量:商品数量很少,远低于实际库存
  • 访问量大:价格低,可以吸引非常多的用户

基于以上特性,做好一个秒杀活动不是一蹴而就,由于系统资源共享,当突发的大流量冲击会造成商品系统其他业务拒绝服务,会对核心的交易链路造成阻塞的风险,故将其独立为单独的秒杀系统,单独对外提供服务。

2.3 代销系统成立

我们商城的主要销售品类还是手机以及手机配件等,商品的品类比较少,为了解决非手机商品品类不丰富的问题,运营考虑与知名电商进行合作,期望引入更多的商品品类。

为了方便后续扩展,以及对原有系统的不侵入性,我们经过考虑专门独立出一个子系统,用于承接代销业务,最后期望做成一个完备平台,后续通过提供开放API的方式让其他电商主动接入我们业务。

2.4 库存剥离

库存管理的痛点:

  • 由于我们的库存都是到商品维度,仅仅一个字段标识数量,每次编辑商品都需要为商品调整库存,无法动态实现库存管理;
  • 同时营销系统也有自己活动库存管理机制,入口分散,关联性较弱;
  • 可售库存和活动库存管理的依据都是实际库存,造成容易配置错误。

基于以上痛点,同时为了更方便运营管理库存,也为未来使用实际库存进行销售打下基础,我们成立库存中心,并提供以下主要功能:

  • 与ecms实际库存进行实时同步;
  • 可以根据实际库存的仓库分布情况,计算商品的预计发货仓库和发货时间,从而计算商品预计送达时间;
  • 完成低库存预警,可以根据可用库存、平均月销等进行计算,动态提醒运营订货。

三、挑战

作为最底层的系统,最主要的挑战就是具备稳定性,高性能,数据一致性的能力。

3.1 稳定性

  • 避免单机瓶颈:根据压测选择合适的节点数量,不浪费,同时也能保证沟通,可以应对突发流量。
  • 业务限流降级:对核心接口进行限流,优先保证系统可用,当流量对系统压力过大时将非核心业务进行降级,优先保证核心业务。
  • 设置合理的超时时间:对Redis、数据库的访问设置合理超时时间,不宜过长,避免流量较大时导致应用线程被占满。
  • 监控&告警:日志规范化,同时接入公司的日志监控和告警平台,做到主动发现问题并及时。
  • 熔断:外部接口接入熔断,防止因为外部接口异常导致本系统受到影响。

3.2 高性能

多级缓存

为了提升查询速度,降低数据库的压力,我们采用多级缓存的方式,接口接入热点缓存组件,动态探测热点数据,如果是热点则直接从本地获取,如果不是热点则直接从redis获取。

读写分离

数据库采用读写分离架构,主库进行更新操作,从库负责查询操作。

接口限流

接入限流组件, 直接操作数据库的接口会进行限流,防止因为突发流量、或者不规范调用导致数据库压力增加,影响其他接口。

不过早期也踩过一些坑:

1、商品列表查询造成redis key过多,导致redis内存不够的风险

由于是列表查询,进行缓存的时候是对入参进行hash,获取唯一的key,由于入参商品较多,某些场景下入参是随时变化的,根据排列组合,会造成基本每次请求都会回源,再缓存,可能造成数据库拒绝服务或者redis内存溢出。

方案一:循环入参列表,每次从redis获取数据,然后返回;

这个方案解决了key过多导致内存溢出的问题,但是很明显,它增加了很多的网络交互,如果有几十个key,可想而知,对性能会有不小的影响,那有什么其他办法能减少网络交互呢,下面我们看方案二。

方案二:我们通过对原有的Redis 组件进行增强,由于Redis集群模式不支持mget,故我们采用pipeline的方式实现,先根据key计算出其所在的slot,然后聚合一次性提交,这样每个商品数据只需缓存一次即可,同时采用mget也大大提升了查询速度。

这就即解决了key值过多的问题,也解决了方案一中多次网络交互的问题,经过压测对比,方案二比方案一性能提升50%以上,key越多,效果越明显。

2、热点数据,导致redis单机瓶颈

商城经常有新品发布会,发布会结束后会直接跳转到新品商详页,此时新品商详页就会出现流量特别大且突发、数据单一,这就导致Redis节点负载不平衡,有些10%不到,有些达到90%多,而一些常规的扩容是没有效果的。

针对热点问题我们有以下解决方案:

  • key的散列,将key分散到不同的节点
  • 采用本地缓存的方式

开始我们采用的是基于开源的Caffeine完成本地缓存组件,本地自动计算请求量,当达到一定的阀值就缓存数据,根据不同的业务场景缓存不同的时间,一般不超过15秒,主要解决热点数据的问题。

后来替换成我们自己研发的热点缓存组件,支持热点动态探测,热点上报,集群广播等功能。

3.3 数据一致性

1、对于Redis的数据一致性比较好解决,采用“Cache Aside Pattern”:

对于读请求采用先读缓存,命中直接返回,未命中读数据库再缓存。对于写请求采用先操作数据库,再删除缓存。

2、由于库存剥离出去,维护入口还是在商品系统,这就导致存在跨库操作,平常的单库事务无法解决。

开始我们采用异常捕获,本地事务回滚的方式,操作麻烦点,但也能解决这个问题。

后来我们通过开源的seata完成分布式事务组件,通过改写代码引入公司的基础组件,目前已经接入使用。

四、总结

本篇主要介绍商城商品系统如何进行拆分、并慢慢下沉为最基础的系统,使其职责更加单一,能够提供高性能的商品服务,并分享在此过程中遇到的技术问题和解决方案,后续会有库存系统的演进历史、分布式事务相关内容,敬请期待。

作者:vivo官网商城开发团队-Ju Changjiang

本文转载自: 掘金

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

Spring Boot(五):Spring Boot Dep

发表于 2021-11-08

Spring Boot 项目开发完毕后,支持两种方式部署到服务器:

  • jar 包(官方推荐)
  • war 包

JAR 包使用内置的 Tomcat,而 WAR 包则是将项目打包到外部的 Tomcat 上运行。

先来看一下演示项目的目录结构:

首先在 pom.xml 文件中引入 web 依赖,其中有内置的默认的 Tomcat 服务器;并在 application.properties 中配置端口号:

pom.xml

1
2
3
4
5
6
xml复制代码<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

application.properties

1
2
properties复制代码# 应用服务 WEB 访问端口
server.port=8080

然后编写一个可以处理请求的 Controller(用于打包后验证):

1
2
3
4
5
6
7
8
9
10
java复制代码@RestController
@RequestMapping("/hello")
public class HelloController {

@RequestMapping("/jar")
public String jarDeploy() {
return "成功将SpringBoot项目打成JAR包!";
}

}

接着点击 package 对项目进行打包即可:

最后执行 java -jar .\deploy-0.0.1-SNAPSHOT.jar 启动这个项目,便可在指定端口号上进行访问:

至此,关于 jar 包部署到这里就结束了;至于 war 包(次要)这里就不再赘述了。

希望本文对你有所帮助🧠

欢迎在评论区留下你的看法🌊,我们一起讨论与分享🔥

本文转载自: 掘金

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

深入浅出TCP三次握手 (多图详解)

发表于 2021-11-08

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

前言

TCP三次握手和四次挥手是面试题的热门考点,它们分别对应TCP的连接和释放过程,今天我们先来认识一下TCP三次握手过程,以及是否可以使用“两报文握手”建立连接?。

1、TCP是什么?

TCP是面向连接的协议,它基于运输连接来传送TCP报文段,TCP运输连接的建立和释放,是每一次面向连接的通信中必不可少的过程。

TCP运输连接有以下三个阶段:

  • 建立TCP连接,也就是通过三报文握手来建立TCP连接。
  • 数据传送,也就是基于已建立的TCP连接进行可靠的数据传输。
  • 释放连接,也就是在数据传输结束后,还要通过四报文挥手来释放TCP连接。

TCP的运输连接管理就是使运输连接的建立和释放都能正常的进行。


2、TCP首部格式


在这里插入图片描述

源端口: 占16比特,写入源端口号,用来 标识发送该TCP报文段的应用进程。
目的端口: 占16比特,写入目的端口号,用来标识接收该TCP报文段的应用进程。

序号: 占32比特,取值范围[0,2^32-1],序号增加到最后一个后,下一个序号就又回到0。指出本TCP报文段数据载荷的第一个字节的序号。

确认号: 占32比特,取值范围[0,2^32-1],确认号增加到最后一个后,下一个确认号就又回到0。指出期望收到对方下一个TCP报文段的数据载荷的第一个字节的序号,同时也是对之前收到的所有数据的确认。若确认号=n,则表明到序号n-1为止的所有数据都已正确接收,期望接收序号为n的数据。

确认标志位ACK: 取值为1时确认号字段才有效;取值为0时确认号字段无效。TCP规定,在连接建立后所有传送的TCP报文段都必须把ACK置1。

数据偏移: 占4比特,并以4字节为单位。用来指出TCP报文段的数据载荷部分的起始处距离TCP报文段的起始处有多远。这个字段实际上是指出了TCP报文段的首部长度。

窗口: 占16比特,以字节为单位。指出发送本报文段的一方的接收窗。

同步标志位SYN: 在TCP连接建立时用来同步序号。终止标志位FIN: 用来释放TCP连接。复位标志位RST: 用来复位TCP连接。

推送标志位PSH: 接收方的TCP收到该标志位为1的报文段会尽快上交应用进程,而不必等到接收缓存都填满后再向上交付。

校验和: 占16比特,检查范围包括TCP报文段的首部和数据载荷两部分。在计算校验和时,要在TCP报文段的前面加上12字节的伪首部。

紧急指针: 占16比特,以字节为单位,用来指明紧急数据的长度。

填充: 由于选项的长度可变,因此使用填充来 确保报文段首部能被4整除,(因为数据偏移字段,也就是首部长度字段,是以4字节为单位的)。

3、TCP的连接建立

TCP 建立连接的过程叫做握手,握手需要在客户和服务器之间交换三个TCP 报文段,称之为三报文握手,采用三报文握手主要是为了防止已失效的连接请求报文段突然又传送到了,因而产生错误。

TCP的连接建立要解决以下三个问题:

  • 1、使TCP双方能够确知对方的存在 。
  • 2、使TCP双方能够协商一些参数( 最大窗口值是否使用窗口扩大选项和时间戳选项,以及服务质量等)。
  • 3、使TCP双方能够对运输实体资源(例如缓存大小连接表中的项目等)进行分配。

4、三次握手图文详解

这是两台要基于TCP进行通信的主机:

  • 主动发起TCP连接建立称为TCP客户(client)。
  • 被动等待TCP连接建立的应用进程称为TCP服务器(server)。

我们可以将TCP建立连接的过程比喻为”握手“,“握手”需要在TCP客户端和服务器之间交换三个TCP报文段。

最初两端的TCP进程都处于关闭状态。


一开始,TCP服务器进程首先创建传输控制块,用来存储TCP连接中的一些重要信息。 例如TCP连接表、指向发送和接收缓存的指针、指向重传队列的指针,当前的发送和接收序号等。之后就准备接受TCP客户进程的连接请求, 此时TCP服务器进程就要进入监听状态等待TCP客户进程的连接请求。


TCP客户进程也是首先创建传输控制块,然后再打算建立。 TCP服务器进程是被动等待来自TCP客户端进程的连接请求,因此称为被动打开连接。


TCP连接时向TCP服务器进程发送TCP连接请求报文段,并进入同步已发送状态。

  • TCP 连接请求报文段首部中的同步位SYN被设置为1,,表明这是一个tcp连接请求报文段。
  • 序号字段seq被设置了一个初始值x作为TCP客户进程所选择的初始序号。

由于TCP连接建立是由TCP客户进程主动发起的,因此称为主动打开连接。 请注意TCP规定SYN被设置为1的报文段不能携带数据但要消耗掉一个序号。


TCP服务器进程收到TCP连接请求报文段后,如果同意建立连接,则向TCP客户进程发送TCP连接请求确认报文段,并进入同步已接收状态。

  • 该报文段首部中的同步位SYN和确认位ACK 都设置为1,表明这是一个TCP连接请求。
  • 序号字段seq被设置了一个初始值y,作为TCP服务器进程所选择的初始序号。
  • 确认号字段ack的值被设置成了x+1,这是对TCP客户进程所选择的初始序号seq的确认。

请注意这个报文段也不能携带数据,因为它是SYN被设置为一的报文段但同样要消耗掉一个序号。


TCP客户进程收到TCP连接请求确认报文段后,还要向TCP服务器进程发送一个普通的TCP 确认报文段并进入连接已建立状态。

  • 该报文段首部中的确认位ACK被设置为1,表明这是一个普通的TCP确认报文段 。
  • 序号字段seq 被设置为x+1,这是因为TCP客户进程发送的第一个TCP报文段的序号为x,并且不携带数据,因此第二个报文段的序号为x +1。
  • 确认号字段ack被设置为y + 1,这是对TCP服务器进程所选择的初始序号的确认。

请注意TCP规定,普通的TCP确认报文段可以携带数据。但如果不携带数据则不消耗序号,在这种情况下所发送的下一个数据报文段的序号仍是x + 1。


TCP服务器进程收到该确认报文段后也进入连接已建立状态,现在TCP双方都进入了连接已建立状态,他们可以基于已建立好的TCP连接进行可靠的数据传输了。


5、三次握手文字总结


三次握手是 TCP 连接的建立过程。在握手之前,主动打开连接的客户端结束 CLOSE 阶段,被动打开的服务器也结束 CLOSE 阶段,并进入 LISTEN 阶段。随后进入三次握手阶段:

① 首先客户端向服务器发送一个 SYN 包,并等待服务器确认,其中:

  • 标志位为 SYN,表示请求建立连接;
  • 序号为 Seq = x(x 一般取随机数);
  • 随后客户端进入 SYN-SENT 阶段。

② 服务器接收到客户端发来的 SYN 包后,对该包进行确认后结束 LISTEN 阶段,并返回一段 TCP 报文,其中:

  • 标志位为 SYN 和 ACK,表示确认客户端的报文 Seq 序号有效,服务器能正常接收客户端发送的数据,并同意创建新连接;
  • 序号为 Seq = y;
  • 确认号为 Ack = x + 1,表示收到客户端的序号 Seq 并将其值加 1 作为自己确认号 Ack 的值,随后服务器端进入 SYN-RECV 阶段。

③ 客户端接收到发送的 SYN + ACK 包后,明确了从客户端到服务器的数据传输是正常的,从而结束 SYN-SENT 阶段。并返回最后一段报文。其中:

  • 标志位为 ACK,表示确认收到服务器端同意连接的信号;
  • 序号为 Seq = x + 1,表示收到服务器端的确认号 Ack,并将其值作为自己的序号值;
  • 确认号为 Ack= y + 1,表示收到服务器端序号 seq,并将其值加 1 作为自己的确认号 Ack 的值。
  • 随后客户端进入 ESTABLISHED。

当服务器端收到来自客户端确认收到服务器数据的报文后,得知从服务器到客户端的数据传输是正常的,从而结束 SYN-RECV 阶段,进入 ESTABLISHED 阶段,从而完成三次握手。

5、是否可以使用“两报文握手”建立连接?

为什么TCP客户进程最后还要发送一个普通的TCP确认报文段?

考虑这样一种情况,TCP客户进程发出一个TCP连接请求报文段,但该报文段在某些网络节点长时间滞留了,这必然会造成该报文段的超时重传。

假设重传的报文段被TCP服务器进程正常接收,TCP服务器进程给TCP客户进程发送一个TCP连接请求确认报文段,并进入连接已建立状态。


请注意,由于我们改为两报文握手,因此TCP服务器进程发送完TCP连接请求确认报文段后,进入的是连接已建立状态,而不像三报文握手那样进入同步已接收状态,TCP服务器进程并等待TCP客户进程发来针对TCP连接请求确认报文段的普通确认报文段。TCP客户进程收到TCP连接请求确认报文段后进入TCP连接已建立状态,但不会给TCP服务器进程发送针对该报文段的普通确认报文段。

现在,TCP双方都处于连接已建立状态,他们可以相互传输数据,之后可以通过四报文挥手来释放连接,TCP双方都进入了关闭状态。


一段时间后,之前滞留在网络中的那个失效的TCP连接请求报文段到达了TCP服务器进程,TCP 服务器进程会误认为这是TCP客户进程又发起了一个新的TCP连接请求,于是给TCP客户进程发送TCP连接请求确认报文段并进入连接已建立状态。

该报文段到达TCP客户进程,由于TCP客户进程并没有发起新的TCP连接请求,并且处于关闭状态,因此不会理会该报文段。

但TCP服务器进程已进入了连接已建立状态,他认为新的TCP连接已建立好了,并一直等待TCP客户进程发来数据。这将白白浪费TCP服务器进程所在主机的很多资源。


综上所述,采用三报文握手,而不是两报文握手来建立TCP连接,是为了防止已失效的连接请求报文段突然又传送到了TCP服务器进程因而导致错误。


6、两次握手文字总结

三次握手的主要目的是确认自己和对方的发送和接收都是正常的,从而保证了双方能够进行可靠通信。若采用两次握手,当第二次握手后就建立连接的话,此时客户端知道服务器能够正常接收到自己发送的数据,而服务器并不知道客户端是否能够收到自己发送的数据。

我们知道网络往往是非理想状态的(存在丢包和延迟),当客户端发起创建连接的请求时,如果服务器直接创建了这个连接并返回包含 SYN、ACK 和 Seq 等内容的数据包给客户端,这个数据包因为网络传输的原因丢失了,丢失之后客户端就一直接收不到返回的数据包。由于客户端可能设置了一个超时时间,一段时间后就关闭了连接建立的请求,再重新发起新的请求,而服务器端是不知道的,如果没有第三次握手告诉服务器客户端能否收到服务器传输的数据的话,服务器端的端口就会一直开着,等到客户端因超时重新发出请求时,服务器就会重新开启一个端口连接。长此以往, 这样的端口越来越多,就会造成服务器开销的浪费。


本文转载自: 掘金

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

【Maven专栏系列】Maven项目从0到1

发表于 2021-11-08

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

前言

本期内容是上期内容的一个延伸,小黑通过从0到1创建一个Maven项目,来实际操作一下。如果你也是初学Maven,也可以按照我的操作步骤来试试。

以下是本期的主要内容:

  1. 从0到1创建一个maven项目
  2. maven目录结构
  3. 常见maven命令
  4. 如何构建一个Fat Jar
  5. Maven内存设置

Maven hello world!

就叫这个项目maven-hello-world吧。

创建项目文件夹

一个Maven项目本质上就是pom文件和一个文件夹,我们先找一个位置,建立一个文件夹,就叫maven-hello-world。

创建pom文件

接下来我们在这个文件夹中创建一个pom.xml文件。

1
2
3
4
5
6
7
8
9
10
11
xml复制代码<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.heiz</groupId>
<artifactId>maven-hello-world</artifactId>
<version>1.0.0</version>

</project>

这是一个最基本的pom.xml文件。在这个文件中我们定义了这个项目的GAV(groupId,artifactId,version),别人如果需要依赖这个项目,则在依赖中加入我们的GAV就可以。

测试pom文件

现在为了保证我们文件的正确性,我们可以执行命令来测试一下。在当前目录打开命令行,然后执行下面的命令:

1
shell复制代码mvn clean

mvn clean命令将清除项目目录中所有以前的临时构建文件。因为我们的项目是全新的,所以没有文件需要删除。如果命令行输出下面的成功信息表示我们的pom.xml没问题。

创建Java源代码目录

接下来我们就可以建立Java源代码的文件目录了。按照如下层级建立文件夹。

1
2
3
markdown复制代码- src
- main
-java

创建一个Java文件

然后我们在src/main/java文件夹中创建一个再创建一个文件夹作为我们代码的包,我们就叫它hello吧,然后再hello/建第一个Java代码文件HelloWorld.java。

1
2
3
4
5
6
7
java复制代码package hello;

public class HelloWorld {
public static void main(String args[]){
System.out.println("Hello World, Maven");
}
}

构建项目

现在我们就可以使用Maven命令来打包我们的项目了。回到项目的根目录maven-hello-world。进入命令行执行下面的命令:

1
shell复制代码maven package

这个命令将将编译Java源文件,并创建一个包含已编译Java类的JAR文件。项目根目录中会创建一个target文件夹,JAR文件就存放在这个文件夹中,还有许多临时文件(例如,一个包含所有已编译类的类目录)。

image-20211105195103799

image-20211105195310190

JAR文件的命名格式为artifactId-version,所以我们的项目Jar包名称为maven-hello-world-1.0.0.jar。

Maven目录结构

Maven有一套标准的目录结构,遵循这套标准很有用,也可以让别人在接手你的项目时更容易理解每个目录的作用。

标准目录结构

下面是一个最常用的maven目录结构,还有一些不太常用的目录我这里省略了。

1
2
3
4
5
6
7
8
9
10
11
12
xml复制代码project_dir
- pom.xml
- .mvn
- jvm.config
- src
- main
- java
- resources
- test
- java
- resources
- target

project_dir是我们项目的根目录。

.mvn是一个存放Maven配置文件的目录,比如jvm.config文件,可用于配置Maven用于构建项目的JVM。可以在这个文件中设置Maven内存限制。

src是应用程序和测试源代码的根目录。

src/main/java存放应用程序的源代码。应用程序需要的任何资源文件(例如properties文件)都放在src/main/resources目录中。资源文件可以通过classpath加载。

src/test/java存放测试源代码。测试代码需要的任何资源文件(例如properties文件)都放在src/test/resources中。同样可以通过classpath加载。

target包含Maven构建项目的所有输出内容。target还包含Maven在构建应用程序时所需的临时文件和中间文件。

也可以访问maven官网查看更全的目录信息。maven官方文件目录说明

Maven命令

Maven包含大量可以执行的命令。Maven命令是Life Cycles、Phases和Goals的混合,因此有时候不太容易理解。因此,我将在本期内容中描述常见的Maven命令,并解释它们正在执行的Life Cycles、Phases和Goals。

常见的Maven命令

下面是一些常见的Maven命令,以及它们的功能描述。

maven命令 Description
mvn –version 打印maven版本
mvn clean 清除target目录中的构建结果
mvn package 构建项目并将生成的JAR文件放到target目录中。
mvn package -Dmaven.test.skip=true 构建项目并将生成的JAR文件放到target目录中。——在构建期间不运行单元测试。
mvn clean package 先清除target目录,然后构建项目并将生成的JAR文件放到target目录中。
mvn clean package -Dmaven.test.skip=true 先清除target目录,然后构建项目并将生成的JAR文件放到target目录中。——在构建期间不运行单元测试。
mvn verify 运行项目中所有的集成测试用例
mvn clean verify 先清除target目录,然后运行项目中所有的集成测试用例
mvn install 构建项目,并将生成的jar包保存到本地Maven仓库中
mvn install -Dmaven.test.skip=true 构建项目,并将生成的jar包保存到本地Maven仓库中,构建时不运行单元测试
mvn clean install 先清除target目录,构建项目,并将生成的jar包保存到本地Maven仓库中
mvn clean install -Dmaven.test.skip=true 先清除target目录,构建项目,并将生成的jar包保存到本地Maven仓库中,构建时不运行单元测试
mvn dependency:copy-dependencies 将依赖项从远程Maven仓库复制到本地Maven仓库。
mvn clean dependency:copy-dependencies 清除项目并将依赖项从远程Maven仓库复制到本地Maven仓库。
mvn clean dependency:copy-dependencies package 清除项目并将依赖项从远程Maven仓库复制到本地Maven仓库,然后打包项目。
mvn dependency:tree 根据pom.xml文件中配置的依赖项,打印出项目的依赖项树。
mvn dependency:tree -Dverbose 根据pom.xml文件中配置的依赖项,打印出项目的依赖项树。包括重复的传递依赖。
mvn dependency:tree -Dincludes=com.fasterxml.jackson.core 打印出项目中依赖com.fasterxml.jackson.core的依赖项。
mvn dependency:tree -Dverbose -Dincludes=com.fasterxml.jackson.core 打印出项目中依赖com.fasterxml.jackson.core的依赖项,包括重复的传递依赖。
mvn dependency:build-classpath 根据pom.xml文件中配置的依赖项,输出依赖项的classpath。

需要注意,执行maven的clean命令时,会将target中的所有文件删除,这意味着会丢失之前已经编译构建过的类。如果项目很大,可能需要花费较多的时间构建。但是一般在项目部署之前,为了保证所有的内容都是重新构建的,一般都会执行clean。

Build Life Cycles, Phases和Goals

这三者的关系可以按下图表示:

Maven包含三个主要的Build Life Cycles:

  • clean
  • default
  • site

在每个Build Life Cycles中都包含Phases,在每个Phases中都包含Goals。

可以理解为Maven将需要做的事情按照粒度进行了划分,将Goals组合成Pahses,然后将Phases组合成Life cycles。

构建一个Fat Jar

首先说一下什么是Fat Jar?这里我们还是用我们的maven-hello-world项目来举个例子。

假设我现在需要将Guava加入到我的项目依赖中,我们在pom.xml中添加Guava的依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.heiz</groupId>
<artifactId>maven-hello-world</artifactId>
<version>1.0.0</version>

<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
</dependencies>
</project>

然后我们使用mvn package进行打包。完成之后我们会在target目录中找到我们的maven-hello-world-1.0.0.jar,如果你使用解压工具打开看的话,这个Jar包中是没有guava包相关的文件的。

那么我们在某些场景中,比如需要发布一个微服务,如果没有将依赖项打包到一个JAR文件中,那么需要将其他依赖项单独的上传到服务中,这很麻烦。如果能够将所有的依赖和项目代码一起打包到一个JAR文件中,只需要上传一个文件就可以启动服务,这会很方便。

Fat JAR就是这种将所有依赖打包到一起的JAR文件。

要想构建一个Fat JAR,需要修改我们的pom.xml。通过在POM文件的plugins部分中包含maven-assembly-plugin:

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
xml复制代码<build>
<finalName>maven-hello-world</finalName>
<plugins>
<!-- other Maven plugins ... -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<archive>
<manifest>
<!--这里指定要运行的main类-->
<mainClass>hello.HelloWorld</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

maven-assembly-plugin配置中的元素包含descriptorRef,它告诉Maven怎样进行组装。

jar-with-dependencies表示构建一个带有依赖项的JAR文件,也就是Fat JAR。

execution表示这个插件应该在哪个阶段和目标执行。

将上面的配置添加到我们的maven-hello-world的pom.xml文件中,再执行一次mvn clean package。执行成功之后,在target目录中会出现一个Fat Jar。

Maven内存设置

如果要构建一个大型Maven项目,或者计算机的内存比较小,可能需要调整Maven的内存限制。从Maven 3.3版本,可以在jvm.config文件中设置内存限制。jvm.config位于项目目录中的.mvn目录中。

在jvm.config文件中,可以使用以下参数控制Maven的内存限制:

1
shell复制代码-Xmx2048m -Xms1024m

可以通过调整-Xmx和-Xms的值控制内存使用大小。

最后

以上是本期的全部内容,下期再专门讲一下Maven依赖相关的一些问题,如果对你有帮助,点个赞是对我最大的鼓励!

本文转载自: 掘金

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

Tomcat 类加载

发表于 2021-11-08

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

Tomcat 类加载需要解决以下问题

  • 一个web容器可能要部署两个或多个应用,不同应用可能需要依赖相同第三方库的不同版本,需要保证隔离多个容器。
  • web容器也有自己的类库,不能和应用程序的类库混淆,需要相互隔离。
  • web容器支持jssp文件修改后不用重启,jsp文件也要编译成class文件的,支持HotSwap功能。
  • 部署在同一个web容器中的相同类库的相同版本可以共享,否则,会有重复的类库被加载进JVM中。

Tomcat 类加载器的问题

Tomcat 为什么要打破双亲委派机制?

使用Java默认的类加载器无法加载两个相同类库的不同版本,它只在乎类的全限定类名,并且只有一份,所以无法解决上面的问题1和问题4,也就是相关隔离的问题。
同时在修改jsp文件后,因为类名一样,默认的类加载器不会重新加载,而是使用方法区中已经存在的类,所以需要每个jsp对应一个唯一的类加载器,当修改jsp的时候,直接卸载唯一的类加载器, 然后重新创建类加载器,并加载jsp文件。
​

Tomcat 的类加载机制

tom01.png

Tomcat 有多个自定义类加载器

  • CommonClassLoader:tomcat 最基本的类加载器,加载路径中class可以被tomcat和各个webapp访问。
  • CatalinaClassLoader:tomcat 私有类加载器,webapp不能访问其加载路径下的class,即对webapp不可见。
  • SharedClassLoader:各个webapp共享的类加载器,对tomcat不可见。
  • WebappClassLoader:webapp 私有的类加载器,只对当前webapp可见。
  • JasperClassLoader:JSP的类加载器,每个web应用程序都对应一个WebappClassLoader,每个 jsp文件对应一个JasperClassLoader,所以这两个类加载器有多个实例。

工作原理

  • CommonClassLoader能加载的类都可以被CatalinaClassLoader使用,从而实现了公有类库的共用。
  • CatalinaClassLoader和SharedClasLoader自己能加载的类则与对方相互隔离。
  • WebappClasLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离,多个WebAppClassLoader是同级关系。
  • JspClassLoader的加载范围仅仅是这个JSP文件所编译出来的那一个class 文件,它出现的目的就是为了被丢弃;当web容器检测到JSP文件被修改时,会替换掉目前的JasperClassLoader实例,并通过在创建一个 Jsp类加载器来实现JSP文件的HotSwap功能。
  • tomcat目录结构,与上面的类加载器对应:
1
2
3
4
bash复制代码-  /common/*
- /server/*
- /shared/*
- WEB-INF/*
  • 默认情况下,conf 目录下的 catalina.properties 文件,没有指定 server.loader 以及sharedloader,所以tomcat没有建立CatalinaClasLoader和SharedClassLoader实例,这两个都会使用CommonClassLoader来代替。
    Tomcat6之后,把common、shared. server 目录合成一个lib目录,所以我.们服务器里看不到common、shared、 server 目录。

Tomcat 应用的默认加载顺序

  • 先从JVM的BootStrapClassLoader中加载。
  • 加载Web应用下/WEB- INF/classes中的类。
  • 加载Web应用下/WEB-INF/lib/* .jap中的jar包中的类。
  • 加载上面定义的System路径下面的类。
  • 加载上面定义的Common路径下面的类。

Tomcat 类加载过程

  • 先在本地缓存中查找是否已经加载过该类(对于一些己经加载了的类,会被缓存在resourceEntries这个数据结构中),如果已经加载即返回,否则继续下一步。
  • 让系统类加载器(ApplicationClassLoader)尝试加载该类,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
  • 前两步均没有加载到目标列,主要是为了防止一些基础类会被web中的类覆盖,如果加载到即返回,返回继续。
  • 前两步均没加载到目标类,那么web应用的类加载器将自行加载,如果加载到则返回,否则继续下一步。
  • 最后还是加载不到的话,则委托父类父类加载器(Common ClassLoader)去加载。

Tomcat 打破双亲委派

tom02.png
如上图所示,上面的橙色部分还是和原来一样,采用的双亲委派机制,黄色部分是tomcat第一部分自定义的类加载器,这部分主要是加载tomcat包中的类,这一部分依然采用的是双亲委派机制;
而绿色部分是tomcat第二部分自定义类加载器,正是这一部分,打破了类的双亲委派机制。

Tomcat第一部分自定义类加载器(黄色部分)

这部分类加载器,在tomcat7及以前是tomcat自定义的三个类加载器,分别在不同文件加载的jar包,而到了tomcat8及以后,tomcat 将这三个文件夹合并了,合并成一个lib包,也就是我们现在看到的lib包。

Tomcat第二部分自定义类加载器(绿色部分)

绿色是Java项目在打war包的时候,tomcat 自动生成的类加载器,也就是说,每一个项目打成war包,tomcat都会自动生成一个类加载器,专门用来加载这个war包,而这个类加载器打破了双亲委派机制,我们可以想象一下,加入这个
webapp类没有打破双亲委派机制会怎么样?
如果没有打破,它就会委托父类加载器去加载,一 旦加载到了,自定义加载器就没有机会加载了,那么Spring4和Spring5的项目就没有可能共存了。所以,这一部分它打破了双亲委派机制,这样一来webapp类加载器就不需要在让上级类去加载,它自己就可以加载对应的war里的class文件,当然了,它的项目文件还是要委托上级加载的。

参考:
tomcat是如何打破双亲委派机制的?
打破双亲委派机制

本文转载自: 掘金

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

1…398399400…956

开发者博客

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