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

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


  • 首页

  • 归档

  • 搜索

关于函数的学习(三) — 返回值

发表于 2021-11-28

返回值

函数并非总是直接显示输出,相反,它可以处理一些数据,并返回一个或一组值。

函数返回的值被称为返回值。

在函数中,可使用return语句将值返回到调用函数的代码行。

返回值让你能够将程序的大部分繁重工作移到函数中去完成,从而简化主程序。

1.返回简单值

下面来看一个函数,它接受名和姓并返回整洁的姓名:

1
2
3
4
5
6
7
8
handlebars复制代码def get_name(frist_name,last_name):
"""返回整洁的名字"""

full_name = frist_name +' ' + last_name
return full_name.title()

a = get_name('zhang','jibin')
print(a)

函数get_name()的定义通过形参接受名和姓。

将结果存储在变量full_name中。

将full_name的值转换为首字母大写格式。

将结果返回到函数调用行。

调用返回值的函数时,需要提供一个变量,用于存储返回的值。

输出姓名为:

1
handlebars复制代码Zhang Jibin

存储大量名和姓的大型程序中,像get_name()这样的函数非常有用。

你分别存储名和姓,每当需要显示姓名时都调用这个函数。

2.让实参变成可选的

有时候,需要让实参变成可选的,这样使用函数的人就只需在必要时才提供额外的信息。可使用默认值来让实参变成可选的。

例如,假设我们要扩展函数get_name(),使其还处理中间名。

将其修改成这样:

1
2
3
4
5
6
7
8
handlebars复制代码def get_name(frist_name,middle_name,last_name):
"""返回整洁的名字"""

full_name = frist_name +' ' +middle_name + ' ' + last_name
return full_name.title()

a = get_name('zhang','jing','ze')
print(a)

只要同时提供名、中间名和姓,这个函数就能正确地运行。它根据这三部分创建一个字符串,加上空格,并将结果转换为首字母大写格式:

1
handlebars复制代码Zhang Jing Ze

然而,并非所有的人都有中间名,但如果你调用这个函数时只提供了名和姓,它将不能正确地运行。为让中间名变成可选的,可给实参middle_name指定一个默认值——空字符串,并在用户没有提供中间名时不使用这个实参。为让get_name()在没有提供中间名时依然可行,可给实参middle_name指定一个默认值——空字符串,并将其移到形参列表的末尾:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
handlebars复制代码def get_name(frist_name,last_name,middle_name = ''):
"""返回整洁的名字"""
if middle_name:
name = frist_name +' ' +middle_name + ' ' + last_name
else:
name = frist_name + ' ' + last_name

return name.title()

a = get_name('chen','feng')
print(a)

a = get_name('zhang','ze','jing')
print(a)

在函数体中,我们检查是否提供了中间名。Python将非空字符串解读为True,因此如果函数调用中提供了中间名,if middle_name将为True。

调用这个函数时,如果只想指定名和姓,调用起来将非常简单。如果还要指定中间名,就必须确保它是最后一个实参,这样Python才能正确地将位置实参关联到形参。

打印结果为:

1
2
handlebars复制代码Chen Feng
Zhang Jing Ze

可选值让函数能够处理各种不同情形的同时,确保函数调用尽可能简单。

3.返回字典

函数可返回任何类型的值,包括列表和字典等较复杂的数据结构。

例如:

1
2
3
4
5
6
7
8
handlebars复制代码def build_person(first_name, last_name):
"""返回一个字典,其中包含有关一个人的信息"""

person = {'first': first_name,'last': last_name}
return person

a = build_person('zhang', 'jibin')
print(a)

函数build_person()接受名和姓,并将这些值封装到字典中。

存储first_name的值时,使用的键为’first’,而存储last_name的值时,使用的键为’last’。

最后,返回表示人的整个字典。打印这个返回的值,此时原来的两项文本信息存储在一个字典中:

1
handlebars复制代码{'first': 'zhang', 'last': 'jibin'}

这个函数接受简单的文本信息,将其放在一个更合适的数据结构中,让你不仅能打印这些信息,还能以其他方式处理它们。

例如,下面的修改让你还能存储年龄:

1
2
3
4
5
6
7
8
9
10
handlebars复制代码def build_person(first_name, last_name, age=''):
"""返回一个字典,其中包含有关一个人的信息"""

person = {'first': first_name, 'last': last_name}
if age:
person['age'] = age
return person

a = build_person('zhang', 'jibin', age=27)
print(a)

在函数定义中,我们新增了一个可选形参age,并将其默认值设置为空字符串。如果函数调用中包含这个形参的值,这个值将存储到字典中。在任何情况下,这个函数都会存储人的姓名,但可对其进行修改,使其也存储有关人的其他信息。

打印结果为:

1
handlebars复制代码{'first': 'zhang', 'last': 'jibin', 'age': 27}

4.结合使用函数和while循环

可将函数同本书前面介绍的任何Python结构结合起来使用。例如,下面将结合使用函数get__name()和while循环,以更正规的方式问候用户。下面尝试使用名和姓跟用户打招呼:

1
2
3
4
5
6
7
8
9
10
11
12
13
handlebars复制代码def get_name(first_name, last_name):
"""返回整洁的姓名"""

name = first_name + ' ' + last_name
return name.title()

# 这是一个无限循环!
while True:
print("\nPlease tell me your name:")
f_name = input("First name: ")
l_name = input("Last name: ")
g_name = get_name(f_name, l_name)
print("\nHello, "+g_name+"!")

我们使用的是get_name()的简单版本,不涉及中间名。

其中的while循环让用户输入姓名:依次提示用户输入名和姓。

打印结果为:

1
2
3
4
5
6
7
8
9
10
11
12
handlebars复制代码Please tell me your name:
First name: zhang
Last name: jibin

Hello, Zhang Jibin!


Please tell me your name:
First name: wang
Last name: fan

Hello, Wang Fan!

while循环存在一个问题:没有定义退出条件。每次提示用户输入时,都使用break语句提供了退出循环的简单途径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
handlebars复制代码def get_name(first_name, last_name):
"""返回整洁的姓名"""

name = first_name + ' ' + last_name
return name.title()

# 这是一个无限循环!
while True:
print("\nPlease tell me your name:")
print("(enter 'q' at any time to quit)")

f_name = input("First name: ")
if f_name == 'q':
break

l_name = input("Last name: ")
if l_name == 'q':
break

g_name = get_name(f_name, l_name)
print("\nHello, "+g_name+"!")

我们添加了一条消息来告诉用户如何退出,然后在每次提示用户输入时,都检查他输入的是否是退出值,如果是,就退出循环。现在,这个程序将不断地问候,直到用户输入的姓或名为’q’为止:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
handlebars复制代码Please tell me your name:
(enter 'q' at any time to quit)
First name: zhang
Last name: jibin

Hello, Zhang Jibin!

Please tell me your name:
(enter 'q' at any time to quit)
First name: wang
Last name: fan

Hello, Wang Fan!

Please tell me your name:
(enter 'q' at any time to quit)
First name: q
>>>

本文转载自: 掘金

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

java使用poi读取跨行跨列excel 1需求背景 2

发表于 2021-11-28

@TOC


1.需求背景

最近有一个工作任务是用户提供了一个基础的excel文件,要求首先将excel中的数据解析并入库,然后再做后续的一些业务处理,因此涉及到excel的数据读取,正常如果是一行一行数据的excel的读取,还是比较简单,但用户提供的数据涉及跨行跨列问题,就稍有点麻烦,比如数据样例如下:
在这里插入图片描述
需要将跨行跨列数据也读取出来最后进行入库处理,比如要解析成如下的数据并入库。
在这里插入图片描述

2.实现思路分析

首先以上的excel样例数据中,第一列共2个跨行区域,天河区和番禺区,每个跨行区域中在第二列又存在子跨行数据的问题,查看了poi的api,如获取到excel的工作表sheet后(通过如XSSFSheet sheet = xssfWorkbook.getSheetAt(0);获取),有个sheet.getNumMergedRegions()方法,此方法会返回excel中的所有关于合并单元格的信息,每个信息中包含了这个合并单元格的开始行,结束行,开始列,结束列。合并单元格的数据能通过第一行和第一列获取到。

