数组

1. 数组的基础定义

  • 数组的下标是从0开始的
  • 数组中的地址是连续的

要删除数组中的元素只能用替换去实现,无法直接删去


2. 二分法

相关题目

  • 例题

Problem 704. 二分查找

  • 类似参考题目:

Problem 35.搜索插入位置

Problem 34.在排序数组中查找元素的第一个和最后一个位置

2.1 使用条件

在求解寻找数组中的元素,或者满足条件的值会出现在数组的范围内(如求平方根)等内容时可使用;

使用时数组需满足如下条件(或变化后):

  • 数组升序或者降序排列,即有序数组
  • 数组中无重复项的出现

2.2 时间复杂度

  • 暴力解法时间复杂度:O(n)
  • 二分法时间复杂度:O(logn)

2.3 主要思想

通过在有序数组中划分中间值,判断所求值与中间值之间的关系,较暴力解法可以直接排除掉一些不在范围之内的比较,提升了运行效率。

有序数组:索引定位数据,索引的大小关系即为数组元素的大小关系。

要定义的几个参数:

开始位置:left = 0;(数组下标索引从0开始)

结束位置:right = nums.size() - 1;

中间值:mid = left + ((right - left) >> 1) (位运算,可以防止数组越界现象出现)

2.4 注意点

  • 区间的划分
1
2
3
4
// 原因是此时mid的值一定不是我们寻找的,否则不会出现在这个循环,那么我们在移动的时候也可以不考虑这个值
if (nums[middle] > target) {
right = middle - 1; // target 在左区间,所以[left, middle - 1]
}
  • 循环终止判断条件
1
while (left <= right) // 当left==right,区间[left, right]依然有效,所以用 <=

2.5 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0; //定义数组的左节点
int right = nums.size()-1; //定义数组的右节点
while(left <= right) {
int mid = left + (right - left) / 2; //定义中间点,防止索引越界
if(target < nums[mid]) {
right = mid - 1; // 目标值在左区间
}
else if(target > nums[mid]) {
left = mid + 1; // 目标值在右区间
}
else if(target == nums[mid]) {
return mid;
}
}
return -1;
}
};



3. 双指针法

相关题目

  • 例题:

Problem 27.移除元素

  • 相关题目:

Problem 977.有序数组的平方

3.1 使用条件

需要双重遍历:需要实现先定位元素,再实现元素修改的题目,都可以用双指针法。

双指针法(快慢指针法): 通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

定义快慢指针

  • 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组
  • 慢指针:指向更新新数组下标的位置

3.2 复杂度

  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

3.3 主要思想

fastIndex指针用来寻找要删除的值,slowIndex指针用来定位要修改的值

在删除元素这道题目中,体现在fastIndex在一直在移动,而通过判断if(nums[fastIndex] != val) 来控制是否替换,相当于核心思想是在原有的数组上替换了一个新的数组,这个数组元素所要满足的条件就是if判断的条件

3.4 注意点

用双指针移动删除元素可以使得时间复杂度下降,只需要一个循环遍历即可,一个指针用来寻找删除元素,另一个指针用来实现替换操作。 这里用for循环,不用while循环的原因是: for循环一般用于有终止条件,变量只有一个并且判断条件可以简单的用一个boolean表达式表现出来,而while循环主要用于迭代条件较为复杂,例如二分查找法的情况,左右节点都需要根据不同情况进行更新。 而在双指针中,fastIndex指针是无条件一直向前运行的,我们只需在循环体中控制slowIndex指针即可。

3.5 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
//双指针删除元素 fast指针用于搜索,slow指针用于替换值
int slowIndex = 0;
//结束条件,搜索指针搜索完成。
for(int fastIndex = 0; fastIndex < nums.size(); fastIndex++ ) {
if(nums[fastIndex] != val) {
nums[slowIndex ++] = nums[fastIndex];
}
}
return slowIndex;
}
};

双指针Pro(相向双指针):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int leftIndex = 0;
int rightIndex = nums.size() - 1;
while (leftIndex <= rightIndex) {
// 找左边等于val的元素
while (leftIndex <= rightIndex && nums[leftIndex] != val){
leftIndex++;
}
// 找右边不等于val的元素
while (leftIndex <= rightIndex && nums[rightIndex] == val) {
rightIndex-- ;
}
// 将右边不等于val的元素覆盖左边等于val的元素
if (leftIndex < rightIndex) {
nums[leftIndex++] = nums[rightIndex--];
}
}
return leftIndex; // leftIndex一定指向了最终数组末尾的下一个元素,原因是因为有几个val就代表被中断了几次,此时只有单向运动,长度就会缩减
}
};

4. 滑动窗口

相关题目

  • 例题:

Problem 209.长度最小的子数组

  • 相关题目:

4.1 使用条件

数组中满足条件的最小子数组。一般求解需要经过两个步骤,首先需要先判断出有哪些满足条件的情况存在。之后再去这些满足条件存在中求解最优

一般会出现如下条件需要去定义:

  • 滑动窗口的起始点(可以理解为快指针)
  • 滑动窗口的终止点(可以理解为慢指针)
  • 滑动窗口内条件的表示:例如最大值,元素相等之类的

4.2 复杂度

  • 时间复杂度:O(n)

主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。

  • 空间复杂度:O(1)

4.3 主要思想

时间复杂度的优化其实可以简略的看成循环的优化,而循环优化的最主要思想之一就是能否对相关项进行合并。换而言之就是变量之间能不能够相互解释。

对比暴力解法和滑动窗口能够发现其精妙之处:

  • 暴力解法:判断满足条件的子数组存在(一层遍历) ===> 子数组内判断是否最优(存在遍历中的遍历)===> 比较
  • 滑动窗口:移动终止点去判断是否存在 ===> 移动起始点去求最优

这时候我们会发现,其实滑动窗口用了条件这个东西同时判断了两个值, 而暴力解法则是在数组内部又进行了一个数组的判断,所以我们其实可以用两个点窗口大小去表示条件的时候,这样做就相当于实现了循环次数的减少。

4.4 注意点

  1. 求和的操作很巧妙,融合在一起表现在头指针移动会影响到数组和值,尾指针移动也能够影响到数组的和值。

  2. 数组最小长度的迭代更新,首先直接替换肯定不行,因为无法确定最后一个就是最小的。然后自己比自己求最小也不行,因为你需要一个0的初始值。所以在这里需要引入一个新的变量,result = INT32_MAX。

INT32_MAX是一个常量,表示极大值,主要作用是有值时第一次比较时一定会被替换成result, 如果没有被比较到,那么最后返回结果用三元运算符返回0即可。

4.5 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
//因为其要变化两个量:最小数组的开始值,数组的长度,所以可能需要两次for循环来找到答案
//滑动窗口法:将结果看成是变化,找到大于target的数组,之后不断缩小,贪心算法?
int fastIndex = 0, slowIndex = 0, sum = 0, length = 0, result = INT32_MAX;
//小于等于是考虑末值的条件
for(;fastIndex < nums.size(); fastIndex++) {
//求和放在和
sum += nums[fastIndex];
while(sum >= target) {
length = (fastIndex - slowIndex + 1) ;
result = result > length ? length : result;
sum -= nums[slowIndex];
slowIndex++;
}
}
return result == INT32_MAX ? 0 : result;
}
};



5. 螺旋矩阵

相关题目

Problem 59.螺旋矩阵

5.1 使用条件

题目告诉你要用螺旋矩阵,没有什么特别算法的意思,更多的是体现了一种对语言的运用和流程的表述

5.2 复杂度

5.3 主要思想

模拟顺时针画矩阵的过程:

  • 填充上行从左到右
  • 填充右列从上到下
  • 填充下行从右到左
  • 填充左列从下到上

由外向内一圈一圈这么画下去。

5.4 注意点

开闭区间的判断

5.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
class Solution {
public:
vector<vector<int>> generateMatrix(int n) {
vector<vector<int>> res(n, vector<int>(n, 0)); // 使用vector定义一个二维数组
int startx = 0, starty = 0; // 定义每循环一个圈的起始位置
int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理
int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2)
int count = 1; // 用来给矩阵中每一个空格赋值
int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位
int i,j;
while (loop --) {
i = startx;
j = starty;

// 下面开始的四个for就是模拟转了一圈
// 模拟填充上行从左到右(左闭右开)
for (j = starty; j < n - offset; j++) {
res[startx][j] = count++;
}
// 模拟填充右列从上到下(左闭右开)
for (i = startx; i < n - offset; i++) {
res[i][j] = count++;
}
// 模拟填充下行从右到左(左闭右开)
for (; j > starty; j--) {
res[i][j] = count++;
}
// 模拟填充左列从下到上(左闭右开)
for (; i > startx; i--) {
res[i][j] = count++;
}

// 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1)
startx++;
starty++;

// offset 控制每一圈里每一条边遍历的长度
offset += 1;
}

// 如果n为奇数的话,需要单独给矩阵最中间的位置赋值
if (n % 2) {
res[mid][mid] = count;
}
return res;
}
};

IP

1.IP基本认识

1.1 IP的基础概念

是位于网络层的一个协议,实现主机和主机之间的通信,也叫点对点通信

通过源IP地址目标IP地址这两个关键信息,在众多路由器的转发中实现通信。

路由器实现转发机制:当一台主机朝网络中发送数据包时,他会将目标IP地址设置为接收方的IP地址。随后传给本地网络,本地网络中的路由器通过检查数据包中的目标IP地址,并与自己的路由表进行匹配。如果发现目标ip地址时自己的网络,就会转发回去,否则,转发到路由表上对应的路由器。

具体转发机制:https://zhuanlan.zhihu.com/p/145946764?utm_id=0

1.2 IP和MAC的区别

  • 位置不同:MAC位于数据链路层,IP位于网络层
  • 管理内容不同:IP管理的是网络间两台没有直连主机之间的位置,数据链路层强调的是两台设备在物理意义上的直接连接
  • 也就是说,数据链路层可以是主机和路由器之间的连接,而IP只能是主机和主机之间的连接.而ip主机和主机之间的连接中间还隔了若干路由器。

2.IP地址的基础知识

2.1 IP地址的基础定义

在 TCP/IP 网络通信时,为了保证能正常通信,每个设备都需要配置正确的 IP 地址,否则无法实现正常的通信。

针对IPV4协议的IP地址进行分析

点分十进制

IPV4协议中的IP地址是32位的,人类为了方便记忆,使用点分十进制的方法对ip地址进行了简化,即将32位IP地址每八位划分为1组,每组之间用点(.)隔开,再将每组都用十进制的方式表示出来。

img

2.2 IP地址的分类

img

ABC类地址

ABC类地址由两个部分组成:网络号和主机号,前面的0,10,110是用于区分这三类地址的最快捷的方式。

img

其对应的最大主机数的计算公式为:2n-2, 其中n为主机号的位数。减去2的原因是因为有两个特殊ip地址不能够被使用:

  • 主机号全为 1 指定某个网络下的所有主机,用于广播
  • 主机号全为 0 指定某个网络

DE类地址

img

因为DE类地址中没有主机号,所以其不可以用于主机ip。

  • 224.0.0.0 ~ 224.0.0.255 为预留的组播地址,只能在局域网中,路由器是不会进行转发的。
  • 224.0.1.0 ~ 238.255.255.255 为用户可用的组播地址,可以用于 Internet 上。
  • 239.0.0.0 ~ 239.255.255.255 为本地管理组播地址,可供内部网在内部使用,仅在特定的本地范围内有效。

广播和组播(多播)的区别:

广播:在一个局域网内,所有的IP地址都会收到这个信息,同一个链路中相互连接的主机之间发送数据包

组播:将信息发送给特定组内的所有主机,也就是说广播比组播的范围更加广泛,但是考虑到安全性的话,组播是更为优秀的。

由于广播无法穿透路由,若想给其他网段发送同样的包,就可以使用可以穿透路由的多播。

2.3 无分类地址 CIDR

针对于ABC类IP地址最大主机数分配不均匀的问题,提出了一种新的地址分类方式。

表示形式 a.b.c.d/x,其中 /x 表示前 x 位属于网络号, x 的范围是 0 ~ 32

子网掩码方式获取主机号:

img

子网掩码还可以获取子网网络号,可以对网络层级做到进一步划分。

2.4 IP地址一些特性

路由控制

计算机使用一个特殊的 IP 地址 127.0.0.1 作为环回地址。与该地址具有相同意义的是一个叫做 localhost 的主机名。使用这个 IP 或主机名时,数据包不会流向网络。

通过路由器和路由表,首先数据会先传到本地的路由器,其根据传输过来数据报文中的目标ip地址,对比自己的路由表,找到这份数据应该去向何方,并将其传向对应的路由器。

分片与重组

那么当 IP 数据包大小大于 MTU 时, IP 数据包就会被分片。

经过分片之后的 IP 数据报在被重组的时候,只能由目标主机进行,路由器是不会进行重组的。

在分片传输中,一旦某个分片丢失,则会造成整个 IP 数据报作废,所以 TCP 引入了MSS也就是在 TCP 层进行分片不由 IP 层分片,那么对于 UDP 我们尽量不要发送一个大于 MTU 的数据报文。

2.5 IPV6 & IPV4

IPV6出现的原因:IPV4是32位的,其能够承受的IP地址只有232(约42亿)个IP地址,是远远不能够满足人类的需求的。所以,出现了具有128位的IPV6地址

IPV6的表示方式

每十六位为一个单位,用:分割开,如果出现连续的 0 时还可以将这些 0 省略,并用两个冒号 「::」隔开。但是,一个 IP 地址中只允许出现一次两个连续的冒号。

img

IPV6的地址类型

img

IPV4和IPV6首部的对比差异

img

IPv6 相比 IPv4 的首部改进:

  • 取消了首部校验和字段。 因为在数据链路层和传输层都会校验,因此 IPv6 直接取消了 IP 的校验。
  • 取消了分片/重新组装相关字段。 分片与重组是耗时的过程,IPv6 不允许在中间路由器进行分片与重组,这种操作只能在源与目标主机,这将大大提高了路由器转发的速度。
  • 取消选项字段。 选项字段不再是标准 IP 首部的一部分了,但它并没有消失,而是可能出现在 IPv6 首部中的「下一个首部」指出的位置上。删除该选项字段使的 IPv6 的首部成为固定长度的 40 字节。

3.IP相关协议技术

3.1 DNS域名解析

域名之间的层级关系

DNS中的域名是用.进行分割的,越往右边表示层级越高,如www.baidu.com表示一共有三个层级域名

分别为:com, baidu.com, www.baidu.com

  • 根 DNS 服务器
  • 顶级域 DNS 服务器(com)
  • 权威 DNS 服务器(baidu.com)

域名解析的全过程

浏览器首先看一下自己的缓存里有没有,如果没有就向操作系统的缓存要,还没有就检查本机域名解析文件 hosts,如果还是没有,就会 DNS 服务器进行查询,查询的过程如下:

  1. 客户端向本地DNS服务器发送请求,询问目标域名的IP
  2. 本地DNS服务器收到客户端的请求后,在自己的缓存中查找,查找得到则直接返回,查找不到则向根域名服务器进行询问。

根域名服务器是最高层次的,它不直接用于域名解析,但能指明一条道路。

  1. 根域名服务器收到请求后,根据后缀找到顶级域名服务器地址,将其发给本地DNS服务器。
  2. 本地DNS服务器根据获得的顶级域名服务器地址,对其发送请求,顶级域名服务器地址将域名查询之后,发给客户端对应的权威DNS服务器地址。
  3. 本地DNS服务器根据获得的权威 DNS 服务器地址,对其发送请求,权威 DNS 服务器查询后将对应的 IP 地址 X.X.X.X 告诉本地 DNS。
  4. 本地 DNS 再将 IP 地址返回客户端,客户端和目标建立连接。

总结:本地 DNS与若干DNS服务器的无限问答,以及本地DNS的无数次寻找,最后找到了IP地址。

3.2 ARP协议和RARP协议

  • ARP:已知IP地址可以求得MAC地址
  • RARP:已知MAC地址可以求得IP地址

ARP协议如何得知MAC地址

  • 主机会通过广播发送 ARP 请求,这个包中包含了想要知道的 MAC 地址的主机 IP 地址。
  • 当同个链路中的所有设备收到 ARP 请求时,会去拆开 ARP 请求包里的内容,如果 ARP 请求包中的目标 IP 地址与自己的 IP 地址一致,那么这个设备就将自己的 MAC 地址塞入 ARP 响应包返回给主机。

操作系统通常会把第一次通过 ARP 获取的 MAC 地址缓存起来,以便下次直接从缓存中找到对应 IP 地址的 MAC 地址。

