雷声大作,辗转反侧,难以入睡。四人的寝室,如今已只剩我和老乔了。四年,转眼而过。原以为毕业是离我很远的一件事,可它已经发生了。一些人,也许随着毕业后就再也见不到了吧。
四年大学,疫情占了两年,让本该欢声笑语,四处奔波的我们,囚禁在了小小的校园。
四年发生了好多好多事,我想一点一点地写下,可奈何自己的语言太过贫瘠,不足以记录这缤纷的生活。
自从买了mbp后,windows就被当成游戏机使用了。可由于毕设的内容关于FL,加上mbp的arm芯片好多库都不支持,很多应用实例都跑不了。但自己又不想折腾windows环境,(加上没钱购买高配服务器),于是决定在Windows笔记本上加装一个ubuntu系统。因为过程繁琐,加上遇到些坑,写篇文章记录一下。
第一步肯定是准备镜像和硬盘了。
系统镜像直接去官网下载就好,我使用的是18.04
因为当初安装windows时,没有想到有今天这些操作,肯定没有未使用的磁盘的。所以需要将已用的磁盘压缩,分出一块未使用的分区。由于过程中没有截图记录,这边我就放出别人的详细教程了,我粗略介绍一下,防止链接失效。
进入设置->搜索磁盘管理->选择需要压缩的磁盘->压缩卷。
制作启动盘->使用ultraISO写镜像
重启电脑->更改启动顺序,将U盘启动放置首位->然后选择安装Ubuntu即可。
1 | #git |
1 | #更改默认shell |
ubuntu默认字体不支持p10k,需要安装支持的字体,我比较喜欢用meslo NF,下载链接
1 | wget https://repo.anaconda.com/archive/Anaconda3-2021.11-Linux-x86_64.sh |
我主要想把ubuntu当服务器使用,所以我需要开机启动clash服务。
1 | #github主页下载对应版本,配置相应的config文件 |
有些服务商给的配置文件可能不是给服务器用的,需要更改proxy,rule之类的。
因为有了mac,所以ubuntu纯当服务器了,所以需要frp反向代理ssh端口。
首先防火墙放行,我摆烂了,直接关掉
1 | systemctl disable ufw |
这里有个坑,我也没细究为什么,root用户启动失败,但是普通用户可以,在frpc.ini的common项加入tls_enable = true
即可避坑。
1 | [common] |
我把frp放到了服务项里。
首先制作服务内容:
在/etc/systemd/system
目录下新建文件vim frpc.service
,编辑内容:
1 | [Unit] |
通过以下命令注册,启动服务。
1 | systemctl daemon-reload |
1 | sudo apt-get install \ |
好不容易配好的系统,再折腾坏了重装可太麻烦了。使用timeshift可以向虚拟机一样给系统创建快照。
1 | sudo add-apt-repository -y ppa:teejee2008/ppa |
这是个图形化工具,操作很简单,就不介绍了~
也可以使用命令操作
1 | sudo timeshift --create --comments 'A new backup' --tags D |
看书、看博客永远是看别人替你总结的东西,想根本弄懂Java我觉得还是要看完Java的源码。jdk1.8是比较经典的一个Java版本,Oracle在该版本加入lambda表达式、新日期API和溢出hotspot永生代很多功能,因此选择该版本作为阅读版本。
为减少篇幅,并没有把所有代码全部粘贴至文中,但是会尽力介绍到每一个方法。
我们从rt.jar包中最常用的数据结构类开始分析,rt全称runtime,是java的核心jar包。
ArrayList
底层是数组队列,相当于动态数组。与 Java 中的数组相比,它的容量能动态增长。在添加大量元素前,应用程序可以使用ensureCapacity操作来增加 ArrayList 实例的容量。这可以减少递增式再分配的数量。它是线程不安全的,允许元素为null。
ArrayList
位于java.util
包下,先看一下它的关系图
ArrayList
继承自AbstractList
类,并且实现了RandomAccess
,Cloneable
,Serializable
和 List
接口,即支持随机访问,克隆,序列化。
如果深入查看,会发现RandomAccess
,Cloneable
,Serializable
这三个都是空接口,没有指定任何方法,这样的接口叫标识接口
,主要作用是为了标识,表明该类实现了哪些功能。这样做的好处是其他方法调用该类对象时,直接查看该类有实现某些接口来做些判断。
1 |
|
ArrayList总共有三个构造函数,第一个参数给定初始容量,源码很好理解,当给定容量为0时,其elementData为被赋为空。DEFAULTCAPACITY_EMPTY_ELEMENTDATA和EMPTY_ELEMENTDATA没有啥本质上的区别,只是为了区分这两种空数组对应不同情况的初始化。
我们对其他api一个一个分析
1 | //将list的容量变为当前大小,利用该操作可以最小化一个ArrayList的存储消耗 |
modCount
是一个特殊的变量,继承自AbstractList
,用来记录修改次数,监测迭代器迭代的时候是否发生修改。
1 | /** |
该函数主要是用于 防止一瞬间存储大量数据,导致不停扩容
(之后的函数会提到)产生的时间消耗 , 通过该函数可以直接扩容到用户指定的容量。
1 | private void ensureExplicitCapacity(int minCapacity) { |
要求的精确容量只有不小于现有容量时,才会发生grow(扩容)。
1 | /** |
1 | public static <T> T[] copyOf(T[] original, int newLength) { |
int newCapacity = oldCapacity + (oldCapacity >> 1),所以 ArrayList 每次扩容之后容量都会变为原来的 1.5 倍左右。如果newCapacity大于规定的最大size,会被通过hugeCapacity
重新赋值。
再通过调用Arrays.copyof方法复制出新的数组给elementData。
1 | private static int hugeCapacity(int minCapacity) { |
如果minCapacity小于0,说明已经溢出,导致符号位置一,所以为负。
1 |
|
返回ArrayList大小。
1 |
|
判空函数
1 | /** |
通过indexOf
函数查找目标下标,下标为负即不存在。
1 |
|
查找目标在ArrayList中第一次出现的位置。
1 | /** |
和indexOf
正巧相反,返回目标在ArrayList最后一次出现的位置。
1 |
|
克隆函数,可以发现ArrayList克隆并不去调用存储对象的克隆,也就是‘浅拷贝’。
1 |
|
返回elementData数组的拷贝,并可以把它强转成用户定义的类型,也因此加上@SuppressWarnings("unchecked")
注解。
1 |
|
获取下标为index的数据,先检查边界(index为负会在elementData函数中报错),然后通过elementData返回值。
1 | public E set(int index, E element) { |
1 | /** |
首先确保ArrayList容量足够,然后进行赋值。
1 | private static int calculateCapacity(Object[] elementData, int minCapacity) { |
1 | /** |
调用remove(int index)
函数会将旧值返回。
由于是数组,所以删除完后会将之后的数组前移。
1 | public void clear() { |
清空数组,是通过遍历而不是直接置换成空数组。
1 | public boolean addAll(Collection<? extends E> c) { |
ArrayList
提供了两种addAll
方法,一个指定下标,一个默认在队尾添加。指定下标会先检查边界。
两者都会先确定容量,对数组进行扩容,在进行赋值操作。
ps:rangeCheck
和rangeCheckForAdd
两个函数是不一样的,rangeCheck
只判断了上界,下界交给了数组自身去判断,而用到rangeCheckForAdd
的函数并不会用到数组自带的下标判断,所以需要判断下下界。
1 | protected void removeRange(int fromIndex, int toIndex) { |
数组左移复制后,将之后的元素全部置空。
1 | public boolean removeAll(Collection<?> c) { |
retainAll
是让elementData仅保存提供的collection
里的元素,而removeAll
正巧相反,删除提供的collection
里的元素。
在batchRemove
中通过一个boolean即区分开来(太巧妙了!)
为了加快处理速度,先定义了一个final的elementData数组(当final变量是基本数据类型以及String类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该final变量的地方,相当于直接访问的这个常量,不需要在运行时确定。这种和C语言中的宏替换有点像。变量被final修饰,会被当做编译器常量,所以在使用到该变量的地方会直接将变量替换为它的值。)
接着遍历elementData数组,通过判断collection中是否存在该元素而判断元素的存留。
由于c.contains
可能会抛出异常,存在r!=size
的情况,未处理到的元素默认保留在数组内。
如果正常处理结束,将超过size的元素置为空,等待gc回收内存。
1 | private void writeObject(java.io.ObjectOutputStream s) |
这两个完成了ArrayList
和ObjectOutputStream
之间的相互转换,也就是让ArrayList
有了序列化的功能。
在writeObject
的过程通过CAS的方法,保证操作的原子性。
由于自己还没阅读序列化相关代码s.readInt()
和SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
还不太懂其用处。
1 |
|
forEach
、replaceAll
和sort
比较好理解,都是直接遍历数组,执行函数式接口的方法。removeIf
其实也一样,只是逻辑比较漂亮,想分析一下。
先遍历数组,执行函数利用bitSet标记要清除的元素,然后再进行遍历数组,筛除需要删除的元素(发现没啥好解释的,大家看源码体会代码的艺术吧)。
剩下的源码也就是ArrayList的几个内部类了,我打算放在第二章来分析。
]]>前几天面试的时候,在介绍项目的布防问题时,被问得有点哑口无言,稀里糊涂搪塞了过去。一是忘了原有的方案失效的原因,二是对后改的方案依旧存在问题的心虚,所以就搪塞了过去。今天在复盘的时候想通了,因此想记录一下。
为防止自己忘记这个问题来源和其他读者(如果有)方便阅读,特地介绍一下。
布防问题是我在项目中遇到的一个具体问题简称(不是什么学术问题),该问题具体是用户控制摄像头报警(布防)的时间段的一个策略问题。
原有的方案是让用户自定义每周布防时间段,然后后端将用户的输入和以前设置的时间段进行合并,然后再将用户全部布防策略发送给第三方(摄像头api端)进行布防。但是在第三方那边处理跨天时间段是无法进行的,所以得分成当天时间段和第二天时间段。
修改后的方案是利用其他两个api(一个让摄像头上线,一个让摄像头下线)来进行布防,算法思路就是将用户设置的时间段处理成上线时间和下线时间,但是这样剥离了上线和下线中间的连续性,如果有重合时间段的设置就会很麻烦。并且在检测用户的设置上线和下线时间时,采用的是每分钟轮询所有用户的数据,然后对摄像头进行上线和下线,
首先帮助用户合并时间段我觉得本身就是自讨苦吃,合并策略有可能不是用户所要的结果,所以从前端下手,在用户输入时就将整周的布防结构图显示出来,用户在此基础上对原有布防进行修改,或者新增时间段,这样就可以避免时间段的合并处理。当然数据到后端后,我们还是需要对时间段是否重合进行校验。
剩下的工作就只剩用户策略的定时上线和下线了,我们其实可以利用定时任务框架来实现,比如Quartz或者InfoQ。只需要用户重新设置布防策略时,对相关的定时任务进行修改和新增即可,这样就避免每分钟从数据库读取数据导致的读取压力。
一直对Enum的实现原理有困惑,所以最近专门去查了查,了解到了Java的12个语法糖,决定依次学习记录一下。
我们先写个简单的Enum的java程序并编译一下。
1 | public enum Week { |
编译程序得到class文件并用jad(下载地址)反编译该文件jad \Week.class
,javap只能反编译为字节码,查看较为麻烦。
得到源码:
1 | // Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov. |
我们可以看到Enum其实本质还是类,所有的Enum都继承自Enum基类。
分析构造函数,我们定义的枚举常量
的名字都用String
类存储,并且函数会自动给常量分配一个ordinal
,用来记录记录枚举常量在声明时的顺序,剩下的参数是我们在枚举中定义的参数。
super函数内容:
1 | protected Enum(String name, int ordinal) { |
static代码块是对枚举类进行初始化并存入到$VALUES数组中。
返回所有的枚举常量,这个很好理解。
valueOf函数通过枚举常量的名字来返回枚举常数。
函数内使用的是基类Enum中的valueOf函数,并传入Week参数,来和其他Enum类区分
1 | public static <T extends Enum<T>> T valueOf(Class<T> enumType, |
enumType.enumConstantDirectory()
函数会返回所有一个包含所有枚举常量的HashMap
。
1 | Map<String, T> enumConstantDirectory() { |
该部分摘自枚举的本质
Jvm编译器处理枚举的做了以下几件事:
一些值得关注的点
javap是jvm自带的一个工具,主要用于反编译Java的class文件,比idea自带的反编译更加细致(我没有仔细研究过idea的反编译设置,默认的idea反编译比较简洁,无法查看语法糖实现细节),可以查看生成的字节码。
javap的用法格式:javap <options> <classes>
其中classes就是你要反编译的class文件。
在命令行中直接输入javap
或javap -help
可以看到javap的options有如下选项:
1 | -help --help -? 输出此用法消息 |
一般常用的是-v -l -c三个选项。
javap -v classes,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息。
javap -l 会输出行号和本地变量表信息。
javap -c 会对当前class字节码进行反编译生成汇编代码。
Java Native方法简单来说就是java调用非java代码的接口。Native方法的实现是由非java语言
实现,比如c
,c++
。这个特征并非java所特有,很多其它的编程语言都有这一机制,比如在c++
中,你可以用extern "C"
告知c++编译器去调用一个C的函数。
“A native method is a Java method whose implementation is provided by non-java code.”
在定义一个native method时,并不提供实现体,我们以java.lang.Object
为例,其类中有很多native方法,如getClass
,hashCode
:
1 | public class Object { |
Java很好,使用的人很多、应用极广,但是Java不是完美的。Java的不足体现在运行速度要比传统的C++慢上许多之外,还有Java无法直接访问到操作系统底层如硬件系统,为此Java提供了JNI来实现对于底层的访问。JNI(Java Native Interface),它是Java的SDK一部分,JNI允许Java代码使用以其他语言编写的代码和代码库,本地程序中的函数也可以调用Java层的函数,即JNI实现了Java和本地代码间的双向交互。
标识符native可以与所有其它的java标识符连用,但是abstract除外。
一个native method方法可以返回任何java类型,包括非基本类型,而且同样可以进行异常控制。
native method的存在并不会对其他类调用这些本地方法产生任何影响,实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法。
如果一个含有本地方法的类被继承,子类会继承这个本地方法并且可以用java语言重写这个方法,同样的如果一个本地方法被fianl标识,它被继承后不能被重写。
本地方法非常有用,因为它有效地扩充了jvm。事实上,我们所写的java代码已经用到了本地方法,在sun的java的并发(多线程)的机制实现中,许多与操作系统的接触点都用到了本地方法,这使得java程序能够超越java运行时的界限。
我使用的是CLion
和idea
做为C++ IDE和java IDE,如果有同学使用的visual studio系列,可以查看参考博客(自己实现一个Native方法的调用)。
1 | package pers.desperado.demo; |
java代码很简单,申明native方法并在main函数中调用,代码块中的代码是Java加载C++的dll,这个路径一定要与第三步中生成的C++ dll路径一致。
通过在Java out目录下(具体目录看下图),调用命令JNI -jni pers.desperado.demo.Demo
,请注意类参数与自己命名的一致。生成后out目录下会多出一个.h文件
CLion通过编写makelist.txt文件来控制程序的输出形式,我们通过如下代码命令生产dll文件
1 | # cmake_minimum_required(VERSION <specify CMake version here>) |
(注意项目名和文件名)
将第二步生成.h文件复制到C项目文件夹下,并且将%JAVA_HOME%/include目录下的“jni.h”和%JAVA_HOME%/include/win32目录下的“jni_md.h”两个文件
也复制过来
修改.h文件中的#include <jni.h>
为#include "jni.h"
(在clion中它会自动报warning,alt+enter即可更改)
编写main.cpp
1 |
|
执行便会生产dll文件。
4.执行java代码
执行后便会在idea命令框中输出Hello
整体代码我已经放到GitHub仓库,可以自行下载尝试。指路
记得之前学习Java的时候完全是突击性的,因为要一个学期内掌握ssm框架,所以从下至下都学的不是很扎实,决定重新学习下Java。该博客是跟从JavaGuide学习的一些要点整理,并且对其讲得不太准确或容易产生误解的地方做些记录。本博文章节名与JavaGuide文章名对应。
Java是编译与解释共存的语言。高级编程语言按照程序的执行方式分为编译型和解释型两种。简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。比如,你想阅读一本英文名著,你可以找一个英文翻译人员帮助你阅读, 有两种选择方式,你可以先等翻译人员将全本的英文名著(也就是源码)都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。
Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(*.class 文件),这种字节码必须由 Java 解释器来解释执行。因此,我们可以认为 Java 语言编译与解释并存。
Java中char类型占两个字节。
在 C 语言中,字符串或字符数组最后都会有一个额外的字符’\0’来表示结束。但是,Java 语言中没有结束符这一概念。原因解释
java的泛型擦除,自己之前写过一篇相关的博客指路
hashCode()和equals(),为了让自己设计的类能够正确地被hashset
存储时,我们要重写hashCode()方法,同时hashCode()也能够让equals()提速不少。
java中的参数传递,一开始还以为教程上讲错了,还打算专门写篇博客,后来才发现自己对引用传递
的概念忘干净了。同样没看懂的同学可以先复习下三种参数传递方式。为什么大家都说Java中只有值传递?这篇博客讲得挺细致的。
对于不想进行序列化的变量,使用 transient 关键字修饰。
使用try-with-resources
来代替try-catch-finally
,java7引入了try-with-resource
,通过如下,我们就可以无需自己关闭资源了。
1 | try (Scanner scanner = new Scanner(new File("test.txt"))) { |
当我们需要使用精确小数时,应当使用BigDecimal
。为了防止精度丢失,推荐使用它的BigDecimal(String)
构造方法来创建对象。
Arrays.asList()会把数组转换为java.util.Arrays$ArrayList
,注意和java.util.Arraylist
做区分(JavaGuide并没有第一时间在文中指出)
翻阅java1.8源码我们可以看到java.util.Arrays$ArrayList
是一个内部定义好的类,整体代码如下:
1 | private static class ArrayList<E> extends AbstractList<E> |
我们可以看到a是被final
关键字修饰了的,并且它的初始化是a = Objects.requireNonNull(array);
,也就是说它本质是一个适配器。
不要在 foreach
循环里进行元素的 remove/add
操作
因为自己在学习使用flex&bison编写编译器时,困难很大,网上的资料很老很陈旧,很多示例代码都无法运行。幸而自己最终找到一份07年的博客,所以想对其整理一下。一来加深自己的理解,为学弟学妹提供一份参考资料,二来也能防止原博客丢失。
flex是lex的加强版,同样bison也是yacc的加强版,lex和yacc的语法适用于flex和bison,之后的博客内容将不区分flex
和lex
,bison
和yacc。
本文所有的代码都在kali2019
运行测试过,没有linux基础的读者可以安装Ubuntu
系列linux,推荐ubuntu16和ubuntu18。
我们在以前的基础上,深入一步,设计一个简单的计算器,包含+,-,*,/四项操作,且支持()运算符,其中对值可以进行变量保存,并打印出内部的分析信息。
lex文件内容(lexya_b.l)
1 | %{ |
Yacc文件全内容(lexya_b.y)
1 | %{ |
待解释编译文本(input)全内容
ps:该input一定要在linux中编写或使用我写好的。Windows换行符和Linux不一致会导致无法完成词法分析。
1 | a=4+2*(3-2-1)+6 |
命令行依次输入下列命令
1 | bison -d lexya_b.y |
运行截图
文中成功了使用了变量,并且操作符运算优先级也没有问题。其实细看过flex&bison完成C-语言编译器前端(二)一文后,理解上面的例子就很轻松了。这里只是做了一些扩充变化:
1.增加了全局数组sMem来存放变量,不过变量名有限制,只支持单字符。
2.增加了全局数组sBuff存放分析过的语句
3.增加debuginfo打印堆栈信息
4.增加printinfo打印目前的分析语句
要进行内部分析,就需要剖析生成的c文件,对程序(parser)进行跟踪调试。(注:Lex编译时加上d参数,会在程序解释时输出详细的调试信息。如:lex -d lexya_b.l
)
通过本示例再加上实际对lexya_b.tab.c的分析理解,会对lex,yacc理论有更进一步的理解。
上面的例子只支持单字符的变量,想支持多字符,需要定义一全局变量如:
1 | struct varIndex { |
同时打印信息加入对变量的显示,下面列出全部文件内容,比较简单,不再附加说明。
userdef.h
1 | typedef struct |
example2.l
1 | %{ |
example2.y
1 | %{ |
这个系列打算就记载到这里了,剩余的内容读者可以访问原博客。
]]>因为自己在学习使用flex&bison编写编译器时,困难很大,网上的资料很老很陈旧,很多示例代码都无法运行。幸而自己最终找到一份07年的博客,所以想对其整理一下。一来加深自己的理解,为学弟学妹提供一份参考资料,二来也能防止原博客丢失。
flex是lex的加强版,同样bison也是yacc的加强版,lex和yacc的语法适用于flex和bison,之后的博客内容将不区分flex
和lex
,bison
和yacc。
本文所有的代码都在kali2019
运行测试过,没有linux基础的读者可以安装Ubuntu
系列linux,推荐ubuntu16和ubuntu18。
Lex使用正则表达式从输入代码中扫描和匹配字符串。每一个字符串会对应一个动作。通常动作返回一个标记给后面的剖析器使用,代表被匹配的字符串。每一个正则表达式代表一个有限状态自动机(FSA)。我们可以用状态和状态间的转换来代表一个(FSA)。其中包括一个开始状态以及一个或多个结束状态或接受状态。
以文章一中的第一个例子来说明:
example1.l
1 | %{ |
这里使用一个相对简单的输入文件file.txt:
1 | i=1.344+39; |
我们假定,
Lex 系统创建一动态列表:内容+规则+状态
Lex 状态:1 接受 2 结束
接受表示该元素可以做为模式匹配
结束表示该元素已完成模式匹配
读入“i”:
[查找元素]查找相邻且状态为1的元素,无元素, [匹配规则]D, [新增列表<元素1>并置数据](存在则覆盖)状态为1,规则为D,内容为"i"。 [操作顺序符] 1
读入“=”:
[查找元素]查找相邻且状态为1的元素,“i=”寻找匹配规则,无规则 [置上一元素]<元素1>状态为2 [匹配规则]F, [新增列表<元素2>并置数据](存在则覆盖)状态为1,规则为F,内容为"=" [操作顺序符] 2
读入“1”:
[查找元素]查找相邻且状态为1的元素,“=1”寻找匹配规则,无规则 [置上一元素]<元素2>状态为2 [匹配规则]B, [新增列表<元素3>并置数据](存在则覆盖)状态为1,规则为B,内容为"1" [操作顺序符] 3
读入“.” :
[查找元素]查找相邻且状态为1的元素,“1.”寻找匹配规则,无规则,但有潜在规则C [匹配规则]F, [新增列表<元素4>并置数据](存在则覆盖)状态为1,规则为F,内容为"." [置上一元素]<元素3>状态为1 [操作顺序符] 4
读入“3”:
[查找元素]查找相邻且状态为1的元素,“1.3”寻找匹配规则,有规则 [置起始元素]状态为1,规则为C,内容为"1.3" [操作顺序符] 3 组合元素的起始操作符
读入“4”:
[查找元素]查找相邻且状态为1的元素,“1.34”寻找匹配规则,有规则 [置起始元素]状态为1,规则为C,内容为"1.34" [操作顺序符] 3 组合元素的起始操作符
读入“4”:
[查找元素]查找相邻且状态为1的元素,“1.344”寻找匹配规则,有规则 [置起始元素]状态为1,规则为C,内容为"1.344" [操作顺序符] 3 组合元素的起始操作符
读入“+”:
[查找元素]查找相邻且状态为1的元素,“1.344+”寻找匹配规则,无规则 [匹配规则]E, [新增列表<元素4>并置数据](存在则覆盖)状态为1,规则为E,内容为"+" [置上一元素]<元素3>状态为2 [操作顺序符] 4
…
最后解析结果为
内容 | 规则 | 状态 | |
---|---|---|---|
<元素1> | i | D | 2 |
<元素2> | = | F | 2 |
<元素3> | 1.344 | C | 2 |
<元素4> | + | E | 2 |
… |
上面列出的算法是仅属于个人的分析,是相对直观且便于理解的,也可以参照这个算法用C语言模拟出lex的效果。不过真正的Lex算法肯定是更为复杂的理论体系,这个没有接触过,有兴趣可以参看相关资料。
个人认为lex理论比较容易理解的,yacc要复杂一些。
我们先从yacc的文法说起。yacc文法采用BNF(Backus-Naur Form)的变量规则描述。BNF文法最初由John Backus和Peter Naur发明,并且用于描述Algol60语言。BNF能够用于表达上下文无关的语言。现代程序语言大多数结构能够用BNF来描述。
举个加减乘除的例子来说明:1+2/3+4*6-3
BNF文法表示如下:
1 | E = num 规约a 优先级0 |
这里像(E表达式)这样出现在左边的结构叫做非终结符(nonterminal)。像(num标识符)这样的结构叫终结符(terminal,读了后面内容就会发现,其实是由lex返回的标记),它们只出现在右边。
我们将”1+2/3+46-3”逐个字符移进堆栈,如下所示:
| | .1+2/3+46-3 | |
| – | – | – |
| 1 | 1.+2/3+46-3 | 移进 |
| 2 | E.+2/3+46-3 | 规约a |
| 3 | E+.2/3+46-3 | 移进 |
| 4 | E+2./3+46-3 | 移进 |
| 5 | E+E./3+46-3 | 规约a |
| 6 | E+E/.3+46-3 | 移进 |
| 7 | E+E/3.+46-3 | 移进 |
| 8 | E+E/E.+46-3 | 规约a |
| 9 | E+E/E+.46-3 | 移进 |
| 10 | E+E/E+4.6-3 | 移进 |
| 11 | E+E/E+E.6-3 | 规约a |
| 12 | E+E/E+E.6-3 | 移进 |
| 13 | E+E/E+E6.-3 | 移进 |
| 14 | E+E/E+EE.-3 | 规约a |
| 15 | E+E/E+EE-.3 | 移进 |
| 16 | E+E/E+EE-3. | 移进 |
| 17 | E+E/E+EE-E. | 规约a |
| 18 | E+E+EE-E. | 规约b |
| 19 | E+E+E-E. | 规约c |
| 20 | E+E-E. | 规约d |
| 21 | E-E. | 规约d |
| 22 | E. | 规约e |
我们在实际运算操作中是把一个表达式逐步简化成一个非终结符。称之为“自底向上”或者“移进归约”的分析法。
点左面的结构在堆栈中,而点右面的是剩余的输入信息。我们以把标记移入堆栈开始。当堆栈顶部和右式要求的记号匹配时,我们就用左式取代所匹配的标记。概念上,匹配右式的标记被弹出堆栈,而左式被压入堆栈。我们把所匹配的标记认为是一个句柄,而我们所做的就是把句柄向左式归约。这个过程一直持续到把所有输入都压入堆栈中,而最终堆栈中只剩下最初的非终结符。
在第1步中我们把1压入堆栈中。第2步对应规则a,把1转换成E。然后继续压入和归约,直到第5步。此时堆栈中剩下E+E,按照规则d,可以进行E=E+E的合并,然而输入信息并没有结束,这就产生了“移进-归约”冲突(shift-reduce conflict)。在yacc中产生这种冲突时,会继续移进。
在第17步,E+E/E,即可以采用E+E规则d,也可以采用E/E规则b,如果使用E=E+E规约,显然从算法角度是错误的,这就有了运算符的优先级概念。这种情况称为“归约-归约”冲突(reduce-reduce conflict)。此时yacc会采用第一条规则,即E=E/E。这个内容会在后面的实例做进一步深化。
源代码:
calculate.l
1 | %{ |
calculate.y
1 | %{ |
编译运行
这里有两个文件calculate.y和calculate.l。calculate.y是yacc文件,bison -d lexya_a.y
编译后会产生 calculate.tab.c
calculate.tab.h
。lex文件calculate.l中头声明已包括了calculate.tab.h
。这两个文件是典型的互相协作的示例。
现在我们来分析下这个代码
(1)定义段和预定义标记部分
yacc文件定义和lex十分相似,分别以%{
,}%
,%%
,%%
分界。
1 | %{ |
这一段十分容易理解,只是头文件一些引用说明。成为“定义”段。
1 | %token INTEGER |
这一段可以看作预定义标记部分。%token INTEGER
定义声明了一个标记。当我们编译后,它会在calculate.tab.c
中生成一个剖析器,同时会在calculate.tab.h
产生包含信息:# define INTEGER 257
其中0-255的之间的标记值约定为字符值,是系统保留的后定义的token。calculate.tab.h
其它部分是默认生成的,与token INTEGER无关。
1 |
|
lex文件需要包含这个头文件,并且使用其中对标记值的定义。为了获得标记,yacc会调用yylex。yylex的返回值类型是整型,可以用于返回标记。而在yylval变量中保存着与返回的标记相对应的值。
yacc在内部维护着两个堆栈,一个分析栈和一个内容栈。分析栈中保存着终结符和非终结符,并且记录了当前剖析状态。而内容栈是一个YYSTYPE类型的元素数组,对于分析栈中的每一个元素都保存着一个对应的值。例如,当yylex返回一个INTEGER标记时,把这个标记移入分析栈。同时,相应的yacc值将会被移入内容栈中。分析栈和内容栈的内容总是同步的,因此从栈中找到对应的标记值是很容易的。
比如lex文件中下面这一段:[0-9]+ { yylval = atoi(yytext); return INTEGER; }
这是将把整数的值保存在yylval中,同时向yacc返回标记INTEGER。即内容栈存在了整数的值,对应的分析栈就为INTEGER标记了。yylval类型由YYSTYPE决定,由于它的默认类型是整型,所以在这个例子中程序运行正常。
lex文件还有一段:[-+*/\n] return *yytext;
这里显然只是向yacc的分析栈返回运算符标记,系统保留的0-255此时便有了作用,内容栈为空。把“-”放在第一位是防止正则表达式发现类似a-z的歧义。
再看下面的:
1 | %left '+' '-' |
%left 表示左结合,%right 表示右结合。最后列出的定义拥有最高的优先权。因此乘法和除法拥有比加法和减法更高的优先权。+ - * / 所有这四个算术符都是左结合的。运用这个简单的技术,我们可以消除文法的歧义。
注:关于结合性,各运算符的结合性分为两种,即左结合性(自左至右)和右结合性(自右至左)。例如算术运算符的结合性是自左至右,即先左后右。如有表达式x-y+z则y应先与“-”号结合, 执行x-y运算,然后再执行+z的运算。这种自左至右的结合方向就称为“左结合性”。而自右至左的结合方向称为“右结合性”。 最典型的右结合性运算符是赋值运算符。如x=y=z,由于“=”的右结合性,应先执行y=z再执行x=(y=z)运算。
(2)规则部分
1 | program: |
这部分其实就是BNF文法,学了编译原理的同学应该能很快看懂。
这个规则乍看起来的确有点晕,关键一点就是要理解yacc的递归解析方式。program
和expr
是规则标记,但是作为一个整体描述表达式。先看expr,可以由单个INTEGER值组成,也可以有多个INTERGER和运算符组合组成。
以表达式“1+4/2*3-0”为例,1 4 2 3
都是expr,就是expr+expr/expr*expr-expr
说到底最后还是个expr。递归思想正好与之相反,逆推下去会发现expr这个规则标记能表示所有的数值运算表达式。
了解了expr后,再看program,首先program可以为空,也可以用单单的expr加下“\n”回车符组成,结合起来看program定义的就是多个表达式组成的文件内容。
粗略有了概念之后,再看lex如何执行相应的行为。以expr: expr '+' expr { $$ = $1 + $3; }
为例:
在分析栈中我们其实用左式替代了右式。在本例中,我们弹出“ expr ‘+’ expr ”然后压入“expr”。我们通过弹出三个成员,压入一个成员来缩小堆栈。在我们的代码中可以看到用相对地址访问内容栈中的值。如$1,$2,这样都是yacc预定义可以直接使用的标记。“$1”代表右式中的第一个成员,“$2”代表第二个,后面的以此类推。“$$”表示缩小后的堆栈顶部。在上面的动作中,把对应两个表达式的值相加,弹出内容栈中的三个成员,然后把得到的和压入堆栈中。这样,保持分析栈和内容栈中的内容依然同步。
而
1 | program: |
说明每当一行表达式结束时,打印出第二个栈值,即expr的值,完成字符运算。
如果到这里能完全理解所述内容,对于lex和yacc就有些感觉了。后面文章会做进一步的深入。
]]>因为自己在学习使用flex&bison编写编译器时,困难很大,网上的资料很老很陈旧,很多示例代码都无法运行。幸而自己最终找到一份07年的博客,所以想对其整理一下。一来加深自己的理解,为学弟学妹提供一份参考资料,二来也能防止原博客丢失。
flex是lex的加强版,同样bison也是yacc的加强版,lex和yacc的语法适用于flex和bison,之后的博客内容将不区分flex
和lex
,bison
和yacc。
本文所有的代码都在kali2019
运行测试过,没有linux基础的读者可以安装Ubuntu
系列linux,推荐ubuntu16和ubuntu18。
在开始之前,你需要拥有一个装有flex
和bison
的操作系统。
Lex(Lexical Analyzar 词法分析生成器),Yacc(Yet Another Compiler Compiler编译器代码生成器)是Unix下十分重要的词法分析,语法分析的工具。经常用于语言分析,公式编译等广泛领域。遗憾的是网上中文资料介绍不是过于简单,就是跳跃太大,入门参考意义并不大。本文通过循序渐进的例子,从0开始了解掌握Lex和Yacc的用法。
一个简单的lex文件: example1.l
1 | {% |
在命令行下执行命令
1 | flex解析,会自动生成lex.yy.c文件 |
如果不加-ll链结选项,cc编译时会出现以下错误,后面会进一步说明。
创建待解析的文件file.txt:
1 | title |
通过已生成的可执行程序,进行文件解析:
到此lex用法会有个直观的了解:
1.定义lex描述文件
2.通过lex,flex工具解析lex.yy.c文件
3.使用cc编译lex.yy.c生成可执行程序
再来看一个比较完整的lex描述文件exsec.l:
1 | %{ |
进行解析编译:
这里就没有加-ll选项,但是可以编译通过。下面开始着重整理下Lex描述文件.l。
Lex工具是一种词法分析程序生成器,它可以根据词法规则说明书的要求来生成单词识别程序,由该程序识别出输入文本中的各个单词。一般可以分为<定义部分><规则部分><用户子程序部分>。其中规则部分是必须的,定义和用户子程序部分是任选的。
定义部分起始于%{
符号,终止于%}
符号,其间可以是包括include语句、声明语句在内的C语句。这部分跟普通C程序开头没什么区别。
1 | %{ |
规则部分起始于%%
符号,终止于%%
符号,其间则是词法规则。词法规则由模式和动作两部分组成。模式部分可以由任意的正则表达式组成,动作部分是由C语言语句组成,这些语句用来对所匹配的模式进行相应处理。需要注意的是,lex将识别出来的单词存放在yytext[]字符数据中,因此该数组的内容就代表了所识别出来的单词的内容。
类似yytext这些预定义的变量函数会随着后面内容展开一一介绍。动作部分如果有多行执行语句,也可以用{}括起来。
1 | %% |
规则部分是Lex描述文件中最为复杂的一部分,下面列出一些模式部分的正则表达式字符含义:
1 | A-Z, 0-9, a-z 构成模式部分的字符和数字。 |
规则部分具有优先级的概念,先举个简单的例子example2.l
:
1 | %{ |
此时,如果输入内容:
Lex分析词法时,是逐个字符进行读取,自上而下进行规则匹配的,读取到第一个A字符时,遍历后发现三个规则皆匹配成功,Lex会继续分析下去,读至第五个字符时,发现”AAAA”只有一个规则可用,即按行为进行处理,以此类推。可见Lex会选择最长的字符匹配规则。
如果将规则AAAA {printf("THREE\n");};
改为AAAAA {printf("THREE\n");};
则结果为
再来一个特殊的例子:
1 | %% |
并输入title
,Lex解析完后发现,仍然存在两个规则,这时Lex只会选择第一个规则,下面的则被忽略的。这里就体现了Lex的顺序优先级。
把这个例子稍微改一下:
1 | %% |
Lex编译时会提示:warning, rule cannot be matched.这时处理title字符时,匹配到第一个规则后,第二个规则就无效了。再把刚才第一个例子修改下,加深下印象!
example3.l:
1 | %{ |
./parser < file1.txt 显示效果是一样的,最后一项规则肯定是会忽略掉的。
且看下面示例example4.l:
1 | %{ |
在%}和%%之间,加入了一些类似变量的东西,注意是没有;的,这表示int,float分别代指特定的含义,在两个%%之间,可以通过{int}{float}进行直接引用,简化模式定义。
最后一个%%后面的内容是用户子程序部分,可以包含用C语言编写的子程序,而这些子程序可以用在前面的动作中,这样就可以达到简化编程的目的。这里需要注意的是,当编译时不带-ll选项时,是必须加入main函数和yywrap(yywrap将下后面说明)。如:
1 | ... |
内部预定义变量:
1 | yytext char * //当前匹配的字符串 |
内部预定义宏:
1 |
|
内部预定义的函数:
1 | int yylex(void) //调用Lex进行词法分析 |
lex和flex都是解析Lex文件的工具,用法相近,flex意为fast lexical analyzer generator,可以看成lex的升级版本。相关更多内容就需要参考flex的man手册了,十分详尽。
Lex其实就是词法分析器,通过配置文件*.l,依据正则表达式逐字符去顺序解析文件,并动态更新内存的数据解析状态。不过Lex只有状态和状态转换能力。因为它没有堆栈,它不适合用于剖析外壳结构。而yacc增加了一个堆栈,并且能够轻易处理像括号这样的结构。Lex善长于模式匹配,如果有更多的运算要求就需要yacc了。
]]>电脑总是需要更新换代,每次换新软件都得重新安装,所以打算在此记下一些相关软件
名称 | 链接 | 用处 |
---|---|---|
translucentb | https://github.com/TranslucentTB/TranslucentTB/releases/tag/2020.2 | 透明化任务栏 |
wallpaper engine | steam下载 | 壁纸 |
rolan | https://getrolan.com/ | 快捷方式整理 |
名称 | 链接 |
---|---|
steam | https://store.steampowered.com/ |
epic | https://www.epicgames.com/store/en-US/ |
雷神加速器 | https://jiasu.nn.com/ |
名称 | 链接 |
---|---|
https://im.qq.com/download/ | |
telegram | https://telegram.org/ |
微信 | https://weixin.qq.com/ |
potplayer | https://potplayer.daum.net/ |
qq音乐 | https://y.qq.com/ |
百度网盘 | https://pan.baidu.com/download |
火绒 | https://www.huorong.cn/ |
腾讯会议 | https://meeting.tencent.com/download-center.html?from=1001 |
xyplorer | 链接:https://pan.baidu.com/s/1Su49_AwaFgOhXK1sou3i-g 提取码:d2y0 解压密码 www.52pojie.cn |
Logitech G Hub | https://www.logitechg.com/en-us/innovation/g-hub.html |
ssr | 百度网盘 |
7-zip | https://www.7-zip.org/ |
windows-terminal | 微软商店 |
名称 | 链接 |
---|---|
010editor | https://www.sweetscape.com/download/010EditorWin64Installer.exe patch包百度网盘 |
antsword | https://doc.u0u.us/zh-hans/getting_started/get_antsword.html |
工具太多太杂乱,直接拷贝
名称 | 适用语言 | 链接 |
---|---|---|
vscode | * | https://code.visualstudio.com/ |
IDEA | Java | https://www.jetbrains.com/idea/download/#section=windows |
phpstorm | php | https://www.jetbrains.com/phpstorm/download/ |
websotrm | js | https://www.jetbrains.com/webstorm/download/#section=windows |
pycharm | python | https://www.jetbrains.com/pycharm/download/#section=windows |
typora | markdown | https://typora.io/ |
名称 | 链接 |
---|---|
git | https://github.com/waylau/git-for-win |
java | idea下载 |
python | py2 py3 |
php | https://www.php.net/downloads.php |
nodejs | https://nodejs.org/zh-cn/download/ |
名称 | 链接 |
---|---|
xshell | https://www.netsarang.com/zh/free-for-home-school/ |
xftp | https://www.netsarang.com/zh/free-for-home-school/ |
postman | https://www.postman.com/downloads/ |
navicat | 百度网盘 |
VMware16
激活码
ZF3R0-FHED2-M80TY-8QYGC-NPKYF
YF390-0HF8P-M81RQ-2DXQE-M2UT6
ZF71R-DMX85-08DQY-8YMNC-PPHV8
安装镜像:
hexo
1 | npm install hexo |
github备份blog
自己一开始安装kali时就遇到了搜索不到wifi的问题,当时千搜万搜还是解决了,也就没当回事.
后来对kali更新了一下,结果网络固件文件被覆盖,又得重新解决,故打算写个文章记录一下.
首先查看一下自己的网卡信息
1 | lspci |
我这边是
1 | Network controller: Qualcomm Atheros QCA6174 802.11ac Wireless Network Adapter (rev 32) |
然后去网上下载相对应的固件(firmware-4.bin,board.bin)
将两个文件覆盖到/lib/firmware/ath10k/QCA6174/hw3.0/
即可(和自己的硬件信息对应)
已经遇见过好几道原型链污染的题目了,一直没有整体学习整理过.国赛又碰见了,然而没做出来,痛定思痛,准备学习一下.
JavaScript是一门面向对象语言,有意思的一点是,它语法中并没有class的概念,但是它仍然大量使用了对象。为了保证对象之间的联系,JavaScript引入了原型和原型链的概念。
为了保证JavaScript“看起来像Java”,JavaScript中也加入了new操作符:
1 | var obj = new FunctionName() |
但与Java不同的是,JavaScript中的new操作符后面跟的并不是类名而是函数名,JavaScript并非通过类二十直接通过构造函数来创建实例。
1 | function Dog(name, color) { |
上述代码就是JavaScript中通过构造函数创建实例的过程,但这种方式有一个很大的问题。通过构造函数创建实例的时候,每创建一个实例,都需要重新创建这个方法,再把它添加新的实例中。
这样就造成了很大的浪费,所以需要一种方法来解决这个问题。原型(prototype)
的使用就是为了解决这个问题:
每一个构造函数都拥有一个prototype属性,这个属性指向一个对象,也就是原型对象。当使用这个构造函数创建实例的时候,prototype属性指向的原型对象就成为实例的原型对象。
原型对象默认拥有一个constructor属性,指向指向它的那个构造函数(也就是说构造函数和原型对象是互相指向的关系)。
每个对象都拥有一个隐藏的属性prototype
,指向它的原型对象,这个属性可以通过 Object.getPrototypeOf(obj) 或 obj.proto 来访问。
例如上述代码
如上文所述,原型对象就是用来存放实例中共有的那部分属性。
在JavaScript中,所有的对象都是由它的原型对象继承而来,反之,所有的对象都可以作为原型对象存在。
访问对象的属性时,JavaScript会首先在对象自身的属性内查找,若没有找到,则会跳转到该对象的原型对象中查找。
上述代码中各对象关系可表示为下图
通过原型,我们可以将上述代码修改为:
1 | function Dog(name, color) { |
实例化dog1和dog2,我们发现他们bark函数相同,也就解决了之前的问题.
上文提到,JavaScript中所有的对象都是由它的原型对象继承而来。而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链(prototype chain)
。
所有原型链的终点都是Object函数的prototype属性,因为在JavaScript中的对象都默认由Object()构造。Objec.prototype指向的原型对象同样拥有原型,不过它的原型是null,而null则没有原型。
通过原型链就可以在JavaScript中实现继承,JavaScript中的继承相当灵活,有多种继承的实现方法,这里只介绍一种最常用的继承方法也就是组合继承。
1 | function Dog(name, color) { |
这里声明了一个新的构造函数Husky,通过call方法继承Dog中的属性(call方法的作用可以简单理解为将Dog中的属性添加到Husky中,因为还涉及到其他的知识点所以不多赘述),并添加了一个weight属性。然后用Dog函数创建了一个实例作为Husky的原型对象赋值给Husky.prototype以继承方法。这样,通过Husky函数创建的实例就拥有了Dog中的属性和方法。
为了更方便描述,我们修改一下之前的代码
1 | function Dog(name) { |
我们发现如果修改dog1.Proto.color = 1, dog2的color也会改变,这样的操作污染了原型链,称之为原型链污染.
也就是,在一个应用中,如果攻击者控制并修改了一个对象的原型,那么将可以影响所有和这个对象来自同一个类、父祖类的对象。这种攻击方式就是原型链污染。
1 | const express = require('express') |
查看源码可知,我们需要令md5(user.admintoken) === req.query.querytoken
且 user.admintoken存在
,而源码中显示user是个 []类
这个类并没有开发完成,所以我们的目标就是给user.admintoken赋值.
根据原型链的概念,当user.admintoken没有值时,会递归访问proto查询,所以我们可以尝试寻找某个[]类
并修改它的原型.
在/api
接口中,函数会接收传参,并matrix[client.row][client.col] = client.data;
.
而matrix[“proto“]就是matrix对象的原型,这样就造成了原型链污染.
payload:
源码如下
1 | function escape(input) { |
我们分析下源码
1 | try { |
主函数将用户传参进行JSON.parse(input)
替换成原有的图片资源,这边造成了原型链污染.
如果我们传入的source值不符合正则,他将删除config.source
我们尝试传入{"source":"*", "__proto__": {"source":"1"}}
,验证了我们之前的分析
接下来要做的就是xss注入的绕过"
了
查看js文档中replace函数性质:
字符串 stringObject 的 replace() 方法执行的是查找并替换的操作。它将在 stringObject 中查找与 regexp 相匹配的子字符串,然后用 replacement 来替换这些子串。如果 regexp 具有全局标志 g,那么 replace() 方法将替换所有匹配的子串。否则,它只替换第一个匹配子串。
replacement 可以是字符串,也可以是函数。如果它是字符串,那么每个匹配都将由字符串替换。但是 replacement 中的 $ 字符具有特定的含义。如下表所示,它说明从模式匹配得到的字符串将用于替换。
我们先来测试下这个特性
这样我们就可以通过'<img src="_posts/js原型污染.md">'.replace('_posts/js原型污染.md', source)
中_posts/js原型污染.md前的"
来闭合,进而注入
payload如下:
1 | {"source":"*", "__proto__": {"source":"$` onerror='prompt(1)' "}} |
题目源码地址https://github.com/phith0n/code-breaking/blob/master/2018/thejs
题目部署地址http://106.15.177.94:10025/
看了很多wp,不太清楚清楚师傅们是怎么泄露到源码或者fuzz到注入点的,可能是赛程中放出了主要代码吧.
这里就直接分享p神的题解了深入理解 JavaScript Prototype 污染攻击
三分钟看完JavaScript原型与原型链
JavaScript 原型链污染
深入理解 JavaScript Prototype 污染攻击
浅析javascript原型链污染攻击
好久没写博客了orz.
最近负责一个科协平台的项目,由于没有经费,用爱发电,所以一切消息通知都使用邮件发送.
一开始打算使用学校的SMTP来实现,在本地已经跑通,所以就部署到了服务器上了.但是前端反应接收不到,后端log显示发送超时.
一阵排除之后才发现阿里云禁用了所有服务器的25号端口,所以只能使用其他接口,奈何学校只有25端口提供SMTP服务,因此只能改用大公司的邮箱服务.
SpringBoot已经为我们整合好了Email相关包,所以我们只要导入就可了.
1 | <dependency> |
然后我们需要在配置文件中配置一些基本信息,以下是开启SSL加密的SMTP服务
1 | #邮箱配置 |
不需要SSL的
1 | #邮箱配置 |
配置写一个发送邮件类就可了
1 |
|
邮件存储类
1 | public class MailVo { |
Header | 解释 | 示例 |
---|---|---|
Accept | 指定客户端能够接收的内容类型 | Accept: text/plain, text/html,application/json |
Accept-Charset | 浏览器可以接受的字符编码集。 | Accept-Charset: iso-8859-5 |
Accept-Encoding | 指定浏览器可以支持的web服务器返回内容压缩编码类型。 | Accept-Encoding: compress, gzip |
Accept-Language | 浏览器可接受的语言 | Accept-Language: en,zh |
Accept-Ranges | 可以请求网页实体的一个或者多个子范围字段 | Accept-Ranges: bytes |
Authorization | HTTP授权的授权证书 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== |
Cache-Control | 指定请求和响应遵循的缓存机制 | Cache-Control: no-cache |
Connection | 表示是否需要持久连接。(HTTP 1.1默认进行持久连接) | Connection: close |
Cookie | HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器。 | Cookie: $Version=1; Skin=new; |
Content-Length | 请求的内容长度 | Content-Length: 348 |
Content-Type | 请求的与实体对应的MIME信息 | Content-Type: application/x-www-form-urlencoded |
Date | 请求发送的日期和时间 | Date: Tue, 15 Nov 2010 08:12:31 GMT |
Expect | 请求的特定的服务器行为 | Expect: 100-continue |
From | 发出请求的用户的Email | From: user@email.com |
Host | 指定请求的服务器的域名和端口号 | Host: www.zcmhi.com |
If-Match | 只有请求内容与实体相匹配才有效 | If-Match: “737060cd8c284d8af7ad3082f209582d” |
If-Modified-Since | 如果请求的部分在指定时间之后被修改则请求成功,未被修改则返回304代码 | If-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT |
If-None-Match | 如果内容未改变返回304代码,参数为服务器先前发送的Etag,与服务器回应的Etag比较判断是否改变 | If-None-Match: “737060cd8c284d8af7ad3082f209582d” |
If-Range | 如果实体未改变,服务器发送客户端丢失的部分,否则发送整个实体。参数也为Etag | If-Range: “737060cd8c284d8af7ad3082f209582d” |
If-Unmodified-Since | 只在实体在指定时间之后未被修改才请求成功 | If-Unmodified-Since: Sat, 29 Oct 2010 19:43:31 GMT |
Max-Forwards | 限制信息通过代理和网关传送的时间 | Max-Forwards: 10 |
Pragma | 用来包含实现特定的指令 | Pragma: no-cache |
Proxy-Authorization | 连接到代理的授权证书 | Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== |
Range | 只请求实体的一部分,指定范围 | Range: bytes=500-999 |
Referer | 先前网页的地址,当前请求网页紧随其后,即来路 | Referer: http://www.zcmhi.com/archives… |
TE | 客户端愿意接受的传输编码,并通知服务器接受接受尾加头信息 | TE: trailers,deflate;q=0.5 |
Upgrade | 向服务器指定某种传输协议以便服务器进行转换(如果支持) | Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11 |
User-Agent | User-Agent的内容包含发出请求的用户信息 | User-Agent: Mozilla/5.0 (Linux; X11) |
Via | 通知中间网关或代理服务器地址,通信协议 | Via: 1.0 fred, 1.1 nowhere.com (Apache/1.1) |
Warning | 关于消息实体的警告信息 | Warn: 199 Miscellaneous warning |
这个标题起得有点唬人,一个成熟的标题在正常使用时是大概率不会出现sql注入的,只有程序员的不当操作才会导致.
本文的写因,也是看到了一篇公众号的文章不敢相信,相同 SQL 下 Mybatis 查询结果和数据库竟然不一样!才心血来潮写出.
如果读者对Mybatis不是很了解,可以移步还没写的Mybits框架分析
项目文件已上传至GitHub,下载
测试项目部署位置 http://139.9.212.218:39002/
输入admin'
发现报错回显,这里是因为我没有配置error界面的原因,但无伤大雅。报错回显表示 SQL: select * from sqlITest where name = 'admin'' and password = ''
,从这我们也就可以分析出,是用户的输入直接嵌入到sql语句中导致的,而出现这样的原因正是因为占位符使用的是${}
,而并不是#{}
.
查看测试项目dao层代码,我们发现
1 |
|
他的返回值必须是User类
,这就以为我们不能拿取数据库的其他信息了吗?
在后端与数据库交互时,返回的User类数据是根据数据库字段和类成员变量对应赋值的,所以我们可以利用sql语句的别名功能,来更改我们所要信息的字段名称.
这里直接贴上所有脚本
1 | name=admin' and 1=2 union select 1, 2,database()# |
通俗来讲,两者就是${}
防不了sql注入,#{}
能防sql注入.
${}叫做表达式语言,亦称EL表达式,是Java中的一种特护的通用编程语言,主要作用于JSP、XML,用以访问页面的上下文以及不同作用域中的对象 ,取得对象属性的值,或执行简单的运算或判断操作.
在mybatis,使用${}嵌入值,其会将数据直接拼接在sql语句中进行预处理,从而引发sql注入.
我们以如下的Mybatis举例
1 | public interface IUserDao { |
这是一个典型的select查询id的功能,利用#{}
占位符来插入id值.
但是当我们打印sql语句时,得到却是:
1 | select * from user where id = ? |
利用如下函数输出sql语句
1 | in = Resources.getResourceAsStream("SqlMapConfig.xml"); |
我们多尝试几次后发现,不管输入什么参数,打印出的SQL语句永远是将参数转为?
.这是因为MyBatis启动了预编译
功能,在SQL执行前,将SQL语句发给数据库进行编译;执行时,直接使用编译好的SQL,替换占位符?
就可以了.因为SQL注入只对编译过程起作用,所以这样就避免了SQL注入的问题.
注:利用
#{}
占位符插入值时,Mybatis会自动将数据前后加上引号,这也就导致有时无法使用#{}
.
当无法使用#{}
作为占位符时,我们必须将数据格式限制在我们可控范围内,或者将参数进行过滤.
NoSQL(NOSQL=not only SQL),是一项全新的数据库理念,泛指非关系型数据库.
NoSQL数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战.
key value形
,文档形式
,图片形式
等等,所以可以存储基础类型以及对象或者是集合等各种形式,而数据库则只支持基础类型.关系型数据库与NoSQL数据库并非对立而是互补的关系,即通常情况下使用关系型数据库,在适合使用NoSQL的时候使用NoSQL数据库,让NoSQL数据库对关系型数据库的不足进行弥补.一般会将数据存储在关系型数据库中,在nosql数据库中备份存储关系型数据库的数据.
Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库,官方提供测试数据,50个并发执行100000个请求,读的速度是110000次/s,写的速度是81000次/s ,且Redis通过提供多种键值数据类型来适应不同场景下的存储需求,目前为止Redis支持的键值数据类型如下:
1) 字符串类型 string2) 哈希类型 hash3) 列表类型 list4) 集合类型 set5) 有序集合类型 sortedset
1. 官网:[redis.io](https://redis.io)2. 中文网:[redis.net.cn](http://www.redis.net.cn/)3. 解压安装
现版本(Redis5.0)只支持Linux系统
安装步骤:\$ wget http://download.redis.io/releases/redis-5.0.5.tar.gz
\$ tar xzf redis-5.0.5.tar.gz
\$ cd redis-5.0.5
\$ make
启动Redis
\$ src/redis-server
启动Redis客户端
\$ src/redis-cli
redis存储的是:key,value格式的数据,其中key都是字符串,value有5种不同的数据结构, value的数据结构:
1) 字符串类型 string
2) 哈希类型 hash : map格式
3) 列表类型 list : linkedlist格式。支持重复元素
4) 集合类型 set : 不允许重复元素
5) 有序集合类型 sortedset:不允许重复元素,且元素有顺序
存储
1 | set key value |
获取
1 | get key |
删除
1 | del key |
存储
1 | hset key field value |
获取
1 | hget key field |
删除
1 | hdel key field |
可以添加一个元素到列表的头部(左边)或者尾部(右边)
添加
1 | lpush key value |
获取
1 | lrange key start end |
删除
1 | lpop key |
set不允许重复元素
存储
1 | sadd key value |
获取
1 | smembers key |
删除
1 | srem key value |
不允许重复元素,且元素有顺序.每个元素都会关联一个double类型的分数.redis正是通过分数来为集合中的成员进行从小到大的排序.
存储
1 | zadd key score value |
获取
1 | zrange start end [withscores] |
删除
1 | zrem key value |
1 | keys * : 查询所有的键 |
redis是一个内存数据库,当redis服务器重启或者电脑重启,数据会丢失,我们可以将redis内存中的数据持久化保存到硬盘的文件中.
1 | # after 900 sec (15 min) if at least 1 key changed |
2.重新启动redis服务器,并指定配置文件名称 src/redis-server redis.conf
AOF:日志记录的方式,可以记录每一条命令的操作。可以每一次命令操作后,持久化数据
1.编辑redis.windwos.conf文件
appendonly no(关闭aof) –> appendonly yes (开启aof)
appendfsync always 每一次操作都进行持久化
appendfsync everysec 每隔一秒进行一次持久化
appendfsync no 不进行持久化
2.重新启动redis服务器,并指定配置文件名称
src/redis-server redis.conf
下载jar包 jedis.jar(目前版本3.2.0)
调用方法
1 | import redis.clients.jedis.Jedis; |
java中redis的方法和客户端操作基本,就不过多举例,如有所需,自行查找.
使用maven进行包管理
添加以下包:
1 | <dependency> |
代码:
1 | public class Demo3 { |
应用方法:
将数据库省份读取出来,将其json化后添加至redis缓存,下次访问时直接从redis中读取.