因此,可以采用一个hashmap将以上的合并单元格的信息进行预先读取,将存在合并单元格的列记录入put进hashmap, 如key为行号+下划线+列号组成,value为记录了合并单元格的起始行号和起始列号组成的数组。

有了上面的hashmap之后,就可以按常规的遍历excel的行,然后再遍历excel的列进行数据的读取处理了,对每一个单元格进行判断,判断此单元格的行号+列号组成的key是不是在hashmap中存在,存在的话说明是一个合并单元格,读取数据就从value中取出合并单元格的起始行号和起始列号,通过合并单元格的起始行号和起始列号组成的单元格读取真正的数据(合并单元数的组据在首行和首列,其他行和列是空白)比如,以下的510075的数据,要通过读B2这一格才能读到数据
在这里插入图片描述

3.重要代码片码说明

  1. 获取存在合并单元格的列记录并以行号列号为key组成的map
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
c复制代码	//将存在合并单元格的列记录入put进hashmap并返回
public Map<String,Integer[]> getMergedRegionMap(Sheet sheet){

Map<String,Integer[]> result = new HashMap<String,Integer[]>();

//获取excel中的所有合并单元格信息
int sheetMergeCount = sheet.getNumMergedRegions();

//遍历处理
for (int i = 0; i < sheetMergeCount; i++) {

//拿到每个合并单元格,开始行,结束行,开始列,结束列
CellRangeAddress range = sheet.getMergedRegion(i);
int firstColumn = range.getFirstColumn();
int lastColumn = range.getLastColumn();
int firstRow = range.getFirstRow();
int lastRow = range.getLastRow();

//构造一个开始行和开始列组成的数组
Integer[] firstRowNumberAndCellNumber = new Integer[]{firstRow,firstColumn};

//遍历,将单元格中的所有行和所有列处理成由行号和下划线和列号组成的key,然后放在hashmap中
for(int currentRowNumber = firstRow; currentRowNumber <= lastRow; currentRowNumber++) {

for(int currentCellNumber = firstColumn; currentCellNumber <= lastColumn; currentCellNumber ++) {
result.put(currentRowNumber+"_"+currentCellNumber, firstRowNumberAndCellNumber);
}

}

}

return result;

}

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
c复制代码package poitest;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ExcelMergegionReadTest {

private Logger logger = LoggerFactory.getLogger(this.getClass());

public static void main(String[] args) {
new ExcelMergegionReadTest().excelRegionReadStart();
}


public void excelRegionReadStart(){

long start = System.currentTimeMillis();

InputStream inputStream = null;// 输入流对象
XSSFWorkbook xssfWorkbook = null; //工作簿

try {
inputStream = this.getClass().getClassLoader().getResourceAsStream("datatest.xlsx");

//定义工作簿
xssfWorkbook = new XSSFWorkbook(inputStream);

//获取第一个sheet
XSSFSheet sheet = xssfWorkbook.getSheetAt(0);

//获取合并单元格信息的hashmap
Map<String,Integer[]> mergedRegionMap = getMergedRegionMap(sheet);

//拿到excel的最后一行的索引
int lastRowNum = sheet.getLastRowNum();

//从excel的第二行索行开始,遍历到最后一行(第一行是标题,直接跳过不读取)
for(int i = 1; i<=lastRowNum ; i++) {

//拿到excel的行对象
XSSFRow row = sheet.getRow(i);

//获取excel的行中有多个列
int cellNum = row.getLastCellNum();

//对每行进行列遍历,即一列一列的进行解析
for(int j=0; j<cellNum; j++) {

//拿到了excel的列对象
Cell cell = row.getCell(j);

//将列对象的行号和列号+下划线组成key去hashmap中查询,不为空说明当前的cell是合并单元列
Integer[] firstRowNumberAndCellNumber = mergedRegionMap.get(i+"_"+j);

//如果是合并单元列,就取合并单元格的首行和首列所在位置读数据,否则就是直接读数据
if(firstRowNumberAndCellNumber != null) {

XSSFRow rowTmp = sheet.getRow(firstRowNumberAndCellNumber[0]);
Cell cellTmp = rowTmp.getCell(firstRowNumberAndCellNumber[1]);

System.out.println(getCellValue(cellTmp));

}else{

System.out.println(getCellValue(cell));

}

}

System.out.println("================================================");

}



} catch (Exception e) {

logger.error("error",e);

} finally {

// 关闭文件流
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
logger.error("error",e);
}
}

// 关闭工作簿
if(xssfWorkbook != null){
try {
xssfWorkbook.close();
} catch (IOException e) {
logger.error("error",e);
}
}

}

long end = System.currentTimeMillis();

System.out.println("spend ms: " + (end - start) + " ms.");


}

//将存在合并单元格的列记录入put进hashmap并返回
public Map<String,Integer[]> getMergedRegionMap(Sheet sheet){

Map<String,Integer[]> result = new HashMap<String,Integer[]>();

//获取excel中的所有合并单元格信息
int sheetMergeCount = sheet.getNumMergedRegions();

//遍历处理
for (int i = 0; i < sheetMergeCount; i++) {

//拿到每个合并单元格,开始行,结束行,开始列,结束列
CellRangeAddress range = sheet.getMergedRegion(i);
int firstColumn = range.getFirstColumn();
int lastColumn = range.getLastColumn();
int firstRow = range.getFirstRow();
int lastRow = range.getLastRow();

//构造一个开始行和开始列组成的数组
Integer[] firstRowNumberAndCellNumber = new Integer[]{firstRow,firstColumn};

//遍历,将单元格中的所有行和所有列处理成由行号和下划线和列号组成的key,然后放在hashmap中
for(int currentRowNumber = firstRow; currentRowNumber <= lastRow; currentRowNumber++) {

for(int currentCellNumber = firstColumn; currentCellNumber <= lastColumn; currentCellNumber ++) {
result.put(currentRowNumber+"_"+currentCellNumber, firstRowNumberAndCellNumber);
}

}

}

return result;

}

/**
* 获取单元格的值
* @param cell
* @return
*/
public String getCellValue(Cell cell){

if(cell == null) return "";

if(cell.getCellType() == CellType.STRING){

return cell.getStringCellValue();

}else if(cell.getCellType() == CellType.BOOLEAN){

return String.valueOf(cell.getBooleanCellValue());

}else if(cell.getCellType() == CellType.FORMULA){

return cell.getCellFormula() ;

}else if(cell.getCellType() == CellType.NUMERIC){

return String.valueOf(cell.getNumericCellValue());

}

return "";

}

}

5.完整的demo代码提供如下

github: github.com/jxlhljh/exc…

gitee: gitee.com/jxlhljh/exc…

6.demo执行结果

在这里插入图片描述

本文转载自: 掘金

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

从零开始摸索VUE,配合Golang搭建导航网站(二十八v

发表于 2021-11-28

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

前言

之前完成了前后端的完善。今天就把vue-admin-template部署到线上

之前的文章: 从零开始摸索VUE,配合Golang搭建导航网站(五.使用doker部署启动)

启动Nginx容器

跟之前的vue cli脚手架一样需要一个Nginx容器
需要更改nginx 配置文件,项目目录

1
bash复制代码docker run --name web-admin -d -p 8101:80 -v /opt/nginx/config/nginx.conf:/etc/nginx/nginx.conf -v /opt/nginx/config/admin.conf.d:/etc/nginx/conf.d -v /opt/nginx/logs:/var/log/nginx -v /opt/vue-admin:/opt/vue-admin --restart=always nginx

Nginx配置

nginx配置宿主机位置/opt/nginx/config/admin.conf.d/admin.conf:

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
ini复制代码server {
listen 80;
server_name localhost;

location / {
root /opt/vue-admin/;
index index.html index.htm;
}


error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}


location /admin/ {
# 把 /admin 路径下的请求转发给真正的后端服务器
proxy_pass http://172.17.0.4:8080;

# 把host头传过去,后端服务程序将收到your.domain.name, 否则收到的是localhost:8080
proxy_set_header Host $http_host;

# 把cookie中的path部分从/api替换成/service
proxy_cookie_path /api /;

# 把cookie的path部分从localhost:8080替换成your.domain.name
proxy_cookie_domain localhost:80 http://172.18.0.4:8080;
}
}