3.3 DHCP动态获取IP地址

  • 客户端首先发起 DHCP 发现报文(DHCP DISCOVER) 的 IP 数据报,由于客户端没有 IP 地址,也不知道 DHCP 服务器的地址,所以使用的是 UDP 广播通信,其使用的广播目的地址是 255.255.255.255(端口 67) 并且使用0.0.0.0(端口 68) 作为源 IP 地址。DHCP 客户端将该 IP 数据报传递给链路层,链路层然后将帧广播到所有的网络中设备。
  • DHCP 服务器收到 DHCP 发现报文时,用 DHCP 提供报文(DHCP OFFER) 向客户端做出响应。该报文仍然使用 IP 广播地址 255.255.255.255,该报文信息携带服务器提供可租约的 IP 地址、子网掩码、默认网关、DNS 服务器以及 IP 地址租用期
  • 客户端收到一个或多个服务器的 DHCP 提供报文后,从中选择一个服务器,并向选中的服务器发送 DHCP 请求报文(DHCP REQUEST进行响应,回显配置的参数。
  • 最后,服务端用 DHCP ACK 报文对 DHCP 请求报文进行响应,应答所要求的参数。

一旦客户端收到 DHCP ACK 后,交互便完成了,并且客户端能够在租用期内使用 DHCP 服务器分配的 IP 地址。

简化来讲,就是客户端去求职BOSS海投,然后看上这个客户端的服务端就给他发送offer,这个offer告诉他我能给你提供的条件(没错,不用面试),然后客户端选来选去,最后只回了一家,签约成功。之后服务端在发个合同入职须知什么的,结束。这是多少人梦寐以求的状态。

如果租约的 DHCP IP 地址快期后,客户端会向服务器发送 DHCP 请求报文:

  • 服务器如果同意继续租用,则用 DHCP ACK 报文进行应答,客户端就会延长租期。
  • 服务器如果不同意继续租用,则用 DHCP NACK 报文,客户端就要停止使用租约的 IP 地址。

3.4 NAT网络地址转换

两个私有 IP 地址都转换 IP 地址为统一的公有地址,但是以不同的端口号作为区分。

生成一个 NAPT 路由器的转换表,就可以正确地转换地址跟端口的组合,令客户端 A、B 能同时与服务器之间进行通信。

这种转换表在 NAT 路由器上自动生成。例如,在 TCP 的情况下,建立 TCP 连接首次握手时的 SYN 包一经发出,就会生成这个表。而后又随着收到关闭连接时发出 FIN 包的确认应答从表中被删除。

缺点:

  • 外部无法主动与 NAT 内部服务器建立连接,因为 NAPT 转换表没有转换记录。
  • 转换表的生成与转换操作都会产生性能开销。
  • 通信过程中,如果 NAT 路由器重启了,所有的 TCP 连接都将被重置。

3.5 ICMP互联网控制报文协议

ICMP 主要的功能包括:确认 IP 包是否成功送达目标地址、报告发送过程中 IP 包被废弃的原因和改善网络设置等。

img

查询报文:回送请求和回送响应是查询报文的两种,主要的作用是用来确定发送的数据包是否到达对端的一种方式

差错报文:相当于我们平时编程语言中的异常

  • 目标不可达:类似于404,可能是网络不可达,可能是端口不可达,可能是主机不可达等

    • 网络不可达代码为 0
    • 主机不可达代码为 1
    • 协议不可达代码为 2
    • 端口不可达代码为 3
    • 需要进行分片但设置了不分片位代码为 4
  • 原点抑制:使用低网速传输的时候可能会出现网络拥堵情况,这时发送一个原点抑制的报文,可以增大 IP 包的传输间隔,减少网络拥堵的情况。

4.扩展知识

4.1 ICMP具体工作原理

https://xiaolincoding.com/network/4_ip/ping.html#ip%E5%8D%8F%E8%AE%AE%E7%9A%84%E5%8A%A9%E6%89%8B-icmp-%E5%8D%8F%E8%AE%AE

不想写了。。。认认真真的看过了!

4.2 ping 127.0.0.1

断网了还能ping的通127.0.0.1吗

可以,因为还没走出到网络,客户端发出网络请求会第一步先走到本地路由器,本地路由器如果判断是自己的本地地址就会直接返回,如果不是再去对比自己的路由表。本地路由器是不需要网络的,所以我们发现ping 127.0.0.1不需要联网

127.0.0.1 和 localhost 以及 0.0.0.0 有区别吗

  • localhost是一个域名,相当于baidu.com, 其ip地址为127.0.0.1,所以意义上有区别但是实际使用是没什么区别的
  • 0.0.0.0 , 表示本机上的所有IPV4地址。是ping不通的

2.LinkList

1. 链表的基础定义

1.1 主要组成

  • data域: 用于存储每一个节点的数据
  • next域: 用于存储指向下一个节点的指针

1.2 特点

  • 最后指向null,意味着链表的结束。
  • 可以无限扩容,通过指针指向其他元素可以实现空间的扩大。
  • 存储结构特点:链表是通过指针域的指针链接在内存中各个节点。所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。
  • 相较于数组而言,插入,删除方便,查找不方便(需要一个一个去找)

1.3 图表写法

1.4 相关类型

近似的我们可以分为如下几种类型:

  • 单链表
    如上图,是一般的数据模型形式。

  • 双链表
    在单链表的基础上增加了一个next域,即一个节点有两个next域,一个指向前面的节点,一个指向后面的节点。这样能够一定程度上提高查找的效率。

  • 循环链表
    即最后的next指向的是head,实现了链表的循环。可以解决约瑟夫环问题。

1.5 数据结构相关操作

下面是链表有关的数据结构操作:

  • 初始化链表结构
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Struct LinkNode{
    int data; // 定义data域内容
    ListNode *next; //指向ListNode类型元素的指针
    // 构造函数的初始化
    ListNode(int x){
    data(x);
    next(null){};
    }
    }

  • 初始化节点
    ListNode* head = new ListNode(5);


  • 删除节点

链表节点删除的主要思想有两个,第一个是寻找到要删除的位置,这个很简单实现,比对要删除的元素和指针指向的元素是否相等,如果不相等就p = p->next,移动到下个节点即可,难点是理解确定寻址指针指向的位置和删除元素的位置关系。而寻址指针指向的位置是删除元素的前一格,这个和后面具体删除的思想有关。

第二个是删除的过程操作,其中细化出来有两步,第一步是指向要删除的节点,之后直接delete带走即可,第二步是将原来指向删除节点的next域指针指向删除节点的下一个。综合这两个我们可以发现在删除节点前一个节点要做的事情是可以和其他联系起来的,他的next指向的就是删除的,而他的next我们后续也要进行操作。所以这就回应了第一个寻址思想的难点。



实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void DeleteElem(link L,int i)
{
link p=L,q;
int j=0;
while(p && j<i-1)
{
p = p->next;
j++;
}
if(!p||!p->next)
{cout<<"输出位置不合法"<<" ";
exit(1);
}
else{
q = p->next;
p->next = q->next;
delete q;
}
}

- 增加节点

很简单也是两步操作,第一步是定位,定位和操作和删除的差不多;第二步是增加,那么应该怎么增加呢?我们需要新引入一个指针来进行节点赋值和节点位置的操作,节点赋值很简单,就是s->data = elem节点的位置也不难,因为插入元素,即要插入位置的原来节点得给你让出来,你要干他的事情,你的next就要指向原来节点的next即,s->next = p->next,那么你要成为他的一部分得连起来,那么原来的next就要指向你的元素,即p->next = s注意这两步的顺序不可以搞反





代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void AddElem(link &L, int i, int e)
{
link p = L, s;
int j=0;
while(p && j<i-1)
{
p = p->next;
j++;
} //搜索插入的位置
if(!p)
{cout<<"增加位置不合法"<<endl;
exit(1);
}
else{
s = new Node; //为新节点创建位置
s->Data = e; //赋值
s->next = p->next;
p->next = s;
}
}
  • 寻找data所对应的节点
    删除增加的基础,不再赘述。

2. 链表设计

相关题目

Problem 203. 移除链表元素
Problem 707. 设计链表

很好的数据结构方式,也是数据结构这门课中第一次学到的关于指针类型的数据结构,有一定的难度,又是我们的启蒙,是一门十分值得敬畏的章节。加油吧。不管前路怎么艰辛,这总是你的第一站,第一站总是会变的比较有难度一些的,那么你就一定要坚持下去,同时照顾好自己的身体,资本要有,但挥霍资本的资本也要有。

2.1 复杂度

大部分是O(n)

2.2 主要思想

这里学到了一个比较重要的东西叫做虚拟头节点,他存在的意义是为了让处理头结点的时候和处理其他节点一样的自然。因为链表这种数据结构本身具有一定的局限性,其头节点无法用next指向,所以设置一个虚拟头节点,可以让头节点获得和其他节点一样的待遇。

2.3 注意点

  • 注意什么时候需要定位到要操作节点的前一个节点,什么时候要确定的定位到那个要操作的节点。要操作节点的前一个节点主要用于添加和删除这两个操作,因为他们都需要用到前一个节点的next指针,来指向下一个节点方能对他们进行操作。而确定的定位到要操作的节点则是根据index取值的操作。
  • 需要警惕提防节点们,因为节点们在你每次更改next域之后,他们之间的关系就进行了一次大洗牌,所以在新增和删除节点的时候一定要按步骤操作好。
  • 有索引就有越界问题,一定看看要搜索定位的索引是否越界或者不存在。
  • 用好while(index--)的写法,他其实和for(int i = 0; i++; i < index)是一样的,但是他的优点就是能加上一些&&,从而可以做出一些防止越界的举动。
  • 记得在操作的时候时刻不要拿着最原始的head变量去操作,因为要返回head的时候能够帮你直接定位,而用一些替代值去进行操作就好了。

2.4 代码实现

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
class MyLinkedList {
public:
struct LinkNode{
int val;
LinkNode* next;
LinkNode(int val): val(val), next(nullptr){}
};
MyLinkedList() {
size = 0; //长度
dummyHead = new LinkNode(0); //虚拟头节点
}

int get(int index) {
//有下标需要考虑越界问题
if(index > size - 1 || index < 0){
return -1;
}
LinkNode* cur = dummyHead -> next;
while(index --) {
cur = cur -> next;
}
return cur -> val;
}

void addAtHead(int val) {
LinkNode* cur = new LinkNode(val);
cur -> next = dummyHead -> next;
dummyHead -> next = cur;
size++;
}

void addAtTail(int val) {
LinkNode* cur = new LinkNode(val);
LinkNode* p = dummyHead;
//把尾节点给找出来
while(p -> next != nullptr){
p = p -> next;
}
p -> next = cur;
size++;
}

void addAtIndex(int index, int val) {
// 小于零则在头部插入节点
if(index < 0){
index = 0;
}
// 越界则无效
if(index > size){
return;
}
LinkNode* p = dummyHead;
LinkNode* cur = new LinkNode(val);
while(index--){
p = p -> next;
}
cur -> next = p -> next;
p -> next = cur;
size++;
}

void deleteAtIndex(int index) {
//判断索引是否越界
if(index > size - 1 || index < 0){
return;
}
LinkNode* cur = dummyHead;
while(index--) {
cur = cur -> next;
}
LinkNode* p = cur -> next;
cur -> next = p -> next;
delete p;
size--;
}

ListNode* removeElements(ListNode* head, int val) {
//虚拟头节点的建立
ListNode* dummyHead = new ListNode();
dummyHead->next = head;
ListNode* cur = dummyHead;
while(cur->next != NULL){
if(cur->next->val == val){
ListNode* tmp = cur->next;
cur->next = cur ->next->next;
delete tmp;
}
else{
cur = cur -> next;
}
}
head = dummyHead->next;
delete dummyHead;
return head;
}

private:
LinkNode* dummyHead;
int size;
};

3. 链表翻转

相关题目

Problem 206 链表翻转

3.2 复杂度

时间复杂度:O(n)
空间复杂度:O(1)

3.3 主要思想

只要实现链表的转向即可,那么就需要有一前一后两个节点,来实现转向的操作,于是链表的转向就可以实现了。 其实这里又是双指针的另一个应用场景,双指针只要涉及到需要对两个东西同时进行操作,他都能够派上用场。

3.4 注意点

我感觉这道题没有什么要注意的东西,就算要有吧,也就是要注意转移到下一个节点进行翻转的时候,定位要不能定next了,因为操作过后已经转移了,所以要在翻转前就给cur->next给标记上temp。

3.5 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* temp = new ListNode(); //临时节点,用来存储要操作的下一个节点的
ListNode* pre = NULL; //指向头节点的前一个节点
ListNode* cur = head; //指向头节点
while(cur){
temp = cur->next; //标记
cur->next = pre; //翻转
pre = cur; //下一组
cur = temp; //下一组
}
return pre;
}
};

4. 两两交换链表中的节点

相关题目

Problem 24 两两交换链表中的节点

4.1 复杂度

时间复杂度:O(n)
空间复杂度:O(1)

4.2 主要思想

要关注好移动的顺序,这道题是很好的让人们能够关注流程顺序的一道题目。

4.3 注意点

关注好移动的顺序

4.4 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Solution {
public:
ListNode* swapPairs(ListNode* head) {
ListNode* p = new ListNode(0);
p->next = head;
ListNode* cur = p;
while(cur->next != nullptr && cur->next->next != nullptr){
ListNode* temp = cur->next; //临时节点1
ListNode* temp1 = cur->next->next->next; //临时节点2
cur->next = cur->next->next; //第一步操作,头节点->第二个
cur->next->next = temp; //第二步操作,第二个->第一个
cur->next->next->next = temp1; //第三步操作,第一个->第三个

cur = cur->next->next;
}
return p->next;
}
};

5. 删除列表的倒数第n个节点

相关题目

Problem 19. 删除列表的倒数第n个节点

5.1 复杂度

时间复杂度:O(n)
空间复杂度:O(1)

5.2 主要思想

还是双指针,删除这种东西好像最好双指针了,一个用来指代他的条件,另一个指针来指向要删除的值,具体而言在这道题上就是慢指针是用来指代值的,快指针的条件判断在于,倒数第几个就移动几次,终止条件本来从虚拟头节点出发到nullptr就遍历完了,那么我们删除倒数第几个就派出另一个往前先走几步就行了,那么先出发的那个到达nullptr的时候就是真正删除的那一个到达要删除的时候。

5.3 注意点

就是我们的前进的时候用while循环,其实for(int i=0; i++; i<n)while(n--)是等价的,但是要考虑他是否越界,所以要添加上fast != null就用while好像更好一点了。

5.4 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummyList = new ListNode();
dummyList->next = head;
ListNode* fast = head;
ListNode* slow = dummyList;
//很好的判断条件,防止倒数第n个本来就是不存在的情况
while(n-- && fast != nullptr){
fast = fast->next;
}
// 一起前进
while(fast != nullptr){
fast = fast->next;
slow = slow->next;
}
ListNode* p = slow->next;
slow->next = p->next;
delete p;
return dummyList->next;
}
};

6. 链表相交

相关题目

Problem 链表相交

6.1 复杂度

时间复杂度:O(n + m)
空间复杂度:O(1)

6.2 主要思想

这道题告诉你之后难度不大,就是看看两个链表的部分是否相等就行了,怎么看呢,一个个移动,直到空为止,很容易知道应该是以最短的那个链表作为基准,因为最短的链表遍历完之后所有有可能的结果也就尘埃落定了。所以我们要做的第一步就是比对两个数组找出长度差值,之后才能够精准的进行定位。

6.3 注意点

计算完长度之后,临时指针要重定向归于原来的头节点,不然他们计算完长度之后的状态是指向nullptr的

6.4 代码实现

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
class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
ListNode* cur1 = headA;
ListNode* cur2 = headB;
int lenA = 0, lenB = 0;
//计算长度
while(cur1 != NULL){
cur1 = cur1->next;
lenA++;
}

while(cur2 != NULL){
cur2 = cur2->next;
lenB++;
}

cur1 = headA;
cur2 = headB;

if(lenA < lenB){
swap(lenA, lenB);
swap(cur1, cur2);
}

int gap = lenA - lenB;
//移动到同一起跑线
while(gap--){
cur1 = cur1->next;
}

while(cur1 != nullptr){
if(cur1 == cur2){
return cur1;
}
cur1 = cur1->next;
cur2 = cur2->next;
}
return NULL;
}
};

7. 环形链表

相关题目

Problem 142. 环形链表

7.1 复杂度

时间复杂度:O(n)
空间复杂度:O(1)

7.2 主要思想

这道题由两个部分组成,第一个部分是有没有环,第二个部分是环的入口到底在哪? 首先有没有环的判断就是依据两个指针一快一慢的走,如果相遇了就能证明有环,但是这是一个必要证明,还得加上一个条件就是快指针走两步慢指针走一步,本质上就是一个追赶问题,快指针每次相对于慢指针多走了一步,那么就是链表中每一个格子都有能够遇上的机会。所以此时遇上和有环形成了一个充分必要的对应证明条件。 那么入口在哪呢,假设入口离起始点的距离为x,第一次相遇两指针在距离入口y处,环的长度为y+z,那么我们可以推断出,慢指针走了x+y距离,快指针走了x+n(y+z)。那么他们相遇的话,即x+y = x+n(y+z),我们要探究x是多少,即可以把等式变化为:x = (n-1)(y+z)+z,那么因为 y+z表示一直在绕圈可以忽略掉 ,所以我们可以得出一个结论叫做,起点到入环点的位置和相遇点到入环点的位置是相等的。

这里x一定没有绕环的论证在慢指针走一圈的时间快指针能走两圈,而快指针相对于慢指针每次走一步,也就是说快指针速度为2v,慢指针速度为v,快指针追上慢指针需要走的路程为y+z-a(不足一圈),追上的时间为(y+z-a)/(2v-v),慢指针走一圈的时间为(y+z)/v,追上的时间小于慢指针走一圈的时间,所以一定能够追得上。

7.4 代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
ListNode* fast = head;
ListNode* slow = head;
while(fast != NULL && fast->next != NULL) {
slow = slow->next;
fast = fast->next->next;
// 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇
if (slow == fast) {
ListNode* index1 = fast;
ListNode* index2 = head;
while (index1 != index2) {
index1 = index1->next;
index2 = index2->next;
}
return index2; // 返回环的入口
}
}
return NULL;
}
};

TCP

1. TCP的基本认识

1.1 头部格式

img

  • 源端口号/目标端口号:分别表示发送方和接收方

  • 序列号:用来保证传输的可靠性,原数据是随机生成的随机数,之后服务端和客户端每交互一次就叠加一次。用来解决网络收发包的乱序问题

  • 确认应答号:下一次收到数据期望的序列号,可以认为此前的序列号都传输成功,用来解决网络丢包问题

  • 首部:表示TCP报文头部长度,按比例缩小进行存储。

  • 控制位:

    • URG(Urgent,紧急标志):表示包中有需要紧急处理的数据,优先处理。
    • ACK(Acknowledgment,确认标志):表示确认数据包已经收到。
    • PSH(Push,推送标志):PSH为1,立即发送数据。PSH为0,先进行缓存。
    • RST(Reset,重置标志):RST标志用于中止连接,用于解决网络连接问题。
    • SYN(Synchronization,同步标志):这个位标志用于发起一个连接,建立连接并设置初始序列号。
    • FIN(Finish,完成标志):表示终止TCP连接,用于数据传输完毕,此段连接不会再有数据往来。
  • 窗口大小:用于流量控制,分别表表明自己能够接受的流量大小。

  • 校验和:确保数据传输没有被篡改,重新通过某种算法计算后得到的数。

  • 紧急指针:当URG标志位为1的时候才存在该字段,表示该部分数据为紧急数据。

  • 选项:用于优化TCP传输性能,对TCP功能做出解释,也是TCP总长度可变的原因。

有的文章保留字段为4比特,标志位为8比特,有的文章保留字段为6比特,标志位为6比特。

1.2 TCP的意义和作用

TCP的面向连接的,可靠的,基于字节流的传输。

ip层是不可靠的,其只负责将数据传输到对应的主机,保证数据在网络传输之间不丢失,但是能够按序到达数据内容的完整无法保证。所以tcp层的出现正是为了保证数据的安全性。

  1. 面向连接:只有建立起来连接,才能进行数据的传输。即只有一对一的连接。保证了数据之间传输的安全
  2. 可靠性:更加偏向于一个目的的描述,保证数据包发送之后能可靠的到达目的地。
  3. 基于字节流:应用程序对数据的发送和接收是没有边界限制的,为了保证其有序性,同时也可以针对其特性建立缓存区,将传输的若干数量包拼装完成后再接收。

