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

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


  • 首页

  • 归档

  • 搜索

sql注入竟然把我们的系统搞挂了,该怎么办? 前言 1 还

发表于 2021-02-07

前言

最近我在整理安全漏洞相关问题,准备在公司做一次分享。恰好,这段时间团队发现了一个sql注入漏洞:在一个公共的分页功能中,排序字段作为入参,前端页面可以自定义。在分页sql的mybatis mapper.xml中,order by字段后面使用$符号动态接收计算后的排序参数,这样可以实现动态排序的功能。

但是,如果入参传入:

1
sql复制代码id; select 1 --

最终执行的sql会变成:

1
sql复制代码select * from test1 order by id; select 1 -- limit 1,20

--会把后面的limit语句注释掉,导致分页条件失效,返回了所有数据。攻击者可以通过这个漏洞一次性获取所有数据。

动态排序这个功能原本的想法是好的,但是却有sql注入的风险。值得庆幸的是,这次我们及时发现了问题,并且及时解决了,没有造成什么损失。

但是,几年前在老东家的时候,就没那么幸运了。

最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。
BAT大佬写的刷题笔记,让我offer拿到手软

一次sql注入直接把我们支付服务搞挂了。

  1. 还原事故现场

有一天运营小姐姐跑过来跟我说,有很多用户支付不了。这个支付服务是一个老系统,转手了3个人了,一直很稳定没有出过啥问题。

我二话不说开始定位问题了,先看服务器日志,发现了很多报数据库连接过多的异常。因为支付功能太重要了,当时为了保证支付功能快速恢复,先找运维把支付服务2个节点重启了。

5分钟后暂时恢复了正常。

我再继续定位原因,据我当时的经验判断一般出现数据库连接过多,可能是因为连接忘了关闭导致。但是仔细排查代码没有发现问题,我们当时用的数据库连接池,它会自动回收空闲连接的,排除了这种可能。

过了会儿,又有一个节点出现了数据库连接过多的问题。

但此时,还没查到原因,逼于无奈,只能让运维再重启服务,不过这次把数据库最大连接数调大了,默认是100,我们当时设置的500,后面调成了1000。(其实现在大部分公司会将这个参数设置成1000)

使用命令:

1
sql复制代码set GLOBAL max_connections=500;

能及时生效,不需要重启mysql服务。

这次给我争取了更多的时间,找dba帮忙一起排查原因。

他使用show processlist;命令查看当前线程执行情况:


还可以查看当前的连接状态帮助识别出有问题的查询语句。

  • id 线程id
  • User 执行sql的账号
  • Host 执行sql的数据库的ip和端号
  • db 数据库名称
  • Command 执行命令,包括:Daemon、Query、Sleep等。
  • Time 执行sql所消耗的时间
  • State 执行状态
  • info 执行信息,里面可能包含sql信息。

果然,发现了一条不寻常的查询sql,执行了差不多1个小时还没有执行完。

dba把那条sql复制出来,发给我了。然后kill -9 杀掉了那条执行耗时非常长的sql线程。

后面,数据库连接过多的问题就没再出现了。

我拿到那条sql仔细分析了一下,发现一条订单查询语句被攻击者注入了很长的一段sql,肯定是高手写的,有些语法我都没见过。

但可以确认无误,被人sql注入了。

通过那条sql中的信息,我很快找到了相关代码,查询数据时入参竟然用的Statment,而非PrepareStatement预编译机制。

知道原因就好处理了,将查询数据的地方改成preparestatement预编译机制后问题得以最终解决。

2.为什么会导致数据库连接过多?

我相信很多同学看到这里,都会有一个疑问:sql注入为何会导致数据库连接过多?

我下面用一张图,给大家解释一下:

  1. 攻击者sql注入了类似这样的参数:-1;锁表语句--。
  2. 其中;前面的查询语句先执行了。
  3. 由于--后面的语句会被注释,接下来只会执行锁表语句,把表锁住。
  4. 正常业务请求从数据库连接池成功获取连接后,需要操作表的时候,尝试获取表锁,但一直获取不到,直到超时。注意,这里可能会累计大量的数据库连接被占用,没有及时归还。
  5. 数据库连接池不够用,没有空闲连接。
  6. 新的业务请求从数据库连接池获取不到连接,报数据库连接过多异常。

sql注入导致数据库连接过多问题,最根本的原因是长时间锁表。

3.预编译为什么能防sql注入?

preparestatement预编译机制会在sql语句执行前,对其进行语法分析、编译和优化,其中参数位置使用占位符?代替了。

当真正运行时,传过来的参数会被看作是一个纯文本,不会重新编译,不会被当做sql指令。

这样,即使入参传入sql注入指令如:

1
sql复制代码id; select 1 --

最终执行的sql会变成:

1
sql复制代码select * from test1 order by 'id; select 1 --' limit 1,20

这样就不会出现sql注入问题了。

最近我建了新的技术交流群,打算将它打造成高质量的活跃群,欢迎小伙伴们加入。

我以往的技术群里技术氛围非常不错,大佬很多。

image.png

加微信:su_san_java,备注:加群,即可加入该群。

4.预编译就一定安全?

不知道你在查询数据时有没有用过like语句,比如:查询名字中带有“苏”字的用户,就可能会用类似这样的语句查询:

1
sql复制代码select * from user where name like '%苏%';

正常情况下是没有问题的。

但有些场景下要求传入的条件是必填的,比如:name是必填的,如果注入了:%,最后执行的sql会变成这样的:

1
sql复制代码select * from user where name like '%%%';

这种情况预编译机制是正常通过的,但sql的执行结果不会返回包含%的用户,而是返回了所有用户。

name字段必填变得没啥用了,攻击者同样可以获取用户表所有数据。

为什么会出现这个问题呢?

%在mysql中是关键字,如果使用like '%%%',该like条件会失效。

如何解决呢?

需要对%进行转义:\%。

转义后的sql变成:

1
sql复制代码select * from user where name like '%\%%';

只会返回包含%的用户。

5.有些特殊的场景怎么办?

在java中如果使用mybatis作为持久化框架,在mapper.xml文件中,如果入参使用#传值,会使用预编译机制。

一般我们是这样用的:

1
2
3
4
5
6
sql复制代码<sql id="query">
select * from user
<where>
name = #{name}
</where>
</sql>

绝大多数情况下,鼓励大家使用#这种方式传参,更安全,效率更高。

但是有时有些特殊情况,比如:

1
2
3
sql复制代码<sql id="orderBy">
order by ${sortString}
</sql>

sortString字段的内容是一个方法中动态计算出来的,这种情况是没法用#,代替$的,这样程序会报错。

使用$的情况就有sql注入的风险。

那么这种情况该怎办呢?

  1. 自己写个util工具过滤掉所有的注入关键字,动态计算时调用该工具。
  2. 如果数据源用的阿里的druid的话,可以开启filter中的wall(防火墙),它包含了防止sql注入的功能。但是有个问题,就是它默认不允许多语句同时操作,对批量更新操作也会拦截,这就需要我们自定义filter了。

6.表信息是如何泄露的?

有些细心的同学,可能会提出一个问题:在上面锁表的例子中,攻击者是如何拿到表信息的?

方法1:盲猜

就是攻击者根据常识猜测可能存在的表名称。

假设我们有这样的查询条件:

1
sql复制代码select * from t_order where id = ${id};

传入参数:-1;select * from user

最终执行sql变成:

1
sql复制代码select * from t_order where id = -1;select * from user;

如果该sql有数据返回,说明user表存在,被猜中了。

建议表名不要起得过于简单,可以带上适当的前缀,比如:t_user。 这样可以增加盲猜的难度。

方法2:通过系统表

其实mysql有些系统表,可以查到我们自定义的数据库和表的信息。

假设我们还是以这条sql为例:

1
sql复制代码select code,name from t_order where id = ${id};

第一步,获取数据库和账号名。

传参为:-1 union select database(),user()#

最终执行sql变成:

1
sql复制代码select code,name from t_order where id = -1 union select database(),user()#

会返回当前 数据库名称:sue 和 账号名称:root@localhost。

第二步,获取表名。

传参改成:-1 union select table_name,table_schema from information_schema.tables where table_schema='sue'#
最终执行sql变成:

1
sql复制代码select code,name from t_order where id = -1 union select table_name,table_schema from information_schema.tables where table_schema='sue'#

会返回数据库sue下面所有表名。

7.sql注入到底有哪些危害?

1. 核心数据泄露

大部分攻击者的目的是为了赚钱,说白了就是获取到有价值的信息拿出去卖钱,比如:用户账号、密码、手机号、身份证信息、银行卡号、地址等敏感信息。

他们可以注入类似这样的语句:

1
sql复制代码-1; select * from user;--

就能轻松把用户表中所有信息都获取到。

所以,建议大家对这些敏感信息加密存储,可以使用AES对称加密。

2. 删库跑路

也不乏有些攻击者不按常理出牌,sql注入后直接把系统的表或者数据库都删了。

他们可以注入类似这样的语句:

1
sql复制代码-1; delete from user;--

以上语句会删掉user表中所有数据。

1
sql复制代码-1; drop database test;--

以上语句会把整个test数据库所有内容都删掉。

正常情况下,我们需要控制线上账号的权限,只允许DML(data manipulation language)数据操纵语言语句,包括:select、update、insert、delete等。

不允许DDL(data definition language)数据库定义语言语句,包含:create、alter、drop等。

也不允许DCL(Data Control Language)数据库控制语言语句,包含:grant,deny,revoke等。

DDL和DCL语句只有dba的管理员账号才能操作。

顺便提一句:如果被删表或删库了,其实还有补救措施,就是从备份文件中恢复,可能只会丢失少量实时的数据,所以一定有备份机制。

3. 把系统搞挂

有些攻击者甚至可以直接把我们的服务搞挂了,在老东家的时候就是这种情况。

他们可以注入类似这样的语句:

1
sql复制代码-1;锁表语句;--

把表长时间锁住后,可能会导致数据库连接耗尽。

这时,我们需要对数据库线程做监控,如果某条sql执行时间太长,要邮件预警。此外,合理设置数据库连接的超时时间,也能稍微缓解一下这类问题。