查看容器列表docker ps:

image.png

frp设置

三个项目容器都在这里了,现在把这个admin后台穿透到外网,参考:使用树莓派搭建内网GitLab,并实现Frp内网穿透和简单CI/CD实践

编辑客户端文件加上一下内容:

1
2
3
4
5
ini复制代码[vue-admin-pi4-master] 
type = tcp
local_ip = 127.0.0.1
local_port = 8101
remote_port = 7033

重启并查看frp状态:

1
2
lua复制代码sudo systemctl restart frpc
sudo systemctl status frpc

image.png

编写CI

还是跟vue Cli项目一样,修改CI的npm打包后的目标路径,注意npm build 命令需要改为npm run build:prod,然后修改env文件名,修改容器名:

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
bash复制代码stages:
- build #任务阶段。只写了一个build阶段

cache:
key: nodemodules-$CI_COMMIT_REF_NAME
paths:
- node_modules/

build_develop:
stage: build #指定这个build_develop任务在build阶段
cache:
key: nodemodules-$CI_COMMIT_REF_NAME
paths:
- node_modules/

script:
- rm -rf .env
- cp .env.production .env
- npm install --unsafe-perm # 执行npm install 安装第三方依赖,--unsafe-perm为了解决一些安装的时候出现了一些无权限创建文件的报错
- rm -rf ./dist/ # 删除之前打包生成的文件
- npm run build:prod #重新打包生成文件
- rm -rf /opt/vue-admin/ #删除之前上次打包的的文件夹
- cp -rf ./dist/ /opt/vue-admin/ #把build生成的文件打包到nginx指定的文件夹
- docker restart web-admin #重启Docker的Nginx容器,这时候前端的所有的静态文件都是新的了
only:
- master # 指定只在master分支执行这个任务。
tags:
- b4master #指定执行任务的runner,安装runner的时候会提示填写runner的标签(tag)

最后把这个文件提交到master分支就会触发自动化部署了
出现了一次错误:

image.png
提示gyp ERR! stack Error: not found: make

使用一下命令安装build-essential

1
arduino复制代码sudo apt-get install build-essential

然后重试任务就行

新建域名解析,反向代理:

设置二级域名,指向到frp服务端服务器

image.png
然后在服务器上宝塔新建网站

image.png
新建网站后设置反向代理

image.png
反向代理设置完成,访问域名就会映射到我的树莓派上的有vue-admin-template打包好的静态文件的Nginx 容器上。

删除mock设置

务必删除下面四行,直接上线我排查了半天,因为线上默认不会报错输出,上线接口直接不请求报Error,我找了半天在eslint设置里把线上打开调试,才发现请求了mock模拟数据
image.png

总结

今天这次CI部署比较容易,因为之前有Vue cli脚手架的项目,照着改一下就可以了,现在直接访问管理后台的域名就可以了。花了一个月的时间,前端后端运维全部由自己研究完善,发现了自己虽然都懂基本的东西但是还有很多瑕疵,比真正的商业项目差了很多大一段。不过正是这次实践深刻理解术业有专攻这个道理,专业的前端还是很有必要的,前端的技术发展是真的快,现在Vue3和TypeScript非常火呀,我还没有开始接触,接下来还是需要花精力了解了解前端。

文章链接汇总&GitHub

从零开始摸索VUE,配合Golang搭建导航网站(一.项目初始化)

从零开始摸索VUE,配合Golang搭建导航网站(二.了解项目结构)

从零开始摸索VUE,配合Golang搭建导航网站(三.做一个简单的单页面)

从零开始摸索VUE,配合Golang搭建导航网站(四.项目运行环境搭建和CI脚本编写)

从零开始摸索VUE,配合Golang搭建导航网站(五.使用doker部署启动)

从零开始摸索VUE,配合Golang搭建导航网站(六.CSS容器布局学习总结)

从零开始摸索VUE,配合Golang搭建导航网站(七.CSS Flex容器布局实战总结)

从零开始摸索VUE,配合Golang搭建导航网站(八.基于Golang的Gin框架的介绍)

从零开始摸索VUE,配合Golang搭建导航网站(九.Gin框架中GORM使用)

从零开始摸索VUE,配合Golang搭建导航网站(十.Gin框架优化,DockerFile编写)

从零开始摸索VUE,配合Golang搭建导航网站(十一.Gin容器化部署上线,CI脚本编写)

从零开始摸索VUE,配合Golang搭建导航网站(十二.使用Docker 新建Mysql应用,持久化数据保存,修改CI流程)

从零开始摸索VUE,配合Golang搭建导航网站(十三.Vue cli axios 简单使用)

从零开始摸索VUE,配合Golang搭建导航网站(十四.Vue cli env环境变量 ,后端跨域设置)

从零开始摸索VUE,配合Golang搭建导航网站(十五.添加数据后CSS样式优化)

从零开始摸索VUE,配合Golang搭建导航网站(十六.CSS动画初探)

从零开始摸索VUE,配合Golang搭建导航网站(十七.VUE锚点跳转,基础模板语法总结)

从零开始摸索VUE,配合Golang搭建导航网站(十八.Gin框架分层优化)

从零开始摸索VUE,配合Golang搭建导航网站(十九.GORM数据增删改查和Gin验证器)

从零开始摸索VUE,配合Golang搭建导航网站(二十.vue-element-admin 快速上手,认识基本架构)

从零开始摸索VUE,配合Golang搭建导航网站(二十一.vue-admin-template模拟账号登录)

从零开始摸索VUE,配合Golang搭建导航网站(二十二.vue-admin-template接入后端接口)

从零开始摸索VUE,配合Golang搭建导航网站(二十三.vue-admin-template接入后端增删改接口)

从零开始摸索VUE,配合Golang搭建导航网站(二十四.vue-admin-template带筛选的列表展示,初识双向绑定)

从零开始摸索VUE,配合Golang搭建导航网站(二十五.vue-admin-template分类详情数据删改查)

从零开始摸索VUE,配合Golang搭建导航网站(二十六.vue-admin-template完善用户登录后台接口)

从零开始摸索VUE,配合Golang搭建导航网站(二十七.vue-admin-template完善前端登陆逻辑)

从零开始摸索VUE,配合Golang搭建导航网站(二十八.vue-admin-template完善CI脚本,整体完成上线)

所有代码已在GitHub开源:
koala9527/Gin-Vue-ElementUI

本文转载自: 掘金

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

大数据Hive学习之旅第七篇 一、Hive 实战 二、常见错

发表于 2021-11-28

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

一、Hive 实战

1、需求描述

统计硅谷影音视频网站的常规指标,各种 TopN 指标:

  1. 统计视频观看数 Top10
  2. 统计视频类别热度 Top10
  3. 统计出视频观看数最高的 20 个视频的所属类别以及类别包含 Top20 视频的个数
  4. 统计视频观看数 Top50 所关联视频的所属类别排序
  5. 统计每个类别中的视频热度 Top10,以 Music 为例
  6. 统计每个类别视频观看数 Top10 – 统计上传视频最多的用户 Top10 以及他们上传的视频观看次数在前 20 的视频

2、数据结构

  1. 视频表

image.png
2. 用户表

image.png

3、准备工作

  1. 准备表
* 需要准备的表


    + 创建原始数据表:gulivideo\_ori,gulivideo\_user\_ori
    + 创建最终表:gulivideo\_orc,gulivideo\_user\_orc
* 创建原始数据表


    + gulivideo\_ori



    