RFC 793定义的连接:

Connections: The reliability and flow control mechanisms described above require that TCPs initialize and maintain certain status information for each data stream. The combination of this information, including sockets, sequence numbers, and window sizes, is called a connection.

从中我们可以看出一个TCP连接中主要包含着三个关键信息:

  • sockets:由ip地址和端口号组成,用于确认地址信息
  • sequence numbers:随机序列号,用来确保传输的有序和安全
  • window sizes:窗口大小,用来进行流量控制

1.3 如何确定一个TCP连接

TCP四元组可以确定一个唯一的TCP连接:

  • 源地址
  • 源端口
  • 目的地址
  • 目的端口

求一个端口能够监听的最大TCP连接数

最大TCP连接数 = 客户端IP地址数量 * 客户端端口数量

1.4 TCP和UDP的区别

img

这里要注意包长度指的是整个UDP的长度,包含数据。校验和为了防止收到损坏的数据包。

  1. 连接
  • TCP是面向连接的协议,他需要经过确定连接之后才可以进行数据的传输。
  • UDP是无连接协议,数据即刻传输
  1. 服务对象
  • TCP只支持一对一的服务,这是他面向连接的特点决定的
  • UDP支持一对一,一对多,多对多的数据传输
  1. 可靠性
  • TCP的可靠性较高,数据丢包和被篡改的风险较低下
  • UDP的可靠性较低,但其也有其改进版本增强其可靠性,如QUIC协议。
  1. 拥塞控制
  • TCP通过头部的窗口大小字段告诉传输方自己最大承受数据的能力,是有限制的。
  • UDP则因为其无连接特性,数据包可以在网络连接中停留拥塞,故可无后顾之忧的发送
  1. 头部开销
  • TCP的头部长度为20个字节加上选项
  • UDP的头部长度为8个字节,且固定
  1. 传输方式
  • TCP采用流式传输方式,保证有序及可靠
  • UDP采用数据包传输方式,是有边界的,可能会导致传输的乱序问题。
  1. 分片方式
  • TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
  • UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。
  1. 应用场景

由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:

  • FTP 文件传输;
  • HTTP / HTTPS;

由于 UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于:

  • 包总量较少的通信,如 DNS 、SNMP 等;
  • 视频、音频等多媒体通信;
  • 广播通信;

1.5 TCP和UDP可共用一个端口号吗

可以的。

传输层端口号的作用是:用于区分同一台主机上不同应用程序的数据包

UDP和TCP是传输层的两个完全不一样的传输协议。

主机接收到数据包之后,首先会根据头部的格式判断出到底是UDP协议还是TCP协议,再交由相应的软件板块进行传输。

2. TCP的连接确立

三次握手是TCP连接最最最重要,也是最最最基础的点。但是也要记住,这个过程是数据传输的准备工作,其目的只是为客户端和服务端建立起连接,确保其能无误收发数据。

2.1 三次握手总流程图

img

  • 第一次握手:

服务端没有收到请求的时候,处于listen状态,同时的,客户端发起请求,发送标志位为1的synclient_isn,表示请求连接,客户端自身进入syn_send状态。

  • 第二次握手:

本次由服务端首先发起请求,其发送了标志位为1的ack状态码,表示对上次请求同意的回复,同时回复的递增1的序列号。至此,服务端告诉客户端自己能够收到他的信息。之后,发送标志位为1的synservice_isn用于确认客户端能否收到自己的信息,同时自身进入syn_revd状态。

  • 第三次握手:

本次握手由客户端发送,表示自己收到了服务端的请求并告知,发送了标志位为1的ack状态码,表示对上次请求同意的回复,同时回复的递增1的序列号。其后,进入Established状态,表示准备就绪,可以进行数据传输,服务端收到客户端发送的Ack,也同时进入Established状态,至此,三次握手完成。

2.2 握手的数量为3的原因

The principle reason for the three-way handshake is to prevent old duplicate connection initiations from causing confusion.

可以看到,三次握手的主要原因是为了防止旧的连接重复初始化导致连接混乱。

通过头部中的序列号和确认应答号可以校验连接双方是否昔日故人,如果是再进行数据传输。少掉任意一次连接,都会造成客户端或者服务端无法对对方身份的确认。这三次连接实质上是不可或缺的两问两答

而连接混乱造成的后果则是资源的浪费。用旧的无意义的连接传输资源,最后得到的也是无法使用的资源。

2.3 初始序列号ISN意义及如何生成

起始 ISN 是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时。

RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)

  • M 是一个计时器,这个计时器每隔 4 微秒加 1。
  • F 是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。

可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。

  • 意义:防止旧的报文被新的连接接收

2.4 握手报文丢失,双方怎么办

总而言之,简单来讲,超时重传,超过了一定的时间还收不到回应,那么则断开连接。

注意,重新发送的只有SYN,ACK不会重新发送

等待时间逐倍的递增。

在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5。

2.5 SYN攻击是什么,如何防范

在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:

  • 半连接队列,也称 SYN 队列,对应着三次握手中的syn_revd状态;
  • 全连接队列,也称 accept 队列,对应着三次握手中的Established状态;

SYN攻击指的是对于服务器的SYN队列进行ddos攻击,发送大量的请求,这时候服务端会产生大量的ACK+SYN加以应答,但是因为客户端是不存在的,所以服务器始终等不到客户端发送过来的ACK该批请求也就无法由SYN队列进入到accept队列,随着SYN队列被占满,服务器再也无法接受请求。

避免 SYN 攻击方式,可以有以下四种方法:

  • 调大 netdev_max_backlog;
  • 增大 TCP 半连接队列;
  • 开启 tcp_syncookies;
  • 减少 SYN+ACK 重传次数

3. TCP断开连接

3.1 四次挥手总流程图

img

  • 首先,客户端服务端双方起始都是可以传输数据的Established状态,正式进入四次挥手断开连接
  • 第一次挥手:客户端发送带有FIN标志位的报文,表示想终止这次连接,之后进入FIN_WAIT1阶段。
  • *第二次挥手:*服务端收到客户端发送的FIN报文,发送带有ACK头部的确认报文给客户端,表示自己已经收到。随后进入Closed_Wait阶段,表示等待连接的关闭,客户端接收到ACK确认报文后进入FIN_WAIT2阶段
  • 第三次挥手:服务端将自己想要发送给客户端的信息处理完之后发送FIN报文,表示服务端已没有信息需要发送给客户端。随后进入Last_Ack阶段,表示等待客户端的最后一次回应。
  • *第四次挥手:*客户端接受到服务端的FIN报文后,发送ACK确认报文给服务端。之后进入为期**2MSL****TIME_WAIT**阶段,之后断开连接。服务端收到应答之后断开连接。

只有在主动发起断开连接的一方才会有time_wait阶段

3.2 服务端的SYN和ACK可否合并

在四次挥手的过程中,服务端在两个报文中间有一段closed_wait的阶段,那么该阶段可否跳过省略呢

为什么会有closed_wait的阶段

首先我们要知道服务端在closed_wait这段时间内做了什么,他主要的作用就是来处理数据,检查是否还有没发送给客户端的数据,如果有,就发送给客户端。FIN标志位的意思是所有数据发送完毕,想要断开请求,所以必须检查数据是否完毕。

他主要实现的原理是在收到客户端发送的FIN报文之后,将一个结束标识符EOF放在待处理数据的末尾,由于TCP的流传输特性,所以之后当服务端将所有数据处理完,才会读取到EOF,读取到EOF之后,才会向服务端发送FIN报文。

三次连接是否可以实现?

可以实现,条件是需要开启TCP延时确认机制并且服务端没有数据要发送

TCP延时确认机制

服务端当收到客户端发送过来的FIN信息之后,需要回复ACK报文,如果只单单传输一个20字节的报文头部不携带任何报文信息,而后续需要发送的报文又独立发送,就会造成网络资源的浪费(因为内容和头部是始终需要携带的)。那么为了提高网络资源的利用效率,TCP延时确认机制产生,它主要做了下面三件事:

  • 当有响应数据需要发送时,数据会随着ACK头部一起发送。
  • 当没有响应数据需要发送时候,服务端会等待一段时间,已确定有没有数据需要发送进而一起发送。
  • 当在等待响应数据时,若又有一个请求发送过来,那么ACK头就会立刻发送。

延迟等待时间由TCP_DELACK_MINTCP_DELACK_MAX决定。linux内核中默认为:

1
2
TCP_DELACK_MIN = HZ/5
TCP_DELACK_MIN = HZ/25

HZ与系统的时钟周期频率相关。

img

TCP延时确认机制是默认开启的

三次连接实现原理

当没有数据要发送的时候,那么服务端可以直接读取到EOF,准备发送带有FIN的报文,但是因为延时确认机制,原先的ACK报文还没有发送,那么就将FIN一起写到ACK中,之后,等待一段时间或者知道客户端再次发送请求(分别对应了其2,3情况),那么这个没有数据但是含有ACKFIN头部信息的报文就直接发送了。

3.3 挥手报文丢失,双方怎么办

总而言之,简单来讲,超时重传,超过了一定的时间还收不到回应,那么则断开连接。

注意,重新发送的只有SYN,ACK不会重新发送

等待时间逐倍的递增。

在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries内核参数决定,默认值是 5。

close和shutdown方法的比较

两者都是用于对进程的关闭,但是shutdown关闭的是该进程的发送信息功能,而接受信息功能仍然存在,close则是两个功能都将其关闭。

在第二次挥手中,客户端shutdown了

这是一个十分有趣的情况,第二次挥手是由服务端对于客户端的FIN标志的回应,发送了ACK确认标志,如果服务端始终不发送过来第三次挥手,正常情况下客户端是需要重发FIN请求的,但是因为此时客户端的shutdown,使得其无法发送,那么最后造就的结果便是客户端一直在FIN_WAIT2这个阶段卡住,无法关闭。服务端重发请求到达上限时间次数便会自己断开连接。

3.4 TIME_WAIT状态

TIME_WAIT状态的时间是多少

2MSL(Maximum Segment Lifetime,报文最大生存时间)。非常巧合的是,我们发现,这个时间便是一次客户端和服务端之间交互一来一回的时间。

TIME_WAIT状态存在的原因

  • 确保新的相同的四元组TCP连接不要接收到旧的报文信息。

主要是为了避免第四次挥手的丢失,客户端发送完成第四次挥手ACK之后就断开连接,那么万一服务端没有收到ACK确认,又重发了一次FIN,此时客户端无法收到他的请求。此时恰好又有一个相同的四元组TCP连接建立了起来,这时候旧的FIN或者数据就会发送到新的连接。会客户端收到错误的信息或者造成新的连接无法将其信息发送完整就断开连接。

  • 保证被动关闭的一方能够***正常关闭***

还是基于第四次挥手的丢失,服务端重新发送FIN报文之后,因为客户端已经关闭,所以无法收到他的ACK报文,那么最后关闭只能是因为超时被动断开连接了。这样既浪费了服务器资源,又对于服务器是有危害的。

TIME_WAIT状态过多的危害

TIME_WAIT状态其实是一种阻塞的表现,等待至少2MSL的时间,而这个时间对端口号来说是占用的。如果网络不通畅,那么端口号资源就被占用的极多,就会无法对于服务端建立连接了。会占用系统资源,比如文件描述符、内存资源、CPU 资源、线程资源等。

如何优化 TIME_WAIT?

  • 打开 net.ipv4.tcp_tw_reuse 和 net.ipv4.tcp_timestamps 选项;
  • net.ipv4.tcp_max_tw_buckets
  • 程序中使用 SO_LINGER ,应用强制使用 RST 关闭。

第一二种方法主要是通过控制TIME_WAIT的时间加以控制,第三种方法则是直接跨过了TIME_WAIT。都存在着一定上诉提到的风险。

long-alive 长连接状态

这个状态我们可以在请求报文的头部信息中看到,开启状态表示一次请求交互完成之后不立刻断开,也就表示着后面还有请求需要进行处理。那么如果每次请求都进行握手挥手网络资源消耗量极大,于是用长连接状态表示不立刻断开。如果交互确实进行完成,那么只要有一方发送断开连接请求,那么就会来到四次挥手的状态。

什么场景下服务端会主动断开连接呢?

  • 第一个场景:HTTP 没有使用长连接
  • 第二个场景:HTTP 长连接超时
  • 第三个场景:HTTP 长连接的请求数量达到上限

3.5 服务器出现大量 CLOSE_WAIT 状态的原因有哪些?

说明没有发出FIN报文,无法到达下一步LAST_ACK。那么也就是说无法调用closed方法。

通常是代码的问题,主要排查方向是为何服务端调用不到closed方法。

3.6 建立连接后一方发生故障会怎么样?

保活机制

通过开启SO_KEEPALIVE 选项生效。

在一个时间段内,如果任何连接相关的传输的活动都没有产生,那么每隔一个时间段就发送一次探测报文,如果均没有得到回应,就断定该TCP连接已经死亡。

linux中默认参数如下:

1
2
3
net.ipv4.tcp_keepalive_time=7200 //没有活动的时间长度
net.ipv4.tcp_keepalive_intvl=75 //发送探测报文的相隔时间段
net.ipv4.tcp_keepalive_probes=9 // 发送探测报文的次数

也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。

进程突然被closed

如前文所讲,挥手报文丢失的情况。

主机突然发生宕机

客户端主机崩溃了,服务端是无法感知到的,在保活机制时间激活之前,服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态,直到服务端重启进程。

所以,我们可以得知一个点,在双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态,并不代表另一方的连接还一定正常。

客户端的网络连接突然断开

TCP 连接在 Linux 内核中是一个名为 struct socket 的结构体,该结构体的内容包含 TCP 连接的状态等信息。当拔掉网线的时候,操作系统并不会变更该结构体的任何内容,所以 TCP 连接的状态也不会发生改变。

换而言之,TCP连接一旦建立,就生成了两个独立的结构存储在本地,和传输层的连接关系已然不大。

之后,发送就如同进程被closed的状态,直至重新连上网络,方可恢复正常。

4. TCP的四个机制

4.1 重传机制

超时重传

顾名思义,超时重传指的是当发送方超过一定时间没有收到回复的时候,重新发送数据报文。

  • RTT(Routing-trip time)往返时延,指的是一个数据包从发送到接受到回应所需要经过的时间长度
  • **RTO (**Retransmission Timeout ) 超时发送时间,指的是超时重传的时间。

一般在超时重传机制中,RTO会略大于RTT。

RTO太大或者太小的坏处:

  • 太大浪费网络资源,网络收发包效率低下,影响用户体验。首先发送等待请求的时候,发送方就无法给其他主机发送信息,RTO过长会导致发送方资源的浪费。而且响应的时间变慢,网络传输效率低下,影响了用户的体验。
  • 太小浪费了传输的资源,增加网络阻塞。也许是回应包还没有发送到,就重新发送,这样会导致发送了两个相同的数据包,对传输资源是一种浪费。

超时重传的弊端:时间过久,影响用户体验。

快速重传

快速重传的工作原理是收到三个相同的ACK报文时,再发送一次ACK报文下一个的请求报文。

因为TCP连接中基于字节流的特性,报文的传输是有序的,因此响应的ACK所发送过来的序列号就表示该序列号之前的所有报文都已经接收完成。但是后面的响应生死未卜。所以快速重传机制会认为对方没有接收到后续的请求报文,所以重新发送一遍。

但是这种做法存在着问题,你后面的数据包有多少是没有收到的,发送方要重新发送多少数据包,这些是快速重传无法得知的,所以我们有了SACK重传机制。

SACK && D-SACK

SACK(select-acknowledge) 选择性确认,接收方发送ACK响应的时候,会告诉发送方自己接受到了哪些数据。这能够解决快速重传机制存在的问题。D-SACK(Duplicate SACK),D-SACK告诉发送方那些数据是重复发送的。

4.2 滑动窗口

TCP头部信息中的窗口大小(16位)指的就是这个窗口的大小。指的是接受数据的能力。在未接受到接受方的应答(数据还没有被处理)的时候,发送方能发送的数据总容量大小。通常该属性由接收方的窗口大小决定。

发送方的窗口大小参数

  • SND.WND:表示发送窗口的大小(大小是由接收方指定的);
  • SND.UNA(Send Unacknoleged):是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。
  • SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。
  • 指向 #4 的第一个字节是个相对指针,它需要 SND.UNA 指针加上 SND.WND 大小的偏移量,就可以指向 #4 的第一个字节了。可用窗口大小 = **SND.WND -(SND.NXT - SND.UNA)**

4.3 流量控制

TCP一般会使用滑动窗口机制来进行流量控制,简单来讲,就是接收方通过发送窗口的大小来告诉发送方,我还有多少地方可以承受你发的数据,如果太多的话,会造成丢包的情况。

丢包情况

但是在业务繁忙的时候,丢包情况也会发生。很简单的一个例子,当接收方窗口大小发生改变的时候,他会立刻发送报文告知发送方,但是发送方在接收到接收方发送的报文之前,就已经将一条数据容量超过窗口大小的数据发送给接收方,那么这时候就会产生丢包的现象。

死锁情况

这里指的是双方都无从得知对方的状态,从而进入了相对隔离的,无法更新对方信息的状态。那么造成这种结果的原因是:接收方更新窗口大小为非0的报文丢失在了网络中,这种情况下,发送方不知道对方已经处理完成信息了,会一直等待,而接收方也在一直等待发送方发送过来的信息。

  • 解决方法:

只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。这个计时器会在没有收到窗口信息传递报文的一定时间内,发送窗口探测报文,而接收方一旦收到这个报文,就会将自己目前的窗口大小发送。通过外界的力量打破了两者之间相互隔离的状态。

糊涂窗口综合症

这里指的是发送方太过于急于求成,可以算是一种贪心算法。只要接收方告诉发送方自己目前有多少个字节的窗口,那么发送方就会毫不犹豫的发送这么多字节的包过去。

但是这样有一个缺点,万一接收方剩余的窗口很小,而TCP的头部是需要占掉很大一部分的,所以说有时候是一笔得不偿失的交易。

那么为了避免这种情况,发送方针对发送数据的大小做出了规定:

接收方采用的策略如下:

不发送小窗口给发送方

发送方通常的策略如下:

使用 Nagle 算法,该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才可以发送数据:

  • 条件一:要等到窗口大小 >= MSS 并且 数据大小 >= MSS;
  • 条件二:收到之前发送数据的 ack 回包;

4.4 拥塞控制

在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大….

拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的