从上面三个方面,能看出sql注入问题的危害真的挺大的,我们一定要避免该类问题的发生,不要存着侥幸的心理。如果遇到一些不按常理出票的攻击者,一旦被攻击了,你可能会损失惨重。

  1. 如何防止sql注入?

1. 使用预编译机制

尽量用预编译机制,少用字符串拼接的方式传参,它是sql注入问题的根源。

2. 要对特殊字符转义

有些特殊字符,比如:%作为like语句中的参数时,要对其进行转义处理。

3. 要捕获异常

需要对所有的异常情况进行捕获,切记接口直接返回异常信息,因为有些异常信息中包含了sql信息,包括:库名,表名,字段名等。攻击者拿着这些信息,就能通过sql注入随心所欲的攻击你的数据库了。目前比较主流的做法是,有个专门的网关服务,它统一暴露对外接口。用户请求接口时先经过它,再由它将请求转发给业务服务。这样做的好处是:能统一封装返回数据的返回体,并且如果出现异常,能返回统一的异常信息,隐藏敏感信息。此外还能做限流和权限控制。

4. 使用代码检测工具

使用sqlMap等代码检测工具,它能检测sql注入漏洞。

5. 要有监控

需要对数据库sql的执行情况进行监控,有异常情况,及时邮件或短信提醒。

6. 数据库账号需控制权限

对生产环境的数据库建立单独的账号,只分配DML相关权限,且不能访问系统表。切勿在程序中直接使用管理员账号。

7. 代码review

建立代码review机制,能找出部分隐藏的问题,提升代码质量。

8. 使用其他手段处理

对于不能使用预编译传参时,要么开启druid的filter防火墙,要么自己写代码逻辑过滤掉所有可能的注入关键字。

最近无意间获得一份BAT大厂大佬写的刷题笔记,一下子打通了我的任督二脉,越来越觉得算法没有想象中那么难了。
BAT大佬写的刷题笔记,让我offer拿到手软

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

本文转载自: 掘金

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

MySql数据是如何存储在磁盘上存储的? MySql中数据在

发表于 2021-02-07

GitHub 19k Star 的Java工程师成神之路,不来了解一下吗!

GitHub 19k Star 的Java工程师成神之路,真的不来了解一下吗!

GitHub 19k Star 的Java工程师成神之路,真的真的不来了解一下吗!

关于MySql数据库,相信很多人都不陌生,这是当今最常用的一种关系型数据库,关于MySql的知识也是很丰富的。

那么,不知道大家有没有想过这样的问题:MySql中的数据是存在哪的?又是如何存储的呢?

本文就来深入分析一下这些问题。文章内容很长,建议收藏,建议大家静下心来仔细阅读,一定会有收获!

Innodb的存储格式

我们知道,关于Mysql这种关系型数据库,里面保存的数据最终都是要持久化到磁盘文件上面的。磁盘文件里存放的物理格式就是数据页(关于数据页,如果不太理解先忽略,后续文章单独介绍),数据页中存放的是一行一行的记录,但是对于数据页中的每一行数据他又是怎么存储的呢?

我们拿Mysql中最常用的Innodb引擎来重点说,介绍下存储格式是怎样的。

MySQL中存储有3种:

  1. server层格式:与存储引擎无关,Binlog存储常用的一种 (Bin Log 我们前面已经详细介绍过了,这个是MySql主从复制的一个很重要的文件)
  2. 索引元组格式:InnoDB存取过程记录的中间状态,是InnoDB在内存中存储的格式 (换句话说我们的增删改的操作都是在内存中执行的,这个只是一种临时状态)
  3. 物理存储格式:记录在物理页面中的存储格式,即compact格式,与索引元组格式一一对应。(这个是数据在磁盘存储的真正的格式)

MySql 的 InnoDB 存储引擎和大多数数据库一样,都是以行的形式存储数据的,我们可以通过SHOW TABLE STATUS查看到行的的存储格式。

InnoDB 储存引擎支持有四种行储存格式:COMPACT、Redundant、Dynamic 和 COMPRESSED。默认为COMPACT。

image-20210108134756093

其他的参数我们这里不关注,仅仅看 Row_format 这列,这里我们可以看到行的存储格式是 Compact,Compact 存储数据的格式大致如下这样

image-20210108140333395

对于我们看到的每一行数据,我们最先看到的好像并不是各个列,而是一些类似列的描述信息。没错,其实在存储的时候都会有一些都字段来描述这一行的信息,这就好比缓存池中的描述缓存页的描述数据类似。

上面的图片大家可以这么简化来对待,事务ID和回滚指针大家先不要关注,免得因为这个产生干扰而难于理解

image-20210108145549094

变长字段 varchar 是如何存储的

一般情况下,我们要存储的数据是并不能确定他的长度的,大部分情况下都是一些变长的数据,以varchar为例,假设现在三个字段,字段类型分别为:varchar(10),char(1),char(1),char大家都是知道的,存储的基本是一些已知的长度固定的数据,假设这三个类型的字段分别有如下的数据:

第一行:mysql a a;第二行:dog b c;画个图来帮助大家想象,现在你看到的是数据中为我们展现的样子。

image-20210108150230975

但是在磁盘中可不是这样子的,前文已经提到过,表空间和行这些其实是逻辑上的概念,而数据页是一种物理概念,也就是说我们看到的样子在磁盘中的样子本本是不一样的。

在磁盘中这两条记录大致是这样子的:mysql a a dog b c,他们在磁盘中都是挨在一起存储的。

是不是瞬间感觉想要去查找一条数据非常麻烦,告诉你:是的,所以 MySql在设计的时候才会使用行格式存储,才会有前面的哪些变长字段列表和标志位以及记录信息,这些就是用来记录一行的记录的信息,换句话说,MySql是通过这些描述信息来定位到一行中的具体记录的。

以第一行记录为例,它在磁盘中的记录情况大致是下面这样子的,首先我们需要明确知道的是各个字段的类型MySql是很清楚的,在这个基础上我们能看明白下面和想通后面的事情。首先我们看到 mysql是5个字符,使用十六进制表示是 0x05,所以他的存储大概是这样子的:

image-20210108164225946

同理第二行数据类似这样子的:

image-20210108164316645

相信大家在看到这里已经大概能推测出MySql这个时候是怎么读读取数据的了,就是他会先根据变长字段长度列表中描述的变长字段的信息去查找变长字段,例如第一行,MySql解析到变长字段是5,所以他会在mysql a a dog b c 这些里面取出5个字符,也就是 mysql,紧接着后面是两个 char(1) 也就是两个 a 在依次取出来。

由浅入深,我们慢慢来,刚刚上面说到的仅仅是一种非常简单的情况,这个首先是帮助大家理解,让大家先明白有这么个回事,是这么回事,然后在慢慢的挖掘,我们一定要一个萝卜一个坑的去踏实学习

现在如果是多个varchar类型的字段怎么办?例如:varchar(3),varchar(10),varchar(4),char(1),他有一条记录是这样子的:aaa ,bb,cccc,d,你根据上面的能推测出磁盘中的行记录是怎么样子的吗?

你是不是这么想的:磁盘中肯定是这样的:0x03,0x02,0x04 null标志位 记录头信息 aaa bb cccc d;这么想的同学请鼻子靠墙:);实际上并不是这样子的。

当有多个变长字段的时候,MySql在 compact 行格式中,把所有变长类型的长度存放在行记录的开头部位形成一个列表(这个列表就是刚刚上面说的变长字段列表),按照列的逆序存放,也就是大致是这样子的:

image-20210108172950660

这里我必须要给大家解释下变长字段列表会逆序存放,因为每行记录的都有一个 next_record指针 指向下一行 记录头信息和 真实数据 之间的位置。因为这个位置刚刚好,向左读取就是行描述相关信息,向右读取就是真实数据。正好对应变长字段长度列表。画个图来帮助大家理解下:

image-20210108180756223

说到这里我们来稍微小结一下

MySql中数据在磁盘的存储小结

  1. 数据在磁盘中的存储在物理空间上面是连续的
  2. 数据是被存放在MySql设计出来的数据页上面的,数据页上面存储的才是最终的一行一行的记录
  3. 行的存储格式默认是Compact
  4. 每一行数据都会有相应的行描述部分,描述部分有【变长字段列表】【NULL标志位】【记录头信息】
  5. 每一行都会有next_record指针,指向记录头和变长字段列表的中间某个位置,方便寻址
  6. 变长列表中的varchar列的描述是逆序的(和字段的顺序相反)这样做的目的在上图中描述的很清楚了

NULL字段是如何存储的

上面说到了情况都是比较正常的情况,也就说上面提到的字段是没有空值的,不管是变长字段还是char字段,都是有值的,那如果某个字段允许为空,且值确实为空,MySql又是怎么处理的呢?是不是直接存储NULL呢。

假设MySql针对与Null直接存储,他实际上是按照“NULL”这样字符串的形式存储的,这样显然不行啊,因为字符串要占用空间的啊(一个 NULL 字符串要占用四个字符呢),你都没有值,还占这么多空间,所以MySql肯定不是这样存储的。其实MySql在处理NULL值的时候是会将它通二进制来存储的,且也是逆序的

MySql是如何通过二进制来存储NULL值的?

上面的 Compact 格式数据中的【NULL标志位(也可以叫NULL列表)】就是用来存储NULL值的。若有某个字段值为 null,将将其 bit 位置为 1 说明值为 NULL,bit为 0 说明该字段值不为空

是不是听了解释还是稀里糊涂的,别急,我画个图再来详细介绍下,先假设我们有一张 sutdents 表

1
2
3
4
5
6
7
8
sql复制代码CREATE TABLE `students` (
`name` varchar(10) NOT NULL,
`address` varchar(255) DEFAULT NULL,
`gender` char(1) DEFAULT NULL,
`class` varchar(10) DEFAULT NULL,
`hobbies` varchar(255) DEFAULT NULL,
PRIMARY KEY (`name`)
)

他有这样一行记录

image-20210109093742113

我们先看变长字段列表部分(记住是逆序存放的):

roles是长度为5记作:0x05;address 为null,不放在变长列表中、gender 是 char 类型,不放在变长列表中、class为空,不放在变长列表中、hobby_xx长度为8记作:0x08;所以变长列表的记录为:0x08 0x05