1
2
3
4
5
6
7
8
9
10
11
12
13
14
hive复制代码create table gulivideo_ori(
videoId string,
uploader string,
age int,
category array<string>,
length int,
views int,
rate float,
ratings int,
comments int,
relatedId array<string>)
row format delimited fields terminated by "\t"
collection items terminated by "&"
stored as textfile;
+ gulivideo\_user\_ori
1
2
3
4
5
6
hive复制代码create table gulivideo_user_ori(
uploader string,
videos int,
friends int)
row format delimited fields terminated by "\t"
stored as textfile;
* 创建 orc 存储格式带 snappy 压缩的表 + gulivideo\_orc
1
2
3
4
5
6
7
8
9
10
11
12
13
hive复制代码create table gulivideo_orc(
videoId string,
uploader string,
age int,
category array<string>,
length int,
views int,
rate float,
ratings int,
comments int,
relatedId array<string>)
stored as orc
tblproperties("orc.compress"="SNAPPY");
+ gulivideo\_user\_orc
1
2
3
4
5
6
7
8
hive复制代码create table gulivideo_user_orc(
uploader string,
videos int,
friends int)
row format delimited
fields terminated by "\t"
stored as orc
tblproperties("orc.compress"="SNAPPY");
+ 向 ori 表插入数据
1
2
hive复制代码load data local inpath "/opt/module/data/video" into table gulivideo_ori;
load data local inpath "/opt/module/data/user" into table gulivideo_user_ori;
+ 向 orc 表插入数据
1
2
hive复制代码insert into table gulivideo_orc select * from gulivideo_ori;
insert into table gulivideo_user_orc select * from gulivideo_user_ori;
  1. 安装 Tez 引擎(了解)

tez.apache.org/

Tez 是一个 Hive 的运行引擎,性能优于 MR。为什么优于 MR 呢?看下。

image.png

用 Hive 直接编写 MR 程序,假设有四个有依赖关系的 MR 作业,上图中,绿色是 ReduceTask,云状表示写屏蔽,需要将中间结果持久化写到 HDFS。

Tez 可以将多个有依赖的作业转换为一个作业,这样只需写一次 HDFS,且中间节点较少,从而大大提升作业的计算性能。

* 将 tez 安装包拷贝到集群,并解压 tar 包



1
2
3
shell复制代码[moe@hadoop102 ~]$ mkdir /opt/module/tez

[moe@hadoop102 ~]$ tar -zxvf /opt/software/tez-0.10.1-SNAPSHOT-minimal.tar.gz -C /opt/module/tez/
* 上传 tez 依赖到 HDFS
1
2
3
shell复制代码[moe@hadoop102 ~]$ hadoop fs -mkdir /tez

[moe@hadoop102 ~]$ hadoop fs -put /opt/software/tez-0.10.1-SNAPSHOT.tar.gz /tez
* 新建 tez-site.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="configuration.xsl"?>
<configuration>

<property>
<name>tez.lib.uris</name>
<value>${fs.defaultFS}/tez/tez-0.10.1-SNAPSHOT.tar.gz</value>
</property>

<property>
<name>tez.use.cluster.hadoop-libs</name>
<value>true</value>
</property>

<property>
<name>tez.am.resource.memory.mb</name>
<value>1024</value>
</property>

<property>
<name>tez.am.resource.cpu.vcores</name>
<value>1</value>
</property>

<property>
<name>tez.container.max.java.heap.fraction</name>
<value>0.4</value>
</property>

<property>
<name>tez.task.resource.memory.mb</name>
<value>1024</value>
</property>

<property>
<name>tez.task.resource.cpu.vcores</name>
<value>1</value>
</property>

</configuration>
* 修改 Hadoop 环境变量
1
shell复制代码[moe@hadoop102 ~]$ vim $HADOOP_HOME/etc/hadoop/shellprofile.d/tez.sh
添加 Tez 的 Jar 包相关信息
1
2
3
4
5
6
7
sh复制代码hadoop_add_profile tez
function _tez_hadoop_classpath
{
hadoop_add_classpath "$HADOOP_HOME/etc/hadoop" after
hadoop_add_classpath "/opt/module/tez/*" after
hadoop_add_classpath "/opt/module/tez/lib/*" after
}
* 修改 Hive 的计算引擎
1
shell复制代码[moe@hadoop102 ~]$ vim $HIVE_HOME/conf/hive-site.xml
添加
1
2
3
4
5
6
7
8
9
xml复制代码<property>
<name>hive.execution.engine</name>
<value>tez</value>
</property>

<property>
<name>hive.tez.container.size</name>
<value>1024</value>
</property>
* 解决日志 Jar 包冲突
1
shell复制代码[moe@hadoop102 ~]$ rm /opt/module/tez/lib/slf4j-log4j12-1.7.10.jar

4、业务分析

4.1、统计视频观看数 Top10

思路:使用 order by 按照 views 字段做一个全局排序即可,同时我们设置只显示前 10 条。

最终SQL:

1
2
3
4
5
6
7
8
hive复制代码SELECT 
videoId,
views
FROM
gulivideo_orc
ORDER BY
views DESC
LIMIT 10;

image.png

4.2、统计视频类别热度 Top10

思路:
(1)即统计每个类别有多少个视频,显示出包含视频最多的前 10 个类别。

(2)我们需要按照类别 group by 聚合,然后 count 组内的 videoId 个数即可。

(3)因为当前表结构为:一个视频对应一个或多个类别。所以如果要 group by 类别,需要先将类别进行 列转行(展开),然后再进行 count 即可。

(4)最后按照热度排序,显示前 10 条。

最终SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
hive复制代码SELECT 
t1.category_name ,
COUNT(t1.videoId) hot
FROM
(
SELECT
videoId,
category_name
FROM
gulivideo_orc
lateral VIEW explode(category) gulivideo_orc_tmp AS category_name
) t1
GROUP BY
t1.category_name
ORDER BY
hot
DESC
LIMIT 10;

image.png

4.3、统计出视频观看数最高的 20 个视频的所属类别以及类别包含Top20 视频的个数

思路:
(1)先找到观看数最高的 20 个视频所属条目的所有信息,降序排列

(2)把这 20 条信息中的 category 分裂出来(列转行)

(3)最后查询视频分类名称和该分类下有多少个 Top20 的视频

最终SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
hive复制代码SELECT 
t2.category_name,
COUNT(t2.videoId) video_sum
FROM
(
SELECT
t1.videoId,
category_name
FROM
(
SELECT
videoId,
views ,
category
FROM
gulivideo_orc
ORDER BY
views
DESC
LIMIT 20
) t1
lateral VIEW explode(t1.category) t1_tmp AS category_name
) t2
GROUP BY t2.category_name;

image.png

4.4、统计视频观看数 Top50 所关联视频的所属类别排序

最终SQL:

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
hive复制代码SELECT
t6.category_name,
t6.video_sum,
rank() over(ORDER BY t6.video_sum DESC ) rk
FROM
(
SELECT
t5.category_name,
COUNT(t5.relatedid_id) video_sum
FROM
(
SELECT
t4.relatedid_id,
category_name
FROM
(
SELECT
t2.relatedid_id ,
t3.category
FROM
(
SELECT
relatedid_id
FROM
(
SELECT
videoId,
views,
relatedid
FROM
gulivideo_orc
ORDER BY
views
DESC
LIMIT 50
)t1
lateral VIEW explode(t1.relatedid) t1_tmp AS relatedid_id
)t2
JOIN
gulivideo_orc t3
ON
t2.relatedid_id = t3.videoId
) t4
lateral VIEW explode(t4.category) t4_tmp AS category_name
) t5
GROUP BY
t5.category_name
ORDER BY
video_sum
DESC
) t6;

image.png

4.5、统计每个类别中的视频热度 Top10,以 Music 为例

思路:

(1)要想统计 Music 类别中的视频热度 Top10,需要先找到 Music 类别,那么就需要将category 展开,所以可以创建一张表用于存放 categoryId 展开的数据。

(2)向 category 展开的表中插入数据。

(3)统计对应类别(Music)中的视频热度。统计 Music 类别的 Top10(也可以统计其他)

最终SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
hive复制代码SELECT 
t1.videoId,
t1.views,
t1.category_name
FROM
(
SELECT
videoId,
views,
category_name
FROM gulivideo_orc
lateral VIEW explode(category) gulivideo_orc_tmp AS category_name
)t1
WHERE
t1.category_name = "Music"
ORDER BY
t1.views
DESC
LIMIT 10;

image.png

4.6、统计每个类别视频观看数 Top10