拥塞窗口 cwnd 变化的规则:

  • 只要网络中没有出现拥塞,cwnd 就会增大;
  • 但网络中出现了拥塞,cwnd 就减少;

拥塞控制主要有如下四个算法:

  • 慢启动
  • 拥塞避免
  • 拥塞发生
  • 快速恢复

慢启动

慢启动的意思就是一点一点的提高发送数据包的数量,当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。慢启动的增长为指数型增长。

有一个叫慢启动门限 ssthresh (slow start threshold)状态变量。

  • cwnd < ssthresh 时,使用慢启动算法。
  • cwnd >= ssthresh 时,就会使用「拥塞避免算法」。

一般来说 ssthresh 的大小是 65535 字节。

拥塞避免

每当收到一个 ACK 时,cwnd 增加 1/cwnd。它属于线性增长,增长较慢。

拥塞发生

就是在拥塞时,将窗口大小紧急下降。

TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:

  • cwnd = cwnd/2 ,也就是设置为原来的一半;
  • ssthresh = cwnd;
  • 进入快速恢复算法

快速恢复

进入快速恢复算法如下:

  • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了);
  • 重传丢失的数据包;
  • 如果再收到重复的 ACK,那么 cwnd 增加 1;
  • 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;

5. TCP优化的连接

5.1 从三次握手的角度进行优化

因为三次握手是服务端和客户端之间建立联系的前提条件,不涉及到任何具体传输数据的传输,而只是进行了一些状态和校验的传输。因为正常情况下的用时基本可以忽略不记。所以我们可以将优化点放在具体传输的异常状态中,去通过调节参数来对三次握手进行加速。

5.1.1 超时重传的参数调节(client)

在网络状态堵塞的情况下,超时重传是不可避免的。但是超时了时候重传几次,采取什么策略。就会对于网络连接的效率起到一定的影响作用。

  • 超时重传的次数过少:可能会引发重复传输,影响网络的资源利用率。只是因为网络拥堵,数据包没有丢失只是到达的时间较慢。而在发送一次请求只会对于本就拥塞的网络进行加塞。
  • 超时重传的次数过多:可能没办法及时发现问题,让一个达不到目的地,没有接收到的连接一直占用着网络资源。导致网络资源利用率的下降。

如何调节超时重传次数:通过控制tcp_syn_retries参数来控制重传次数。在linx内核中默认值为5,每次超时重传的时间为上一次的两倍。初始值为1

5.1.2 半连接队列的参数调节(service)

半连接队列指的是服务端接受到客户端的第一次握手请求发送的时候,会将这个连接的中间态放到半连接队列中,主要用来存储准备启用但尚未启用的连接。那么对于三次握手的SYN攻击指的是通过不断发送只发送第一次请求的连接,将半连接队列占满,使得后面与服务端请求的连接无法获得通讯。那么怎么去尽量避免这种情况呢,有如下两种做法:

  • 调节syn队列的大小:通过调节somaxconn, backlog, tcp_max_syn_conn三个参数来共同调节syn队列的大小,其中,前两个参数是调整accept队列的大小,但是也能够同时调整到半连接队列的大小。
  • 通过设置synCookie参数来进行调节: 这个cookie的主要作用是存储半连接队列中连接的信息。将这个信息连同第二次握手一起发送,这样就可以替代半连接队列的作用了。但是也不能一直开启,一直开启就表示忽略半连接队列,将之前的信息全部再做一次传输,那么服务器的压力就会增大。

syn_cookie有三个值:0表示不开启,1表示当半连接队列满时开启,2表示一直开启。默认为1

5.1.3 全连接队列的参数调节(service)

tcp_abort_on_overflow 共有两个值分别是 0 和 1,其分别表示:

  • 0 :如果 accept 队列满了,那么 server 扔掉 client 发过来的 ack ;
  • 1 :如果 accept 队列满了,server 发送一个 RST 包给 client,表示废掉这个握手过程和这个连接;

accept队列的长度取决于 somaxconnbacklog 之间的最小值,也就是 min(somaxconn, backlog)

5.1.4 如何绕过三次握手

linux3.7 版本后,提供了tcp fast open的方式:

在客户端首次建立连接时的过程:

  1. 客户端发送 SYN 报文,该报文包含 Fast Open 选项,且该选项的 Cookie 为空,这表明客户端请求 Fast Open Cookie;
  2. 支持 TCP Fast Open 的服务器生成 Cookie,并将其置于 SYN-ACK 数据包中的 Fast Open 选项以发回客户端;
  3. 客户端收到 SYN-ACK 后,本地缓存 Fast Open 选项中的 Cookie。

支持 TCP Fast Open 的服务器会对收到 Cookie 进行校验:如果 Cookie 有效,服务器将在 SYN-ACK 报文中对 SYN 和「数据」进行确认,服务器随后将「数据」递送至相应的应用程序;如果 Cookie 无效,服务器将丢弃 SYN 报文中包含的「数据」,且其随后发出的 SYN-ACK 报文将只确认 SYN 的对应序列号;

节约了一个TTL的时间。

5.2 从四次挥手的角度进行优化

img

主动方的优化

主动发起 FIN 报文断开连接的一方,如果迟迟没收到对方的 ACK 回复,则会重传 FIN 报文,重传的次数由 tcp_orphan_retries 参数决定。

当主动方收到 ACK 报文后,连接就进入 FIN_WAIT2 状态,根据关闭的方式不同,优化的方式也不同:

  • 如果这是 close 函数关闭的连接,那么它就是孤儿连接。如果 tcp_fin_timeout 秒内没有收到对方的 FIN 报文,连接就直接关闭。同时,为了应对孤儿连接占用太多的资源,tcp_max_orphans 定义了最大孤儿连接的数量,超过时连接就会直接释放。
  • 反之是 shutdown 函数关闭的连接,则不受此参数限制;

当主动方接收到 FIN 报文,并返回 ACK 后,主动方的连接进入 TIME_WAIT 状态。这一状态会持续 1 分钟,为了防止 TIME_WAIT 状态占用太多的资源,tcp_max_tw_buckets 定义了最大数量,超过时连接也会直接释放。

当 TIME_WAIT 状态过多时,还可以通过设置 tcp_tw_reusetcp_timestamps 为 1 ,将 TIME_WAIT 状态的端口复用于作为客户端的新连接,注意该参数只适用于客户端。

被动方的优化

被动关闭的连接方应对非常简单,它在回复 ACK 后就进入了 CLOSE_WAIT 状态,等待进程调用 close 函数关闭连接。因此,出现大量 CLOSE_WAIT 状态的连接时,应当从应用程序中找问题。

当被动方发送 FIN 报文后,连接就进入 LAST_ACK 状态,在未等到 ACK 时,会在 tcp_orphan_retries 参数的控制下重发 FIN 报文。

5.3 传输过程中进行优化

img

HTTP

1. HTTP的基本概念

1.1 HTTP基本定义

全称: 超文本传输协议

  • 超文本: 针对于传输的内容,不只是文字文件,将内容扩展到了字节流(视频,音频,图片等)内容,只要能用字节传输的内容都可以叫做超文本。
  • 传输:指的是客户端和服务端的关系,是双向的。请求内容的同时也能够成为接受响应的主体。

http的传输是无状态的不安全的,无需身份的认证就可以向对方发送和接受消息。

  • 协议:约定的内容,约定了传输时候的行为和规范。

1.2 HTTP常见状态码

状态码分类 表示含义 常见状态码
1xx 中间状态,正在等待恢复响应
2xx 成功 200 OK - 完全成功 204 Content - 成功但没有响应头body 206 Partial Content - 断点续传或分片中传输一部分成功
3xx 重定向 301 Moved Permanently - 永久重定向,表示资源不存在了 302 Found - 临时重定向,暂时需要用别的URL访问 304 Not Modified - 重定向到缓存,用于缓存服务
4xx 客户端问题 400 Bad Request - 请求问题(笼统) 403 Forbidden - 权限问题 404 Not Found - 路径问题(和3xx对比,这个完全找不到)
5xx 服务端问题 500 Internal Server Error - 服务器错误(笼统) 502 Bad Gateway - 服务器本身没问题,网关问题 503 Service Unavailable - 资源正在加载中

1.3 GET和POST的区别

  • get一般用于资源的读取,也就是只读操作。一般不会有请求体,通过路径参数或者json表单传递即可实现。所以说他的操作是幂等的。对数据安全不会产生影响。所以对于一些不重复修改的数据,我们可以使用缓存技术降低访问数据库的频率,提高系统的性能。

幂等:两次操作后,获取的结果一定是相同的。

  • post一般用于资源的修改或者新增,涉及到数据的写入操作,所以有可能会出现多线程情况下的脏读,重复读,幻读等问题,在高并发的场景下需要通过锁的机制来实现对数据库资源的保护。因为每次都会产生一次修改,所以说post操作不是幂等的,也是不安全的。

2. HTTP缓存技术

缓存技术是为了解决静态资源反复从服务器中加载,但是并没有得到改变,从而消耗服务器性能而提出来的一种解决方案。它可以将读取的静态资源放在内存中,下次可以直接从自己的内存中读取资源。无需耗费服务器性能。常用于静态标签页。、

2.1 强制缓存

当你从服务器上加载了一个网页的时候,他就将该网页加载到你的缓存中,并且设置了过期时间Cache-Control,在过期时间之前,如果没有对浏览器进行强刷新(shirt + F5)的话,是无法看到更新的内容的。

img

2.2 协商缓存

协商缓存时强制缓存失效后的方式。也就是说,只要强制缓存没有过期,协商缓存就不会出现。

有两个变量控制协商缓存的行为EtagLast-Modified

第一个变量Etag 是资源全局唯一标识,可以判断该资源有没有被修改,第二个变量Last-Modified可以判断该资源最后修改的时间。一般先判断第一个变量,在判断第二个变量。如果响应头部中有一个变量,那么就会交给服务器进行决策,判断是200还是304。304表示无需修改,可重定向到缓存,这时继续使用缓存。200表示有更新,则更新界面和缓存。

img

Etag原因是有些服务器判断最后修改时间细粒度不够高,同步性不强。以及有些非资源内部修改操作也会导致资源修改时间被重置。

3. HTTP如何优化

这里的优化指的是通过一些手段来传输时间的减少,从而提升用户使用时候的体验

img

3.1 尽可能避免HTTP请求发送

利用HTTP缓存技术,将热点数据存储在本地,要发送请求的时候先判断该数据是否已经先加载过,如果在本地缓存中查找到对应内容,那么直接读取本地缓存。所以这种方式主要是通过缓存的方式来尽可能避免数据的传输来优化时间上的体验。但是会占用一定的内存存储空间。

3.2 减少HTTP每次请求交互次数

  • 减少重定向次数

当客户端访问的地址迁移的时候,访问的原地址只会给客户端返回302状态码,告诉他下一步要去访问的url,如果多次访问失效的话就会浪费极多次不必要的重定向。那么这时候我们可以采用代理服务器来记录每个服务端口之间的信息,将重定向的工作交由代理服务器完成,降低了一次信息的交换,提高了信息传输的效率。

  • 将碎片数据集中成大文件统一进行传输

主要应用于**.gif**头像小文件的传输,将多张小图片合并为一张大图片进行传输,可以减少客户端和服务端之间交互的次数。同时我们也可以将图片转换为**base64**编码文件附送到url路径中,这样服务端收到请求的时候会自动解析该文件,也减少了一次图片请求的传输。

  • 非紧急文件可延迟发送

常用于分页查询,每次只需要加载一个页面的数据即可,翻页的时候再加载其他资源,可以提高响应的速度

查询100w条数据,如何快速定位到较为后面的页数 –子查询

3.3 减少HTTP请求发送大小

  • 有损压缩
  • 无损压缩

4. RSA四次握手

RSA是HTTP请求前的安全传输技术,因为HTTP主要通过明文传输,会有一定的窃取篡改以及冒充的风险。所以在HTTP层和TCP层之间新增了一层TSL层,而RSA的TSL的主要实现方式之一,主要通过传输前的四次握手实现数据加密,校验机制,身份鉴权来避免以上的风险。

img

4.1 第一次握手

客户端发送client Hello, 发起RSA加密请求, 传输数据有三个

  • TSL Version : TSL加密的版本, 用于和服务端校验统一, 相同TSL版本才可以进行握手
  • Client Random: 客户端随机数, 用于后续加密
  • Cipher Suiter: 支持的密码套件, 用于确认其他加密的方式

4.2 第二次握手

服务端发送server Hello, 发起RSA加密请求, 传输数据有三个

  • TSL Version : TSL加密的版本, 校验
  • Server Random: 服务端随机数, 用于后续加密
  • Cipher Suiter: 从接收到的客户端的密码套件中选择一个

密码套件基本的形式是「密钥交换算法 + 签名算法 + 对称加密算法 + 摘要算法

Cipher Suite: TLS_RSA_WITH_AES_128_GCM_SHA256

  • CA数字证书 : 用于身份校验

4.3 CA数字证书校验流程

数字证书包含的内容:

  • 公钥
  • 持有者信息
  • 信息认证机构(CA)的信息
  • CA采用的签名和算法
  • 证书有效期
  • 额外信息

img

客户端已知: 能通过的CA证书加密之后的hash value1, 服务端发送过来的经过CA机构颁发的私钥加密的数字证书。

客户端通过公钥对于服务端发送过来的加密后的数字证书进行解密,得到hash value2, 将可通过的hash value1列表和hash value2进行对比, 如果相同, 就说明证书是可靠的

证书信任链:根证书信任中间证书,中间证书信任服务端证书,那么客户端信任了根证书,也就相当于信任了服务端证书。

主要目的是将根证书进行隔离,保证信任的安全性。

4.4 第三次握手

客户端首先需要校验发送过来的数字证书是否合法,如果合法,得到公钥pubkey,进行下列操作:

生成随机数pre-master,用pubkey加密传输给服务端;

此时,客户端和服务端双方都共享了三个随机数,分别是 Client Random、Server Random、pre-master

双方利用这三个随机数生成会话密钥,用于后续信息的加密和解密。

之后客户端给服务端发送change cipher spec, 用于通知服务端后续的会话将由通过会话密钥加密的方式传递信息。

最后,finish是最后一步的校验,将前面信息做摘要, 来判断是否前面信息又被篡改。

4.5 第四次握手

服务器也是同样的操作,发 [ Change Cipher Spec ] 和 [ finish ] 消息,如果双方都验证加密和解密没问题,那么握手正式完成。

最后客户端和服务端就会通过加密的方式进行会话。

4.6 RSA算法的缺点

无法保证【前向加密】,针对于第三次握手,因为是用公钥进行加密的pre-master,如果服务端的私钥被破解,那么全部数据就会被截取和破解,会产生冒充的风险。

前向加密:一个密钥只能访问由它所保护的数据;用来产生密钥的元素一次一换,不能再产生其他的密钥;一个密钥被破解,并不影响其他密钥的安全性。

4.7 ECDHE 密钥协商算法

  • 运用了离散对数, 圆锥曲线等算法,保证了数据在加密前的安全性,这是相较于RSA算法的一大突破.
  • RSA算法不支持前向保密机制,而ECDHE密钥协商算法支持前向保密机制。
  • ECDHE算法的客户端可以不用等服务端的最后一次 TLS 握手,就可以提前发出加密的 HTTP 数据,节省了一个消息的往返时间

5. HTTP2的优化点

5.1 HTTP1. 1的缺陷

HTTP/2 针对于HTTP1.1的优化主要体现在其传输效率上,对于其安全性没有做出过多的改变。安全主要靠的是HTTPS协议来进行改善。

那么,HTTP/2主要针对以下几个问题进行改良:

  1. 串行化运行导致传输效率过慢,完成一个HTTP请求之后才能进行下一个
  2. 不支持服务器主动推送信息,单向化
  3. 头部信息过于庞大且重复,浪费空间大

针对以上问题,做出优化点如下:

5.2 头部压缩

针对于HTTP协议的头部大部分的重复特性,HTTP/2编写了编码表,可以类比于我们的哈希结构,以空间换时间,首先是很平常的字段就用静态编码表记录下来,剩下的字段用动态编码表两边各储存一份,到了一定的时间后,就可以只通过发送编码表的序号就可以达到头部的传输功能了。

5.2.1 静态编码表

定义:为高频出现在HTTP协议头部的字段建立了一张key: set表,总共有61组键值对

img

其中index为字段的编号,剩下的为键值对信息。

img

格式如上:首先开头两位为01,表示占位,后面六位index为对应静态编码表中的序号。

第二个字节首位表示Value 是否经过 Huffman 编码,后七个字节表示每个值所需要表示的字节长度。

剩下内容为头部的其他信息,以第二个字节为规定的长度为一个单位。

5.2.2 动态编码表

边存储边积累,将双方链接过程中的所包含的头部信息共同存储为一张动态表,随着时间的推移,后期双方的链接只需要通过传递序号就可以表达所需要传递的头部信息了。但是这样做会造成极大的空间内存损耗。web服务器会提供类似于http2_max_requests这样的字段来限制最大字典表字段数。

5.3 二进制帧

img

帧长度(24bits):表示的是帧数据(Frame Payload)的长度,不包含帧字节

帧类型(8bits):分为数据帧和控制帧两类

标志位(8bits):携带一些控制信息

流标识符(24bits):表示该帧是属于哪个流的,可以用来组装帧。

5.4 并发传输

img

  • connection:表示HTTP连接,是一次服务端和客户端之间的对话联系
  • stream:一个connection中包含了多个stream,这是http/2的优化之处,他保证了http/2的帧可以乱序发送,因为帧的头部信息携带了流标识符,最后传到在重新拼装即可,实现了Http链接之间的并行传输。

但是同一个流之中的帧不可乱序传输,因为帧头没有确定帧先后顺序的标识位。

  • Response Message:响应信息,表示一个Request或者Reponse,由各个帧组成了一条完整的响应信息。

这个模式还突破了原有的全单工模式的传输,服务器和客户端可以同时发送信息了,只要双方各自建立自己的stream,在不同的流上发送即可,极大的提高了传输的效率。

但是这里有一个规定,客户端仅可建立编号为奇数的stream,服务端仅可以建立编号为偶数的stream

5.5 服务器主动推送资源

由于stream的引入,服务器可以自己主动推送资源了。

二叉树

1. 树

1.1 树的基本概念