现在到了NULL标志位了:依旧是从右往左记录字段:name 在设计的时候就是 not null,所示是不会出现在NULL标志为中(Null标志为是用来记录字段可为NULL的字段,字段不可以为NULL的不是会被记录到NULL标志位的),address为NULL记作1,gender不为null记作0,class 为null记作1,hobbies不为null记作0;所以按照字段的顺序结果就是:0101,但是NULL标志位是逆序的,所以NULL标志位存放的结果大概是这样子的:0101,高位补0即可

image-20210109100554617

我们来模拟读取下这条记录:MySql 对于字段的类型一定是已知的(这个是在创建数据表的时候就已经定下来了),所以对于 name 这种 not nul l的字段是不会去存放在null标志位的,下面是详细的读取步骤:

  • name字段是主键,不可能在NULL 标志位中的,又因为 name 是varchar 字段,所以就会去变长字段列中查找,找到值为 0x05 接着就会去字段列表中读取5个字符的长度,也就是 roles ,第一个字段读取成功;
  • 接着是 address 字段,因为类型是 MySql 已知的,又因为字段值为 null 所以就不需要去读取了,第二个字段读取结束;
  • 接着是gender字段,是char类型的,直接拿到 f 就可以了;
  • 下一个是class 字段,因为是null 所以根本不会去变长字段中查找;
  • 最后一个是 hobbies 字段,因为不为null ,又是第二个变长字段,这个时候就会去 变长字段列表中查找,结果定位到是 0x08 那就读取 8 个字符的长度出来,拿出来是hobby_xx;

说到这里,关于一行记录的中的变长字段列表和 NULL 标志位具体是如何读取字段值的就给大家介绍完了,不知道大家看到以上内容脑子是不是会展现一条条行记录的描述信息。目前我们只需要了解 varchar 和 NULL 存储的基本就足够了,因为这两个表特殊,也是最经常使用的,其他的字段类型本篇暂且不展开讨论了。

上面的记录头的信息我们还没有讨论过,下面我们再详细介绍下记录头信息是什么。

记录头信息

记录头信息由40位的bit位组成,其各个位的划分和含义如下:

bit位 名称 作用
1 预留位 没有作用
1 预留位 没有作用
1 delete_mask 删除标志位
1 min_rec_mask B+树的每一层的非叶子节点的最小值有会有这个标志
4 n_owned 当前记录拥有的记录数
13 heap_no 示意当前纪录在纪录堆的位置信息
3 record_type 标识当前纪录的类型:0代表的是通俗类型,1代表的是B+树非叶子节点,2代表的是最小值数据,3代表的是最大值数据。
16 next_record 示意下一条纪录的相对位置

记录头的各个位的作用其实就已经说的很清楚了,一些概念现在还没法讲解,很多东西需要到索引的时候才能展开讲,这里大家需要明确的就是各个标志位的含义。

我认为对于记录头的了解到这里就足够了,各个标志位的含义明确了到这个程度就行了,至于更多的可能我们根本接触不到。这一小节就当是科普。

数据在磁盘上到底是怎么存储的

上面画过这样一张图:

image-20210109103941219

之前说的是数据大致是这样子在磁盘中存储的:0x03 NULL标志位 记录头信息 dog b c,但是实际上后面的列的数据并是不是我们看到的这个样子,磁盘在存储的时候是根据数据库指定的字符集编码存储起来的你以为可能是上面那样子存储的。

实际上可能是在样子的:0x03 NULL 标志位 记录头信息 1233 323 223,也就是说实际的数据在磁盘上存储根本不是我们人能认识的,后面的 1233 323 223 这几个是我乱写的,没什么含义,主要是想表明是计算在实际存储的时候是以特定的字符编码来存储的。

另外每一行数据在被存储的时候实际上还会有隐藏的字段,相信大家对这个应该不会陌生的,row_id 大家应该是知道的,哪怕自己没用过可能也是听过的,这个是数据库自己为我们的每一行记录生成的一个唯一的表示,如果我们没有为数据表指定主键字段,也没有指定 Unique key,那么这个时候数据库内部会帮我们维护一个自增长的 ROW_ID 字段作为主键。

还有一个隐藏字段就是 事务ID 上面的第二张图上层画出来过,这个顾名思义了,就是和事务相关的一个字段属性字段名为DB_TRX_ID,这个再详解到事务的时候再详细介绍;最后一个也是在上面的第二张图上画出来了,就是回滚指针 DB_ROLL_PTR,回滚也是事务使用到的概念,也是放在事务那边跟大家介绍

现在再来整体回顾下一行记录在磁盘中的存储的结构大概是什么样子的:

0x08 0x05 00000101 0000010100000000000000000000000000000010 21134 44 232343

说到了存储,我们顺便聊聊和存储相关的一个概念,行溢出。

行溢出

说到这里,不知道大家有没有想过一个问题,就是我们一直在说 MySql 存储是以数据页的形式来存储的,然后数据页中记录的是一行行的记录,但是往往常规情况下不会有什么问题。

但是如果现在有一行记录非常大,因为数据页大小默认也就是16KB,假设某张表里面有text字段也有BLOB字段,且这一行的记录的大小远远超过了一个数据页的大小16KB,这种情况称之为行溢出。

MySql 是怎么来处理这种行溢出的情况的呢?实际上很简单,一个数据页不够就使用多个数据页,数据页和数据页之间使用链表连起来,之所以能够使用链表连接因为数据页里面是包含了存放指针的 bit 位。对于行溢出的概念了解到这个程度就足够了。我们学习是有的放矢,不是什么都要去刨根问底的。

image-20210109111104116

结束语

本片文章详细的介绍了 MySql 存储数据的格式和数据具体在磁盘中是怎么存储的,被存储的数据又是怎么查找的,说白了很多事情都是已经是既定的规则,所谓既定的规则就是很对东西已经被更早的设计出来。

所以你在使用和了解的使用只需要按照被人的规则来执行,然后在此基础上深入了解下别人为什么这么设计?这样会更有助于我们掌握和理解某个知识点。

关于作者:Hollis,一个对Coding有着独特追求的人,阿里巴巴技术专家,《程序员的三门课》联合作者,《Java工程师成神之路》系列文章作者。

如果您有任何意见、建议,或者想与作者交流,都可以关注公众号【Hollis】,直接后台给我留言。

本文转载自: 掘金

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

浅谈第三方登录用户表结构设计方案 0x01:我方用户表与第三

发表于 2021-02-06

国民两大流量入口,大家不说也想到了,分别是微信和QQ。所以为了方便获取用户来源都对接了微信登录或者QQ登录,这一类型的第三方登录入口。今天就以对接微信登录、QQ登录与苹果登录。来说说对第三方用户体系与我方系统用户体系的对接的一些可行性方案。

0x01:我方用户表与第三方用户表同为一张表

一般系统都会有自己的一套用户系统,主管用户的注册、登录、登出、权限等。比如我方用户系统的用户表 t_user 大致包含如下一些字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bash复制代码id:主键id

username:用户名

age:用户年龄

mobile:手机号号码

password:登录密码

source_from:用户来源

auth_flag:用户认证状态

create_date:注册日期

以上是最简单的一些用户信息了,那现在要对接第三方用户体系。比如,对接微信。这是最普遍的第三方用户对接了。因为这种方案我方用户表与第三方用户表在一张表里,所以需要在用户表 t_user 中添加一个标识,表示我方用户与微信用户唯一绑定的字段,一般使用微信的 openid,这样的话需要修改表,添加一个wx_openid字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bash复制代码id:主键id

username:用户名

age:用户年龄

mobile:手机号号码

password:登录密码

source_from:用户来源

auth_flag:用户认证状态

wx_openid:微信的openid

create_date:注册日期

如果有要对接 QQ 和 Apple,这样的话有的修改表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码id:主键id

username:用户名

age:用户年龄

mobile:手机号号码

password:登录密码

source_from:用户来源

auth_flag:用户认证状态

wx_openid:微信openid

qq_openid:qq openid

appleid:苹果id

create_date:注册日期

这种方案设计简单,只要对接一个第三方,就是需要对原来的用户表进行修改,如果对接的第三方过多,用户表就慢慢的变得非常臃肿。从另外一个方面看,对原来用户代码进行修改。

0x02:我方用户表一张表、第三方用户表一张表

由于第一种方案如果对接额外的第三方需要不断的修改用户,以及原来的代码逻辑,对生产可能造成不确定因素。所以可以采取另外一种方案我方用户表一张表、第三方用户表一张表这种方案。比如用户表 t_user 设计大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bash复制代码id:主键id

username:用户名

age:用户年龄

mobile:手机号号码

password:登录密码

source_from:用户来源

auth_flag:用户认证状态

create_date:注册日期
  • 第三方用户表 t_third_acount 设计大致如下:
1
2
3
4
5
6
7
8
9
bash复制代码user_id:对应 t_user的用户id

third_unique_acount:第三方唯一用户id,可以是微信的openid,可以是QQ的openid,抑或苹果id

type:标识第三方类型,这里规定1.代表微信,2.代表QQ,3.代表苹果

bind_flag:标识是否绑定,1绑定,2解绑

create_date:绑定时间

这样设计的话,以后一般不需要修改表结构;但是新添加第三方用户对接时,还是免不了需要对原来的代码逻辑做改动。

0x03:我方用户表一张表、第三方用户表多张表

基于第二种方案,第三方用户表使用了一个 type 字段来表示不同的第三方用户体系,通过不断的新增不同的枚举来标识不同的第三方。所以可以去除这个字段,然后不同的第三方使用不同的表来标识。比如用户表 t_user 设计大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
bash复制代码id:主键id

username:用户名

age:用户年龄

mobile:手机号号码

password:登录密码

source_from:用户来源

auth_flag:用户认证状态

create_date:注册日期

+ 微信用户体系的表 t_wechat_acount 设计大致如下如下:

user_id:对应 t_user的用户id

wx_openid:微信的openid

bind_flag:标识是否绑定,1绑定,2解绑

create_date:绑定时间
  • QQ用户体系的表 t_qq_acount 设计大致如下如下:
1
2
3
4
5
6
7
bash复制代码user_id:对应 t_user的用户id

qq_openid:QQ的openid

bind_flag:标识是否绑定,1绑定,2解绑

create_date:绑定时间
  • 苹果用户体系的表 t_apple_acount 设计大致如下如下:
1
2
3
4
5
6
7
bash复制代码user_id:对应 t_user的用户id

appleid:苹果id

bind_flag:标识是否绑定,1绑定,2解绑

create_date:绑定时间

这些方案的话,第三方用户表就有点膨胀的意思,系统对接了多少个第三方用户体系,就有多少张第三方用户体系表。

以上三种方案,属谁最优,不下定论。我觉得根据项目的要求,满足自身项目的需要,符合可用的业务场景的方案就是最优解。

本文转载自: 掘金

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

API 框架的终结:论面向文档框架的优势

发表于 2021-02-05

这是一篇有关于我最近改写的框架的理念总结,Ruby 程序员、Rails 程序员、Grape 程序员将容易懂。

面向文档的开发

当今环境下,有许多的开发范式供后端开发者选择,例如测试驱动开发、行为驱动开发、敏捷软件开发等等。与之相对的,我提出了一个新的想法,我将其称为面向文档的开发。

写 API 项目的同时是要准备文档的。我不知道大家是如何准备文档的,但往往都逃不出一个怪圈:同一个接口,我的实现要写一份,我的文档也要同时写一份。我常常在想,为什么我在写接口的同时不能将文档同步地完成呢?换个角度想,接口文档的契约精神为何不能自动地成为实现的一部分?如果,我能发明一个 DSL,在编写文档的同时就能够制约接口的行为,那不正是我想要的吗?

说干就干!

我发现 Grape 框架就已经提供了类似的 DSL 了。例如你在制定参数时可以像这样:

1
2
3
4
5
6
ruby复制代码params do
requires :user, type: Hash do
requires :name, type: String, desc: '用户的姓名'
requires :age, type: Integer, desc: '用户的年龄'
end
end

上面的代码就可以对参数进行制约,限制参数为 name 和 age 两个字段,并分别限制它们的类型为 String 和 Integer. 与此同时,一个名为 grape-swagger 的库可以将 params 的宏定义渲染成 Swagger 文档的一部分。完美,文档和实现在这里结合了。

另外,Grape 框架提供了 desc 宏,它是一个纯文档的声明供第三方库读取,不会对接口行为产生任何影响。

1
2
3
4
ruby复制代码desc '创建新用户' do
tags 'users'
entity Entities::User
end

但是,毕竟 Grape 框架不是完全的面向文档的开发框架,它有很多重要的使命,所以它和文档的无缝衔接也就仅限于此了。你能看到,params 宏是个完美结合的范例,desc 宏很可惜只与文档渲染有关,然后就别无其他了。

鉴于 Grape 框架是个开源框架,修改它以添加几个小零件还是很简易的一件事。我用了几天的时间添加了一个 status 宏,可以用它来声明返回值:

1
2
3
4
5
6
7
ruby复制代码status 200 do
expose :user, deep: true do
expose :id, documentation: { type: Integer, desc: '用户的 id' }
expose :name, documentation: { type: String, desc: '用户的姓名' }
expose :age, documentation: { type: Integer, desc: '用户的年龄' }
end
end

上述声明主要起两个作用:

  1. 在接口逻辑中调用 present 方法不用显示地指定 Entity 类型,它是自动解析的。

以前,你必须这样调用:

1
ruby复制代码present :user, user, with: Entities::User

现在,只用这样:

1
ruby复制代码present :user, user

因为在 status 声明中它已经知道如何渲染 user 实体了。
2. grape-swagger 库可以解析 status 宏生成文档。

一切还只是冰山一角。

你真的不需要 Controller 测试吗?

有关接口的单元测试,有两个观点争论不休:接口测试应该是针对 Integration 测试还是 Controller 测试?Integration 测试像是一个黑匣子,开发者调用接口,然后针对接口返回的视图进行测试。而 Controller 测试也会一样地调用接口,但会测到内部的状态。

通过下面的两个案例直观地感受一下 Controller 测试和 Integration 测试在 Rails 框架中的不同写法。

在早期的 Rails 版本中,是有 Controller 测试的:

1
2
3
4
5
6
7
ruby复制代码class ArticlesControllerTest < ActionController::TestCase
test "should get index" do
get :index
assert_response :success
assert_equal users(:one, :two, :three), assigns(:articles)
end
end

Rails 5 以后,更推荐 Integration 测试:

1
2
3
4
5
6
ruby复制代码class ArticlesControllerTest < ActionDispatch::IntegrationTest
test "should get index" do
get articles_url
assert_response :success
end
end

注意到 Integration 测试中是没有对应的 assert_equal 语句的,因为它比较难写。如果视图返回的是 JSON 数据,可以尝试用下面的等效写法:

1
ruby复制代码assert_equal users(:one, :two, :three).as_json, JSON.parse(last_response.body)

但是测试视图的代码及其依赖视图的变化,也会经常性的失效,上面只是尝试性的一种写法而已。

我不想在这里探讨 Controller 测试和 Integration 测试究竟孰优孰劣的问题,尽管你可能已经从字里行间察觉到我的倾向性。关于这个话题能够从 2012 年讨论到 2020 年,不信你可以看这个帖子。我可不想让情况变得更糟。很多次我在想,那些反对 Controller 测试的人可能仅仅把 Controller 的实例变量看成是其内部状态而已,而没有意识到它也是 Controller 与 Template 之间的契约。这不怪他们,因为用传递实例变量的方式定义契约确实不够优雅。

好在我做了一些简单的工作,让其在 Grape 框架内可以更优雅地测试接口的返回。你只需要在逻辑接口中使用 present 方法指定渲染数据:

1
ruby复制代码present :users, users

然后就可以在测试用例中结合特殊的 presents 方法测试它:

1
ruby复制代码assert_equal users(:one, :two, :three), presents(:users)

跟 assigns 很像,但是它更舒服不是么?

写在最后

这就是我对 Grape 框架的改造过程,已经开始并将持续下去。我的改造理念无外乎两个想法:更好的文档结合和更好的测试。而事实上,只需要一点点工作,确实就可以起作用了。

如果你也想用到我所改造的 Grape 框架,直接克隆我的脚手架就好了:

1
bash复制代码git clone https://github.com/run27017/grape-app-demo.git

脚手架中使用的是我 Fork 后的框架集,它们是:

  • grape
  • grape-entity
  • grape-swagger

点击它们的链接看一看吧,也许你也能成为开源世界的参与者呢。

本文转载自: 掘金

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

5分钟让你理解K8S必备架构概念,以及网络模型(中)

发表于 2021-02-05

写在前面

在这用XMind画了一张导图记录Redis的学习笔记和一些面试解析(源文件对部分节点有详细备注和参考资料,欢迎关注我的公众号:阿风的架构笔记 后台发送【导图】拿下载链接, 已经完善更新):

前言

在上一篇文章中介绍了K8S的基础架构流程,以及核心的组件;这篇文章继续K8S的相关的概念。

上篇介绍到了NodePort Service解决了外部请求K8S内部的应用的问题。下面我们看看如何搭建应用服务集群的?

应用集群

图片

在传统应用中,我们一般利用nginx反向代理,通过配置域名指向多个IP地址,从而实现了应用的集群。如果需要增加应用或减少应用,都需要调整nginx的配置;还是相当繁琐的。

那K8S是如何实现应用集群的呢?

副本集ReplicaSet

图片

上一篇文章中介绍了利用NodePort Service 的Selector 选择Label标签,路由到后端的其中一个Pod。

上图中由3个Pod组成的应用集群,那如何保证Pod集群的高可用呢?如果其中一个Pod挂了,被删除了,K8S会怎么处理?

K8S有个Replica Set组件,从字面上面来看就是副本集意思;它的作用就是用来保证Pod的高可用,如果我们在Replica Set中定义了应用数量为3,那么它会保证应用数量;即使一个pod挂了,它会自动会启动1个,始终保证pod应用数量为3。

图片

编写yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
yaml复制代码apiVersion: extensions/v1beta1  #指定api版本
kind: ReplicaSet #指定创建资源的角色/类型
metadata:
name: mc-user
spec:
replicas: 3 #副本集数量
template: #pod模板
metadata: #资源的元数据/属性
labels: #标签定义
app: mc-user #标签值
spec: # 指定该资源的内容
containers: #容器定义
- name: mc-user #容器的名字
image: rainbow/mc-user:1.0.RELEASE #容器镜像

上面就是定义了 mc-user的pod,副本集数始终为3。Service的yaml和之前一样,注意Selector的Label,可提供给外部访问端口31001

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码apiVersion: v1
kind: Service
metadata:
name: mc-user
spec:
ports:
- name: http
port: 8080
targetPort: 8080
nodePort: 31001
selector:
app: mc-user
type: NodePort

执行kubectl apply -f 命令,启动ReplicaSet和Service

我们可以试着查看启动的3个pod,并选择其中一个pod将它删除。

1
2
yaml复制代码# kubectl get all
# kubectl delete po mc-user-6adfw

我们再查看pod

1
yaml复制代码kubectl get all

还是有3个pod,可以看出即使删除了一个pod;ReplicaSet会又帮我们启动了一个pod。

这个就是ReplicaSet的自愈能力,自我恢复能力。

滚动发布Rolling Update

我们先来谈谈什么是滚动发布?滚动发布是一种高级发布策略,按批次依次替换老版本,逐步升级到新版本。发布过程中,应用不中断,用户体验平滑。

图片

现在Pod中是V1的版本,现在我们想升级到V2版本,整个流程是什么样子呢?

图片

先删除其中一个V1的pod

图片

然后发布V2的Pod

图片

再删除一个V1的pod

图片

再启动一个V2的Pod

图片

再删除最后一个V1的pod

图片

最终升级完成。

我们可以发现滚动发布的特点,就是老版本和新版本会共存一段时间。所以此种发布方式适用版本兼容的应用。也可以支持滚动回退。我们来看看和蓝绿发布的区别

图片

滚动发布抽象Deployment