最终SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
hive复制代码SELECT 
t2.videoId,
t2.views,
t2.category_name,
t2.rk
FROM
(
SELECT
t1.videoId,
t1.views,
t1.category_name,
rank() over(PARTITION BY t1.category_name ORDER BY t1.views DESC ) rk
FROM
(
SELECT
videoId,
views,
category_name
FROM gulivideo_orc
lateral VIEW explode(category) gulivideo_orc_tmp AS category_name
)t1
)t2
WHERE t2.rk <=10;

4.7、统计上传视频最多的用户 Top10以及他们上传的视频观看次数在前 20 的视频

思路:

(1)求出上传视频最多的 10 个用户

(2)关联 gulivideo_orc 表,求出这 10 个用户上传的所有的视频,按照观看数取前 20

最终SQL:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
hive复制代码SELECT 
t2.videoId,
t2.views,
t2.uploader
FROM
(
SELECT
uploader,
videos
FROM gulivideo_user_orc
ORDER BY
videos
DESC
LIMIT 10
) t1
JOIN gulivideo_orc t2
ON t1.uploader = t2.uploader
ORDER BY
t2.views
DESC
LIMIT 20;

image.png

二、常见错误及解决方案

  1. 如果更换 Tez 引擎后,执行任务卡住,可以尝试调节容量调度器的资源调度策略

将$HADOOP_HOME/etc/hadoop/capacity-scheduler.xml 文件中的

1
2
3
4
5
6
7
8
9
xml复制代码<property>
<name>yarn.scheduler.capacity.maximum-am-resource-percent</name>
<value>0.1</value>
<description>
Maximum percent of resources in the cluster which can be used to run
application masters i.e. controls number of concurrent running
applications.
</description>
</property>

改成

1
2
3
4
5
6
7
8
9
xml复制代码<property>
<name>yarn.scheduler.capacity.maximum-am-resource-percent</name>
<value>1</value>
<description>
Maximum percent of resources in the cluster which can be used to run
application masters i.e. controls number of concurrent running
applications.
</description>
</property>
  1. JVM 堆内存溢出

描述:java.lang.OutOfMemoryError: Java heap space

解决:在 yarn-site.xml 中加入如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
xml复制代码<property>
<name>yarn.scheduler.maximum-allocation-mb</name>
<value>2048</value>
</property>

<property>
<name>yarn.scheduler.minimum-allocation-mb</name>
<value>2048</value>
</property>

<property>
<name>yarn.nodemanager.vmem-pmem-ratio</name>
<value>2.1</value>
</property>

<property>
<name>mapred.child.java.opts</name>
<value>-Xmx1024m</value>
</property>
  1. 虚拟内存限制

在 yarn-site.xml 中添加如下配置:

1
2
3
4
xml复制代码<property>
<name>yarn.nodemanager.vmem-check-enabled</name>
<value>false</value>
</property>

三、友情链接

大数据Hive学习之旅第六篇

大数据Hive学习之旅第五篇

大数据Hive学习之旅第四篇

大数据Hive学习之旅第三篇

大数据Hive学习之旅第二篇

大数据Hive学习之旅第一篇

本文转载自: 掘金

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

关于Javaweb必知的网络基础知识(一) 引言

发表于 2021-11-28

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

网络基础知识很重要,很后悔没好好学,果然该以前欠下的债终究还是要还,推荐阅读书籍《图解HTTP》,贴上下载链接:

链接:pan.baidu.com/s/1AK_rxTES… 提取码:ya91

本文学习参考来源:www.runoob.com/w3cnote/sum…

引言

计算机网络学习的核心内容就是网络协议的学习。网络协议是为计算机网络中进行数据交换而建立的规则、标准或者说是约定的集合。因为不同用户的数据终端可能采取的字符集是不同的,两者需要进行通信,必须要在一定的标准上进行。目前我们所熟知的TCP/IP协议就是Internet中的通用语言。不同计算机之间就是通过相同的协议进行通信,如:

1638003573864.png

  1. 网络层次划分

计算机网络体系结构最初是 “开放系统互联参考模型”,即著名的OSI/RM模型(Open System Interconnection/Reference Model)。 它将计算机网络体系结构的通信协议划分为七层,自下而上依次为:物理层(Physics Layer)、数据链路层(Data Link Layer)、网络层(Network Layer)、传输层(Transport Layer)、会话层(Session Layer)、表示层(Presentation Layer)、应用层(Application Layer)。其中第四层完成数据传送服务,上面三层面向用户。 之后还有五层模型以及四层模型,示意图如下:

1638004411090.png

2.OSI七层网络模型

TCP/IP协议是互联网的基础协议,没有它就无法上网,任何和互联网有关的操作都离不开TCP/IP协议。 下图展示了每层的协议:

1638004608663.png

1)物理层

激活、维持、关闭通信端点之间的机械特性、电气特性、功能特性以及过程特性。该层为上层协议提供了一个传输数据的可靠的物理媒体。简单的说,物理层确保原始的数据可在各种物理媒体上传输。 物理层记住两个重要的设备名称,中继器(Repeater,也叫放大器)和集线器。

2)数据链路层

数据链路层在物理层提供的服务的基础上向网络层提供服务,其最基本的服务是将源自网络层来的数据可靠地传输到相邻节点的目标机网络层。 为达到这一目的,数据链路必须具备一系列相应的功能,主要有:如何将数据组合成数据块,在数据链路层中称这种数据块为帧(frame),帧是数据链路层的传送单位;如何控制帧在物理信道上的传输,包括如何处理传输差错,如何调节发送速率以使与接收方相匹配;以及在两个网络实体之间提供数据链路通路的建立、维持和释放的管理。数据链路层在不可靠的物理介质上提供可靠的传输。该层的作用包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。

有关数据链路层的重要知识点:

  • 数据链路层为网络层提供可靠的数据传输;
  • 基本数据单位为帧;
  • 主要的协议:以太网协议;
  • 两个重要设备名称:网桥和交换机。

3)网络层

网络层的目的是实现两个端系统之间的数据透明传送,具体功能包括寻址和路由选择、连接的建立、保持和终止等。它提供的服务使传输层不需要了解网络中的数据传输和交换技术。 网络层涉及众多的协议,最主要的功能就是 “路径选择、路由及逻辑寻址” 。

有关网络层的重点为:

  • 网络层负责对子网间的数据包进行路由选择。此外,网络层还可以实现拥塞控制、网际互连等功能;
  • 基本数据单位为IP数据报;
  • 包含的主要协议:
+ **IP协议(Internet Protocol,因特网互联协议);**
+ **ICMP协议(Internet Control Message Protocol,因特网控制报文协议);**
+ **ARP协议(Address Resolution Protocol,地址解析协议);**
+ **RARP协议(Reverse Address Resolution Protocol,逆地址解析协议)。**
  • 重要的设备:路由器。

4 )传输层

传输层负责将上层数据分段并提供端到端的、可靠的或不可靠的传输以及端到端的差错控制和流量控制问题; 包含的主要协议:TCP协议(Transmission Control Protocol,传输控制协议)、UDP协议(User Datagram Protocol,用户数据报协议) ,主要的设备:网关。

5)会话层

会话层管理主机之间的会话进程,即负责建立、管理、终止进程之间的会话。会话层还利用在数据中插入校验点来实现数据的同步。

6)表示层

表示层对上层数据或信息进行变换以保证一个主机应用层信息可以被另一个主机的应用程序理解。表示层的数据转换包括数据的加密、压缩、格式转换等。

5)应用层

为操作系统或网络应用程序提供访问网络服务的接口。

会话层、表示层和应用层重点:

  • 数据传输基本单位为报文;
  • 包含的主要协议:FTP(文件传送协议)、Telnet(远程登录协议)、DNS(域名解析协议)、SMTP(邮件传送协议),POP3协议(邮局协议),HTTP协议(Hyper Text Transfer Protocol)。

3.网络编程三要素

  • IP地址

要想让网络中的计算机能够互相通信,必须为每台计算机指定一个标识号,通过这个标识号来指定要接收数据的计算机和识别发送的计算机,而IP地址就是这个标识号。也就是设备的标识。

  • 端口