定义:除了根节点,每个节点有且仅有一个前驱
基本术语:

  • 节点:树上的一个个元素,包含数据元素和指向子树的指针
  • 节点的度: 节点的子树个数
  • 树的度: 树中各节点的度的最大值
  • 叶子节点: 又称终端节点,指度为0的节点
  • 分支节点: 又称非终端节点,指度不为0的节点
  • 孩子: 节点的子树的根
  • 双亲: 孩子的上一个节点
  • 兄弟: 同一个双亲的孩子之间互为兄弟
  • 祖先: 根到某节点路径上的所有节点,都是这个节点的祖先
  • 层次:根为第一层,往下为第二层
  • 高度:树中节点的最大层次

1.2 树的存储结构

  1. 顺序存储结构
    浪费空间,一般不使用
  2. 链式存储结构
  • 孩子存储结构
  • 双亲存储结构
  • 孩子兄弟存储结构

2. 二叉树

定义:
每个节点最多只能有两个子树,即每个子树的度为0,1,2
且有左右之分,不能颠倒

2.1 二叉树的主要性质

  1. 非空二叉树上叶子节点数等于双分支节点数加1
    即:n0 = n2 + 1
    证明:设二叉树上叶子节点数为n0,单分支节点数为n1,双分支节点数为n2;
    则总结点数为n0+n1+n2
    总分支数为n1+2n2
    根据总分支数=总节点数-1,有n0+n1+n2-1 = n1+2n2
    化简后得到:n0 = n2 + 1
  2. 二叉树上的第i层最多有2^(i-1)个节点
  3. 高度为h的二叉树最多有2^h - 1个节点
  4. 在有n个节点的完全二叉树下,如果i为某节点的编号,那么有
  • i/2(向下取整)为其双亲的节点编号
  • 2i为其左孩子编号,2i+1为其右孩子编号(若>n,那么无左右孩子)
  1. 具有n个节点的完全二叉树的高度为log2 n + 1(向下取整)

2.2 二叉树的存储结构

  1. 顺序存储结构
    最适用于完全二叉树,适用于普通二叉树易导致浪费存储空间
  2. 链式存储结构
    用一个节点和两个树之间的关系表示二叉树的链式存储结构
    1
    2
    3
    4
    5
    6
    typedef struct BTNode
    {
    Elemtype data; //数据域
    struct BTNode *lchlid; //左指针域
    struct BTNode *rchlid; //右指针域
    }BTNode;

2.3 二叉树的遍历算法

  • 先序遍历(根 -> 左 -> 右)
    1
    2
    3
    4
    5
    6
    7
    void preorder(BTNode *p) {
    if(p != NULL) {
    visit(p); //假设visit为打印等我们需要的操作
    preorder(p -> lchild); //遍历左子树
    preorder(p -> rchild); //遍历右子树
    }
    }
  • 中序遍历(左 -> 根 -> 右)
    1
    2
    3
    4
    5
    6
    7
    void inorder(BTNode *p) {
    if(p != NULL) {
    inorder(p -> lchild); //遍历左子树
    visit(p); //假设visit为打印等我们需要的操作
    inorder(p -> rchild); //遍历右子树
    }
    }
  • 后序遍历(左 -> 右 -> 根)
    1
    2
    3
    4
    5
    6
    7
    void postorder(BTNode *p) {
    if(p != NULL) {
    postorder(p -> lchild); //遍历左子树
    postorder(p -> rchild); //遍历右子树
    visit(p); //假设visit为打印等我们需要的操作
    }
    }
  • 层序遍历
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    void level(BTNode *p) {
    int front, rear;
    BTNode *queue[maxSize];
    front = rear = 0;
    BTNode *q;
    if(p != NULL) {
    rear = (rear + 1)% maxSize;
    queue[rear] = p;
    while(front != rear) {
    front = (front + 1)% maxSize;
    q = queue[front];
    visit(q);
    if(q -> lchild != NULL) {
    rear = (rear + 1)% maxSize;
    queue[rear] = q -> lchild;
    }
    if(q -> rchild != NULL) {
    rear = (rear + 1)% maxSize;
    queue[rear] = q -> rchild;
    }
    }
    }
    }

2.4 线索二叉树

节点定义:

1
2
3
4
5
6
typedef struct TBTNode {
char data;
int ltag, rtag; //线索标记,判断是子树还是线索
struct TBTNode *lchild;
struct TBTNode *rchild;
}TBTNode;

中序遍历线索二叉树算法:

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
void InThread(TBTNode *p, TBTNode *&pre) {
if(p != null) {
InThread(p -> lchild, pre); // 递归,左子树线索化
if(p -> lchild == NULL) {
// 建立当前节点的前驱线索
p -> lchild = pre;
p -> ltag = 1;
}
if(pre != NULL && pre -> rchlid == NULL) {
// 建立前驱节点的后继线索
p -> rchild = p;
p -> rtag = 1;
}
pre = p;
p = p->rchlid;
InThread(p,pre); // 递归,右子树线索化
}
}

// 建立线索二叉树
void createInThread(TBTNode *root) {
TBTNode *pre = NULL;
if(root != NULL) {
InThread(root, pre);
pre -> rchild = null; //非空二叉树线索化
pre -> rtag = 1; // 处理中序最后一个节点
}
}

2.5 树和二叉树的应用

2.5.1 二叉排序树(BTS)

2.5.1.1 定义

  1. 若其左子树不为空,则左子树上所有关键字的值均不大于根关键字的值;
  2. 若其右子树不为空,则右子树上所有关键字的值均不小于根关键字的值;
  3. 每个根节点下的子树都满足此规则

2.5.1.2 存储结构

1
2
3
4
5
typedef struct BTNode {
int key;
struct BTNode *lchild;
struct BTNode *rchild;
}BTNode;

2.5.1.3 查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BTNode* BSTSearch(BTNode* bt, int key) {
if(bt == NULL)
return NULL;
else {
if(bt->key == key) {
return bt;
}
else if(key < bt->key) {
// 小于根节点关键字时,到左子树查找
return BSTSearch(bt->lchild, key)
}
else {
// 大于根节点关键字时,到右子树查找
return BSTSearch(bt->rchild, key)
}
}
}

2.5.1.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
int BSTInsert(BTNode* &bt, int key) {
// 找到插入位置,创建新节点并插入
if(bt == null) {
bt = (BTNode*)malloc(sizeof(BTNode)); //创建新节点
bt->lchild = bt->rchild = NULL;
bt->key = key;
return 1;
}
else //节点不为空,查找插入位置,和查找算法类似
{
if(key == bt->key) {
// 已经存在值,不用插入
return 0;
}
else if(key < bt->key) {
// 小于根节点关键字时,到左子树查找
return BSTSearch(bt->lchild, key)
}
else {
// 大于根节点关键字时,到右子树查找
return BSTSearch(bt->rchild, key)
}
}
}

2.5.1.4 构造

1
2
3
4
5
6
7
8
void CreateBTS(BTNode *&bt, int key[], int n) {
// 初始化一棵空树
bt = NULL;
for(int i = 0; i < n; i++) {
// 逐个插入节点
BSTInsert(bt, key[i]);
}
}

2.5.1.5 删除

  1. p节点为叶子节点,直接删除
  2. p节点只有左子树或者右子树,直接删除,将子树与原来双亲节点连接即可
  3. p节点既有左子树又有右子树,将其直接前驱(或后继)作为双亲节点的连接点

2.5.2 平衡二叉树(AVL树)

2.5.2.1 定义

是一种特殊的二叉排序树,其左右子树高度的差的绝对值不超过1;其考点主要为平衡调整,共有LL, RR, LR, RL四种类型

2.5.2.2 LL

  1. 当前操作节点是A (A这个节点是最小失衡树的根节点)
  2. 断开该节点的根节点的左孩子连接线 (此时变成了两棵树,设以A为根节点的树为原根树,以B为根节点的树为新根树)
  3. 判断新根树的根节点的右子树是否为空
  • 若空,直接把原根树作为新根树的右子树。
  • 若不空:
    – 将新根树的根节点的右子树独立出来,设其名为新原独树。
    – 把新原独树作为原根树的左子树。
    – 把原根树作为新根树的右子树。

2.5.2.3 RR

  1. 当前操作节点是66 (66这个节点是最小失衡树的根节点)
  2. 断开该节点的右孩子连接线 (此时变成了两棵树,设以66为根节点的树为原根树,以77为根节点的树为新根树)
  3. 判断新根树的根节点的左子树是否为空
  • 若空,直接把原根树作为新根树的左子树。
  • 若不空:
    – 将新根树的根节点的左子树独立出来,设其名为新原独树。
    – 把新原独树作为原根树的右子树。
    – 把原根树作为新根树的左子树。

2.5.2.4 LR

先左旋,再右旋

2.5.2.5 RL

先右旋,再左旋

2.5.3 哈夫曼树与哈夫曼编码

2.5.4 并查集

2.5.5 红黑树

2.5.5.1 性质

  1. 节点包含红黑信息,根节点是黑色
  2. 所有叶子节点都是黑色,标记为NIL节点
  3. 每个红色节点必须有两个黑色的子节点(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  4. 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

Spring_1

Spring_day01

今日目标

  • 掌握Spring相关概念
  • 完成IOC/DI的入门案例编写
  • 掌握IOC的相关配置与使用
  • 掌握DI的相关配置与使用

1,课程介绍

对于一门新技术,我们需要从为什么要学学什么以及怎么学这三个方向入手来学习。那对于Spring来说:

1.1 为什么要学?

  • 从使用和占有率看

    • Spring在市场的占有率与使用率高

    • Spring在企业的技术选型命中率高

    • 所以说,Spring技术是JavaEE开发必备技能,企业开发技术选型命中率>==90%==

      说明:对于未使用Spring的项目一般都是些比较老的项目,大多都处于维护阶段。

  • 从专业角度看

    • 随着时代发展,软件规模与功能都呈几何式增长,开发难度也在不断递增,该如何解决?
      • Spring可以==简化开发==,降低企业级开发的复杂性,使开发变得更简单快捷
    • 随着项目规模与功能的增长,遇到的问题就会增多,为了解决问题会引入更多的框架,这些框架如何协调工作?
      • Spring可以==框架整合==,高效整合其他技术,提高企业级应用开发与运行效率

    综上所述,==Spring是一款非常优秀而且功能强大的框架,不仅要学,而且还要学好。==

1.2 学什么?

从上面的介绍中,我们可以看到Spring框架主要的优势是在简化开发框架整合上,至于如何实现就是咱们要学习Spring框架的主要内容:

  • 简化开发: Spring框架中提供了两个大的核心技术,分别是:

    • ==IOC==
    • ==AOP==
      • ==事务处理==

    1.Spring的简化操作都是基于这两块内容,所以这也是Spring学习中最为重要的两个知识点。

    2.事务处理属于Spring中AOP的具体应用,可以简化项目中的事务管理,也是Spring技术中的一大亮点。

  • 框架整合: Spring在框架整合这块已经做到了极致,它可以整合市面上几乎所有主流框架,比如:

    • ==MyBatis==
    • MyBatis-plus
    • Struts
    • Struts2
    • Hibernate
    • ……

    这些框架中,我们目前只学习了MyBatis,所以在Spring框架的学习中,主要是学习如何整合MyBatis。

    综上所述,对于Spring的学习,主要学习四块内容:

    ==(1)IOC,(2)整合Mybatis(IOC的具体应用),(3)AOP,(4)声明式事务(AOP的具体应用)==

1.3 怎么学?

  • 学习Spring框架设计思想
    • 对于Spring来说,它能迅速占领全球市场,不只是说它的某个功能比较强大,更重要是在它的思想上。
  • 学习基础操作,思考操作与思想间的联系
    • 掌握了Spring的设计思想,然后就需要通过一些基础操作来思考操作与思想之间的关联关系
  • 学习案例,熟练应用操作的同时,体会思想
    • 会了基础操作后,就需要通过大量案例来熟练掌握框架的具体应用,加深对设计思想的理解。

介绍完为什么要学学什么怎么学Spring框架后,大家需要重点掌握的是:

  • Spring很优秀,需要认真重点的学习
  • Spring的学习主线是IOC、AOP、声明式事务和整合MyBais

接下来,咱们就开始进入Spring框架的学习。

2,Spring相关概念

2.1 初识Spring

在这一节,主要通过以下两个点来了解下Spring:

2.1.1 Spring家族

  • 官网:https://spring.io,从官网我们可以大概了解到:

    • Spring能做什么:用以开发web、微服务以及分布式系统等,光这三块就已经占了JavaEE开发的九成多。
    • Spring并不是单一的一个技术,而是一个大家族,可以从官网的Projects中查看其包含的所有技术。
  • Spring发展到今天已经形成了一种开发的生态圈,Spring提供了若干个项目,每个项目用于完成特定的功能。

    • Spring已形成了完整的生态圈,也就是说我们可以完全使用Spring技术完成整个项目的构建、设计与开发。

    • Spring有若干个项目,可以根据需要自行选择,把这些个项目组合起来,起了一个名称叫==全家桶==,如下图所示

      说明:

      图中的图标都代表什么含义,可以进入https://spring.io/projects网站进行对比查看。

      这些技术并不是所有的都需要学习,额外需要重点关注Spring FrameworkSpringBootSpringCloud:

      • Spring Framework:Spring框架,是Spring中最早最核心的技术,也是所有其他技术的基础。
      • SpringBoot:Spring是来简化开发,而SpringBoot是来帮助Spring在简化的基础上能更快速进行开发。
      • SpringCloud:这个是用来做分布式之微服务架构的相关开发。

      除了上面的这三个技术外,还有很多其他的技术,也比较流行,如SpringData,SpringSecurity等,这些都可以被应用在我们的项目中。我们今天所学习的Spring其实指的是==Spring Framework==。

2.1.2 了解Spring发展史

接下来我们介绍下Spring Framework这个技术是如何来的呢?

Spring发展史

  • IBM(IT公司-国际商业机器公司)在1997年提出了EJB思想,早期的JAVAEE开发大都基于该思想。
  • Rod Johnson(Java和J2EE开发领域的专家)在2002年出版的Expert One-on-One J2EE Design and Development,书中有阐述在开发中使用EJB该如何做。
  • Rod Johnson在2004年出版的Expert One-on-One J2EE Development without EJB,书中提出了比EJB思想更高效的实现方案,并且在同年将方案进行了具体的落地实现,这个实现就是Spring1.0。
  • 随着时间推移,版本不断更新维护,目前最新的是Spring5
    • Spring1.0是纯配置文件开发
    • Spring2.0为了简化开发引入了注解开发,此时是配置文件加注解的开发方式
    • Spring3.0已经可以进行纯注解开发,使开发效率大幅提升,我们的课程会以注解开发为主
    • Spring4.0根据JDK的版本升级对个别API进行了调整
    • Spring5.0已经全面支持JDK8,现在Spring最新的是5系列所以建议大家把JDK安装成1.8版

本节介绍了Spring家族与Spring的发展史,需要大家重点掌握的是:

  • 今天所学的Spring其实是Spring家族中的Spring Framework
  • Spring Framework是Spring家族中其他框架的底层基础,学好Spring可以为其他Spring框架的学习打好基础

2.2 Spring系统架构

前面我们说spring指的是Spring Framework,那么它其中都包含哪些内容以及我们该如何学习这个框架?

针对这些问题,我们将从系统架构图课程学习路线来进行说明:

2.2.1 系统架构图

  • Spring Framework是Spring生态圈中最基础的项目,是其他项目的根基。

  • Spring Framework的发展也经历了很多版本的变更,每个版本都有相应的调整

  • Spring Framework的5版本目前没有最新的架构图,而最新的是4版本,所以接下来主要研究的是4的架构图

    (1)核心层

    • Core Container:核心容器,这个模块是Spring最核心的模块,其他的都需要依赖该模块

    (2)AOP层

    • AOP:面向切面编程,它依赖核心层容器,目的是==在不改变原有代码的前提下对其进行功能增强==
    • Aspects:AOP是思想,Aspects是对AOP思想的具体实现

    (3)数据层

    • Data Access:数据访问,Spring全家桶中有对数据访问的具体实现技术
    • Data Integration:数据集成,Spring支持整合其他的数据层解决方案,比如Mybatis
    • Transactions:事务,Spring中事务管理是Spring AOP的一个具体实现,也是后期学习的重点内容

    (4)Web层

    • 这一层的内容将在SpringMVC框架具体学习

    (5)Test层

    • Spring主要整合了Junit来完成单元测试和集成测试

2.2.2 课程学习路线

介绍完Spring的体系结构后,从中我们可以得出对于Spring的学习主要包含四部分内容,分别是:

  • ==Spring的IOC/DI==
  • ==Spring的AOP==
  • ==AOP的具体应用,事务管理==
  • ==IOC/DI的具体应用,整合Mybatis==

对于这节的内容,大家重点要记住的是Spring需要学习的四部分内容。接下来就从第一部分开始学起。

2.3 Spring核心概念

在Spring核心概念这部分内容中主要包含IOC/DIIOC容器Bean,那么问题就来了,这些都是什么呢?

2.3.1 目前项目中的问题

一个概念的提出,总是因为有对应的问题需要去解决,那么我们就需要先分析下目前咱们代码在编写过程中遇到的问题:

(1)业务层需要调用数据层的方法,就需要在业务层new数据层的对象

(2)如果数据层的实现类发生变化,那么业务层的代码也需要跟着改变,发生变更后,都需要进行编译打包和重部署

(3)所以,现在代码在编写的过程中存在的问题是:== 耦合度偏高 ==

针对这个问题,该如何解决呢?

我们就想,如果能把框中的内容给去掉,不就可以降低依赖了么,但是又会引入新的问题,去掉以后程序能运行么?

答案肯定是不行,因为bookDao没有赋值为Null,强行运行就会出空指针异常。

所以现在的问题就是,业务层不想new对象,运行的时候又需要这个对象,该咋办呢?

针对这个问题,Spring就提出了一个解决方案:

  • 使用对象时,在程序中不要主动使用new产生对象,转换为由==外部==提供对象

这种实现思就是Spring的一个核心概念

2.3.2 IOC、IOC容器、Bean、DI

  1. ==IOC(Inversion of Control)控制反转==

(1)什么是控制反转呢?

  • 使用对象时,由主动new产生对象转换为由==外部==提供对象,此过程中对象创建控制权由程序转移到外部,此思想称为控制反转。
    • 业务层要用数据层的类对象,以前是自己new
    • 现在自己不new了,交给别人[外部]来创建对象
    • 别人[外部]就反转控制了数据层对象的创建权
    • 这种思想就是控制反转
    • 别人[外部]指定是什么呢?继续往下学

(2)Spring和IOC之间的关系是什么呢?

  • Spring技术对IOC思想进行了实现
  • Spring提供了一个容器,称为==IOC容器==,用来充当IOC思想中的”外部”, IOC容器是IOC思想的具体实现
  • IOC思想中的别人[外部]指的就是Spring的IOC容器

(3)IOC容器的作用以及内部存放的是什么?

  • IOC容器负责对象的创建、初始化等一系列工作,其中包含了数据层和业务层的类对象
  • 被创建或被管理的对象在IOC容器中统称为**==Bean==**
  • IOC容器中放的就是一个个的Bean对象

(4)当IOC容器中创建好service和dao对象后,程序能正确执行么?

  • 不行,因为service运行需要依赖dao对象
  • IOC容器中虽然有service和dao对象
  • 但是service对象和dao对象没有任何关系
  • 需要把dao对象交给service,也就是说要绑定service和dao对象之间的关系

像这种在容器中建立对象与对象之间的绑定关系就要用到DI:

  1. ==DI(Dependency Injection)依赖注入==

(1)什么是依赖注入呢?

  • 在容器中建立bean与bean之间的依赖关系的整个过程,称为依赖注入
    • 业务层要用数据层的类对象,以前是自己new
    • 现在自己不new了,靠别人[外部其实指的就是IOC容器]来给注入进来
    • 这种思想就是依赖注入

(2)IOC容器中哪些bean之间要建立依赖关系呢?

  • 这个需要程序员根据业务需求提前建立好关系,如业务层需要依赖数据层,service就要和dao建立依赖关系

介绍完Spring的IOC和DI的概念后,我们会发现这两个概念的最终目标就是:==充分解耦==,具体实现靠:

  • 使用IOC容器管理bean(IOC)
  • 在IOC容器内将有依赖关系的bean进行关系绑定(DI)
  • 最终结果为:使用对象时不仅可以直接从IOC容器中获取,并且获取到的bean已经绑定了所有的依赖关系.

2.3.3 核心概念小结

这节比较重要,重点要理解什么是IOC/DI思想什么是IOC容器什么是Bean

(1)什么IOC/DI思想?

  • IOC:控制反转,控制反转的是对象的创建权
  • DI:依赖注入,绑定对象与对象之间的依赖关系

(2)什么是IOC容器?

Spring创建了一个容器用来存放所创建的对象,这个容器就叫IOC容器

(3)什么是Bean?

容器中所存放的一个个对象就叫Bean或Bean对象

3,入门案例

介绍完Spring的核心概念后,接下来我们得思考一个问题就是,Spring到底是如何来实现IOC和DI的,那接下来就通过一些简单的入门案例,来演示下具体实现过程:

3.1 IOC入门案例

对于入门案例,我们得先分析思路然后再代码实现

3.1.1 入门案例思路分析

(1)Spring是使用容器来管理bean对象的,那么管什么?

  • 主要管理项目中所使用到的类对象,比如(Service和Dao)

(2)如何将被管理的对象告知IOC容器?

  • 使用配置文件

(3)被管理的对象交给IOC容器,要想从容器中获取对象,就先得思考如何获取到IOC容器?

  • Spring框架提供相应的接口

(4)IOC容器得到后,如何从容器中获取bean?

  • 调用Spring框架提供对应接口中的方法

(5)使用Spring导入哪些坐标?

  • 用别人的东西,就需要在pom.xml添加对应的依赖

3.1.2 入门案例代码实现

需求分析:将BookServiceImpl和BookDaoImpl交给Spring管理,并从容器中获取对应的bean对象进行方法调用。

1.创建Maven的java项目

2.pom.xml添加Spring的依赖jar包

3.创建BookService,BookServiceImpl,BookDao和BookDaoImpl四个类

4.resources下添加spring配置文件,并完成bean的配置

5.使用Spring提供的接口完成IOC容器的创建

6.从容器中获取对象进行方法调用

步骤1:创建Maven项目

步骤2:添加Spring的依赖jar包

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
步骤3:添加案例中需要的类

创建BookService,BookServiceImpl,BookDao和BookDaoImpl四个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface BookDao {
public void save();
}
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
}
public interface BookService {
public void save();
}
public class BookServiceImpl implements BookService {
private BookDao bookDao = new BookDaoImpl();
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
步骤4:添加spring配置文件

resources下添加spring配置文件applicationContext.xml,并完成bean的配置

步骤5:在配置文件中完成bean的配置
1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<!--bean标签标示配置bean
id属性标示给bean起名字
class属性表示给bean定义类型
-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl"/>

</beans>

==注意事项:bean定义时id属性在同一个上下文中(配置文件)不能重复==

步骤6:获取IOC容器

使用Spring提供的接口完成IOC容器的创建,创建App类,编写main方法

1
2
3
4
5
6
public class App {
public static void main(String[] args) {
//获取IOC容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
}
}
步骤7:从容器中获取对象进行方法调用
1
2
3
4
5
6
7
8
9
10
public class App {
public static void main(String[] args) {
//获取IOC容器
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
// BookDao bookDao = (BookDao) ctx.getBean("bookDao");
// bookDao.save();
BookService bookService = (BookService) ctx.getBean("bookService");
bookService.save();
}
}
步骤8:运行程序

测试结果为:

Spring的IOC入门案例已经完成,但是在BookServiceImpl的类中依然存在BookDaoImpl对象的new操作,它们之间的耦合度还是比较高,这块该如何解决,就需要用到下面的DI:依赖注入

3.2 DI入门案例

对于DI的入门案例,我们依然先分析思路然后再代码实现

3.2.1 入门案例思路分析

(1)要想实现依赖注入,必须要基于IOC管理Bean

