白菜的开发日记4

为了节省费用,我尝试将白菜移植到一台必须有Linux环境的机子上运行(为了和节点共存)。
一开始打算使用WSL,无奈这东西在桌面Win10已经普及了,但是Server系统下只有一个Insider Preview。

Preview到什么程度呢,那个系统没有Explorer,甚至没办法移植(comctl32.dll还是哪个系统文件是定制的,没法覆盖也没法兼容,毕竟VPS可没有PE和安全模式)。

在一番挣扎之后我们彻底放弃了它,改为Ubuntu,于是我的灾难来了……

在此之前我也曾经在CentOS等系统下配过Java+MySQL的环境,但是酷Q是用易语言写的,于是就涉及到一个Docker的使用问题。

在坑了一天之后(其实是我耐不下心去啃文档,只能自己瞎几把试),大概了解了这点:
docker pull是下载一个镜像,类似于创建一个类
docker run是创造一个容器,类似于实例化对象
而docker start和stop是操作这个容器,docker rm是删除容器(对象),docker rmi是删除镜像(类)。

大概遇到的坑有:
在docker内运行的酷Q,虽然能提供5700的HTTP Server,但是同一台机子上运行的Tomcat开放的8080端口它却无法作为客户端使用,
指定localhost:8080和127.0.0.1:8080都无效,必须把酷Q的消息上报地址改为服务器的公网IP(我实际上是改为了域名)

我的代码里需要处理ppy给的类似于 1970-01-01 00:00:00的日期格式,
在Win下面,Gson默认转换没有问题,但是Ubuntu下就不行(似乎和时区无关,我本地的WSL也是这样),必须硬编码一个日期格式才能起作用。

这次重构,把CQService里具体的处理拆出来一个CmdUtil,实体类、util全部分包处理,
为了避免Win/Ubuntu的路径不一致将所有素材以二进制形式存到了数据库……四个语音base64编码之后直接硬编码到程序里(

同时还发现,我之前给ImgUtil加了一个static的Map,然后写了个静态方法去用NIO爬目录,把图片塞到那个Image里,最后在构造器里调用这个方法……
但是我ImgUtil是用prototype模式注入,每次有HTTP连接都会实例化一个新的ImgUtil,重新爬一次,压根没起到缓存的作用_(:з」∠)_

还把oppai换成了Java实现,那作者自称不喜欢Java,也别问他要更多Java程序或者支持,虽然oppai的功能全部搬了过去。
用了一下,我发现他用了一大堆静态内部类,而且要使用它 必须把它和调用它的类放在同一个包里(有个关键东西的构造函数是包访问权限的)。
本来想自己改造,但是想到万一PP算法一改,作者一更新那我不就歇逼了?于是老老实实按他规定的方法来_(:з」∠)_

一开始没掌握正确的使用方法,于是昨晚上漏掉了HR和DT(我把MOD应用之前的star穿进去了,柑橘妖怪作者不提供详细的example)。

再一次体会到了“你以为你写的代码是这样跑的,其实根本不是”的心情。

接下来就是写网站部分,然后还有一个准备咕很久的东西……还有白菜的邀请入群机制也要改改,自动清理文件的机制也要改,不过这两天我有点心力憔悴,先咕着吧……

白菜的开发日记3

许久没更博客了,写点白菜最近的进度吧。

整个Web版重构完成,我还在路由器上搭了一个mysql server,然后用mysqldump每天凌晨备份白菜的数据库。

顺手加上了凌晨把获取失败的ID以邮件形式发到自己邮箱,还加了清空当日生成的临时文件。

弱智啪啪啪提了一个req,!sleep 按小时为单位禁言自己,没想到做出来真的一堆弱智用……

日记2里提到的绘图类,被我从静态代码块改成了静态方法,然后在!sudo bg命令时调用它(否则会导致修改用户bg不能马上生效)。

顺手做了一个!fp功能,不过mp4和5没什么人用……

我还研究了一下HTTP Client,做了模拟登陆osu并且下图,还写了解析.osu的正则表达式,来从官网获取BG。

甚至还处理了0,0,”sb\bg.png”这种情况……

在这里吹自己一波,我居然想到了把官网下图时的InputStream包装为ZIPInputStream,直接在内存中解压osz……

写完才发现“卧槽我居然真的把这个想法实现了”。。可以说白菜现在的粗壮性又上了一个台阶(

Java中的值传递和引用传递

今天突然提到Java传递变量的方式,研究了一下值传递和引用传递。

我的结论是:Java是以值传递的方式传递对象的引用/基本数据类型本身。

可能听起来有点拗口,首先明确一下值传递和引用传递。

值传递,是在方法内无法通过赋值等手段改变这个变量本身,因为传递过程中这个变量被复制了一份传入方法内,方法内对这个形参的操作并不会影响方法外。

而引用传递,则是将对象本身的内存地址传入方法内,方法内操作这个变量会对这个内存地址上这个变量的本体进行操作。

首先Java的方法里,用赋值等号=是改变不了调用者里形参的值的,因此传递的方式是值传递无误。

当然,如果调用这个对象本身的方法去改变自身(例如StringBuilder的append方法,或者是Javabean的Setter方法)是可以做到的,因为这个引用依然引用着这个对象,只是这个对象本身变了而已。

所以给数组的某个索引赋值之后,是可以改变数组本身,因为数组本身也是对象,继承自Object……只不过是一个特殊的对象,可以参考
http://www.blogjava.net/flysky19/articles/92763.html?opt=admin
数组的类是运行时生成的,它没有构造方法。证明数组是对象的最简单方法是:用等号赋值时并没有克隆一份数组,而只是重新指定了引用,要克隆数组得用System.arraycopy()方法。

所以手写排序时递归调用本体,并不会内存爆炸,因为数组本身并没有被复制。

(以上仅仅是个人看法,欢迎指正)

白菜的开发日记2

白菜之前的代码基本已经稳定运行了,于是我准备把它改成一个Web项目,采用HTTP API来收发消息,同时将来可能扩展出网页什么的……

在重构的时候遇到一个问题:之前我在绘图类,用nio扫出结算界面需要的所有图片,然后用static代码块包裹:

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
static {
final Path resultPath = Paths.get(rb.getString("path") + "\\data\\image\\resource\\result");
//使用NIO扫描文件夹
final List<File> resultFiles = new ArrayList<>();
Images = new ArrayList<>();
Nums = new ArrayList<>();
Mods = new ArrayList<>();
SimpleFileVisitor<Path> resultFinder = new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
resultFiles.add(file.toFile());
return super.visitFile(file, attrs);
}
};

try {
//将所有文件分为三个List
java.nio.file.Files.walkFileTree(resultPath, resultFinder);
for (int i = 0; i < 23; i++) {
Images.add(ImageIO.read(resultFiles.get(i)));
}
for (int i = 23; i < 37; i++) {
Nums.add(ImageIO.read(resultFiles.get(i)));
}
for (int i = 37; i < 48; i++) {
Mods.add(ImageIO.read(resultFiles.get(i)));
}
zPP = ImageIO.read(resultFiles.get(48));
zPPTrick = ImageIO.read(resultFiles.get(49));
} catch (IOException e) {
logger.error("读取result相关资源失败");
logger.error(e.getMessage());
}

这样这些对象只会在这个类第一次被加载的时候生成,避免了每次绘图重复扫描文件夹,降低效率的问题。

然后后面画图的时候,我只需要调用List中的BufferedImage对象

1
2
3
//右下角两个FPS
g2.drawImage(Images.get(1), 1300, 699, null);
g2.drawImage(Images.get(2), 1300, 723, null);

但是如果我要新加功能就会显得不便:我只能添加z开头的文件名,否则我得把所有的Images.get()后面的数字调一下。。

看了一眼它们的文件名,我觉得ppy应该是在osu启动的时候把皮肤全部加载到内存,然后根据文件名绘制……

看了一下nio,好像能生成文件名,那我把List换成Map好了……

白菜的开发日记1

emm。从16号早上出门前Initial Commit,到23号中午12点白菜正式上线,这一周的时间过的真是快啊……

https://github.com/Arsenolite/osubot/commits/master

43次commit之后,终于有时间坐下来写点开发日记了。严格的说这是第一个我写出来能用的程序啊(x

先介绍一下她吧,她是一个面向osu群的QQ机器人,具体用法可以参见github的readme(普通用户就两条指令你还写个readme真是凑不要脸x)

相对别的机器人的亮点就是她返回的是图片,样式主要由啊哇设计,我们从旁修改,当然最后是我实现的。(记得18号那天我画一行丢一个图在群里x

作为Java程序员,老本行就是折腾数据库,当然白菜也有这方面的功能。每天凌晨4点将所有登记了的玩家的数据爬取,然后存入,提供一个对比功能。

18号晚上写完用户名片绘制,19号上午写完凌晨的定时任务,20号做了一些sudo命令。

22号凌晨1点终于搞定BP绘制,然后白天研究maven打包,开通vps,关闭mysql占用空闲内存缓存等等。

22号晚上怕他突然提出来一个大BUG,之前数据库中我存入的是用户名,但是这样会导致无法识别改名玩家的问题。

于是晚上赶工重构代码+改表结构,由于发现操作系统的时区就是北京时间,而BP返回时间是根据API key对应玩家的国籍来的(也是北京时间),砍掉了所有的时区转换。

在vps上运行的时候查API失败率非常低,于是将api工具类中加上重试机制,砍掉了其他地方代码对网络错误的try-catch。

然后就是些小bug(之前改的时候没改彻底导致的各种问题),昨天中午正式上线,下午晚上研究了jsoup爬取网页指定元素,又把爬score rank改成了二分法。

顺手写了个自动欢迎新人,由于啊哇还没想好scorerank的呈现方式,把绘制scorerank的代码注释掉发布到了服务器上。

……挺后悔没有边开发边写博文的,现在有的遇到的bug都已经忘记了……

总之这个项目增强了我对SQL、前端(jsoup提供的是js的getElementById方法,和css的div.Class风格的选择器)的熟练度,顺便还达成了第一次使用WinServer系统的成就,勉强算是linux+WinServer都能干的运维?

复习了以前练手搞的多线程机制,IO流,各种包装类,日期处理,字符串处理之类的基础问题,初次尝试手写二分法(特别新鲜),可以说它弥补了我Java基础代码写得少的缺陷……

毕竟之前在单位搞的SSM框架更多是做填空题,把XML配好,crud和参数验证写好,甚至那个小项目都用不着组装复杂pojo……到处都是提供好的最佳实践(

……虽然这个项目用的也都是给好的类和方法,充其量起到了熟悉JDK本身的作用,毕竟不是和c一样要手写String的查找替换啥的(x

emm,白菜大概是不会弃坑的,以后尽量写出详细的遇到/解决bug的过程吧233

修改hexo-material主题的代码高亮风格

其实比较简单,做几个填空题而已。

根据主题官网:https://material.viosey.com/expert/ 的说明,
从 1.3.0 版本开始,您可以使用 hexo-prism-plugin 进行代码染色,具体文档请参阅 Hexo-Prism-Plugin 插件文档

转到插件文档:https://github.com/ele828/hexo-prism-plugin

运行npm安装,根据文档,在博客的_config.yml中加上:

1
2
3
4
prism_plugin:
mode: 'preprocess' # realtime/preprocess
theme: 'default'
line_number: false # default false

并且关闭自带的highlight即可。

另外所有主题的预览在这里:
https://github.com/PrismJS/prism-themes#available-themes

记一次代码重构

最近在编写Service层的时候,出现了这样的问题:

由于对数据合法性的要求比较高,光靠外键显然不能胜任,因此我全面抛弃外键,在Service层用大量代码校验数据的合法性。

于是就出现了这样的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 public Map editXXX(Entity entity){

Map result = new Map<>;
boolean flag = true;
if(entity.getparam1() is invaild){
flag = false;
logger.log;
result.put("msg","param1 is invaild");
}
...(param2)

result.put("result",flag);
if(!flag){
return result;
}

...mapper.Update(entity);
logger.log;
result.put("msg","success");

return result;
}

这里具体检测是否有效的代码比较复杂,例如update方法的id肯定不能是null,积分明细的uid必须在user表里查得到。

这样写着写着,写了五个Service之后开始觉得迷茫,懵逼,姜硬,可读性、可维护性差,耦合度高。

于是萌生出重构的想法,某人一语惊醒我把这些if独立成一个方法

直接独立肯定不行,我Service是面向接口写的,难道在接口中先定义好这样的check方法?

本来想将它抽出来做切面,但是毕竟这是业务逻辑的核心代码,不是日志和性能这些边缘功能,AOP印象里是改变不了方法某个局部变量的值的……

看了一眼之前整合shiro的util包,决定在util包下建一个subpackage叫checker,然后定义baseChecker类,里面定义几个常用方法。

其他具体的实体检查器就继承它,然后将这些checker对象注入Service类进行调用即可。

在定义baseChecker类的时候,由于对Java基础语法不够熟悉,我写出了这样的代码:

1
2
3
4
5
6
public class baseChecker {

public boolean checkNull(Object object,boolean flag){
return flag;
};
}

然后我发现这里需要使用的是泛型,而不是Object,而且泛型声明应该放在类里,正确的写法应该是:

1
2
3
4
5
6
public class baseChecker<T> {

public boolean checkNull(T t,boolean flag){
return flag;
};
}

然后子类这么写:

1
2
3
4
5
6
public class userChecker extends baseChecker {
@Override
public boolean checkNull(Object o , boolean flag) {
return flag;
}
}

路由器上ngrok设置的一点笔记

鸟枪换炮,用k3把残疾的k2给换了。

k3除了千兆LAN口外,最大的好处就是补上了U口,而且还是3.0的。既然能挂载硬盘,那这不就成了一个小型的NAS吗?

目前我是在出租屋里蹭着房东的网,本来想通过阿里云DDNS,完成在外网访问路由器(aria2随时上车)的目的。

作为二级路由,要实现DDNS,必须要一级路由开启端口转发,但是房东的路由器管理权并不在我手中。

一开始想爆破,找不到太好的爆破机器,自己又懒得写,试了几个弱口令失败后放弃了。看样子房东家里应该有略懂一点的人在吧。

于是只好另辟蹊径,采用ngrok客户端完成内网映射。

我使用的是https://ngrok.cc/ 这家免费的ngrok服务提供者,注册账户之后会提供一个Token,直接开通免费的HTTP隧道。

先随便填写一个前置域名,用户名和密码也随便填写,本地端口写了127.1:80,用于访问路由器的Web管理页面。

保存,点击进入修改,将域名改为自定义域名,我直接在我买的域名上开了个二级域名,并且按照网站的提示,把CNAME解析到了server.ngrok.cc。

连上k3,在客户端里把令牌填好,下面的通道和网站上设置保持一致,保存。


这里遇到另一个问题,aria2是分为本体+web控制台的,而web控制台连接的是本体提供的RPC接口,端口是6800。

而目前的隧道只有80端口转发,因此会出现连不上的情况。

这里有一个坑,表面看起来aria2用的是http://xxx:6800/jsonrpc的地址,但是网站和路由器中一定要开TCP隧道,本地端口写6800,服务器端口随便选一个没被占用的。

接下来修改aria2Web控制台配置,把RPC地址改成隧道的地址即可。同时为了安全,将aria2的rpc连接修改为需要Token,保持Web控制台和路由器中的设置一致即可。

关于hexo标题问题

今天出于让hexo更优雅的目的,修改了一下配置,给默认模板加上了categories属性。

之前的新建文章,是用hexo new 2017-7-27这样的命令。由于默认情况下文件名=标题,因此会出现用日期命名的.md文件,再进行编辑。

本想直接在hexo的_config.yml中,把文件名改为用时间命名,这样就能用hexo new 标题的命令,简化流程。

然而在本地调试的时候出现了错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ValidationError: `slug` is required!
at ValidationError.WarehouseError (e:\hexo\blog\node_modules\warehouse\lib\error.js:17:11)
at new ValidationError (e:\hexo\blog\node_modules\warehouse\lib\error\validation.js:14:18)
at SchemaTypeString.SchemaType.validate (e:\hexo\blog\node_modules\warehouse\lib\schematype.js:107:11)
at SchemaTypeString.validate (e:\hexo\blog\node_modules\warehouse\lib\types\string.js:45:45)
at Array.<anonymous> (e:\hexo\blog\node_modules\warehouse\lib\schema.js:161:23)
at Schema._applySetters (e:\hexo\blog\node_modules\warehouse\lib\schema.js:305:13)
at Model._insertOne (e:\hexo\blog\node_modules\warehouse\lib\model.js:190:10)
at e:\hexo\blog\node_modules\warehouse\lib\model.js:214:17
at tryCatcher (e:\hexo\blog\node_modules\bluebird\js\release\util.js:16:23)
at e:\hexo\blog\node_modules\bluebird\js\release\using.js:185:26
at tryCatcher (e:\hexo\blog\node_modules\bluebird\js\release\util.js:16:23)
at Promise._settlePromiseFromHandler (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:512:31)
at Promise._settlePromise (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:569:18)
at Promise._settlePromise0 (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:614:10)
at Promise._settlePromises (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:693:18)
at Promise._fulfill (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:638:18)
at PromiseArray._resolve (e:\hexo\blog\node_modules\bluebird\js\release\promise_array.js:126:19)
at PromiseArray._promiseFulfilled (e:\hexo\blog\node_modules\bluebird\js\release\promise_array.js:144:14)
at Promise._settlePromise (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:574:26)
at Promise._settlePromise0 (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:614:10)
at Promise._settlePromises (e:\hexo\blog\node_modules\bluebird\js\release\promise.js:693:18)
at Async._drainQueue (e:\hexo\blog\node_modules\bluebird\js\release\async.js:133:16)

这就很尴尬了……最后把_config.yml中new_post_name的值改为:year-:i_month-:day-:title.md # File name of new posts,问题解决。

应该是hexo不允许这样做(不会自动处理重复命名。)

关于密码加盐存储的笔记

  • 黑客入侵后,期望达到的目的:拿到原文去撞其他库。

  • 密码学的理论安全:就算你知道了整个具体算法,整个加解密的协议,以及密文保存的方法,所有源代码和数据库,只要不知道密钥是什么,就无法从密文破解出明文。(散列算法已经能做到知道密文拿不到原文。)

  • 密码学的应用安全:让破解成本超过获得的利益。

一开始的情况:MD5(原文)=密文

黑客拿到源码、密文之后:

使用记载大量原文+密文的彩虹表,加上可碰撞的特性(MD5(n原文)=1密文),获取大量弱口令用户的密码原文。

加盐:MD5(加盐(原文,盐))=密文

黑客拿到源码、数据库之后:
  • 没加盐的彩虹表失效了。足够长的盐,可以让加盐(原文,盐)的长度加长,不会出现在简单彩虹表里。

  • 每个用户的密文都不一样,无法通过出现频率高的密文猜出弱密码。

注意:如果盐值固定,那只需要对彩虹表进行修改,所有原文重新计算一次。相当于没有加盐

于是情况成了这样:

就算黑客拿到了密文、盐值、甚至加盐、加密算法,针对每个用户,都需要做一个加了这个用户专属的盐的彩虹表,去撞出原文,破解难度几何级增长。

疑惑:

在用户表中新增盐字段用来存盐,和直接用邮箱(邮箱是用户的唯一识别标识)做盐,在提高破解难度的角度上是否有区别?