网络的通信,本质上是两个应用程序的通信。每台计算机都有很多的应用程序,那么在网络通信时,如何区分这些应用程序呢?如果说IP地址可以唯一标识网络中的设备,那么端口号就可以唯一标识设备中的应用程序了。也就是应用程序的标识。

  • 协议

通过计算机网络可以使多台计算机实现连接,位于同一个网络中的计算机在进行连接和通信时需要遵守一定的规则,这就好比在道路中行驶的汽车一定要遵守交通规则一样。在计算机网络中,这些连接和通信的规则被称为网络通信协议,它对数据的传输格式、传输速率、传输步骤等做了统一规定,通信双方必须同时遵守才能完成数据交换。常见的协议有UDP协议和TCP协议。

4.IP地址

IP地址:是网络中设备的唯一标识。

IP地址主要分为两大类:

  • IPv4:是给每个连接在网络上的主机分配一个32bit地址。按照TCP/IP规定,IP地址用二进制来表示,每个IP地址长32bit,也就是4个字节。例如一个采用二进制形式的IP地址是“11000000 10101000 00000001 01000010”,这么长的地址,处理起来也太费劲了。为了方便使用,IP地址经常被写成十进制的形式,中间使用符号“.”分隔不同的字节。于是,上面的IP地址可以表示为“192.168.1.66”。IP地址的这种表示法叫做“点分十进制表示法”,这显然比1和0容易记忆得多
  • IPv6:由于互联网的蓬勃发展,IP地址的需求量愈来愈大,但是网络地址资源有限,使得IP的分配越发紧张。为了扩大地址空间,通过IPv6重新定义地址空间,采用128位地址长度,每16个字节一组,分成8组十六进制数,这样就解决了网络地址资源数量不够的问题

还有常见的127.0.0.1:是回送地址,可以代表本机地址,一般用来测试使用。

5.端口和协议

  • 端口
+ 设备上应用程序的唯一标识
  • 端口号
+ 用两个字节表示的整数,它的取值范围是0~~65535。其中,0~~1023之间的端口号用于一些知名的网络服务和应用,普通的应用程序需要使用1024以上的端口号。如果端口号被另外一个服务或应用所占用,会导致当前程序启动失败
  • 协议
+ 计算机网络中,连接和通信的规则被称为网络通信协议
  • UDP协议
+ 用户数据报协议(User Datagram Protocol)
+ UDP是无连接通信协议,即在数据传输时,数据的发送端和接收端不建立逻辑连接。简单来说,当一台计算机向另外一台计算机发送数据时,发送端不会确认接收端是否存在,就会发出数据,同样接收端在收到数据时,也不会向发送端反馈是否收到数据。
+ 由于使用UDP协议消耗系统资源小,通信效率高,所以通常都会用于音频、视频和普通数据的传输
+ 例如视频会议通常采用UDP协议,因为这种情况即使偶尔丢失一两个数据包,也不会对接收结果产生太大影响。但是在使用UDP协议传送数据时,由于UDP的面向无连接性,不能保证数据的完整性,因此在传输重要数据时不建议使用UDP协议
  • TCP协议
+ 传输控制协议 (Transmission Control Protocol)
+ TCP协议是面向连接的通信协议,即传输数据之前,在发送端和接收端建立逻辑连接,然后再传输数据,它提供了两台计算机之间可靠无差错的数据传输。在TCP连接中必须要明确客户端与服务器端,由客户端向服务端发出连接请求,每次连接的创建都需要经过“三次握手”
+ 三次握手:TCP协议中,在发送数据的准备阶段,客户端与服务器之间的三次交互,以保证连接的可靠


第一次握手,客户端向服务器端发出连接请求,等待服务器确认


第二次握手,服务器端向客户端回送一个响应,通知客户端收到了连接请求


第三次握手,客户端再次向服务器端发送确认信息,确认连接
+ 完成三次握手,连接建立后,客户端和服务器就可以开始进行数据传输了。由于这种面向连接的特性,TCP协议可以保证传输数据的安全,所以应用十分广泛。例如上传文件、下载文件、浏览网页等

本文转载自: 掘金

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

Docker——镜像原理之联合文件系统和分层理解

发表于 2021-11-28

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

1、联合文件系统

UnionFS( 联合文件系统)

UnionFS( 联合文件系统):Union文件系统(UnionFS )是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下(unite several directories into a single virtualfilesystem)。Union文件系统是Docker镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。

另外,不同 Docker 容器就可以共享一些基础的文件系统层,同时再加上自己独有的改动层,大大提高了存储的效率。

Docker 中使用的 AUFS(AnotherUnionFS)就是一种联合文件系统。 AUFS 支持为每一个成员目录(类似 Git 的分支)设定只读(readonly)、读写(readwrite)和写出(whiteout-able)权限, 同时 AUFS 里有一个类似分层的概念, 对只读权限的分支可以逻辑上进行增量地修改(不影响只读部分的)。

Docker 目前支持的联合文件系统种类包括 AUFS, btrfs, vfs 和 DeviceMapper。

特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录。

base 镜像

base 镜像简单来说就是不依赖其他任何镜像,完全从0开始建起,其他镜像都是建立在他的之上,可以比喻为大楼的地基,docker镜像的鼻祖。

base 镜像有两层含义:(1)不依赖其他镜像,从 scratch 构建;(2)其他镜像可以之为基础进行扩展。

所以,能称作 base 镜像的通常都是各种 Linux 发行版的 Docker 镜像,比如 Ubuntu, Debian, CentOS 等。

Docker 镜像加载原理

docker的镜像实际上由一层一层的文件系统组成,这种层级的文件系统就是UnionFS。

典型的 Linux 启动到运行需要两个FS,bootfs + rootfs:

在这里插入图片描述

bootfs(boot file system)主要包含 bpotloader 和 kernel,bootloader主要是引导加载 kernel,Linux 刚启动时会加载 bootfs文件系统,在Docker镜像的最底层是bootfs。这一层与我们典型的Linux/Unix系统是一样的,包含boot加载器bootloader和内核kernel。当boot加载完成之后整个内核就都在内存中了,此时内存的使用权已由bootfs转交给内核,此时系统也会卸载bootfs。

rootfs (root file system),在bootfs之上。包含的就是典型Linux系统中的/dev, /proc, /bin, /etc等标准目录和文件。roots就是各种不同的操作系统发行版,比如Ubuntu,Centos等等。

Docker 镜像中为什么没有内核

从镜像大小上面来说,一个比较小的镜像只有1KB多点,或几MB,而内核文件需要几十MB, 因此镜像里面是没有内核的,镜像在被启动为容器后将直接使用宿主机的内核,而镜像本身则只提供相应的rootfs,即系统正常运行所必须的用户空间的文件系统,比如/dev/,/proc,/bin,/etc等目录,所以容器当中基本是没有/boot目录的,而/boot当中保存的就是与内核相关的文件和目录。

由于容器启动和运行过程中是直接使用了宿主机的内核,不会直接调用过物理硬件,所以也不会涉及到硬件驱动,因此也用不上内核和驱动。而如果虚拟机技术,对应每个虚拟机都有自已独立的内核

2、分层结构

Docker 镜像是一种分层结构,每一层构建在其他层之上,从而实现增量增加内容的功能,Docker 镜像下载的时候也是分层下载,以下载redis镜像为例:

在这里插入图片描述

在这里插入图片描述

可以看到,新镜像是从 base 镜像一层一层叠加生成的。每安装一个软件,就在现有镜像的基础上增加一层。

为什么 Docker 镜像要采用这种分层结构呢?

最大的好处,莫过于是资源共享了。比如有多个镜像都从相同的Base镜像构建而来,那么宿主机只需在磁盘上保留一份base镜像,同时内存中也只需要加载一份base镜像,这样就可以为所有的容器服务了,而且镜像的每一层都可以被共享。

可写的容器层

Docker 镜像都只是可读(read-only)的,当容器启动时,一个新的可写层被加载到镜像顶部。

这新的一层就是可写的容器层,容器之下都叫镜像层。

在这里插入图片描述