  • DI的入门案例要依赖于前面IOC的入门案例

(2)Service中使用new形式创建的Dao对象是否保留?

  • 需要删除掉,最终要使用IOC容器中的bean对象

(3)Service中需要的Dao对象如何进入到Service中?

  • 在Service中提供方法,让Spring的IOC容器可以通过该方法传入bean对象

(4)Service与Dao间的关系如何描述?

  • 使用配置文件

3.2.2 入门案例代码实现

需求:基于IOC入门案例,在BookServiceImpl类中删除new对象的方式,使用Spring的DI完成Dao层的注入

1.删除业务层中使用new的方式创建的dao对象

2.在业务层提供BookDao的setter方法

3.在配置文件中添加依赖注入的配置

4.运行程序调用方法

步骤1: 去除代码中的new

在BookServiceImpl类中,删除业务层中使用new的方式创建的dao对象

1
2
3
4
5
6
7
8
9
public class BookServiceImpl implements BookService {
//删除业务层中使用new的方式创建的dao对象
private BookDao bookDao;

public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
步骤2:为属性提供setter方法

在BookServiceImpl类中,为BookDao提供setter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class BookServiceImpl implements BookService {
//删除业务层中使用new的方式创建的dao对象
private BookDao bookDao;

public void save() {
System.out.println("book service save ...");
bookDao.save();
}
//提供对应的set方法
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
}

步骤3:修改配置完成注入

在配置文件中添加依赖注入的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--bean标签标示配置bean
id属性标示给bean起名字
class属性表示给bean定义类型
-->
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>

<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<!--配置server与dao的关系-->
<!--property标签表示配置当前bean的属性
name属性表示配置哪一个具体的属性
ref属性表示参照哪一个bean
-->
<property name="bookDao" ref="bookDao"/>
</bean>

</beans>

==注意:配置中的两个bookDao的含义是不一样的==

  • name=”bookDao”中bookDao的作用是让Spring的IOC容器在获取到名称后,将首字母大写,前面加set找对应的setBookDao()方法进行对象注入
  • ref=”bookDao”中bookDao的作用是让Spring能在IOC容器中找到id为bookDao的Bean对象给bookService进行注入
  • 综上所述,对应关系如下:

步骤4:运行程序

运行,测试结果为:

4,IOC相关内容

通过前面两个案例,我们已经学习了bean如何定义配置DI如何定义配置以及容器对象如何获取的内容,接下来主要是把这三块内容展开进行详细的讲解,深入的学习下这三部分的内容,首先是bean基础配置。

4.1 bean基础配置

对于bean的配置中,主要会讲解bean基础配置,bean的别名配置,bean的作用范围配置==(重点)==,这三部分内容:

4.1.1 bean基础配置(id与class)

对于bean的基础配置,在前面的案例中已经使用过:

1
<bean id="" class=""/>

其中,bean标签的功能、使用方式以及id和class属性的作用,我们通过一张图来描述下

这其中需要大家重点掌握的是:==bean标签的id和class属性的使用==。

思考:

  • class属性能不能写接口如BookDao的类全名呢?

答案肯定是不行,因为接口是没办法创建对象的。

  • 前面提过为bean设置id时,id必须唯一,但是如果由于命名习惯而产生了分歧后,该如何解决?

在解决这个问题之前,我们需要准备下开发环境,对于开发环境我们可以有两种解决方案:

  • 使用前面IOC和DI的案例

  • 重新搭建一个新的案例环境,目的是方便大家查阅代码

    • 搭建的内容和前面的案例是一样的,内容如下:

4.1.2 bean的name属性

环境准备好后,接下来就可以在这个环境的基础上来学习下bean的别名配置,

首先来看下别名的配置说明:

步骤1:配置别名

打开spring的配置文件applicationContext.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<!--name:为bean指定别名,别名可以有多个,使用逗号,分号,空格进行分隔-->
<bean id="bookService" name="service service4 bookEbi" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>

<!--scope:为bean设置作用范围,可选值为单例singloton,非单例prototype-->
<bean id="bookDao" name="dao" class="com.itheima.dao.impl.BookDaoImpl"/>
</beans>

说明:Ebi全称Enterprise Business Interface,翻译为企业业务接口

步骤2:根据名称容器中获取bean对象
1
2
3
4
5
6
7
8
public class AppForName {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
//此处根据bean标签的id属性和name属性的任意一个值来获取bean对象
BookService bookService = (BookService) ctx.getBean("service4");
bookService.save();
}
}
步骤3:运行程序

测试结果为:

==注意事项:==

  • bean依赖注入的ref属性指定bean,必须在容器中存在

  • 如果不存在,则会报错,如下:

    这个错误大家需要特别关注下:

    获取bean无论是通过id还是name获取,如果无法获取到,将抛出异常==NoSuchBeanDefinitionException==

4.1.3 bean作用范围scope配置

关于bean的作用范围是bean属性配置的一个==重点==内容。

看到这个作用范围,我们就得思考bean的作用范围是来控制bean哪块内容的?

我们先来看下bean作用范围的配置属性:

4.1.3.1 验证IOC容器中对象是否为单例
验证思路

​ 同一个bean获取两次,将对象打印到控制台,看打印出的地址值是否一致。

具体实现
  • 创建一个AppForScope的类,在其main方法中来验证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class AppForScope {
    public static void main(String[] args) {
    ApplicationContext ctx = new
    ClassPathXmlApplicationContext("applicationContext.xml");

    BookDao bookDao1 = (BookDao) ctx.getBean("bookDao");
    BookDao bookDao2 = (BookDao) ctx.getBean("bookDao");
    System.out.println(bookDao1);
    System.out.println(bookDao2);
    }
    }
  • 打印,观察控制台的打印结果

  • 结论:默认情况下,Spring创建的bean对象都是单例的

获取到结论后,问题就来了,那如果我想创建出来非单例的bean对象,该如何实现呢?

4.1.3.2 配置bean为非单例

在Spring配置文件中,配置scope属性来实现bean的非单例创建

  • 在Spring的配置文件中,修改<bean>的scope属性

    1
    <bean id="bookDao" name="dao" class="com.itheima.dao.impl.BookDaoImpl" scope=""/>
  • 将scope设置为singleton

    1
    <bean id="bookDao" name="dao" class="com.itheima.dao.impl.BookDaoImpl" scope="singleton"/>

    运行AppForScope,打印看结果

  • 将scope设置为prototype

    1
    <bean id="bookDao" name="dao" class="com.itheima.dao.impl.BookDaoImpl" scope="prototype"/>

    运行AppForScope,打印看结果

  • 结论,使用bean的scope属性可以控制bean的创建是否为单例:

    • singleton默认为单例
    • prototype为非单例
4.1.3.3 scope使用后续思考

介绍完scope属性以后,我们来思考几个问题:

  • 为什么bean默认为单例?

    • bean为单例的意思是在Spring的IOC容器中只会有该类的一个对象
    • bean对象只有一个就避免了对象的频繁创建与销毁,达到了bean对象的复用,性能高
  • bean在容器中是单例的,会不会产生线程安全问题?

    • 如果对象是有状态对象,即该对象有成员变量可以用来存储数据的,

      因为所有请求线程共用一个bean对象,所以会存在线程安全问题。

    • 如果对象是无状态对象,即该对象没有成员变量没有进行数据存储的,

      因方法中的局部变量在方法调用完成后会被销毁,所以不会存在线程安全问题。

  • 哪些bean对象适合交给容器进行管理?

    • 表现层对象
    • 业务层对象
    • 数据层对象
    • 工具对象
  • 哪些bean对象不适合交给容器进行管理?

    • 封装实例的域对象,因为会引发线程安全问题,所以不适合。

4.14 bean基础配置小结

关于bean的基础配置中,需要大家掌握以下属性:

4.2 bean实例化

对象已经能交给Spring的IOC容器来创建了,但是容器是如何来创建对象的呢?

就需要研究下bean的实例化过程,在这块内容中主要解决两部分内容,分别是

  • bean是如何创建的
  • 实例化bean的三种方式,构造方法(重点),静态工厂实例工厂()FactoryBean()(重点)

在讲解这三种创建方式之前,我们需要先确认一件事:

bean本质上就是对象,对象在new的时候会使用构造方法完成,那创建bean也是使用构造方法完成的。

基于这个知识点出发,我们来验证spring中bean的三种创建方式,

4.2.1 环境准备

为了方便大家阅读代码,重新准备个开发环境,

  • 创建一个Maven项目
  • pom.xml添加依赖
  • resources下添加spring的配置文件applicationContext.xml

这些步骤和前面的都一致,大家可以快速的拷贝即可,最终项目的结构如下:

4.2.2 构造方法实例化

在上述的环境下,我们来研究下Spring中的第一种bean的创建方式构造方法实例化:

步骤1:准备需要被创建的类

准备一个BookDao和BookDaoImpl类

1
2
3
4
5
6
7
8
9
10
11

public interface BookDao {
public void save();
}

public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}

}
步骤2:将类配置到Spring容器
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>

</beans>
步骤3:编写运行程序
1
2
3
4
5
6
7
8
9
public class AppForInstanceBook {
public static void main(String[] args) {
ApplicationContext ctx = new
ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
bookDao.save();

}
}
步骤4:类中提供构造函数测试

在BookDaoImpl类中添加一个无参构造函数,并打印一句话,方便观察结果。

1
2
3
4
5
6
7
8
9
public class BookDaoImpl implements BookDao {
public BookDaoImpl() {
System.out.println("book dao constructor is running ....");
}
public void save() {
System.out.println("book dao save ...");
}

}

运行程序,如果控制台有打印构造函数中的输出,说明Spring容器在创建对象的时候也走的是构造函数

步骤5:将构造函数改成private测试
1
2
3
4
5
6
7
8
9
public class BookDaoImpl implements BookDao {
private BookDaoImpl() {
System.out.println("book dao constructor is running ....");
}
public void save() {
System.out.println("book dao save ...");
}

}

运行程序,能执行成功,说明内部走的依然是构造函数,能访问到类中的私有构造方法,因为无论私有公有都能够被访问,所以我们可以说,Spring底层用的是反射

步骤6:构造函数中添加一个参数测试
1
2
3
4
5
6
7
8
9
public class BookDaoImpl implements BookDao {
private BookDaoImpl(int i) {
System.out.println("book dao constructor is running ....");
}
public void save() {
System.out.println("book dao save ...");
}

}

运行程序,

程序会报错,说明Spring底层使用的是类的无参构造方法

(原因:有参构造器通常意味着类要进行实例化,而实例化的类在IOC的单例模式下很容易造成线程安全问题,所以IOC拒绝访问有参构造器生成的对象)

底层是运用反射获取类的无参构造器来构造类的单例对象。

4.2.3 分析Spring的错误信息(简单看看就好了)

接下来,我们主要研究下Spring的报错信息来学一学如阅读。

  • 错误信息从下往上依次查看,因为上面的错误大都是对下面错误的一个包装,最核心错误是在最下面

    Caused by: java.lang.NoSuchMethodException: com.itheima.dao.impl.BookDaoImpl.<init>()

    • Caused by 翻译为引起,即出现错误的原因
    • java.lang.NoSuchMethodException:抛出的异常为没有这样的方法异常
    • com.itheima.dao.impl.BookDaoImpl.<init>():哪个类的哪个方法没有被找到导致的异常,<init>()指定是类的构造方法,即该类的无参构造方法

如果最后一行错误获取不到错误信息,接下来查看第二层:

Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.itheima.dao.impl.BookDaoImpl]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.itheima.dao.impl.BookDaoImpl.<init>()

  • nested:嵌套的意思,后面的异常内容和最底层的异常是一致的
    • Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.itheima.dao.impl.BookDaoImpl]: No default constructor found;
      • Caused by: 引发
      • BeanInstantiationException:翻译为bean实例化异常
      • No default constructor found:没有一个默认的构造函数被发现

看到这其实错误已经比较明显,给大家个练习,把倒数第三层的错误分析下吧:

Exception in thread “main” org.springframework.beans.factory.BeanCreationException: Error creating bean with name ‘bookDao’ defined in class path resource [applicationContext.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.itheima.dao.impl.BookDaoImpl]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.itheima.dao.impl.BookDaoImpl.<init>()。

至此,关于Spring的构造方法实例化就已经学习完了,因为每一个类默认都会提供一个无参构造函数,所以其实真正在使用这种方式的时候,我们什么也不需要做。这也是我们以后比较常用的一种方式。

4.2.4 静态工厂实例化

接下来研究Spring中的第二种bean的创建方式静态工厂实例化:

4.2.4.1 工厂方式创建bean

在讲这种方式之前,我们需要先回顾一个知识点是使用工厂来创建对象的方式:

(1)准备一个OrderDao和OrderDaoImpl类

1
2
3
4
5
6
7
8
9
public interface OrderDao {
public void save();
}

public class OrderDaoImpl implements OrderDao {
public void save() {
System.out.println("order dao save ...");
}
}

(2)创建一个工厂类OrderDaoFactory并提供一个==静态方法==

1
2
3
4
5
6
//静态工厂创建对象
public class OrderDaoFactory {
public static OrderDao getOrderDao(){
return new OrderDaoImpl();
}
}

(3)编写AppForInstanceOrder运行类,在类中通过工厂获取对象