之前介绍的ReplicaSet 其实是对Pod的一次包装,Deployment又在基础上面对ReplicaSet的又一次包装。

图片

注意点:ReplicaSet 和 Deployment是一个软件概念,它是没有具体的组件的;是抽象出来的名词,方便大家理解

图片

上图就是描述了deployment滚动发布的架构;Deployment的滚动发布,对用户请求以及Service是透明的,无感知。

Deployment的yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yaml复制代码apiVersion: apps/v1  #指定api版本,此值必须在kubectl apiversion中
kind: Deployment #指定创建资源的角色/类型
metadata:
name: mc-user
spec:
selector: #此deployment选择哪个标签进行滚动的发布
matchLabels: #滚动发布pod的标签,要跟下面template中的labels一致
app: mc-user
minReadySeconds: 10 #最小10s等待就绪时间,可以方便看到滚动发布流程
replicas: 3 #副本集数量
template: #pod模板
metadata: #资源的元数据/属性
labels: #标签定义
app: mc-user #标签值
spec: #指定该资源的内容
containers: #容器定义
- name: mc-user #容器的名字
image: rainbow/mc-user:1.0.RELEASE #容器镜像

上面的yaml和ReplicaSet很类似,需要注意的

1
2
3
yaml复制代码selector:        #此deployment选择哪个标签进行滚动的发布
matchLabels: #滚动发布pod的标签,要跟下面template中的labels一致
app: mc-user

定义deployment管理哪个标签pod

Service的yaml

Service的yaml没有变化,需要定义selector,选择标签就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码apiVersion: v1
kind: Service
metadata:
name: mc-user
spec:
ports:
- name: http
port: 8080
targetPort: 8080
nodePort: 31000
selector:
app: mc-user
type: NodePort

我们用kubectl apply -f命令执行 deployment和service

我们再用kubectl get all获取运行情况,我们就可以发现有两个类型

deployment.apps/mc-user 以及 replicaset.apps/mc-user-4345afaa

要升级的时候,只需要更改deployment中的image名称,再执行apply

1
yaml复制代码image: rainbow/mc-user:1.1.RELEASE    #容器镜像

我们用kubectl get all查看,就会发现replicaset 有2个;一个是老版本的,一个是新版本的。 老版本的pod数逐渐减少,新版本的pod数量逐渐增加,一直到新版本为3,老版本为0。

回退版本

如果发现版本有问题,我们可以回退版本,可以使用下面命令

1
yaml复制代码kubectl rollout undo deployment/mc-user

这个我们就回退到V1.0的老版本了。

ConfigMap配置

在我们日常业务过程中,需要会配置一些配置参数,如:一次性的静态配置(数据库连接字符串,用户名,密码),以及可以运行过程中的动态配置(如:限购数量)等;那K8S中的Pod如何获得外部的配置信息呢?

图片

上图中,K8S提供了ConfigMap这个功能,提供用户在外部进行配置,然后K8S把ConfigMap以环境变量的方式提供给Pod中的容器或者也可以通过Volume文件持久化的方式提供给Pod容器。

共享配置

因为我们会有很多服务的配置是相同的,那实现微服务之间共享一份配置信息,如下图

图片

一份ConfigMap可以提供给多个服务使用,ConfigMap会把配置信息以env方式存在于每个服务的环境变量中。

ConfigMap的yaml

1
2
3
4
5
6
7
8
yaml复制代码apiVersion: v1  #指定api版本,此值必须在kubectl apiversion中
kind: ConfigMap #指定创建资源的角色/类型
metadata:
name: mc-user-config
data: #定义配置信息
DATASOURCE_URL: jdbc:mysql://mysql/mc-user
DATASOURCE_USERNAME: root
DATASOURCE_PASSWORD: 123456

修改Deployment配置文件

增加envFrom属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
yaml复制代码apiVersion: apps/v1  #指定api版本,此值必须在kubectl apiversion中
kind: Deployment #指定创建资源的角色/类型
metadata:
name: mc-user
spec:
selector: #此deployment选择哪个标签进行滚动的发布
matchLabels: #滚动发布pod的标签,要跟下面template中的labels一致
app: mc-user
minReadySeconds: 10 #最小10s等待就绪时间,可以方便看到滚动发布流程
replicas: 3 #副本集数量
template: #pod模板
metadata: #资源的元数据/属性
labels: #标签定义
app: mc-user #标签值
spec: #指定该资源的内容
containers: #容器定义
- name: mc-user #容器的名字
image: rainbow/mc-user:1.0.RELEASE #容器镜像
envFrom: #环境变量来源
- configMapRef: #容器应用的configmap引用
name: mc-user-config #configMap的名称

envFrom中的configMapRef配置引用名称;这样我们就可以在pod容器中获取到configmap的配置信息了。我们可以用

1
yaml复制代码kubectl exec mc-user-34wrwq-3423 printenv | grep DATASOURCE_NAME

获得pod容器中的环境变量。

configMap变更

图片

如果服务已经运行中,我们更新了ConfigMap的配置信息,那么POD中的容器会即时获得新的配置信息吗?

很不幸,更新了configMap;再用kubectl apply -f 重新发布configmap;之前的pod容器是不会获得最新的配置信息的。

那如何让pod容器用最新的ConfigMap配置值呢?我们可以删除pod,因为replicaset会保证pod数量,会自动重启,那新的pod就会应用新的配置信息了。

总结

今天介绍K8S的副本集ReplicaSet、滚动发布Deployment、配置ConfigMap的概念;下一篇会介绍网络相关的模型。谢谢!!!

看完三件事❤️

​
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
​

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 阿风的架构笔记 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. 关注后回复【666】扫码即可获取架构进阶学习资料包

本文转载自: 掘金

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

一款很好用的程序托管工具AlwaysUp

发表于 2021-02-05

alwaysup是一个计算机系统软件,能将可执行文件、批处理文件及快捷方式作为windows系统服务,并且进行管理和监视确保100%运行。当程序崩溃、挂起、弹出错误对话框时,AlwaysUp 能自动重启程序,并运行自定义的检查功能确保程序一直可用。

下载安装

百度云链接:pan.baidu.com/s/19xEPEYs6…

提取码:xmsb

安装成功

使用用法

例如我们springboot项目,我们打成jar包后,想要托管运行,就可以执行一个bat文件,编辑 java - jar xxx.jar,将这个bat文件托管的alwaysUp上,如下图:

路径设置好,点击保存即可,然后就可以启动程序了:

主要功能和好处

  • 确保任何应用程序(.exe,批处理文件,.com,.pif,脚本,快捷方式,perl脚本,java应用程序,php,delphi,vb等)运行24x7;
  • 非常易于使用-只需几秒钟即可将应用程序作为Windows服务安装!
  • 能够在计算机启动时自动启动您的应用程序,在没有用户登录时运行,并且即使登录/注销也能运行-所有这些都保证了正常运行时间而无需用户手动干预;

常见问题及解决

AlwaysUP运行后,可能会经常弹出“交互式文件检测”的对话框,可按照下列步骤进行解决:

  1. 打开“开始”菜单,单击运行,输入services.msc,回车。
  2. 找到Interactive Services Detection服务,先【停止】,然后【禁用】,之后就再也不会出现『交互式文件检测』对话框了。

本文转载自: 掘金

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

word文档转html格式在线预览,使用了phpoffice

发表于 2021-02-05

最近客户要做一个word,excel 文件在线预览功能,以下是实现此功能的全过程。
由于我们用的是PHP开发项目,最开始想到的是用PHPoffice里的phpword来进行转换,以下是关键代码。

1
2
3
4
php复制代码<?php
$phpWord = \PhpOffice\PhpWord\IOFactory::load('test.doc');
$xmlWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, "HTML");
$xmlWriter->save('test.html);

用这种方法转是可以转,但是转出来的html文件相对原文件,丢失了很多字,如果说样式和原文不一样还可以忍受,但是内容丢失,就不太好了,而且对DOC格式又无法处理,所以这种方法,我最终选择了放弃。
然后,我就想用python来解决这个问题,查到了python有个pydocx库可以处理word文档,于是我就安装了一下。

1
复制代码pip install pydocx

这个库用起来也很简单,主要代码如下:

1
2
3
4
5
ini复制代码from pydocx import PyDocX
html = PyDocX.to_html("test2.doc")
f = open("test.html", 'w', encoding="utf-8")
f.write(html)
f.close()

转换效果也还可以,除了表格样式和原文有点不一样以外,内容倒是没丢失,但是有一个问题,这个库是转换docx的,对doc转换不了,我们客户还上传挺多doc格式的文件的,于是我只好另外想办法。
查资料发现java有个poi库可以用来对word文件进行转换, Apache POI是Apache软件基金会的开放源码函式库,POI提供API给Java程序对Microsoft Office格式档案读和写的功能。我想试一下,查资料半天,就开始写了,先Maven引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
xml复制代码    <dependencies>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>4.1.2</version>
</dependency>
<!-- 针对2003版本的库 -->
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>fr.opensagres.xdocreport</groupId>
<artifactId>fr.opensagres.poi.xwpf.converter.xhtml</artifactId>
<version>2.0.2</version>
</dependency>
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.4.3</version></dependency>
</dependencies>

以下是引用别人的可用代码:

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
ini复制代码import cn.hutool.core.img.ImgUtil;
import fr.opensagres.poi.xwpf.converter.xhtml.Base64EmbedImgManager;
import fr.opensagres.poi.xwpf.converter.xhtml.XHTMLConverter;
import fr.opensagres.poi.xwpf.converter.xhtml.XHTMLOptions;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.converter.WordToHtmlConverter;
import org.apache.poi.openxml4j.util.ZipSecureFile;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.w3c.dom.Document;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.awt.image.BufferedImage;
import java.io.*;
/**
* office转换工具测试
*
*/
public class OfficeConvertUtil {

/**
* 将word2003转换为html文件 2017-2-27
*
* @param wordPath word文件路径
* @param wordName word文件名称无后缀
* @param suffix word文件后缀
* @throws IOException
* @throws TransformerException
* @throws ParserConfigurationException
*/
public static String Word2003ToHtml(String wordPath, String wordName,
String suffix) throws IOException, TransformerException,
ParserConfigurationException {
String htmlPath = wordPath + File.separator + "html"
+ File.separator;
String htmlName = wordName + ".html";
final String imagePath = htmlPath + "image" + File.separator;

// 判断html文件是否存在,每次重新生成
File htmlFile = new File(htmlPath + htmlName);
// if (htmlFile.exists()) {
// return htmlFile.getAbsolutePath();
// }

// 原word文档
final String file = wordPath + File.separator + wordName + suffix;
InputStream input = new FileInputStream(new File(file));
HWPFDocument wordDocument = new HWPFDocument(input);
WordToHtmlConverter wordToHtmlConverter = new WordToHtmlConverter(
DocumentBuilderFactory.newInstance().newDocumentBuilder()
.newDocument());

wordToHtmlConverter.setPicturesManager((content, pictureType, suggestedName, widthInches, heightInches) -> {
BufferedImage bufferedImage = ImgUtil.toImage(content);
String base64Img = ImgUtil.toBase64(bufferedImage, pictureType.getExtension());
// 带图片的word,则将图片转为base64编码,保存在一个页面中
StringBuilder sb = (new StringBuilder(base64Img.length() + "data:;base64,".length()).append("data:;base64,").append(base64Img));
return sb.toString();
});

// 解析word文档
wordToHtmlConverter.processDocument(wordDocument);
Document htmlDocument = wordToHtmlConverter.getDocument();
// 生成html文件上级文件夹
File folder = new File(htmlPath);
if (!folder.exists()) {
folder.mkdirs();
}

// 生成html文件地址
OutputStream outStream = new FileOutputStream(htmlFile);

DOMSource domSource = new DOMSource(htmlDocument);
StreamResult streamResult = new StreamResult(outStream);

TransformerFactory factory = TransformerFactory.newInstance();
Transformer serializer = factory.newTransformer();
serializer.setOutputProperty(OutputKeys.ENCODING, "utf-8");
serializer.setOutputProperty(OutputKeys.INDENT, "yes");
serializer.setOutputProperty(OutputKeys.METHOD, "html");

serializer.transform(domSource, streamResult);

outStream.close();

return htmlFile.getAbsolutePath();
}

/**
* 2007版本word转换成html 2017-2-27
*
* @param wordPath word文件路径
* @param wordName word文件名称无后缀
* @param suffix word文件后缀
* @return
* @throws IOException
*/
public static String Word2007ToHtml(String wordPath, String wordName, String suffix)
throws IOException {
ZipSecureFile.setMinInflateRatio(-1.0d);
String htmlPath = wordPath + File.separator + "html"
+ File.separator;
String htmlName = wordName + ".html";
String imagePath = htmlPath + "image" + File.separator;

// 判断html文件是否存在
File htmlFile = new File(htmlPath + htmlName);
// if (htmlFile.exists()) {
// return htmlFile.getAbsolutePath();
// }

// word文件
File wordFile = new File(wordPath + File.separator + wordName + suffix);

// 1) 加载word文档生成 XWPFDocument对象
InputStream in = new FileInputStream(wordFile);
XWPFDocument document = new XWPFDocument(in);

// 2) 解析 XHTML配置 (这里设置IURIResolver来设置图片存放的目录)
File imgFolder = new File(imagePath);
// 带图片的word,则将图片转为base64编码,保存在一个页面中
XHTMLOptions options = XHTMLOptions.create().indent(4).setImageManager(new Base64EmbedImgManager());
// 3) 将 XWPFDocument转换成XHTML
// 生成html文件上级文件夹
File folder = new File(htmlPath);
if (!folder.exists()) {
folder.mkdirs();
}
OutputStream out = new FileOutputStream(htmlFile);
XHTMLConverter.getInstance().convert(document, out, options);

return htmlFile.getAbsolutePath();
}
public static void main(String[] args) throws Exception {
System.out.println(Word2003ToHtml("D:\\tmp", "test", ".doc"));
System.out.println(Word2007ToHtml("D:\\tmp", "test2", ".docx"));
}
}

用java 倒是转换doc格式转的挺好的,但是转换docx格式的时候,样式全乱了,我查了半天POI的文档,网上也没有哪位大佬来解决这个样式乱的问题,于是我想用python来转docx ,java来转doc,但是又觉得太麻烦。
在查了半天资料以后,我最终的解决办法如下。
还是回到了用php处理,但是不是用phpoffice来处理,而是用unocov进行转换,先装libreoffice

1
复制代码yum install libreoffice

然后装unocov

1
复制代码yum install unoconv

用以下命令就可以转换了

1
css复制代码unoconv -f html -o test.html test.doc

-f是输出格式,-o是输出文件 最后面是输入文件,具体用法可以查相关文档,我在php里执行外部命令,生成转换好的文件以后再重定向到生成的文件上面去,由于excel 转html报错,所以我针对excel 转成了pdf.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash复制代码        if (file_exists($source)) {
$dir = dirname($source);
$ext=pathinfo($source)['extension'];
if(!in_array($ext,['xls','xlsx'])){
$filetype='html';
}else $filetype='pdf';
$filename = strstr(basename($source), '.', true) . '.'.$filetype;
$file = $filename;
if(!file_exists('data/' . $file)){
//echo "sudo /usr/bin/unoconv -f {$filetype} -o " . '/data/web/public/data/' . $file . ' ' . '/data/web/data_manage/public/' . $source;exit;
$res= shell_exec("sudo /usr/bin/unoconv -f {$filetype} -o " . '/data/web/public/data/' . $file . ' ' . '/data/web/data_manage/public/' . $source);
if(!file_exists('data/' . $file)){
dump($res);
exit('生成预览文件出错');
}
}
header("Location:".'/data/' . $file);
exit();
} else exit('文件不存在');

最后,总算是把doc,docx 还有excel文件,wps文件都能预览出来,样式还是有点变化,内容没有丢失,客户也还算是能接受,以上是我解决这个问题的心得,希望能帮到大家。

本文转载自: 掘金

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

90% 的 Java 程序员都说不上来的为何 Java 代码

发表于 2021-02-05

经常听到 Java 性能不如 C/C++ 的言论,也经常听说 Java 程序需要预热,那么其中主要原因是啥呢?

面试的时候谈到 JVM,也有很多面试官喜欢问,为啥 Java 程序越执行越快呢?

一般人都能回答上来,类加载,缓存预热等等,但是深入下去,最重要的却没有答上来,今天本系列文章就来帮助大家理解这个问题的关键。本篇文章是 TLAB 预热。

TLAB(Thread Local Allocation Buffer)线程本地分配缓存区,这是一个线程专用的内存分配区域。

image

既然是一个内存分配区域,我们就先要搞清楚 Java 内存大概是如何分配的。

image

我们这里不考虑栈上分配,这些会在 JIT 的章节详细分析,我们这里考虑的是无法栈上分配需要共享的对象。

对于 HotSpot JVM 实现,所有的 GC 算法的实现都是一种对于堆内存的管理,也就是都实现了一种堆的抽象,它们都实现了接口 CollectedHeap。当分配一个对象堆内存空间时,在 CollectedHeap 上首先都会检查是否启用了 TLAB,如果启用了,则会尝试 TLAB 分配;如果当前线程的 TLAB 大小足够,那么从线程当前的 TLAB 中分配;如果不够,但是当前 TLAB 剩余空间小于最大浪费空间限制(这是一个动态的值,我们后面会详细分析),则从堆上(一般是 Eden 区) 重新申请一个新的 TLAB 进行分配。否则,直接在 TLAB 外进行分配。TLAB 外的分配策略,不同的 GC 算法不同。例如G1:

  • 如果是 Humongous 对象(对象在超过 Region 一半大小的时候),直接在 Humongous 区域分配(老年代的连续区域)。
  • 根据 Mutator 状况在当前分配下标的 Region 内分配

这里,我们先只关心 TLAB 分配。 对于单线程应用,每次分配内存,会记录上次分配对象内存地址末尾的指针,之后分配对象会从这个指针开始检索分配。这个机制叫做 bump-the-pointer (撞针)。 对于多线程应用来说,内存分配需要考虑线程安全。最直接的想法就是通过全局锁,但是这个性能会很差。为了优化这个性能,我们考虑可以每个线程分配一个线程本地私有的内存池,然后采用 bump-the-pointer 机制进行内存分配。这个线程本地私有的内存池,就是 TLAB。只有 TLAB 满了,再去申请内存的时候,需要扩充 TLAB 或者使用新的 TLAB,这时候才需要锁。这样大大减少了锁使用。

TLAB 初始化

image

TLAB 分配

image

GC 时 TLAB 回收与重计算期望大小

image

为何 Java 代码越执行越快 - TLAB预热

根据之前的分析,每个线程的 TLAB 的大小,会根据线程分配的特性,不断变化并趋于稳定,大小主要是由分配比例 EMA 决定,但是这个采集是需要一定运行次数的。并且 EMA 的前 100 次采集默认是不够稳定的,所以 TLAB 大小也在程序一开始的时候变化频繁。当程序线程趋于稳定,运行一段时间后, 每个线程 TLAB 大小也会趋于稳定并且调整到最适合这个线程对象分配特性的大小。这样,就更接近最理想的只有 Eden 区满了才会 GC,所有 Eden 区的对象都是通过 TLAB 分配的高效分配情况。这就是 Java 代码越执行越快在 TLAB 方面的原因。

本文转载自: 掘金

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

C/C++就业领域与学习方向 学习计划

发表于 2021-02-04

话说,**“学习编程,我到底应该如何选择学哪一门语言?”**这应该是大多数初学者内心的烦人bug!

如果抱着“学一门语言好找工作”的心态,那可能你的眼界和学习思路都会受限变窄。“软件人员要把眼光放长远,尽量往中高级方向走,提升自己的核心竞争力,才不会被时代淘汰。”来自一位前辈的真诚建议。

这是一个悲伤的故事.jpg

所以,如果你希望把工作当成事业、深入了解软件编程的主心骨,那么C/C++这一类底层语言,是你必须要学习和掌握的。

尤其是C++,它作为C语言的延伸和强化,一直是大公司大厂钦点的开发语言。

资料图.png

如今自学成风,C++也不乏大佬自学……

但是,在初期找不到感觉和项目练手写代码的话,学习效率非常低。这不,这里给大家找到了一份全面的、众多初学者认可的C/C++学习计划。

《C/C++就业领域与学习方向计划》

**一、**嵌入式

**(一)**嵌入式就业领域

图片1.png

1、就业领域:

物联网、智能家居、智能医疗、智能汽车电子、智能硬件产品开发等

**2、**具体职位:

嵌入式软件工程师、嵌入式硬件工程师、嵌入式系统工程师、嵌入式应用/驱动/内核/移植工程师等

图片2.png

(二)嵌入式知识架构体系

**1、**懂硬件

  • 懂硬件的一些原理,至少能看懂原理图,会设计原理图加分;
  • PCB设计软件Altium Designer,PADS等;
  • 会结构设计,会焊板,交付专业制板公司制板

2、C/C++**语言*和\***数据结构以及简单算法

基本C/C++语法、数据类型、数组、指针、结构体、链表、类、模板、文件操作、队列、栈等

3、Linux**操作系统**基础

  • Linux操作系统的概念、安装方法;
  • 详细了解Linux下的目录结构、基本命令、编辑器VI、编译器GCC、调试器GDB和Make 项目管理工具;
  • Makefile Shell脚本编写等知识;
  • 嵌入式开发环境的搭建;
  • SDK做二次开发

4、Linux****网络编程

  • TCP/IP协议、socket编程、TCP/UDP网络编程、HTTP;
  • 走物联网方向,更要了解一些与云存储相关的网络接口,比如亚马逊、阿里云等

5、外设协议**与**射频

  • 简单通信接口:UART、I2C、SPI、CAN、USB、GPIO、WiFi、SDIO芯片;
  • 网络通信接口:以太网;
  • 其它输入接口:键盘、鼠标、AD器件、LCD、433、2.4G等无线通信模块;
  • 走物联网方向,务必要熟悉MQTT协议

6、了解嵌入式平台系统原理

  • 系统资源、时钟控制器、电源管理、异常中断控制器、nand flash控制器等模块,为底层平台搭建做好准备;
  • Linux平台包括:内核裁减、内核移植、交叉编译、GNU工具使用、内核调试、Bootloader制作与原理分析、根文件系统制作,以及向内核中添加自己的模块,完整的移动软件

7、驱动开发

  • 熟悉Linux的内核机制、驱动程序与用户级应用程序的接口,掌握系统对设备的并发操作;
  • 熟悉所开发硬件的工作原理,具备ARM硬件接口的基础知识;
  • 熟悉处理器各资源、掌握Linux设备驱动原理框架;
  • 熟悉工程中常见Linux高级字符设备、块设备、网络设备、USB设备等驱动开发;
  • 在工作中能独立胜任底层驱动开发,做好配置

**(****三)**嵌入式公司推荐

  • 一线:华为海思、中兴通讯、C/C++
  • 二线:米尔科技、周立功、联发科

(四)学习项目推荐(附源码)

某智能锁厂锁后板源码

1 某智能锁厂 锁后板源码.png

二、桌面应用程序开发(MFC/QT)

(一)桌面应用就业方向

传统工业公司、工控、数字图像软件、图形渲染引擎、上位机、逆向、UI、视频会议等

(二)桌面应用知识架构体系

1、**C/**C++**语言、常用数据结构以及常用**设计模式

  • 基本C/C++语法;
  • 数据类型、数组、指针、结构体、链表、文件操作、队列、栈、二叉树、单例;
  • 工厂模式、策略模式等设计模式

2、控件和组件应用

  • VC++控件,组件、MFC文档类;
  • Menu、Windows消息;
  • sendmsg函数、动态链接库;
  • Qt Core、Qt GUI、Qt Multimedia、Qt Multimedia Widgets、Qt Network、Qt QML、Qt Quick、Qt SQL、Qt Test、Qt Widgets等Qt基本模块(Qt Essentials);
  • 应用QPainter绘图系统、QSS样式表,实现各种自绘窗口

3、操作系统原理和IDE

  • Windows核心编程、多线程、多进程、线程同步以及进程间的通信;
  • 字符解析协议,如XML和Json;
  • 熟悉VS和QT环境

4、网络编程

  • TCP/IP协议、socket编程、HTTP;
  • 重点学习网络编程相关API;
  • 了解HTTP协议及其实现方法;
  • 熟悉UDP广播、多播的原理及编程方法;
  • 掌握混合C/S架构网络通信系统设计;
  • QTCP、QHttp等实现网络编程;
  • 异步非阻塞框架IOCP等

5、调试技术

  • IDA+windbg高级调试Windows;
  • 产生dump文件进行代码调试;
  • 使用DebugView工具调试

6、数据库设计

  • 掌握SQL语言的实用技巧;
  • SQLite与Oracle、MySQL的使用方式及区别

7、良好的框架设计

(三)桌面应用公司推荐

道通科技、深信服、今日头条、迅雷、百度、360、酷狗

(四)学习项目推荐(附源码)

超级任务管理器:processhacker

2 超级任务管理器:processhacker.png

三、图像处理与音视频开发

(一)图像处理与音视频就业方向

流媒体、大学里的研究所、今日头条(抖音)、优酷

(二)图像处理与音视频知识架构体系

1、**C/**C++**语言**和设计模式

  • 基本C/C++语法、数据类型、数组、指针、类、模板等;
  • 常用的设计模式

2、OpenCV/OpenGL知识体系和各种图像格式和视频格式

  • OpenCV的环境配置,数字图像处理相关知识;
  • 图像滤波,图像识别,膨胀,腐蚀,直方分布图,二值化,灰度,目标识别、检测、定位等图像处理相关技术;
  • 模式识别相关算法如二值化,SIFT特征,边缘轮廓检测等;
  • 计算机图形学/计算机视觉/相机标定;
  • 三维建模;
  • 各种图像格式的基本概念;
  • rgb yuv jpg;FFmpeg常见编解码格式用法;
  • 写MP4容器

3、操作系统原理和****网络编程以及相关协议

  • Linux/windows操作系统核心原理、线程/进程;
  • tcp/udp协议 rtmp rtp RTSP

4、良好的算法能力

  • 掌握深度学习算法原理和基本模型;
  • 熟练使用深度学习开源框架(如TensorFlow,Keras,Caffe等);
  • 人群、手势、人脸识别

5、良好的****数学知识

  • 熟悉matlab的使用、矩阵论;
  • 具备阅读SCI论文等能力

6、开源代码

  • 音视频:Live555;webrtc;
  • 图像处理:OpenCV源码

(三)图像处理与音视频公司推荐

今日头条、优酷、三星、索尼、清华同方、腾讯研究院、部分外企

(四)学习项目推荐(附源码)

跨平台流媒体解决方案:live555项目**3 跨平台流媒体解决方案:live555项目.png**

**四、**Linux后台服务开发

(一)Linux后台服务就业方向

  • 企业级分布式服务器、智慧城市、智慧交通等场景AI后台系统;
  • 游戏后台服务开发;
  • 云计算、银行系统、企业web服务器等

(二)Linux后台服务知识架构体系

1、C/C++**语言**和设计模式

基本C/C++语法、数据类型、数组、指针、结构体、链表、文件操作、队列、栈等

2、操作系统****基础

  • Linux操作系统的概念、安装方法,线程、进程;
  • 详细了解Linux下的目录结构、基本命令、编辑器VI、编译器GCC、调试器GDB和Make项目管理工具;
  • Makefile Shell脚本编写等知识

3、Linux系统编程****与linux操作系统

  • 重点学习标准I/O库,Linux多任务编程中的多进程和多线程;
  • 进程间通信(pipe、FIFO、消息队列、信号量、共享内存、信号)同步与互斥,对共享资源访问控制等重要知识;
  • 提升对Linux应用开发的理解和代码调试的能力;
  • Linux文件系统

4、网络**原理与网络**编程

  • TCP/IP协议、socket编程、TCP网络编程、UDP网络编程、HTTP;
  • 重点学习网络编程相关API;
  • 熟悉HTTP协议及其实现方法;
  • 对比网络框架,做源码分析,如ACE/ICE/ZMQ/Libevent/Muduo等

5、数据库、中间件

Mysql、redis nginx mongdb等中间件

6、集群与分布式架构

7、高性能组件造轮子的能力高并发IO

(三)Linux后台服务公司推荐

腾讯、华为云、网易游戏、小米云平台、虎牙直播、快手后台等

(四)学习项目推荐(附源码)

分布式版本控制系统:Git源码

4 分布式版本控制系统:git 源码.png

五、逆向与反外挂

(一)逆向与反外挂****就业方向

防火墙、入侵检测/入侵防御、Web应用防火墙、上网行为管理、VPN、、抗DDoS、数据库安全、数据防泄漏、漏洞扫描

(二)逆向与反外挂****知识架构体系

1、 C/C++**语言**的逆向表现与X86汇编语言

C语言/C++逆向语法表现,内存分布,X86汇编语言、数组和指针的逆向表现、函数的逆向表现;

学习逆向如果不掌握一些汇编,基本就是盲人摸象,能出成果就只能靠运气。

随着64位机器的普及,32位程序会越来越少,这块的学习难点在于64位CPU和32位CPU差异明显,甚至可以说是差异巨大,大家学习时要注意区分。

2、 操作系统****与windowsPE

  • 操作系统进程/线程概念,互斥量、信号量、事件等;
  • PE头,导入/导出表;
  • 重定位/资源/延迟加载导入表

3、 游戏反外挂/HOOK/注入

  • 基于注入的反外挂攻防;
  • 基于HOOK的反外挂攻防;
  • 基于CALL的反外挂攻防;
  • 基于窗口的外挂检测;
  • 游戏多开的逆向分析

4、 加密/编码算法

  • 基础的,比如异或、url编码等;
  • base64(出镜率极高)crc校验算法md5;
  • 学习tea和aes des

这里建议,可以先学习正向开发的部分算法。用不同的算法处理过的数据,会有一些特征;见得多了,很多时候可以在你研究逆向时给予很大帮助。

5、 工具类

  • OD、CE、Windbg等调试工具;
  • wireshark、x64dbg、frida、visual studio,其中有一些是开发工具,比如visual studio;
  • 虽然这些工具是开发工具,但是其中包含很多小工具,比如spy++、depends等

6、 框架

  • 逆向框架比较多,比如Cydia、xposed、substrate、ghidra等;
  • 每个框架所适用平台和条件也略有差异,例如:
  • Cydia是在苹果下做逆向;
  • xposed是Android下逆向;
  • substrate是一种拦截方案;
  • ghidra是一个集成环境等

7、 其他

逆向与反外挂这一部分比较特别,它们的知识点没有尽头。

除了网络协议(Http、Https、ftp、ssh)、CPU架构(arm架构、x86架构、64位架构、机器码)、操作系统(Windows、Linux、android、iOS)、驱动开发中断原理、动态库的加载和卸载,还有文件格式也需要掌握。

文件格式这块的内容也较为庞杂,简单的包括wav、bmp、apk文件格式;有了一定的了解,就可以尝试去解析pe、elf文件格式;然后还可以了解png、jpeg、jar、dex文件格式。当然,有一些没有详细文档的文件格式,比如luac、ocx,也是值得去学习和探索的。

(三)逆向与反外挂****公司推荐

奇安信集团、奇虎360、深信服、启明星辰、绿盟科技、金山

有兴趣或疑问的小伙伴,欢迎添加评论或私信我一起交流。

本文转载自: 掘金

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

5分钟让你理解K8S必备架构概念,以及网络模型(上)

发表于 2021-02-04

写在前面

在这用XMind画了一张导图记录Redis的学习笔记和一些面试解析(源文件对部分节点有详细备注和参考资料,欢迎关注我的公众号:阿风的架构笔记 后台发送【导图】拿下载链接, 已经完善更新):

前言

很多小伙伴学习K8S的时候,会被K8S里面的概念搞乱了,望而生畏;而且很多文章里面介绍的时候讲的太专业了。今天来帮小伙伴们梳理一下,讲的不深入,目的是帮忙小伙伴更好的理解,各个概念的由来。

架构图

图片

上图中,有两种Node节点,一个是Master、一个是Work。

从字面上来看Work Node就是用来工作的,也就是真正承担服务的机器节点。如服务A部署到K8S后,它的运行环境就在WorkNode节点。

那么Master Node是干嘛用的?小伙伴可以认为是用来分配服务到哪一台work node节点的;可以理解为大管家,它会知道现有work node的资源运行情况,决定服务安排到哪些work nodes上。

在Work Node节点上面有2个重要的组件,一个是Pod、一个是Container;

Pod是K8S的最小单元,它里面可以有多个Container。Container就是服务/组件运行的环境。

一般情况下一个Pod只有一个业务服务Container,而其他的Container是系统所需要的容器(其实就是一些进程组件,如网络组件、Volume组件等)。所以一般可以理解为我们的服务就在Pod里面。

上面只是简单的介绍了K8S基本的架构,以及核心点。

小伙伴们基本使用,理解到这里也就可以了

当然需要深入了解具体Master和Work节点有哪些组件,以及组件之间的发布流程是什么?继续往下看哦。

Master Node组件

图片

上面中,用户一般采用kubectl命令,以及dashboard控制台去操作k8s。所有的操作都是通过API Server组件,需要持久化的就存储到etcd。Scheduler、Controller Manager组件一直订阅API Server的变化。

整体流程

如用户需要创建服务A的3个pod,那整体流程:

1)通过Kubectl提交一个创建RC的请求,该请求通过API Server被写入etcd中。

2)此时Controller Manager通过API Server的监听资源变化的接口监听到这个RC事件,分析之后,发现当前集群中还没有它所对应的Pod实例,于是根据RC里的Pod模板定义生成一个Pod对象,通过API Server写入etcd。

3)接下来,此事件被Scheduler发现,它立即执行一个复杂的调度流程,为这个新Pod选定一个落户的Work Node,然后通过API Server讲这一结果写入到etcd中。

4)随后,目标Work Node上运行的Kubelet进程通过API Server监测到这个“新生的”Pod,并按照它的定义,启动该Pod。

5)用户的需求是3个pod;那到底有没有启动了3个;是由Controller Manager监控管理的,它会保证资源达到用户的需求。

etcd

用于持久化存储集群中所有的资源对象,如Node、Service、Pod、RC、Namespace等;API Server提供了操作etcd的封装接口API,这些API基本上都是集群中资源对象的增删改查及监听资源变化的接口。

API Server

提供了资源对象的唯一操作入口,其他所有组件都必须通过它提供的API来操作资源数据,通过对相关的资源数据“全量查询”+“变化监听”,这些组件可以很“实时”地完成相关的业务功能。

Controller Manager

集群内部的管理控制中心,其主要目的是实现Kubernetes集群的故障检测和恢复的自动化工作,比如根据RC的定义完成Pod的复制或移除,以确保Pod实例数符合RC副本的定义;根据Service与Pod的管理关系,完成服务的Endpoints对象的创建和更新;其他诸如Node的发现、管理和状态监控、死亡容器所占磁盘空间及本地缓存的镜像文件的清理等工作也是由Controller Manager完成的。

Scheduler

集群中的调度器,负责Pod在集群节点中的调度分配。

Work Node组件

图片

上图右侧是Work Node的组件,整体流程

1)kubelet监听到Api Server的变化后,如果有本work node节点需要创建pod;则会通知Container Runtime组件

2)Container Runtime是管理节点Pod组件,在启动pod时,如果本地没有镜像,则会从docker hub里面拉取镜像,启动容器pod

3)kubelet会把相关信息再传给Api Server

Kubelet

负责本Node节点上的Pod的创建、修改、监控、删除等全生命周期管理,同时Kubelet定时“上报”本Node的状态信息到API Server里。本质Pod的管理是Container Runtime组件负责的

kube-proxy

实现了Service的代理与软件模式的负载均衡器,这个是因为pod的网络ip是经常变化的。这个网络知识,下一篇文章会介绍

Pod发布

上面介绍了K8S整体架构流程,现在先从pod开始,一步步引出K8S的其他概念。

我们先编辑yaml,定义一个pod对象

1
2
3
4
5
6
7
8
xml复制代码apiVersion: v1  #指定api版本,此值必须在kubectl apiversion中
kind: Pod #指定创建资源的角色/类型
metadata: #资源的元数据/属性
name: mc-user #资源的名字,在同一个namespace中必须唯一
spec: #specification of the resource content 指定该资源的内容
containers: #容器定义
- name: mc-user #容器的名字
image: rainbow/mc-user:1.0.RELEASE #容器镜像

我们通过kubectl命令,来创建这个pod

1
xml复制代码kubectl apply -f mc-user-pod.yaml

我们mc-user:1.0.RELEASE的镜像就是一个web应用,8080端口;但是我们发现pod启动后,我们无法通过pod的ip地址访问此web服务

图片

那怎么才能访问pod呢?

反向代理

在要解决访问pod的问题前,我们先来看看我们之前是如何部署网站的?

图片

外网访问我们内部的网站,一般我们会在中间部署一个nginx,反向代理我们的web服务。根据这个思路,K8S体系中也有反向代理这个概念

NodePort Service

图片

K8S中我们可以采用类型为NodePort的Service实现反向代理

K8S的Service很多,其中NodePort Service是提供反向代理的实现

这样外网就可以访问内部的pod了。实现流程:

1
2
3
java复制代码1)pod需要打上一个Label标签
2)外部流量请求到NodePort Service,通过Selector 进行路由,
3)NodePort Service根据Label标签进行路由转发到后端的Pod

从上面的流程中,其实Service也起到了负载均衡的作用;后端Pod可以有多个,同时打上相同的Label标签,Service会路由转发到其中一个Pod。

Service Type还可以为 LoadBalancer、ClusterIP LoadBalancer:这个是部署到云端(如阿里云)的时候需要用的,也是反向代理+负载均衡的作用,用作外部访问K8S内部。ClusterIP:这个Service是K8S集群内部做反向代理用的

Label与Selector

图片

上图中有2个pod定义了Label为app:nginx;1个pod定义了app:apache;

那么Service的Selector筛选app:nginx,只会路由到nginx的pod。

Service发布

我们来编写一个NodePort Service发布文件

1
2
3
4
5
6
7
8
9
10
11
12
13
xml复制代码apiVersion: v1
kind: Service
metadata:
name: mc-user
spec:
ports:
- name: http #通讯协议
port: 8080 #这里的端口和clusterIP对应,即ip:8080,供内部访问。
targetPort: 8080 #端口一定要和container暴露出来的端口对应
nodePort: 31001 #节点都会开放此端口,此端口供外部调用
selector:
app: mc-user #这里选择器一定要选择容器的标签
type: NodePort #这里代表是NodePort类型的

nodePort的端口范围:30000~32767

上面是NodePort Service的yaml文件,我们还要修改一个之前的Pod的yaml文件

1
2
3
4
5
6
7
8
9
10
xml复制代码apiVersion: v1  #指定api版本,此值必须在kubectl apiversion中
kind: Pod #指定创建资源的角色/类型
metadata: #资源的元数据/属性
name: mc-user #资源的名字,在同一个namespace中必须唯一
labels: #标签定义
app: mc-user #标签值
spec: #specification of the resource content 指定该资源的内容
containers: #容器定义
- name: mc-user #容器的名字
image: rainbow/mc-user:1.0.RELEASE #容器镜像

我们可以利用kubectl命令去分别执行pod和service的yaml文件;这样就可以通过外网直接访问了。http://localhost:31001端口不要忘了是 nodePort定义的端口哦。

总结

今天介绍了K8S的基本概念,以及架构流程;核心的是小伙伴们需要理解Pod、Service、Labels、Selector的这个组件为什么会产生?他们的解决了是什么问题?后续会继续介绍K8S其他的组件概念,希望能够帮助小伙伴们理解,减少K8S的学习难度;谢谢!!!

看完三件事❤️

​
如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
​

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 阿风的架构笔记 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. 关注后回复【666】扫码即可获取架构进阶学习资料包

本文转载自: 掘金

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

1…722723724…956

开发者博客

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