Docker通过一个修改时复制策略copy-on-write来保证base镜像的安全性,以及更高的性能和空间利用率。

  • 当容器需要读取文件的时候

从最上层的镜像层开始往下找,找到后读取到内存中,若已经在内存中,可以直接使用。换句话说,运行在同一台机器上的Docker容器共享运行时相同的文件。

  • 当容器需要修改文件的时候

从上往下查找,找到后复制到容器层,对于容器来说,可以看到的是容器层的这个文件,看不到镜像层里的文件,然后直接修改容器层的文件。

  • 当容器需要删除文件的时候

从上往下查找,找到后在容器中记录删除,并不是真正的删除,而是软删除。这导致镜像体积只会增加,不会减少。

  • 当容器需要增加文件的时候

直接在最上层的容器可写层增加,不会影响镜像层。

所有对容器的改动,无论添加、删除、还是修改文件都只会发生在容器层中。只有容器层是可写的,容器层下面的所有镜像层都是只读的,所以镜像可以被多个容器共享。

3、分层实践——commit 提交镜像

通过镜像创建容器,然后对容器层进行操作,镜像层不动,再把操作后的容器层和镜像层打包成一个新的镜像提交。

docker commit:用容器创建一个新的镜像。

语法:

1
css复制代码docker commit [OPTIONS] CONTAINER [REPOSITORY[:TAG]]

OPTIONS说明:

  • **-a :**提交的镜像作者;
  • **-c :**使用Dockerfile指令来创建镜像;
  • **-m :**提交时的说明文字;
  • **-p :**在commit时,将容器暂停。

使用实例:通过镜像创建容器,然后对容器层进行操作,再把操作后的容器层和镜像层打包成一个新的镜像提交。

1、先下载 tomcat 镜像

2、通过tomcat 镜像创建运行tomcat 容器:

1
shell复制代码docker run -d --name="tomcat01" tomcat

3、进入正在运行的tomcat容器:

1
shell复制代码docker exec -it tomcat01 /bin/bash

4、把tomcat容器 webapps.dist目录下的文件拷贝到webapps目录下:

1
shell复制代码cp -r webapps.dist/* webapps

5、docker commit 提交镜像

将容器 dc904437d987 保存为新的镜像,并添加提交人信息和说明信息,提交后的镜像名为tomcatplus,版本为1.0:

1
shell复制代码docker commit -a="wanli" -m="add webapps files" dc904437d987 tomcatplus:1.0

在这里插入图片描述

可以看到commit提交后的新 tomcat 镜像大小比原来的tomcat镜像要大一点,因为我们在容器层中进行了复制文件操作。

在这里插入图片描述

本文转载自: 掘金

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

dart系列之 还在为编码解码而烦恼吗?用dart试试 简介

发表于 2021-11-28

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

简介

在我们日常使用的数据格式中json应该是最为通用的一个。很多时候,我们需要把一个对象转换成为JSON的格式,也可以说需要把对象编码为JSON。

虽然在dart中所有的字符都是以UTF-16来存储的,但是更加通用的格式应该是UTF-8,同样的dart也提供了对UTF-8的编码支持。

所有的这一切,都包含在dart:convert包中。

要想使用convet包,简单的引入即可:

1
arduino复制代码import 'dart:convert';

为JSON编码和解码

首先要注意的是,虽然dart中可以用单引号或者双引号来表示字符串,但是在json中,字符串必须是以双引号来表示的,否则就不是一个真正的json,这点一定要注意,所以,我们需要这样定义JSON :

1
2
3
4
5
6
ini复制代码var studentJson = '''
[
{"name": "jack"},
{"age": 18}
]
''';

而不是这样:

1
2
3
4
5
6
ini复制代码var studentJson = '''
[
{'name': 'jack'},
{'age': 18}
]
''';

如果要让json字符串转换成为对象,则可以使用convert包里面的jsonDecode方法:

1
2
3
4
5
6
ini复制代码var studentList = jsonDecode(studentJson);
assert(studentList is List);

var student = studentList[0];
assert(student is Map);
assert(student['name'] == "jack");

除了decode之外,还可以将对象encode成为Json字符串:

1
2
3
4
5
6
7
8
ini复制代码var studentList = [
{"name": "jack"},
{"age": 18}
];

var studentString = jsonEncode(studentList);
assert(studentString ==
'[{"name":"jack"},{"age":18}]');

上面对象只是一个简单的对象,可以直接转换成为JSON。如果是复杂对象怎么办呢?

比如对象中嵌套对象,那么嵌入的对象是否也会被转换成为JSON呢?

dart考虑到了这个问题,所以在jsonEncode方法中还有第二个参数,表示如何将不可直接encode的对象转换成为可以encode的对象:

1
2
3
css复制代码String jsonEncode(Object? object,
{Object? toEncodable(Object? nonEncodable)?}) =>
json.encode(object, toEncodable: toEncodable);

如果第二个参数被忽略了,那么会调用对应对象的.toJson()方法。

UTF-8编码和解码

先看下UTF-8的解码方法:

1
arduino复制代码 String decode(List<int> codeUnits, {bool? allowMalformed})

第一个参数是传入一个UTF-8的codeUnits数组,第二个参数表示是否替换Unicode替换字符的字符序列U+FFFD。 如果传入false的话,遇到这样的字符就会抛出FormatException。

看一个使用的例子:

1
2
3
4
ini复制代码List<int> utf8Bytes = [119, 119, 119, 46, 102, 108, 121, 100, 101, 97, 110, 46, 99, 111, 109];

var site = utf8.decode(utf8Bytes);
assert(site == 'www.flydean.com');

相应的,可以使用encode对字符串或者其他对象进行编码:

1
bash复制代码print(utf8.encode('www.flydean.com'));

总结

以上dart对json和UTF-8的支持。

本文已收录于 www.flydean.com/19-dart-dec…

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

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

【Go实战 电商平台】(8) 创建商品 写在前面 1

发表于 2021-11-28

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

写在前面

与前一章一样,我们这个步骤也是需要jwt鉴权的,因为你要知道是谁创建了商品,所以我们要在请求头上加上 token 连同 data 的信息一起传来创建商品

  1. 创建商品

1.1 路由接口注册

  • post 请求
1
go复制代码authed.POST("product", api.CreateProduct)

1.2 接口函数编写

1.2.1 service层

  • 定义一个创建商品的服务结构体
1
2
3
4
5
6
7
8
9
go复制代码type CreateProductService struct {
Name string `form:"name" json:"name"`
CategoryID int `form:"category_id" json:"category_id"`
Title string `form:"title" json:"title" bind:"required,min=2,max=100"`
Info string `form:"info" json:"info" bind:"max=1000"`
ImgPath string `form:"img_path" json:"img_path"`
Price string `form:"price" json:"price"`
DiscountPrice string `form:"discount_price" json:"discount_price"`
}
  • 定义一个创建商品的create方法

传入进来的有id是上传者的id,file和fileSize 是上传的商品图片以及其图片大小。

1
2
3
go复制代码func (service *CreateProductService)Create(id uint,file multipart.File,fileSize int64) serializer.Response {
...
}

1.2.2 api层

  • 定义创建商品的对象
1
go复制代码createProductService := service.CreateProductService{}
  • 获取token,并解析当前对象的id
1
go复制代码claim ,_ := util.ParseToken(c.GetHeader("Authorization"))
  • 获取传送过来的文件
1
go复制代码file , fileHeader ,_ := c.Request.FormFile("file")
  • 绑定上下文数据
1
go复制代码c.ShouldBind(&createProductService)
  • 执行创建对象下的create()方法,传入用户的id,文件以及文件大小等
1
go复制代码res := createProductService.Create(claim.ID,file,fileSize)

1.3 服务函数编写

编写创建商品的服务函数

  • 验证用户
1
2
go复制代码	var boss model.User
model.DB.Model(&model.User{}).First(&boss,id)
  • 上传图片到七牛云
1
2
3
4
5
6
7
8
go复制代码status , info := util.UploadToQiNiu(file,fileSize)
if status != 200 {
return serializer.Response{
Status: status ,
Data: e.GetMsg(status),
Error:info,
}
}
  • 获取分类
1
go复制代码model.DB.Model(&model.Category{}).First(&category,service.CategoryID)
  • 构建商品对象
1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码product := model.Product{
Name: service.Name,
Category: category,
CategoryID: uint(service.CategoryID),
Title: service.Title,
Info: service.Info,
ImgPath: info,
Price: service.Price,
DiscountPrice: service.DiscountPrice,
BossID: int(id),
BossName: boss.UserName,
BossAvatar: boss.Avatar,
}
  • 在数据库中创建
1
2
3
4
5
6
7
8
9
10
go复制代码err := model.DB.Create(&product).Error
if err != nil {
logging.Info(err)
code = e.ErrorDatabase
return serializer.Response{
Status: code,
Msg: e.GetMsg(code),
Error: err.Error(),
}
}
  • 返回序列化的商品信息
1
2
3
4
5
go复制代码	return serializer.Response{
Status: code,
Data: serializer.BuildProduct(product),
Msg: e.GetMsg(code),
}

1.4 验证服务

  • 发送请求

在这里插入图片描述

  • 请求响应

在这里插入图片描述

本文转载自: 掘金

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

Linux samba搭建和使用

发表于 2021-11-28

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

前言

Samba是在Linux和UNIX系统上实现SMB协议的一个免费软件,由服务器及客户端程序构成。SMB(Server Messages Block,信息服务块)是一种在局域网上共享文件和打印机的一种通信协议,它为局域网内的不同计算机之间提供文件及打印机等资源的共享服务。SMB协议是客户机/服务器型协议,客户机通过该协议可以访问服务器上的共享文件系统、打印机及其他资源。通过设置“NetBIOS over TCP/IP”使得Samba不但能与局域网络主机分享资源,还能与全世界的电脑分享资源。

那么如何配置samba呢?

一、Linux Samba安装

1、配置yum源

由于samba是linux额外的功能,需要使用镜像进行安装,因此需要挂载镜像并且配置yum源!

1
2
3
4
5
6
7
8
9
bash复制代码#挂载磁盘镜像
mount /dev/cdrom /mnt

#配置本地yum源
echo "[local]" >> /etc/yum.repos.d/local.repo
echo "name = local" >> /etc/yum.repos.d/local.repo
echo "baseurl = file:///mnt/" >> /etc/yum.repos.d/local.repo
echo "enabled = 1" >> /etc/yum.repos.d/local.repo
echo "gpgcheck = 0" >> /etc/yum.repos.d/local.repo

2、关闭防火墙

在配置samba时,建议关闭防火墙,可以配置好之后再开启对应端口。

1
2
3
bash复制代码#确保防火墙关闭,445端口开放
systemctl stop firewalld
systemctl disable firewalld

3、安装samba包

使用 yum 安装 samba 软件:

1
2
bash复制代码#安装samba
yum install samba -y

4、smb配置

使用一下方式配置 samba 服务端:

编辑smb配置文件:

1
2
3
4
5
6
7
8
9
bash复制代码cat <<EOF>>/etc/samba/smb.conf
[Share]
comment = Shared Folder
path = /oradata/rmanbak
public = yes
writable = yes
available = yes
browseable = yes
EOF

添加root到smb服务器,设定密码oracle:

1
2
bash复制代码smbpasswd -a root
密码:oracle

重新加载重启服务:

1
2
3
bash复制代码service smb reload
service smb restart
service nmb restart

注意:path是需要共享的文件路径。

5、Windows访问samba服务器

Windows访问samba服务器:

1
2
bash复制代码本地映射驱动:\\10.211.55.101\share
root/oracle

如果觉得文章对你有帮助,点赞、收藏、关注、评论,一键四连支持,你的支持就是我创作最大的动力,技术交流可以添加公众号:Lucifer三思而后行~

本文转载自: 掘金

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

简单的HTTP协议

发表于 2021-11-28

本文正在参与 “网络协议必知必会”征文活动


关于HTTP协议

  • 应用HTTP协议时,必定是一端担任客户端角色,一端担任服务器端角色
  • HTTP协议通过请求和响应的交换达成通信。请求从客服端发出,最后服务器端响应该请求并返回
  • HTTP协议是不保存状态的,即无状态协议,协议对发送或响应的请求都不作持久化处理

告知服务器意图的HTTP方法

  • GET:获取资源

GET方法用来请求已经被URI标识的资源

Snip20210719_18.png

  • POST:传输实体主体

POST方法用来传输实体的主体。POST方法和GET方法功能相似,但是POST的主要目的并不是获取响应的主体内容

Snip20210719_19.png

  • PUT:传输文件

PUT方法用来传输文件。在请求报文的主体中包含文件内容,然后保存到请求URI指定的位置

Snip20210719_20.png

  • DELETE:删除文件

DELETE方法用来删除文件。与PUT相反的方法,DELETE方法按请求URI删除指定的资源

Snip20210719_28.png

  • HEAD:获得报文首部

HEAD方法和GET方法一样,但是不返回报文主体部分,用于确认URI的有效性及资源更新的日期时间等

Snip20210719_29.png

  • OPTIONS:询问支持的方法

OPTIONS方法用来查询针对请求URI指定的资源支持的方法

Snip20210719_30.png

  • TRACE:追踪路径

发送请求时,在Max-Forward首部字段中填入数值,每经过一个服务器端就将该数字减1,当数值刚好减到0时,就停止继续传输,最后接收到请求的服务器端则返回状态码200 OK的响应

客户端通过TRACE方法可以查询发出去的请求是怎么被加工修改的,这是因为,请求想要连接到源目标服务器可能会通过代理中转,TRACE方法就是用来确认连接过程中发生的一些列操作

Snip20210719_31.png

  • CONNECT:要求用隧道协议连接代理

CONNECT方法要求在代理服务器通信时建立隧道,实现用隧道协议进行TCP通信,主要使用SSL(Secure Sockets Layer,安全套接层)和TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输

Snip20210719_32.png


持久连接节省通信量

  • HTTP协议的初始版本中,每进行一次HTTP通信就要断开一次TCP连接

比如,使用浏览器浏览一个包含多张图片的HTML页面时,在发送请求访问HTML页面资源的同时,也会请求该HTML页面里包含的其他资源。因此,每次的请求都会造成无谓的TCP连接建立和断开,增加通信量的开销

Snip20210720_37.png

  • 持久连接

只要任意一端没有明确提出断开连接,则保持TCP连接状态

持久连接的好处在于减少了TCP连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。另外,减少开销的那部分时间,使HTTP请求和响应能够更早地结束,这样Web页面的显示速度也就相应提高了

Snip20210720_36.png

  • 管线化

持久连接使得多数请求以管线化(pipelining)方式发送成为可能

从前发送请求后需等待并收到响应,才能发送下一个请求。管线化技术出现后,不用等待响应亦可直接发送下一个请求。这样就能够做到同时并行发送多个请求,而不需要一个接一个地等待响应了。

Snip20210720_35.png


使用Cookie的状态管理

  • 引入Cookie技术的原因

由于HTTP是无状态协议,假设要求登录认证的Web页面本身无法进行状态的管理(不记录登录状态),那么每次跳转新页面都需要再次登录

  • 使用Cookie技术管理状态

Cookie技术通过在请求和响应报文中写入Cookie信息来控制客户端的状态。

Cookie 会根据从服务器端发送的响应报文内的一个叫做Set-Cookie的首部字段信息,通知客户端保存Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入Cookie值后发送出去。服务器端发现客户端发送过来的Cookie后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到之前的状态信息。

+ 第一次,没有`Cookie`信息状态下的请求


![Snip20210720_33.png](https://gitee.com/songjianzaina/juejin_p7/raw/master/img/6b98172c7e8b3a38c7ab9d025c3e561c0c8685534b43c943cf701cbb23dfdc05)
+ 第二次及以后,存在`Cookie`信息状态下的请求
![Snip20210720_34.png](https://gitee.com/songjianzaina/juejin_p7/raw/master/img/6249fdc246270cf15c1f5faf9b60d7272956c22b5f6d5b2fe3bd74864057ea9b)

本文转载自: 掘金

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

1…144145146…956

开发者博客

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