1
2
3
4
5
6
7
public class AppForInstanceOrder {
public static void main(String[] args) {
//通过静态工厂创建对象
OrderDao orderDao = OrderDaoFactory.getOrderDao();
orderDao.save();
}
}

(4)运行后,可以查看到结果

如果代码中对象是通过上面的这种方式来创建的,如何将其交给Spring来管理呢?

4.2.4.2 静态工厂实例化

这就要用到Spring中的静态工厂实例化的知识了,具体实现步骤为:

(1)在spring的配置文件application.properties中添加以下内容:

1
<bean id="orderDao" class="com.itheima.factory.OrderDaoFactory" factory-method="getOrderDao"/>

class:工厂类的类全名

factory-mehod:具体工厂类中创建对象的方法名

对应关系如下图:

(2)在AppForInstanceOrder运行类,使用从IOC容器中获取bean的方法进行运行测试

1
2
3
4
5
6
7
8
9
10
public class AppForInstanceOrder {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");

OrderDao orderDao = (OrderDao) ctx.getBean("orderDao");

orderDao.save();

}
}

(3)运行后,可以查看到结果

看到这,可能有人会问了,你这种方式在工厂类中不也是直接new对象的,和我自己直接new没什么太大的区别,而且静态工厂的方式反而更复杂,这种方式的意义是什么?

主要的原因是:

  • 在工厂的静态方法中,我们除了new对象还可以做其他的一些业务操作,这些操作必不可少,如:
1
2
3
4
5
6
public class OrderDaoFactory {
public static OrderDao getOrderDao(){
System.out.println("factory setup....");//模拟必要的业务操作
return new OrderDaoImpl();
}
}

之前new对象的方式就无法添加其他的业务内容,重新运行,查看结果:

介绍完静态工厂实例化后,这种方式一般是用来兼容早期的一些老系统,所以==了解为主==。

4.2.5 实例工厂与FactoryBean

接下来继续来研究Spring的第三种bean的创建方式实例工厂实例化:

4.2.3.1 环境准备

(1)准备一个UserDao和UserDaoImpl类

1
2
3
4
5
6
7
8
9
10
public interface UserDao {
public void save();
}

public class UserDaoImpl implements UserDao {

public void save() {
System.out.println("user dao save ...");
}
}

(2)创建一个工厂类OrderDaoFactory并提供一个普通方法,注意此处和静态工厂的工厂类不一样的地方是方法不是静态方法

1
2
3
4
5
public class UserDaoFactory {
public UserDao getUserDao(){
return new UserDaoImpl();
}
}

(3)编写AppForInstanceUser运行类,在类中通过工厂获取对象

1
2
3
4
5
6
7
8
public class AppForInstanceUser {
public static void main(String[] args) {
//创建实例工厂对象
UserDaoFactory userDaoFactory = new UserDaoFactory();
//通过实例工厂对象创建对象
UserDao userDao = userDaoFactory.getUserDao();
userDao.save();
}

(4)运行后,可以查看到结果

对于上面这种实例工厂的方式如何交给Spring管理呢?

4.2.3.2 实例工厂实例化

具体实现步骤为:

(1)在spring的配置文件中添加以下内容:

1
2
<bean id="userFactory" class="com.itheima.factory.UserDaoFactory"/>
<bean id="userDao" factory-method="getUserDao" factory-bean="userFactory"/>

实例化工厂运行的顺序是:

  • 创建实例化工厂对象,对应的是第一行配置

  • 调用对象中的方法来创建bean,对应的是第二行配置

    • factory-bean:工厂的实例对象

    • factory-method:工厂对象中的具体创建对象的方法名,对应关系如下:

factory-mehod:具体工厂类中创建对象的方法名

(2)在AppForInstanceUser运行类,使用从IOC容器中获取bean的方法进行运行测试

1
2
3
4
5
6
7
8
public class AppForInstanceUser {
public static void main(String[] args) {
ApplicationContext ctx = new
ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = (UserDao) ctx.getBean("userDao");
userDao.save();
}
}

(3)运行后,可以查看到结果

实例工厂实例化的方式就已经介绍完了,配置的过程还是比较复杂,所以Spring为了简化这种配置方式就提供了一种叫FactoryBean的方式来简化开发。

4.2.3.3 FactoryBean的使用

具体的使用步骤为:

(1)创建一个UserDaoFactoryBean的类,实现FactoryBean接口,重写接口的方法

1
2
3
4
5
6
7
8
9
10
public class UserDaoFactoryBean implements FactoryBean<UserDao> {
//代替原始实例工厂中创建对象的方法
public UserDao getObject() throws Exception {
return new UserDaoImpl();
}
//返回所创建类的Class对象
public Class<?> getObjectType() {
return UserDao.class;
}
}

(2)在Spring的配置文件中进行配置

1
<bean id="userDao" class="com.itheima.factory.UserDaoFactoryBean"/>

(3)AppForInstanceUser运行类不用做任何修改,直接运行

这种方式在Spring去整合其他框架的时候会被用到,所以这种方式需要大家理解掌握。

查看源码会发现,FactoryBean接口其实会有三个方法,分别是:

1
2
3
4
5
6
7
T getObject() throws Exception;

Class<?> getObjectType();

default boolean isSingleton() {
return true;
}

方法一:getObject(),被重写后,在方法中进行对象的创建并返回

方法二:getObjectType(),被重写后,主要返回的是被创建类的Class对象

方法三:没有被重写,因为它已经给了默认值,从方法名中可以看出其作用是设置对象是否为单例,默认true,从意思上来看,我们猜想默认应该是单例,如何来验证呢?

思路很简单,就是从容器中获取该对象的多个值,打印到控制台,查看是否为同一个对象。

1
2
3
4
5
6
7
8
9
10
public class AppForInstanceUser {
public static void main(String[] args) {
ApplicationContext ctx = new
ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao1 = (UserDao) ctx.getBean("userDao");
UserDao userDao2 = (UserDao) ctx.getBean("userDao");
System.out.println(userDao1);
System.out.println(userDao2);
}
}

打印结果,如下:

通过验证,会发现默认是单例,那如果想改成单例具体如何实现?

只需要将isSingleton()方法进行重写,修改返回为false,即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//FactoryBean创建对象
public class UserDaoFactoryBean implements FactoryBean<UserDao> {
//代替原始实例工厂中创建对象的方法
public UserDao getObject() throws Exception {
return new UserDaoImpl();
}

public Class<?> getObjectType() {
return UserDao.class;
}

public boolean isSingleton() {
return false;
}
}

重新运行AppForInstanceUser,查看结果

从结果中可以看出现在已经是非单例了,但是一般情况下我们都会采用单例,也就是采用默认即可。所以isSingleton()方法一般不需要进行重写。

4.2.6 bean实例化小结

通过这一节的学习,需要掌握:

(1)bean是如何创建的呢?

1
构造方法

(2)Spring的IOC实例化对象的三种方式分别是:

  • 构造方法(常用)
  • 静态工厂(了解)
  • 实例工厂(了解)
    • FactoryBean(实用)

这些方式中,重点掌握构造方法FactoryBean即可。

需要注意的一点是,构造方法在类中默认会提供,但是如果重写了构造方法,默认的就会消失,在使用的过程中需要注意,如果需要重写构造方法,最好把默认的构造方法也重写下。

4.3 bean的生命周期

关于bean的相关知识还有最后一个是bean的生命周期,对于生命周期,我们主要围绕着bean生命周期控制来讲解:

  • 首先理解下什么是生命周期?
    • 从创建到消亡的完整过程,例如人从出生到死亡的整个过程就是一个生命周期。
  • bean生命周期是什么?
    • bean对象从创建到销毁的整体过程。
  • bean生命周期控制是什么?
    • 在bean创建后到销毁前做一些事情。

现在我们面临的问题是如何在bean的创建之后和销毁之前把我们需要添加的内容添加进去。

4.3.1 环境准备

还是老规矩,为了方便大家后期代码的阅读,我们重新搭建下环境:

  • 创建一个Maven项目
  • pom.xml添加依赖
  • resources下添加spring的配置文件applicationContext.xml

这些步骤和前面的都一致,大家可以快速的拷贝即可,最终项目的结构如下:

(1)项目中添加BookDao、BookDaoImpl、BookService和BookServiceImpl类

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
public interface BookDao {
public void save();
}

public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
}

public interface BookService {
public void save();
}

public class BookServiceImpl implements BookService{
private BookDao bookDao;

public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}

public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}

(2)resources下提供spring的配置文件

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
</beans>

(3)编写AppForLifeCycle运行类,加载Spring的IOC容器,并从中获取对应的bean对象

1
2
3
4
5
6
7
8
public class AppForLifeCycle {
public static void main( String[] args ) {
ApplicationContext ctx = new
ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
bookDao.save();
}
}

4.3.2 生命周期设置

接下来,在上面这个环境中来为BookDao添加生命周期的控制方法,具体的控制有两个阶段:

  • bean创建之后,想要添加内容,比如用来初始化需要用到资源
  • bean销毁之前,想要添加内容,比如用来释放用到的资源
步骤1:添加初始化和销毁方法

针对这两个阶段,我们在BooDaoImpl类中分别添加两个方法,==方法名任意==

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
//表示bean初始化对应的操作
public void init(){
System.out.println("init...");
}
//表示bean销毁前对应的操作
public void destory(){
System.out.println("destory...");
}
}
步骤2:配置生命周期

在配置文件添加配置,如下:

1
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl" init-method="init" destroy-method="destory"/>
步骤3:运行程序

运行AppForLifeCycle打印结果为:

从结果中可以看出,init方法执行了,但是destroy方法却未执行,这是为什么呢?

  • Spring的IOC容器是运行在JVM中
  • 运行main方法后,JVM启动,Spring加载配置文件生成IOC容器,从容器获取bean对象,然后调方法执行
  • main方法执行完后,JVM退出,这个时候IOC容器中的bean还没有来得及销毁就已经结束了
  • 所以没有调用对应的destroy方法

知道了出现问题的原因,具体该如何解决呢?

4.3.3 close关闭容器

  • ApplicationContext中没有close方法

  • 需要将ApplicationContext更换成ClassPathXmlApplicationContext

    1
    2
    ClassPathXmlApplicationContext ctx = new 
    ClassPathXmlApplicationContext("applicationContext.xml");
  • 调用ctx的close()方法

    1
    ctx.close();
  • 运行程序,就能执行destroy方法的内容

4.3.4 注册钩子关闭容器

  • 在容器未关闭之前,提前设置好回调函数,让JVM在退出之前回调此函数来关闭容器

  • 调用ctx的registerShutdownHook()方法

    1
    ctx.registerShutdownHook();

    **注意:**registerShutdownHook在ApplicationContext中也没有

  • 运行后,查询打印结果

两种方式介绍完后,close和registerShutdownHook选哪个?

相同点:这两种都能用来关闭容器

不同点:close()是在调用的时候关闭,registerShutdownHook()是在JVM退出前调用关闭。

分析上面的实现过程,会发现添加初始化和销毁方法,即需要编码也需要配置,实现起来步骤比较多也比较乱。

Spring提供了两个接口来完成生命周期的控制,好处是可以不用再进行配置init-methoddestroy-method

接下来在BookServiceImpl完成这两个接口的使用:

修改BookServiceImpl类,添加两个接口InitializingBeanDisposableBean并实现接口中的两个方法afterPropertiesSetdestroy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BookServiceImpl implements BookService, InitializingBean, DisposableBean {
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
public void save() {
System.out.println("book service save ...");
bookDao.save();
}
public void destroy() throws Exception {
System.out.println("service destroy");
}
public void afterPropertiesSet() throws Exception {
System.out.println("service init");
}
}

重新运行AppForLifeCycle类,

那第二种方式的实现,我们也介绍完了。

小细节

  • 对于InitializingBean接口中的afterPropertiesSet方法,翻译过来为属性设置之后

  • 对于BookServiceImpl来说,bookDao是它的一个属性

  • setBookDao方法是Spring的IOC容器为其注入属性的方法

  • 思考:afterPropertiesSet和setBookDao谁先执行?

    • 从方法名分析,猜想应该是setBookDao方法先执行

    • 验证思路,在setBookDao方法中添加一句话

      1
      2
      3
      4
      5
      public void setBookDao(BookDao bookDao) {
      System.out.println("set .....");
      this.bookDao = bookDao;
      }

    • 重新运行AppForLifeCycle,打印结果如下:

      验证的结果和我们猜想的结果是一致的,所以初始化方法会在类中属性设置之后执行。

4.3.5 bean生命周期小结

(1)关于Spring中对bean生命周期控制提供了两种方式:

  • 在配置文件中的bean标签中添加init-methoddestroy-method属性
  • 类实现InitializingBeanDisposableBean接口,这种方式了解下即可。

(2)对于bean的生命周期控制在bean的整个生命周期中所处的位置如下:

  • 初始化容器
    • 1.创建对象(内存分配)
    • 2.执行构造方法
    • 3.执行属性注入(set操作)
    • ==4.执行bean初始化方法==
  • 使用bean
    • 1.执行业务操作
  • 关闭/销毁容器
    • ==1.执行bean销毁方法==

(3)关闭容器的两种方式:

  • ConfigurableApplicationContext是ApplicationContext的子类
    • close()方法
    • registerShutdownHook()方法

5,DI相关内容

前面我们已经完成了bean相关操作的讲解,接下来就进入第二个大的模块DI依赖注入,首先来介绍下Spring中有哪些注入方式?

我们先来思考

  • 向一个类中传递数据的方式有几种?
    • 普通方法(set方法)
    • 构造方法
  • 依赖注入描述了在容器中建立bean与bean之间的依赖关系的过程,如果bean运行需要的是数字或字符串呢?
    • 引用类型
    • 简单类型(基本数据类型与String)

Spring就是基于上面这些知识点,为我们提供了两种注入方式,分别是:

  • setter注入
    • 简单类型
    • ==引用类型==
  • 构造器注入
    • 简单类型
    • 引用类型

依赖注入的方式已经介绍完,接下来挨个学习下:

5.1 setter注入

  1. 对于setter方式注入引用类型的方式之前已经学习过,快速回顾下:
  • 在bean中定义引用类型属性,并提供可访问的==set==方法
1
2
3
4
5
6
public class BookServiceImpl implements BookService {
private BookDao bookDao;
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}
}
  • 配置中使用==property==标签==ref==属性注入引用类型对象
1
2
3
4
5
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>

<bean id="bookDao" class="com.itheima.dao.imipl.BookDaoImpl"/>

5.1.1 环境准备

为了更好的学习下面内容,我们依旧准备一个新环境:

  • 创建一个Maven项目
  • pom.xml添加依赖
  • resources下添加spring的配置文件

这些步骤和前面的都一致,大家可以快速的拷贝即可,最终项目的结构如下:

(1)项目中添加BookDao、BookDaoImpl、UserDao、UserDaoImpl、BookService和BookServiceImpl类

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
public interface BookDao {
public void save();
}

public class BookDaoImpl implements BookDao {
public void save() {
System.out.println("book dao save ...");
}
}
public interface UserDao {
public void save();
}
public class UserDaoImpl implements UserDao {
public void save() {
System.out.println("user dao save ...");
}
}

public interface BookService {
public void save();
}

public class BookServiceImpl implements BookService{
private BookDao bookDao;

public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}

public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}

(2)resources下提供spring的配置文件

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>
</beans>

(3)编写AppForDISet运行类,加载Spring的IOC容器,并从中获取对应的bean对象

1
2
3
4
5
6
7
public class AppForDISet {
public static void main( String[] args ) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookService bookService = (BookService) ctx.getBean("bookService");
bookService.save();
}
}

接下来,在上面这个环境中来完成setter注入的学习:

5.1.2 注入引用数据类型

需求:在bookServiceImpl对象中注入userDao

1.在BookServiceImpl中声明userDao属性

2.为userDao属性提供setter方法

3.在配置文件中使用property标签注入

步骤1:声明属性并提供setter方法

在BookServiceImpl中声明userDao属性,并提供setter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BookServiceImpl implements BookService{
private BookDao bookDao;
private UserDao userDao;

public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}

public void save() {
System.out.println("book service save ...");
bookDao.save();
userDao.save();
}
}
步骤2:配置文件中进行注入配置

在applicationContext.xml配置文件中使用property标签注入

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
<property name="userDao" ref="userDao"/>
</bean>
</beans>
步骤3:运行程序

运行AppForDISet类,查看结果,说明userDao已经成功注入。

5.1.3 注入简单数据类型

需求:给BookDaoImpl注入一些简单数据类型的数据

参考引用数据类型的注入,我们可以推出具体的步骤为:

1.在BookDaoImpl类中声明对应的简单数据类型的属性

2.为这些属性提供对应的setter方法

3.在applicationContext.xml中配置

思考:

引用类型使用的是<property name="" ref=""/>,简单数据类型还是使用ref么?

ref是指向Spring的IOC容器中的另一个bean对象的,对于简单数据类型,没有对应的bean对象,该如何配置?

步骤1:声明属性并提供setter方法

在BookDaoImpl类中声明对应的简单数据类型的属性,并提供对应的setter方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BookDaoImpl implements BookDao {

private String databaseName;
private int connectionNum;

public void setConnectionNum(int connectionNum) {
this.connectionNum = connectionNum;
}

public void setDatabaseName(String databaseName) {
this.databaseName = databaseName;
}

public void save() {
System.out.println("book dao save ..."+databaseName+","+connectionNum);
}
}
步骤2:配置文件中进行注入配置

在applicationContext.xml配置文件中使用property标签注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<property name="databaseName" value="mysql"/>
<property name="connectionNum" value="10"/>
</bean>
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
<property name="userDao" ref="userDao"/>
</bean>
</beans>

说明:

value:后面跟的是简单数据类型,对于参数类型,Spring在注入的时候会自动转换,但是不能写成

1
<property name="connectionNum" value="abc"/>

这样的话,spring在将abc转换成int类型的时候就会报错。

步骤3:运行程序

运行AppForDISet类,查看结果,说明userDao已经成功注入。

**注意:**两个property注入标签的顺序可以任意。

对于setter注入方式的基本使用就已经介绍完了,

  • 对于引用数据类型使用的是<property name="" ref=""/>
  • 对于简单数据类型使用的是<property name="" value=""/>

5.2 构造器注入

5.2.1 环境准备

构造器注入也就是构造方法注入,学习之前,还是先准备下环境:

  • 创建一个Maven项目
  • pom.xml添加依赖
  • resources下添加spring的配置文件

这些步骤和前面的都一致,大家可以快速的拷贝即可,最终项目的结构如下:

(1)项目中添加BookDao、BookDaoImpl、UserDao、UserDaoImpl、BookService和BookServiceImpl类

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
public interface BookDao {
public void save();
}

public class BookDaoImpl implements BookDao {

private String databaseName;
private int connectionNum;

public void save() {
System.out.println("book dao save ...");
}
}
public interface UserDao {
public void save();
}
public class UserDaoImpl implements UserDao {
public void save() {
System.out.println("user dao save ...");
}
}

public interface BookService {
public void save();
}

public class BookServiceImpl implements BookService{
private BookDao bookDao;

public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}

public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}

(2)resources下提供spring的配置文件

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>
</beans>

(3)编写AppForDIConstructor运行类,加载Spring的IOC容器,并从中获取对应的bean对象

1
2
3
4
5
6
7
public class AppForDIConstructor {
public static void main( String[] args ) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookService bookService = (BookService) ctx.getBean("bookService");
bookService.save();
}
}

5.2.2 构造器注入引用数据类型

接下来,在上面这个环境中来完成构造器注入的学习:

需求:将BookServiceImpl类中的bookDao修改成使用构造器的方式注入。

1.将bookDao的setter方法删除掉

2.添加带有bookDao参数的构造方法

3.在applicationContext.xml中配置

步骤1:删除setter方法并提供构造方法

在BookServiceImpl类中将bookDao的setter方法删除掉,并添加带有bookDao参数的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
public class BookServiceImpl implements BookService{
private BookDao bookDao;

public BookServiceImpl(BookDao bookDao) {
this.bookDao = bookDao;
}

public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}
步骤2:配置文件中进行配置构造方式注入

在applicationContext.xml中配置

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<constructor-arg name="bookDao" ref="bookDao"/>
</bean>
</beans>

说明:

标签

  • name属性对应的值为构造函数中方法形参的参数名,必须要保持一致。

  • ref属性指向的是spring的IOC容器中其他bean对象。

步骤3:运行程序

运行AppForDIConstructor类,查看结果,说明bookDao已经成功注入。

5.2.3 构造器注入多个引用数据类型

需求:在BookServiceImpl使用构造函数注入多个引用数据类型,比如userDao

1.声明userDao属性

2.生成一个带有bookDao和userDao参数的构造函数

3.在applicationContext.xml中配置注入

步骤1:提供多个属性的构造函数

在BookServiceImpl声明userDao并提供多个参数的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BookServiceImpl implements BookService{
private BookDao bookDao;
private UserDao userDao;

public BookServiceImpl(BookDao bookDao,UserDao userDao) {
this.bookDao = bookDao;
this.userDao = userDao;
}

public void save() {
System.out.println("book service save ...");
bookDao.save();
userDao.save();
}
}

步骤2:配置文件中配置多参数注入

在applicationContext.xml中配置注入

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<constructor-arg name="bookDao" ref="bookDao"/>
<constructor-arg name="userDao" ref="userDao"/>
</bean>
</beans>

**说明:**这两个<contructor-arg>的配置顺序可以任意

步骤3:运行程序

运行AppForDIConstructor类,查看结果,说明userDao已经成功注入。

5.2.4 构造器注入多个简单数据类型

需求:在BookDaoImpl中,使用构造函数注入databaseName和connectionNum两个参数。

参考引用数据类型的注入,我们可以推出具体的步骤为:

1.提供一个包含这两个参数的构造方法

2.在applicationContext.xml中进行注入配置

步骤1:添加多个简单属性并提供构造方法

修改BookDaoImpl类,添加构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class BookDaoImpl implements BookDao {
private String databaseName;
private int connectionNum;

public BookDaoImpl(String databaseName, int connectionNum) {
this.databaseName = databaseName;
this.connectionNum = connectionNum;
}

public void save() {
System.out.println("book dao save ..."+databaseName+","+connectionNum);
}
}
步骤2:配置完成多个属性构造器注入

在applicationContext.xml中进行注入配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<constructor-arg name="databaseName" value="mysql"/>
<constructor-arg name="connectionNum" value="666"/>
</bean>
<bean id="userDao" class="com.itheima.dao.impl.UserDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<constructor-arg name="bookDao" ref="bookDao"/>
<constructor-arg name="userDao" ref="userDao"/>
</bean>
</beans>

**说明:**这两个<contructor-arg>的配置顺序可以任意

步骤3:运行程序

运行AppForDIConstructor类,查看结果

上面已经完成了构造函数注入的基本使用,但是会存在一些问题:

  • 当构造函数中方法的参数名发生变化后,配置文件中的name属性也需要跟着变
  • 这两块存在紧耦合,具体该如何解决?

在解决这个问题之前,需要提前说明的是,这个参数名发生变化的情况并不多,所以上面的还是比较主流的配置方式,下面介绍的,大家都以了解为主。

方式一:删除name属性,添加type属性,按照类型注入

1
2
3
4
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<constructor-arg type="int" value="10"/>
<constructor-arg type="java.lang.String" value="mysql"/>
</bean>
  • 这种方式可以解决构造函数形参名发生变化带来的耦合问题
  • 但是如果构造方法参数中有类型相同的参数,这种方式就不太好实现了

方式二:删除type属性,添加index属性,按照索引下标注入,下标从0开始

1
2
3
4
<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">
<constructor-arg index="1" value="100"/>
<constructor-arg index="0" value="mysql"/>
</bean>
  • 这种方式可以解决参数类型重复问题
  • 但是如果构造方法参数顺序发生变化后,这种方式又带来了耦合问题

介绍完两种参数的注入方式,具体我们该如何选择呢?

  1. 强制依赖使用构造器进行,使用setter注入有概率不进行注入导致null对象出现
    • 强制依赖指对象在创建的过程中必须要注入指定的参数
  2. 可选依赖使用setter注入进行,灵活性强
    • 可选依赖指对象在创建过程中注入的参数可有可无
  3. Spring框架倡导使用构造器,第三方框架内部大多数采用构造器注入的形式进行数据初始化,相对严谨
  4. 如果有必要可以两者同时使用,使用构造器注入完成强制依赖的注入,使用setter注入完成可选依赖的注入
  5. 实际开发过程中还要根据实际情况分析,如果受控对象没有提供setter方法就必须使用构造器注入
  6. ==自己开发的模块推荐使用setter注入==

这节中主要讲解的是Spring的依赖注入的实现方式:

  • setter注入

    • 简单数据类型

      1
      2
      3
      <bean ...>
      <property name="" value=""/>
      </bean>
    • 引用数据类型

      1
      2
      3
      <bean ...>
      <property name="" ref=""/>
      </bean>
  • 构造器注入

    • 简单数据类型

      1
      2
      3
      <bean ...>
      <constructor-arg name="" index="" type="" value=""/>
      </bean>
    • 引用数据类型

      1
      2
      3
      <bean ...>
      <constructor-arg name="" index="" type="" ref=""/>
      </bean>
  • 依赖注入的方式选择上

    • 建议使用setter注入
    • 第三方技术根据情况选择

5.3 自动配置

前面花了大量的时间把Spring的注入去学习了下,总结起来就一个字==麻烦==。

问:麻烦在哪?

答:配置文件的编写配置上。

问:有更简单方式么?

答:有,自动配置

什么是自动配置以及如何实现自动配置,就是接下来要学习的内容:

5.3.1 什么是依赖自动装配?

  • IoC容器根据bean所依赖的资源在容器中自动查找并注入到bean中的过程称为自动装配

5.3.2 自动装配方式有哪些?

  • ==按类型(常用)==
  • 按名称
  • 按构造方法
  • 不启用自动装配

5.3.3 准备下案例环境

  • 创建一个Maven项目
  • pom.xml添加依赖
  • resources下添加spring的配置文件

这些步骤和前面的都一致,大家可以快速的拷贝即可,最终项目的结构如下:

(1)项目中添加BookDao、BookDaoImpl、BookService和BookServiceImpl类

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
public interface BookDao {
public void save();
}

public class BookDaoImpl implements BookDao {

private String databaseName;
private int connectionNum;

public void save() {
System.out.println("book dao save ...");
}
}
public interface BookService {
public void save();
}

public class BookServiceImpl implements BookService{
private BookDao bookDao;

public void setBookDao(BookDao bookDao) {
this.bookDao = bookDao;
}

public void save() {
System.out.println("book service save ...");
bookDao.save();
}
}

(2)resources下提供spring的配置文件

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl">
<property name="bookDao" ref="bookDao"/>
</bean>
</beans>

(3)编写AppForAutoware运行类,加载Spring的IOC容器,并从中获取对应的bean对象

1
2
3
4
5
6
7
public class AppForAutoware {
public static void main( String[] args ) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookService bookService = (BookService) ctx.getBean("bookService");
bookService.save();
}
}

5.3.4 完成自动装配的配置

接下来,在上面这个环境中来完成自动装配的学习:

自动装配只需要修改applicationContext.xml配置文件即可:

(1)将<property>标签删除

(2)在<bean>标签中添加autowire属性

首先来实现按照类型注入的配置

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean class="com.itheima.dao.impl.BookDaoImpl"/>
<!--autowire属性:开启自动装配,通常使用按类型装配-->
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl" autowire="byType"/>

</beans>

==注意事项:==

  • 需要注入属性的类中对应属性的setter方法不能省略
  • 被注入的对象必须要被Spring的IOC容器管理
  • 按照类型在Spring的IOC容器中如果找到多个对象,会报NoUniqueBeanDefinitionException

一个类型在IOC中有多个对象,还想要注入成功,这个时候就需要按照名称注入,配置方式为:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean class="com.itheima.dao.impl.BookDaoImpl"/>
<!--autowire属性:开启自动装配,通常使用按类型装配-->
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl" autowire="byName"/>

</beans>

==注意事项:==

  • 按照名称注入中的名称指的是什么?

    • bookDao是private修饰的,外部类无法直接方法
    • 外部类只能通过属性的set方法进行访问
    • 对外部类来说,setBookDao方法名,去掉set后首字母小写是其属性名
      • 为什么是去掉set首字母小写?
      • 这个规则是set方法生成的默认规则,set方法的生成是把属性名首字母大写前面加set形成的方法名
    • 所以按照名称注入,其实是和对应的set方法有关,但是如果按照标准起名称,属性名和set对应的名是一致的
  • 如果按照名称去找对应的bean对象,找不到则注入Null

  • 当某一个类型在IOC容器中有多个对象,按照名称注入只找其指定名称对应的bean对象,不会报错

两种方式介绍完后,以后用的更多的是==按照类型==注入。

最后对于依赖注入,需要注意一些其他的配置特征:

  1. 自动装配用于引用类型依赖注入,不能对简单类型进行操作
  2. 使用按类型装配时(byType)必须保障容器中相同类型的bean唯一,推荐使用
  3. 使用按名称装配时(byName)必须保障容器中具有指定名称的bean,因变量名与配置耦合,不推荐使用
  4. 自动装配优先级低于setter注入与构造器注入,同时出现时自动装配配置失效

5.4 集合注入

前面我们已经能完成引入数据类型和简单数据类型的注入,但是还有一种数据类型==集合==,集合中既可以装简单数据类型也可以装引用数据类型,对于集合,在Spring中该如何注入呢?

先来回顾下,常见的集合类型有哪些?

  • 数组
  • List
  • Set
  • Map
  • Properties

针对不同的集合类型,该如何实现注入呢?

5.4.1 环境准备

  • 创建一个Maven项目
  • pom.xml添加依赖
  • resources下添加spring的配置文件applicationContext.xml

这些步骤和前面的都一致,大家可以快速的拷贝即可,最终项目的结构如下:

(1)项目中添加添加BookDao、BookDaoImpl类

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
public interface BookDao {
public void save();
}

public class BookDaoImpl implements BookDao {

public class BookDaoImpl implements BookDao {

private int[] array;

private List<String> list;

private Set<String> set;

private Map<String,String> map;

private Properties properties;

public void save() {
System.out.println("book dao save ...");

System.out.println("遍历数组:" + Arrays.toString(array));

System.out.println("遍历List" + list);

System.out.println("遍历Set" + set);

System.out.println("遍历Map" + map);

System.out.println("遍历Properties" + properties);
}
//setter....方法省略,自己使用工具生成
}

(2)resources下提供spring的配置文件,applicationContext.xml

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
</beans>

(3)编写AppForDICollection运行类,加载Spring的IOC容器,并从中获取对应的bean对象

1
2
3
4
5
6
7
public class AppForDICollection {
public static void main( String[] args ) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
bookDao.save();
}
}

接下来,在上面这个环境中来完成集合注入的学习:

下面的所以配置方式,都是在bookDao的bean标签中使用进行注入

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl">

</bean>
</beans>

5.4.2 注入数组类型数据

1
2
3
4
5
6
7
<property name="array">
<array>
<value>100</value>
<value>200</value>
<value>300</value>
</array>
</property>

5.4.3 注入List类型数据

1
2
3
4
5
6
7
8
<property name="list">
<list>
<value>itcast</value>
<value>itheima</value>
<value>boxuegu</value>
<value>chuanzhihui</value>
</list>
</property>

5.4.4 注入Set类型数据

1
2
3
4
5
6
7
8
<property name="set">
<set>
<value>itcast</value>
<value>itheima</value>
<value>boxuegu</value>
<value>boxuegu</value>
</set>
</property>

5.4.5 注入Map类型数据

1
2
3
4
5
6
7
<property name="map">
<map>
<entry key="country" value="china"/>
<entry key="province" value="henan"/>
<entry key="city" value="kaifeng"/>
</map>
</property>

5.4.6 注入Properties类型数据

1
2
3
4
5
6
7
<property name="properties">
<props>
<prop key="country">china</prop>
<prop key="province">henan</prop>
<prop key="city">kaifeng</prop>
</props>
</property>

配置完成后,运行下看结果:

说明:

  • property标签表示setter方式注入,构造方式注入constructor-arg标签内部也可以写<array><list><set><map><props>标签
  • List的底层也是通过数组实现的,所以<list><array>标签是可以混用
  • 集合中要添加引用类型,只需要把<value>标签改成<ref>标签,这种方式用的比较少

C++指针和结构体的浅显认知

因为数据结构中会设计到C++的一些基本指针操作,在进行算法的时候或多或少链表,哈希表,二叉树也会涉及一些相应的指针操作,而本人对于C++这么博大精深的语言体系又只有浅浅的一些了解,所以将这些数据结构涉及到的底层的指针做一个简单的知识梳理,方便以后在理解算法的时候尽量少花点笔墨放在基础语法知识上。


1. 指针

1.1 指针的意义

对于C++程序的一个数据,我们一般需要做到关注他如下的三个部分:

  • data : 数据的内容
  • address : 数据的地址(存储位置)
  • dtype : 数据的类型

那么平时我们在定义一个变量的时候,一般只会定义他的数据类型和数据的内容(int a = 5),那么他的地址又指的是什么呢?

我们可以这样去理解:代码中出现的变量存放在主机里存储器中的一个个存储单元中,每一个存储单元都有一个地址。换而言之,我们作为程序员更多的会去关注字面值的内容,而地址更像是计算机底层程序自己考虑的。当我们定义完一个变量要去调用他的时候,我们需要进行的操作仅仅是运用运算符=, 而计算机底层则需要对应的去寻找这个变量的地址,再将其调用出来。指针像是一种工具,模拟了这种过程,我们可以通过指针直接知道其指向的地址,也可以获取他指向的值

用听起来厉害一点的话来讲就是:**指针可以精准控制了内存中的地址,从而高效率的传递和更改数据。**

1.2 指针和引用

  • & : 取地址运算符
    针对于变量,& + 变量名称表示的是该变量在内存中的地址。

    1
    2
    int a = 10;
    cout<< &a << endl;

    输出的结果是:004FFDE0

  • *: 间接运算符

针对的是指针,* + 指针名称表示引用该指针指向的值。
*p表示取p指针指向地址的值
int* p表示p的数据类型是指向int类型的指针

1
2
3
4
int a = 10;
cout<< &a << endl;
int* p = &a; //表示p是指向int类型的a变量的地址的指针
cout<< *p <<endl;

输出的结果是:004FFDE0; 10

某种意义上,int* p =int * p ; 但是他们表示的含义完全不同。左值定义了一个指针,右值定义了一个变量。但是他们最终的结果是相等的。但是要注意的是,在程序编译器中,int* p1, p2指的是定义一个p1为指向int类型的指针,p2为int类型的变量。


2. 结构体

2.1 结构体的定义

类似于类,可以表示一个需要用各种数据类型表示的数据集体,如学生有他的姓名年龄,那么此时就需要进行结构体的定义,将学生封装为char类型的姓名和包括int类型的年龄。

具体在C++中表达如下:

1
2
3
4
struct Student {
char[20] name;
int age;
}

他在数据结构中用处十分之大,比如链表中需要定义datanext,又比如在二叉树中需要定义双亲和头节点等。都需要用到结构体。

2.2 结构体的初始化

结构体的初始化有如下两种方式:

  • 通过直接定义初始化
    struct Student s1;:表示构造出一个数据类型为Student的学生变量s1
    注意:此类型构造需要确定结构体定义的位置,如果在同一个方法内可以调用,在不同方法内不能调用。如果结构体被设置为全局则每个方法都可以引用。
  • 在构造时完成初始化
    1
    2
    3
    4
    struct Student {
    char[20] name;
    int age;
    },s1,s2
    这样就表示s1,s2两个变量是Student类型,并且再后续无需再定义就可以直接使用,在构造的时候就完成了初始化。

2.3 typedef关键字

typedef关键字可以简化结构体变量的初始化,通过该关键字可以给结构体起别名,从而直接实现通过别名来进行调用。

1
2
3
4
typedef struct Student {
char[20] name;
int age;
},S,s1

其中,S为结构体的别名,s1为结构体类型的变量。

2.4 结构体成员变量的调用

通过.的形式调用。
s1.age表示第一名学生的年龄;s2.name表示第二名学生的姓名。


3. 指向结构体的指针

此时结构体可以视为数据类型,所以指针定义写法和原先指针写法差别不大。
struct Student* p= &s1

指向结构体Student的指针p(左值),指向(=)数据类型为Student的s1的地址(右值)。

  • 指针访问结构体成员变量
    用箭头函数:
    p->name

    无需带变量名,因为p指向了地址,知道需要获取哪个变量的值


本篇只针对于目前个人认为可能会在数据结构中用到的知识点,对于真正的指针和结构体的知识知识九牛一毛不尽完整,而对于有关于理解数据结构语法有帮助的知识点,如有遗漏或者后续想到的,接下来再来进行补充。

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment