ps -ef|grep java|grep address
,查看对应的debug端口号。java -server -Xms512m -Xmx512m -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5555 -Djava.ext.dirs=. ${main_class} |
其中 address
为调试端口号。
Remote
配置:选择对于源码项目,填写需要debug的 IP
和 address
,如图:右键断点可以看到Suspend
和Condition
两个关键字:
Suspend
-不阻塞其他线程Suspend是断点暂停的生效范围。
Suspend:未勾选,程序运行到断点处并不会阻塞,而会继续执行后面的逻辑。
Suspend:勾选(是默认值),代表程序运行到断点处会阻塞。
Stop The World
的可怕情况。Condition
-不阻塞其他请求通过设置上面的 Suspend
参数为 Thread
可以不停止整个程序(Stop The World
),但是被调试代码存在被其他人调用的情况,这样就会干扰我们的调试,有没有更加细粒度的调试?
有,答案就是 condition
,如上图,通过 设置 content="1"
这样只有满足条件的请求会被断点。
ALT + F8
就可以打开 Evaluate Expression
,如图:EVAL表达式
,如图:参考文档:
]]>常用命令如下:mvn deploy:deploy-file -DgroupId=<group-id> \
-DartifactId=<artifact-id> \
-Dversion=<version> \
-Dpackaging=<一般是jar> \
-Dfile=<相对路径和绝对路径都可> \
-Durl=<公司仓库地址> \
-DrepositoryId=<一般是snapshots或者releases,根据.m2/settings.xml文件servers配置来> \
-DpomFile=<pom.xml> \
-Dsources=<源码file地址,可不填>
上面这个命令会生成jar并且上传到Nexus 私服中。
这个参数指定pom文件为我们自己的pom文件,方便当其他人引入我们的jar的时候把我们的jar包的依赖都一起引入。但是如果我们不指定的话,默认会生成一个空pom.xml,没有依赖关系,这个时候如果别人引用了我们的 jar 包,就会抛出 NoClassDefFoundError 错误,因为编译时没有问题,但运行时却找不到 class 文件。
下面提供了maven deploy plugin的xml 配置,方面在snapshots阶段快速部署。
<plugin> |
因为maven部署插件的sources
配置是文件,因此我们需要在部署jar之前将 source.jar
打包出来,所以将jar-no-fork绑定在verify流程节点上。
<build> |
参考文档:
]]>经过排查是代码绑定了 127.0.0.1
导致,而当你不绑定 127.0.0.1
时,系统默认绑定 0.0.0.0
。下面介绍下 换回地址及任何地址的相关知识。
IPv4 网络标准将整个 127.0.0.0/8 地址块订为保留地址,供本地环回使用,整个地址块内有超过1600万个IP地址。所以,发送到这些地址(127.0.0.1 到 127.255.255.255)的所有数据包都会返回本机。 地址 127.0.0.1 是 IPv4 环回的常用标准地址; 其余地址并不是所有的操作系统都支持(多数人也不知道有这些地址)。但是,使用127.0.0.1/8内的不同地址,就可以在本机上设置侦听相同端口的多个服务器。 IPv6 标准只分配了一个本地环回地址:[::1]。
要将本地主机名localhost解析到一个或多个 IP 地址,一般是通过在操作系统的 hosts 文件中添加以下代码实现的:
127.0.0.1 localhost |
本地主机名也可以由 DNS 服务器解析,但这一主机名的解析请求,应在本地处理,而非发送到远程服务器。此地址块中的任何地址都将环回到本地主机中。此地址块中的任何地址都绝不会出现在任何网络中。
环回地址:所有发往该类地址的数据包都应该被 loop back。任何发往环回地址的数据包,其处理都在 TCP/IP 协议叠的链路层中实现的。这些数据包不会交由网卡(NIC)或者设备驱动程序处理,既不应在计算机系统以外出现,也不可经路由器转发。
环回地址主要用来做回环测试,通过使用ping 127.0.0.1 测试某台机器上的网络设备,操作系统或者TCP/IP实现是否工作正常。
0.0.0.0 不能ping通。0.0.0.0就是任何地址IP_ADDR_ANY,任何地址的意思就是包括环回地址。
任何地址IP_ADDR_ANY的意思也就是,不管主机有多少个网口,多少个IP,如果监听本机的0.0.0.0上的端口,就等于监听机器上的所有IP端口。数据报的目的地址只要是机器上的一个IP地址,就能被接受。
最近更换了服务器,需要把自己的Hexo Next重新部署到新服务器上,本文记录一下在vps上搭建hexo博客的过程。
在vps上搭建hexo博客需要下面这些工具:
整体流程为:
想要完成Git推送,首先得设置SSH登录。过程如下:
添加hexo用户 |
测试是否设置成功:ssh -v hexo@服务器ip
yum install git |
Git的钩子脚本位于版本库.git/hooks目录下,当Git执行特定操作时会调用特定的钩子脚本。当版本库通过git init或者git clone创建时,会在.git/hooks目录下创建示例脚本,用户可以参照示例脚本的写法开发适合的钩子脚本。
钩子脚本要设置为可运行,并使用特定的名称。Git提供的示例脚本都带有.sample扩展名,是为了防止被意外运行。如果需要启用相应的钩子脚本,需要对其重命名(去掉.sample扩展名)。
post-update
该钩子脚本由远程版本库的git receive-pack命令调用。当从本地版本库完成一个推送之后,即当所有引用都更新完毕后,在远程服务器上该钩子脚本被触发执行。
因此我们需要配置post-update钩子以便可以及时更新我们在VPS上存放Hexo 静态文件的目录。
回到hexo目录 |
exec git update-server-info |
git --work-tree="静态文件VPS存放目录" --git-dir="刚才新建的VPS git地址" checkout -f |
例:
yum install nginx |
使用nginx -v
查看,显示版本号则安装成功。
server { |
找到本地Hexo博客的站点配置文件_config.yml
,找到以下内容并修改:
deploy: |
然后在根目录执行以下命令:
hexo clean |
nginx root 403
问题: 在我配置nginx碰到一个403问题,改了文件权限还是403,后来发现是nginx.conf中 user默认设置错了,把 user nginx
改成user root
就好了。systemctl status nginx.service -l
查看详细错误)参考文档:
]]>地址 : https://leetcode.com/problems/edit-distance/description/
dp[i][j]
用来表示word1
的0~i-1
、word2
的0~j-1
的最小编辑距离dp[i][0] = i
、dp[0][j] = j
,代表从 ""
变为 dp[0~i-1]
或 dp[0][0~j-1]
所需要的次数同时对于两个字符串的子串,都能分为最后一个字符相等或者不等的情况:
word1[i-1] == word2[j-1]
:dp[i][j] = dp[i-1][j-1]
word1[i-1] != word2[j-1]
: dp[i][j] = dp[i][j-1] + 1
dp[i][j] = dp[i-1][j] + 1
dp[i][j] = dp[i-1][j-1] + 1
public int minDistance(String word1, String word2) { |
当我学了廖大的Python教程后,感觉总得做点什么,正好自己想随时查阅,于是就开始有了制作PDF这个想法。
想要把教程变成PDF有三步:
BeautifulSoup
)wkhtmltopdf
)Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式.Beautiful Soup会帮你节省数小时甚至数天的工作时间.
pip3 install BeautifulSoup4 |
将一段文档传入 BeautifulSoup
的构造方法,就能得到一个文档的对象, 可以传入一段字符串或一个文件句柄.
如下所示:from bs4 import BeautifulSoup
soup = BeautifulSoup(open("index.html"))
soup = BeautifulSoup("<html>data</html>")
Beautiful Soup
将复杂 HTML
文档转换成一个复杂的树形结构,每个节点都是 Python
对象,所有对象可以归纳为 4 种: Tag , NavigableString , BeautifulSoup , Comment .
Tag
:通俗点讲就是 HTML
中的一个个标签,类似 div,p
。NavigableString
:获取标签内部的文字,如,soup.p.string
。BeautifulSoup
:表示一个文档的全部内容。Comment:Comment
对象是一个特殊类型的 NavigableString
对象,其输出的内容不包括注释符号.Tag
就是html
中的一个标签,用BeautifulSoup
就能解析出来Tag
的具体内容,具体的格式为soup.name
,其中name
是html
下的标签,具体实例如下:
print soup.title
输出title
标签下的内容,包括此标签,这个将会输出
<title>The Dormouse's story</title> |
print soup.head
输出head
标签下的内容
<head><title>The Dormouse's story</title></head> |
如果 Tag 对象要获取的标签有多个的话,它只会返回所以内容中第一个符合要求的标签。
每个 Tag
有两个重要的属性 name
和 attrs
:
name
:对于Tag
,它的name
就是其本身,如soup.p.name
就是p
attrs
是一个字典类型的,对应的是属性-值,如print soup.p.attrs
,输出的就是{'class': ['title'], 'name': 'dromouse'}
,当然你也可以得到具体的值,如print soup.p.attrs['class']
,输出的就是[title]
是一个列表的类型,因为一个属性可能对应多个值,当然你也可以通过get方法得到属性的,如:print soup.p.get('class')
。还可以直接使用print soup.p['class']
get
方法用于得到标签下的属性值,注意这是一个重要的方法,在许多场合都能用到,比如你要得到<img src="#">
标签下的图像url
,那么就可以用soup.img.get('src')
,具体解析如下:
# 得到第一个p标签下的src属性 |
得到标签下的文本内容,只有在此标签下没有子标签,或者只有一个子标签的情况下才能返回其中的内容,否则返回的是None
具体实例如下:
# 在上面的一段文本中p标签没有子标签,因此能够正确返回文本的内容 |
get_text()
可以获得一个标签中的所有文本内容,包括子孙节点的内容,这是最常用的方法。
BeautifulSoup 主要用来遍历子节点及子节点的属性,通过Tag
取属性的方式只能获得当前文档中的第一个 tag,例如,soup.p
。如果想要得到所有的<p>
标签,或是通过名字得到比一个 tag 更多的内容的时候,就需要用到 find_all()find_all(name, attrs, recursive, text, **kwargs )
find_all是用于搜索节点中所有符合过滤条件的节点。
name参数:是Tag的名字,如p,div,title# 1. 节点名
print(soup.find_all('p'))
# 2. 正则表达式
print(soup.find_all(re.compile('^p')))
# 3. 列表
print(soup.find_all(['p', 'a']))
另外 attrs 参数可以也作为过滤条件来获取内容,而 limit 参数是限制返回的条数。
以 CSS 语法为匹配标准找到 Tag。同样也是使用到一个函数,该函数为select()
,返回类型是 list。它的具体用法如下:
# 1. 通过 tag 标签查找 |
- wkhtmltopdf主要用于HTML生成PDF。
- pdfkit是基于wkhtmltopdf的python封装,支持URL,本地文件,文本内容到PDF的转换,其最终还是调用wkhtmltopdf命令。
先安装wkhtmltopdf,再安装pdfkit。
pip3 install pdfkit |
import pdfkit |
pdfkit.from_url(['google.com', 'baidu.com'], 'out.pdf') |
with open('file.html') as f: |
options = { |
爬取十几篇教程之后触发了这个错误:
看来廖大的反爬虫做的很好,于是只好使用代理ip了,尝试了免费的西刺免费代理后,最后选择了付费的 蘑菇代理 ,感觉响应速度和稳定性还OK。
运行过程截图:
生成的效果图:
代码如下:import re
import time
import pdfkit
import requests
from bs4 import BeautifulSoup
# 使用 阿布云代理
def get_soup(target_url):
proxy_host = "http-dyn.abuyun.com"
proxy_port = "9020"
proxy_user = "HKQL6V46321071VD"
proxy_pass = "1759D9C2F6DE34B3"
proxy_meta = "http://%(user)s:%(pass)s@%(host)s:%(port)s" % {
"host": proxy_host,
"port": proxy_port,
"user": proxy_user,
"pass": proxy_pass,
}
proxies = {
"http": proxy_meta,
"https": proxy_meta,
}
headers = {'User-Agent':
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'}
flag = True
while flag:
try:
resp = requests.get(target_url, proxies=proxies, headers=headers)
flag = False
except Exception as e:
print(e)
time.sleep(0.4)
soup = BeautifulSoup(resp.text, 'html.parser')
return soup
def get_toc(url):
soup = get_soup(url)
toc = soup.select("#x-wiki-index a")
print(toc[0]['href'])
return toc
# ⬇️教程html
def download_html(url, depth):
soup = get_soup(url)
# 处理目录
if int(depth) <= 1:
depth = '1'
elif int(depth) >= 2:
depth = '2'
title = soup.select("#x-content h4")[0]
new_a = soup.new_tag('a', href=url)
new_a.string = title.string
new_title = soup.new_tag('h' + depth)
new_title.append(new_a)
print(new_title)
# 加载图片
images = soup.find_all('img')
for x in images:
x['src'] = 'https://static.liaoxuefeng.com/' + x['data-src']
# 将bilibili iframe 视频更换为链接地址
iframes = soup.find_all('iframe', src=re.compile("bilibili"))
for x in iframes:
x['src'] = "http:" + x['src']
a_tag = soup.new_tag("a", href=x['src'])
a_tag.string = "vide play:" + x['src']
x.replace_with(a_tag)
div_content = soup.find('div', class_='x-wiki-content')
return new_title, div_content
def convert_pdf(template):
html_file = "python-tutorial-pdf.html"
with open(html_file, mode="w") as code:
code.write(str(template))
pdfkit.from_file(html_file, 'python-tutorial-pdf.pdf')
if __name__ == '__main__':
# html 模板
template = BeautifulSoup(
'<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="https://cdn.liaoxuefeng.com/cdn/static/themes/default/css/all.css?v=bc43d83"> <script src="https://cdn.liaoxuefeng.com/cdn/static/themes/default/js/all.js?v=bc43d83"></script> </head> <body> </body> </html>',
'html.parser')
# 教程目录
toc = get_toc('https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000')
for i, x in enumerate(toc):
url = 'https://www.liaoxuefeng.com' + x['href']
# ⬇️教程html
content = download_html(url, x.parent['depth'])
# 往template添加新的教程
new_div = template.new_tag('div', id=i)
template.body.insert(3 + i, new_div)
new_div.insert(3, content[0])
new_div.insert(3, content[1])
time.sleep(0.4)
convert_pdf(template)
参考文档:
]]>puppeteer是谷歌官方出品的一个通过 DevTools 协议控制 headless Chrome 的Node库。可以通过Puppeteer的提供的api直接控制Chrome模拟大部分用户操作来进行UI Test或者作为爬虫访问页面来收集数据。
直接运行安装命令:npm install puppeteer
如果出现无法安装的问题,可以使用淘宝镜像。
在我 puppeteer 使用截全屏的过程中发现有些图片无法截取到,而实际上是因为有些图片是懒加载的,如果你没有滑动到图片的位置,那么这个图片是不会加载。
现在我的方式是采用模拟浏览器滚动条滑动的方式滑动底部来使图片加载出来。
代码如下:
const puppeteer = require('puppeteer'); |
动图如下:
在某些情况下我们只想要针对html的某个位置进行截图而不是针对页面截全屏。
puppeteer提供了ElementHandle.screenshot 方法,该方法参数和page.screenshot 一样。而ElementHandle 对象是页面内的Dom对象。可以帮助我对 html element进行截图。这样的话你想截取页面的哪部分就截取页面的哪部分。
代码如下:const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: false
});
const page = await browser.newPage();
await page.goto('https://www.cnblogs.com/morethink/p/6525216.html');
await page.setViewport({
width: 1200,
height: 800
});
//获取页面Dom对象
let body = await page.$('#cnblogs_post_body');
//调用页面内Dom对象的 screenshot 方法进行截图
await body.screenshot({
path: '2.png'
});
await browser.close();
})();
参考文档:
]]>由于在豆瓣发了个租房帖子,发现很快就被其他的帖子淹没,但是手动顶帖实在太累,😭,所以想通过自动顶帖的方式来解放双手!
通过Chrome network 分析
https://www.douban.com/group/topic/129122199/add_comment
Python 模拟请求:
# 豆瓣具体帖子 |
直接运行即可。
但是多运行几次就会发现,返回的状态码是200,而且没有顶帖成功。实际上是触发了豆瓣的防爬虫。
而且在我们顶帖的时候发送请求的时候还带有 captcha-solution 和 captcha-id 字段。
目前发现,每次评论就算相隔1分钟,只要满3次,就一定会弹出这个验证码进行验证。
遇到验证码我们就来破解验证码。
识别图形验证码需要安装tesserocr这个库,下面介绍下tesserocr。
tesserocr是Python的一个OCR识别库,但其实是对tesseract做了一层Python Api的封装,核心还是tesseract,所以在安装tesserocr之前,需要先安装tesseract。Tesseract
(/‘tesərækt/) 这个词的意思是”超立方体”,指的是几何学里的四维标准方体,又称”正八胞体”,是一款被广泛使用的开源 OCR
工具。
在Mac下,使用 brew 安装
brew install tesseract --all-languages |
接下来再安装tesserocr即可:
brew install imagemagick |
Python 代码如下:
import tesserocr |
验证的图片如下:
结果无法识别。
换一张简单的图片试试:
结果如下:5594
看来 Tesseract 只能识别一些简单的验证码,不适合豆瓣验证码识别。
试试识别验证码平台。
官方接入文档: 文字识别-Python SDK接入文档
注意:
支持2.7.+及3.+
点击右上角,用户中心,用户ID也需要用到:
需要的信息准备好了,pip 安装一波
pip install baidu-aip |
import json |
验证的图片如下:
结果走一波:
{"log_id": 3968431492157876638, "words_result_num": 1, "words_result": [{"words": " minute:"}]} |
从结果可以看出识别出了这个验证码。
words_result_num
是识别结果数words_result
是定位和识别结果数组words
是识别结果字符串再来试试
结果如下:{"log_id": 5251449865676063710, "words_result_num": 0, "words_result": []}
没有识别出来,可以看到对于复杂一些的验证码还是会出现无法识别的情况,但是胜在免费。
对于无法识别的情况就需要打码平台了,业界比较出名的是 超级鹰 。
超级鹰是按量级收费,量大便宜,标准价格:1元=1000题分,不同验证码类型,需要的题分不一样,详情可以到这里查询 http://www.chaojiying.com/price.html
python 代码如下:from hashlib import md5
import requests
import json
# 通过超级鹰识别验证码
def recognition_captcha(filename, code_type):
im = open(filename, 'rb').read()
params = {
'user': '账号',
'pass2': md5('密码'.encode('utf8')).hexdigest(),
'softid': 'softid',
'codetype': code_type
}
headers = {
'Connection': 'Keep-Alive',
'User-Agent': 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0)',
}
files = {'userfile': ('ccc.jpg', im)}
resp = requests.post('http://upload.chaojiying.net/Upload/Processing.php', data=params, files=files,
headers=headers).json()
return resp
# 调用代码
if __name__ == '__main__':
print(json.dumps(recognition_captcha('/Users/liwenhao/Desktop/douban-captcha-example2.jpg', 1006)))
上传的验证码就是上面百度 OCR 未曾识别的验证码,如下:
结果如下:{"err_str": "OK", "err_no": 0, "md5": "0475b05654c376deb409bfef7eee75cd", "pic_id": "8054415552001300054", "pic_str": "yacvmd"}
发现 验证码 yacvmd
已出来。但是时间花了5s左右。后来测试发现对于豆瓣比较建的验证码花费的时间在1s内,因此从时间和准确性上面,最后还是采用了超级鹰打码平台。
无论采用什么方式,都有可能出现失败的情况,我总不能采取 轮询 的方式,隔几个小时就去看看到底前面几次是否🆙成功,因此需要一个 异步通知 ,最开始想用 邮件,后来发现了 Server酱 这个神器,可以帮助我们发送微信通知,而且特别简单。
具体可以查看 Server酱。
采用 python2
import os |
运行结果:
第1次:
2018-12-30 16:09:35.589 INFO: 没有验证码 |
第4次:
2018-12-30 16:13:02.135 INFO: 发现验证码,下载验证码 |
效果图:
注:
这个处理器的原理是接收HttpObject对象,按照HttpRequest,HttpContent来做处理,文件内容是在HttpContent消息带来的。
然后在HttpContent中一个chunk一个chunk读,chunk大小可以在初始化HttpServerCodec时设置。将每个chunk交个httpDecoder复制一份,当读到LastHttpContent对象时,表明上传结束,可以将httpDecoder中缓存的文件通过HttpDataFactory写到磁盘上,然后在删除缓存的HttpContent对象。
4j |
做了改动:
// 新增ChunkedHandler,主要作用是支持异步发送大的码流(例如大文件传输),但是不占用过多的内存,防止发生java内存溢出错误 |
4j |
由于RandomAccessFile
是一种文件资源,所以我习惯性的在最后关闭文件资源,采用的是Java7的 try-with-resources
语法,于是问题就出现了,由于 ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise());
是异步的,在我关闭RandomAccessFile
时,文件还未传输完毕,就会导致下载文件停止。
代码放在: https://github.com/morethink/code/tree/master/java/netty-example
]]>最近遇到一个问题:
执行命令docker exec f4af9b sh -c 'bash /tmp/build.sh'
在docker中执行shell,会出现中文乱码的问题。但是在docker容器中单独执行shell脚本却没有出现乱码。查看环境变量存在LANG=en_US.UTF-8
,因此从原理上来说是不应该出现乱码的。
但是既然出现了乱码,那么LANG=en_US.UTF-8
应该就没有读取到,于是在 build.sh
中运行env
命令,发现通过docker exec f4af9b sh -c 'bash /tmp/build.sh'
方式没有LANG=en_US.UTF-8
环境变量,那么原因是什么?
原因如下:docker exec f4af9b sh -c 'bash /tmp/build.sh'
对于docker 容器来说是非登录和非交互式shell,这样就不会读取某些配置文件,导致LANG=en_US.UTF-8
没有加载成功。
下面介绍一下Linux交互式和非交互式shell、登录和非登录shell之间的区别。
交互式shell(interactive shell)和非交互式shell(non-interactive shell):
bash script.sh
这类的shell。在这种模式下,shell不与你进行交互,而是读取存放在文件中的命令,并且执行它们。当它读到文件的结尾EOF,shell也就终止了。登录式shell(login shell)和非登陆式shell(no-login shell):
对于常用环境变量设置文件,整理出如下加载情况表:
文件 | 非交互+登陆式 | 交互+登陆式 | 交互+非登陆式 | 非交互+非登陆式 |
---|---|---|---|---|
/etc/profile | 加载 | 加载 | - | - |
/etc/bashrc | 加载 | 加载 | - | - |
~/.bash_profile | 加载 | 加载 | - | - |
~/.bashrc | 加载 | 加载 | 加载 | - |
BASH_ENV | - | - | - | 加载 |
执行脚本,如bash script.sh
是属于non-login + non-interactive。
因而,执行命令docker exec f4af9b sh -c 'bash /tmp/build.sh'
对于docker容器来说是属于non-login + non-interactive。
将上面的bash /tmp/build.sh
改为bash --login /tmp/build.sh
变为登录shell,就可以读取/etc/profile和~/.bash_profile等文件。
或者在执行bash /tmp/build.sh
时在build.sh
加入export LANG="en_US.UTF-8"
手动设置。
PATH:决定了shell将到哪些目录中寻找命令或程序 |
HttpServerCodec
handler进行Http协议的解析,但是需要自己提供路由。最开始是通过对Http method及uri 采用多层if else 嵌套判断的方法路由到真正的controller类:String uri = request.uri();
HttpMethod method = request.method();
if (method == HttpMethod.POST) {
if (uri.startsWith("/login")) {
//url参数解析,调用controller的方法
} else if (uri.startsWith("/logout")) {
//同上
}
} else if (method == HttpMethod.GET) {
if (uri.startsWith("/")) {
} else if (uri.startsWith("/status")) {
}
}
在只需提供login
及logout
API时,代码可以完成功能,可是随着API的数量越来越多,需要支持的方法及uri越来越多,else if
越来越多,代码越来越复杂。
在阿里开发手册中也提到过:
因此首先考虑采用状态设计模式及策略设计模式重构。
首先我们知道每个http请求都是由method及uri来唯一标识的,所谓路由就是通过这个唯一标识定位到controller类的中的某个方法。
因此把HttpLabel作为状态
public class HttpLabel {
private String uri;
private HttpMethod method;
}
状态接口:public interface Route {
/**
* 路由
*
* @param request
* @return
*/
GeneralResponse call(FullHttpRequest request);
}
为每个状态添加状态实现:public void route() {
//单例controller类
final DemoController demoController = DemoController.getInstance();
Map<HttpLabel, Route> map = new HashMap<>();
map.put(new HttpLabel("/login", HttpMethod.POST), demoController::login);
map.put(new HttpLabel("/logout", HttpMethod.POST), demoController::login);
}
接到请求,判断状态,调用不同接口:public class ServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
String uri = request.uri();
GeneralResponse generalResponse;
if (uri.contains("?")) {
uri = uri.substring(0, uri.indexOf("?"));
}
Route route = map.get(new HttpLabel(uri, request.method()));
if (route != null) {
ResponseUtil.response(ctx, request, route.call(request));
} else {
generalResponse = new GeneralResponse(HttpResponseStatus.BAD_REQUEST, "请检查你的请求方法及url", null);
ResponseUtil.response(ctx, request, generalResponse);
}
}
}
使用状态设计模式重构代码,在增加url时只需要网map里面put一个值就行了。
后来看了 JAVA反射+运行时注解实现URL路由 发现反射+注解的方式很优雅,代码也不复杂。
下面介绍Netty使用反射实现URL路由。
路由注解: (ElementType.METHOD)
(RetentionPolicy.RUNTIME)
public RequestMapping {
/**
* 路由的uri
*
* @return
*/
String uri();
/**
* 路由的方法
*
* @return
*/
String method();
}
json格式的body
:@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestBody {
}
异常类(用于全局异常处理,实现 @ControllerAdvice
异常处理):
|
扫描classpath下带有@RequestMapping
注解的方法,将这个方法放进一个路由Map:Map<HttpLabel, Action<GeneralResponse>> httpRouterAction
,key为上面提到过的Http唯一标识 HttpLabel
,value为通过反射调用的方法: 4j
public class HttpRouter extends ClassLoader {
private Map<HttpLabel, Action<GeneralResponse>> httpRouterAction = new HashMap<>();
private String classpath = this.getClass().getResource("").getPath();
private Map<String, Object> controllerBeans = new HashMap<>();
protected Class<?> findClass(String name) throws ClassNotFoundException {
String path = classpath + name.replaceAll("\\.", "/");
byte[] bytes;
try (InputStream ins = new FileInputStream(path)) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024 * 5];
int b = 0;
while ((b = ins.read(buffer)) != -1) {
out.write(buffer, 0, b);
}
bytes = out.toByteArray();
}
} catch (Exception e) {
throw new ClassNotFoundException();
}
return defineClass(name, bytes, 0, bytes.length);
}
public void addRouter(String controllerClass) {
try {
Class<?> cls = loadClass(controllerClass);
Method[] methods = cls.getDeclaredMethods();
for (Method invokeMethod : methods) {
Annotation[] annotations = invokeMethod.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType() == RequestMapping.class) {
RequestMapping requestMapping = (RequestMapping) annotation;
String uri = requestMapping.uri();
String httpMethod = requestMapping.method().toUpperCase();
// 保存Bean单例
if (!controllerBeans.containsKey(cls.getName())) {
controllerBeans.put(cls.getName(), cls.newInstance());
}
Action action = new Action(controllerBeans.get(cls.getName()), invokeMethod);
//如果需要FullHttpRequest,就注入FullHttpRequest对象
Class[] params = invokeMethod.getParameterTypes();
if (params.length == 1 && params[0] == FullHttpRequest.class) {
action.setInjectionFullhttprequest(true);
}
// 保存映射关系
httpRouterAction.put(new HttpLabel(uri, new HttpMethod(httpMethod)), action);
}
}
}
} catch (Exception e) {
log.warn("{}", e);
}
}
public Action getRoute(HttpLabel httpLabel) {
return httpRouterAction.get(httpLabel);
}
}
通过反射调用controller
类中的方法:
4j
public class Action {
private Object object;
private Method method;
private List<Class> paramsClassList;
public GeneralResponse call(Object... args) {
try {
return (GeneralResponse) method.invoke(object, args);
} catch (InvocationTargetException e) {
Throwable targetException = e.getTargetException();
//实现 `@ControllerAdvice` 异常处理,直接抛出自定义异常
if (targetException instanceof MyRuntimeException) {
return ((MyRuntimeException) targetException).getGeneralResponse();
}
log.warn("method invoke error: {}", e);
return new GeneralResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, String.format("Internal Error: %s", ExceptionUtils.getRootCause(e)), null);
} catch (IllegalAccessException e) {
log.warn("method invoke error: {}", e);
return new GeneralResponse(HttpResponseStatus.INTERNAL_SERVER_ERROR, String.format("Internal Error: %s", ExceptionUtils.getRootCause(e)), null);
}
}
}
ServerHandler.java
处理如下:public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) {
String uri = request.uri();
GeneralResponse generalResponse;
if (uri.contains(DELIMITER)) {
uri = uri.substring(0, uri.indexOf(DELIMITER));
}
//根据不同的请求API做不同的处理(路由分发)
Action action = httpRouter.getRoute(new HttpLabel(uri, request.method()));
if (action != null) {
String s = request.uri();
if (request.headers().get(HttpHeaderNames.CONTENT_TYPE.toString()).equals(HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED.toString())) {
s = s + "&" + request.content().toString(StandardCharsets.UTF_8);
}
QueryStringDecoder queryStringDecoder = new QueryStringDecoder(s);
Map<String, List<String>> parameters = queryStringDecoder.parameters();
Class[] classes = action.getMethod().getParameterTypes();
Object[] objects = new Object[classes.length];
for (int i = 0; i < classes.length; i++) {
Class c = classes[i];
//处理@RequestBody注解
Annotation[] parameterAnnotation = action.getMethod().getParameterAnnotations()[i];
if (parameterAnnotation.length > 0) {
for (int j = 0; j < parameterAnnotation.length; j++) {
if (parameterAnnotation[j].annotationType() == RequestBody.class &&
request.headers().get(HttpHeaderNames.CONTENT_TYPE.toString()).equals(HttpHeaderValues.APPLICATION_JSON.toString())) {
objects[i] = JsonUtil.fromJson(request, c);
}
}
//处理数组类型
} else if (c.isArray()) {
String paramName = action.getMethod().getParameters()[i].getName();
List<String> paramList = parameters.get(paramName);
if (CollectionUtils.isNotEmpty(paramList)) {
objects[i] = ParamParser.INSTANCE.parseArray(c.getComponentType(), paramList);
}
} else {
//处理基本类型和string
String paramName = action.getMethod().getParameters()[i].getName();
List<String> paramList = parameters.get(paramName);
if (CollectionUtils.isNotEmpty(paramList)) {
objects[i] = ParamParser.INSTANCE.parseValue(c, paramList.get(0));
} else {
objects[i] = ParamParser.INSTANCE.parseValue(c, null);
}
}
}
ResponseUtil.response(ctx, HttpUtil.isKeepAlive(request), action.call(objects));
} else {
//错误处理
generalResponse = new GeneralResponse(HttpResponseStatus.BAD_REQUEST, "请检查你的请求方法及url", null);
ResponseUtil.response(ctx, HttpUtil.isKeepAlive(request), generalResponse);
}
}
DemoController
方法配置:"/login", method = "POST") (uri =
public GeneralResponse login(@RequestBody User user, FullHttpRequest request,
String test, Integer test1, int test2,
long[] test3, Long test4, String[] test5, int[] test6) {
System.out.println(test2);
log.info("/login called,user: {} ,{} ,{} {} {} {} {} {} {} {} ", user, test, test1, test2, test3, test4, test5, test6);
return new GeneralResponse(null);
}
测试结果如下:
netty-route
得到结果如下:
user=User(username=hah, password=dd),test=111,test1=null,test2=0,test3=[1],test4=null,test5=[d,a, 1],test6=[1, 2] |
下面介绍对InfluxDB的使用。
InfluxDB是一款用Go语言编写的开源分布式时序、事件和指标数据库,无需外部依赖。该数据库现在主要用于存储涉及大量的时间戳数据,如DevOps监控数据,APP metrics, loT传感器数据和实时分析数据。
InfluxDB特征:
PS:有了InfluxDB+Grafana后,你就可以写一些简单的程序了,可以只负责写后端逻辑部分,数据都可以存入InfluxDB,然后通过Grafana展示出来。
安装 |
vim /usr/local/etc/influxdb.conf |
开启udp配置,其他为默认值[[udp]]
enabled = true
udp配置含义:[[udp]] – udp配置
enabled:是否启用该模块,默认值:false。
bind-address:绑定地址,默认值:”:8089″。
database:数据库名称,默认值:”udp”。
retention-policy:存储策略,无默认值。
batch-size:默认值:5000。
batch-pending:默认值:10。
read-buffer:udp读取buffer的大小,0表示使用操作系统提供的值,如果超过操作系统的默认配置则会出错。 该配置的默认值:0。
batch-timeout:超时时间,默认值:”1s”。
precision:时间精度,无默认值。
我们知道InfluxDB是支持Http的,为什么我们还要采用UDP方式发送数据呢?
基于下列原因:
我们采用了worker线程调用addMetric
方法将数据存储到缓存 map
中,send线程池来进行每个指定时间发送数据到Influxdb。
代码如下(也可参考Jmeter
的UdpMetricsSender
类): 4j
public class InfluxDBClient implements Runnable {
private String measurement = "example";
private final Object lock = new Object();
private InetAddress hostAddress;
private int udpPort;
private volatile Map<String, List<Response>> metrics = new HashMap<>();
private long time;
private String transaction;
public InfluxDBClient(String influxdbUrl, String transaction) {
this.transaction = transaction;
try {
log.debug("Setting up with url:{}", influxdbUrl);
String[] urlComponents = influxdbUrl.split(":");
if (urlComponents.length == 2) {
hostAddress = InetAddress.getByName(urlComponents[0]);
udpPort = Integer.parseInt(urlComponents[1]);
} else {
throw new IllegalArgumentException("InfluxDBClient url '" + influxdbUrl + "' is wrong. The format shoule be <host/ip>:<port>");
}
} catch (Exception e) {
throw new IllegalArgumentException("InfluxDBClient url '" + influxdbUrl + "' is wrong. The format shoule be <host/ip>:<port>", e);
}
}
public void addMetric(Response response) {
synchronized (lock) {
if (metrics.containsKey(response.getLabel())) {
metrics.get(response.getLabel()).add(response);
} else {
metrics.put(response.getLabel(), new ArrayList<>(Collections.singletonList(response)));
}
}
}
public void run() {
sendMetrics();
}
private void sendMetrics() {
Map<String, List<Response>> tempMetrics;
//复制数据到tempMetrics,清空原来metrics并初始化上次的大小
synchronized (lock) {
if (isEmpty(metrics)) {
return;
}
time = System.currentTimeMillis();
tempMetrics = metrics;
metrics = new HashMap<>();
for (Map.Entry<String, List<Response>> entry : tempMetrics.entrySet()) {
metrics.put(entry.getKey(), new ArrayList<>(entry.getValue().size()));
}
}
final Map<String, List<Response>> copyMetrics = tempMetrics;
final List<MetricTuple> aggregateMetrics = aggregate(copyMetrics);
StringBuilder sb = new StringBuilder(aggregateMetrics.size() * 200);
//发送tempMetrics,生成一行数据,然后换行
for (MetricTuple metric : aggregateMetrics) {
sb.append(metric.getMeasurement()).append(metric.getTag()).append(" ")
.append(metric.getField()).append(" ").append(metric.getTimestamp() + "000000").append("\n");
}
//udp发送数据到Influxdb
try (DatagramSocket ds = new DatagramSocket()) {
byte[] buf = sb.toString().getBytes();
DatagramPacket dp = new DatagramPacket(buf, buf.length, this.hostAddress, this.udpPort);
ds.send(dp);
log.debug("send {} to influxdb", sb.toString());
} catch (SocketException e) {
log.error("Cannot open udp port!", e);
} catch (IOException e) {
log.error("Error in transferring udp package", e);
}
}
/**
* 得到聚合数据
*
* @param metrics
* @return
*/
private List<MetricTuple> aggregate(Map<String, List<Response>> metrics) {
}
public boolean isEmpty(Map<String, List<Response>> map) {
for (Map.Entry<String, List<Response>> entry : map.entrySet()) {
if (!entry.getValue().isEmpty()) {
return false;
}
}
return true;
}
}
参考文档:
]]>java.util.zip.ZipEntry
和 java.util.zip.ZipFile
,通过设置Charset
为StandardCharsets.UTF_8
支持中文。commons-compress
包的TarArchiveInputStream
和GzipCompressorInputStream
。commons-compress
包的TarArchiveInputStream
和BZip2CompressorInputStream
。在这里有个问题如果使用TarInputStream
搭配jdk的 GZIPInputStream
会产生乱码。而使用commons-compress
包的TarArchiveInputStream
和GzipCompressorInputStream
则可解决乱码问题。
代码如下:
public class ZipUtil { |
参考文档:
]]>打开Git 配置文件
vi ~/.gitconfig |
添加如下配置:
[http "https://github.com/"] |
本文通过分析B-Tree及B-/+Tree数据结构及索引性能分析及磁盘存取原理尝试着回答一下问题:
B 树(B-Tree)是为磁盘等辅助存取设备设计的一种平衡查找树,它实现了以 $O(\lg n)$ 时间复杂度执行查找、顺序读取、插入和删除操作。由于 B 树和 B 树的变种在降低磁盘 I/O 操作次数方面表现优异,所以经常用于设计文件系统和数据库。
使用阶来定义 B 树,一棵 m 阶的 B 树,需要满足下列条件:
ceil(m/2)-1
,小于等于m-1
,非根节点关键字数必须>=2注:
B 树中的节点分为内部节点(Internal Node)和叶节点(Leaf Node),内部节点也就是非叶节点(Non-Leaf Node)。
B-Tree的查找过程:根据给定值查找结点和在结点的关键字中进行查找交叉进行。
首先从根结点开始重复如下过程:若比结点的第一个关键字小,则查找在该结点第一个指针指向的结点进行;若等于结点中某个关键字,则查找成功;若在两个关键字之间,则查找在它们之间的指针指向的结点进行;若比该结点所有关键字大,则查找在该结点最后一个指针指向的结点进行;若查找已经到达某个叶结点,则说明给定值对应的数据记录不存在,查找失败。
例如:
在一棵 5 阶B-树中查找元素 29
首先29比根节点值大,所以找根节点的右子数,然后再根据值得判断,发现 29 介于 28 和 48 之间,然后在从中间子树继续查找下去。
插入的过程分两步完成:
利用前述的B-树的查找算法查找关键字的插入位置。若找到,则说明该关键字已经存在,直接返回。否则查找操作必失败于某个最低层的非终端结点上。
判断该结点是否还有空位置。即判断该结点的关键字总数是否满足n<=m-1。若满足,则说明该结点还有空位置,直接把关键字k插入到该结点的合适位置上。若不满足,说明该结点己没有空位置,需要把结点分裂成两个。
分裂的方法是:
生成一新结点。把原结点上的关键字和k按升序排序后,从中间位置把关键字(不包括中间位置的关键字)分成两部分。左部分所含关键字放在旧结点中,右部分所含关键字放在新结点中,中间位置的关键字连同新结点的存储位置插入到父结点中。如果父结点的关键字个数也超过(m-1),则要再分裂,再往上插。直至这个过程传到根结点为止。
例子:
如果该节点的元素个数还没达到 m,则插入完后无需处理
比如:
如果该节点元素个数达到 m 时,这时候将元素插入到合适的位置,将最中间的元素取出,成为该节点的父节点元素,然后将其余左右元素拆成两个新节点
比如:
刚才的操作可能导致父节点的元素个数达到 m,这时候用情况 2 迭代处理,直到如果遇到根结点元素个数达到 m,则最中间元素将成为新的根结点。
比如:
我们需要分两种情况进行讨论:
B-Tree有许多变种,其中最常见的是B+Tree,例如MySQL就普遍使用B+Tree实现其索引结构。
与B-Tree相比,B+Tree有以下不同点:
红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用B-/+Tree作为索引结构,这一节将结合计算机组成原理相关知识讨论B-/+Tree作为索引的理论基础。
一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。
下面先介绍内存和磁盘存取原理,然后再结合这些原理分析B-/+Tree作为索引的效率。
索引一般以文件形式存储在磁盘上,索引检索需要磁盘I/O操作。与主存不同,磁盘I/O存在机械运动耗费,因此磁盘I/O的时间消耗是巨大的。
下面是磁盘的整体结构示意图:
一个磁盘由大小相同且同轴的圆形盘片组成,磁盘可以转动(各个磁盘必须同步转动)。在磁盘的一侧有磁头支架,磁头支架固定了一组磁头,每个磁头负责存取一个磁盘的内容。磁头不能转动,但是可以沿磁盘半径方向运动(实际是斜切向运动),每个磁头同一时刻也必须是同轴的,即从正上方向下看,所有磁头任何时候都是重叠的(不过目前已经有多磁头独立技术,可不受此限制)。
下面是磁盘结构的示意图:
盘片被划分成一系列同心环,圆心是盘片中心,每个同心环叫做一个磁道,所有半径相同的磁道组成一个柱面。磁道被沿半径线划分成一个个小的段,每个段叫做一个扇区,每个扇区是磁盘的最小存储单元。为了简单起见,我们下面假设磁盘只有一个盘片和一个磁头。
当需要从磁盘读取数据时,系统会将数据逻辑地址传给磁盘,磁盘的控制电路按照寻址逻辑将逻辑地址翻译成物理地址,即确定要读的数据在哪个磁道,哪个扇区。为了读取这个扇区的数据,需要将磁头放到这个扇区上方,为了实现这一点,磁头需要移动对准相应磁道,这个过程叫做寻道,所耗费时间叫做寻道时间,然后磁盘旋转将目标扇区旋转到磁头下,这个过程耗费的时间叫做旋转时间。
由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分分之一,因此为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。
这样做的理论依据是计算机科学中著名的局部性原理:
当一个数据被用到时,其附近的数据也通常会马上被使用。
程序运行期间所需要的数据通常比较集中。
由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。
预读的长度一般为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页得大小通常为4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。
一般使用磁盘I/O次数评价索引结构的优劣。先从B-Tree分析,根据B-Tree的定义,可知检索一次最多需要访问h个节点。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入。为了达到这个目的,在实际实现B-Tree还需要使用如下技巧:
每次新建节点时,直接申请一个页的空间,这样就保证一个节点物理上也存储在一个页里,加之计算机存储分配都是按页对齐的,就实现了一个node只需一次I/O。
B-Tree中一次检索最多需要h-1次I/O(根节点常驻内存),渐进复杂度为$O(h)=O(\log_d N)$。一般实际应用中,出度d是非常大的数字,通常超过100,因此h非常小(通常不超过3)。
综上所述,用B-Tree作为索引结构效率是非常高的。
而红黑树这种结构,h明显要深的多。由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,所以红黑树的I/O渐进复杂度也为O(h),效率明显比B-Tree差很多。
上文还说过,B+Tree更适合外存索引,原因和内节点出度d有关。从上面分析可以看到,d越大索引的性能越好,而出度的上限取决于节点内key和data的大小:
$$
d_{max}=floor({pagesize \over keysize+datasize+pointsize})
$$
floor表示向下取整。
由于B+Tree内节点去掉了data域,因此可以拥有更大的出度,容纳更多的节点,能够有效减少磁盘IO次数。
一般在数据库系统或文件系统中使用的B+Tree结构都在经典B+Tree的基础上进行了优化,增加了顺序访问指针。
如上图图所示,在B+Tree的每个叶子节点增加一个指向相邻叶子节点的指针,就形成了带有顺序访问指针的B+Tree。做这个优化的目的是为了提高区间访问的性能,例如图4中如果要查询key为从18到49的所有数据记录,当找到18后,只需顺着节点和指针顺序遍历就可以一次性访问到所有数据节点,极大提到了区间查询效率。
综上所述:
B+Tree做索引的优势是:
而MongoDB索引选择B树可能是因为:
MongoDB 是文档型的数据库,是一种nosql,它使用BSON格式保存数据,归属于聚合型数据库。被设计用在数据模型简单,性能要求高的场合。之所以采用B树,是因为B树key和data域聚合在一起。
参考文档:
]]>brew update |
$ vi ~/.bash_profile
export PATH=$PATH:/usr/local/sbin
$ source ~/.bash_profile
rabbitmq-server
这里需要注意下,从3.3.1版本开始,RabbitMQ默认不允许远程ip登录,即只能使用localhost登录。如果希望远程登录,需要添加用户权限。
由于账号guest具有所有的操作权限,并且又是默认账号,出于安全因素的考虑,guest用户只能通过localhost登陆使用,并建议修改guest用户的密码以及新建其他账号管理使用rabbitmq。
这里我们以创建个test帐号,密码123456为例,创建一个账号并支持远程ip访问。
创建账号
rabbitmqctl add_user test 123456 |
设置用户角色
rabbitmqctl set_user_tags test administrator |
设置用户权限
rabbitmqctl set_permissions -p "/" test ".*" ".*" ".*" |
设置完成后可以查看当前用户和角色(需要开启服务)
rabbitmqctl list_users |
这是你就可以通过其他主机的访问RabbitMQ的Web管理界面了,访问方式,浏览器输入:serverip:15672。其中serverip是RabbitMQ-Server所在主机的ip。
用户管理
用户管理包括增加用户,删除用户,查看用户列表,修改用户密码。
新增一个用户
rabbitmqctl add_user Username Password |
删除一个用户
rabbitmqctl delete_user Username |
修改用户的密码
rabbitmqctl change_password Username Newpassword |
查看当前用户列表
rabbitmqctl list_users |
当我们如下书写markdown时:![](https://images.morethink.cn/092017231747399.jpg "TCP的三次握手和四次挥手")
会被博客园渲染成<p><img src="https://images.morethink.cn/092017231747399.jpg" title="TCP的三次握手和四次挥手"></p>
于是我就想通过在img标签后面动态添加一个带有title
的p标签来给博客园图片添加标题。
将下面代码放入页首Html代码
代码中即可(需要申请js权限)。
<!-- 引入jQuery --> |
markdown图片:![](https://images.morethink.cn/092017231747399.jpg "TCP的三次握手和四次挥手")
*/5 * * * * ?
:每隔5秒
执行一次0 */1 * * * ?
:每隔1分钟执行一次0 0 23 * * ?
:每天23点执行一次0 0 1 * * ?
:每天凌晨1点执行一次:0 0 1 1 * ?
:每月1号凌晨1点执行一次0 0 23 L * ?
: 每月最后一天23点执行一次0 0 1 ? * L
:每周星期天凌晨1点实行一次0 26,29,33 * * * ?
: 在26分、29分、33分执行一次0 0 0,13,18,21 * * ?
: 每天的0点、13点、18点、21点都执行一次Cron表达式由7个部分组成,各部分用空格隔开,Cron表达式的7个部分从左到右代表的含义如下:
秒
分
时
日
月
周
年
其中 年
是可选的。
字段名 | 允许的值 | 允许的特殊字符 | |
---|---|---|---|
秒 | 0-59 | ,- * / | |
分 | 0-59 | ,- * / | |
时 | 0-23 | ,- * / | |
日 | 1-31 | ,- * ? / L W C | |
月 | 1-12 or JAN-DEC | ,- * / | |
周 | 1-7 or SUN-SAT | ,- * ? / L C # | |
年 (可选字段) | empty,1970-2099 | ,- * / |
,
:表示列出枚举值值。例如在分
使用5,20,则意味着在5和20分每分钟触发一次。-
:表示范围。例如在分
使用5-20,表示从5分到20分钟每分钟触发一次。*
:表示匹配该域的任意值。假如在分
域使用*
,即表示每分钟都会触发事件。/
:表示起始时间开始触发,然后每隔固定时间触发一次,例如在Minutes域使用5/20,则意味着5分钟触发一次,而25,45等分别触发一次。?
:只能用在周
和日
。它也匹配域的任意值,但实际不会。因为周
和日
会相互影响。例如想在每月的20日触发调度,不管20日到底是星期几,则只能使用如下写法: 13 13 15 20 ?,其中最后一位只能用?,而不能使用,如果使用*表示不管星期几都会触发,实际上并不是这样。 L
: 表示最后,只能出现在日
和周
,如果在日
使用5L,意味着在最后的一个星期四触发。 W
:表示有效工作日(周一到周五),只能出现在周
域,系统将在离指定日期的最近的有效工作日触发事件。例如:在日
使用5W,如果5日是星期六,则将在最近的工作日:星期五,即4日触发。如果5日是星期天,则在6日触发;如果5日在星期一到星期五中的一天,则就在5日触发。另外一点,W的最近寻找不会跨过月份。#
:用于确定每个月第几个星期几,只能出现在周
。例如在4#2,表示某月的第二个星期三。参考文档:
]]>$ git clone git@github.com:morethink/git-recipes.git
进入仓库,增加源分支地址到你项目远程分支列表中
此处是关键,先得将原来的仓库指定为 upstream,命令为:$ git remote add upstream git@github.com:geeeeeeeeek/git-recipes.git
此处可使用 git remote -v
查看远程分支列表
$ git remote -v |
fetch 源分支的新版本到本地$ git fetch upstream
$ git checkout master
$ git merge upstream/master
$ git push origin master
参考文档:
]]>项目地址: https://github.com/morethink/web-security
XSS攻击:跨站脚本攻击(Cross-Site Scripting),为了不和层叠样式表(Cascading Style Sheets, CSS)的缩写混淆,故将跨站脚本攻击缩写为XSS。XSS是一种常见的web安全漏洞,它允许攻击者将恶意代码植入到提供给其它用户使用的页面中。不同于大多数攻击(一般只涉及攻击者和受害者),XSS涉及到三方,即攻击者、客户端与Web应用。XSS的攻击目标是为了盗取存储在客户端的cookie或者其他网站用于识别客户端身份的敏感信息。一旦获取到合法用户的信息后,攻击者甚至可以假冒合法用户与网站进行交互。
XSS通常可以分为两大类:
恶意用户的Html输入Web程序->进入数据库->Web程序->用户浏览器
。比如说我写了一个网站,然后攻击者在上面发布了一个文章,内容是这样的 <script>alert(document.cookie)</script>
,如果我没有对他的内容进行处理,直接存储到数据库,那么下一次当其他用户访问他的这篇文章的时候,服务器从数据库读取后然后响应给客户端,浏览器执行了这段脚本,就会将cookie展现出来,这就是典型的存储型XSS。
如图:
答案很简单,坚决不要相信用户的任何输入,并过滤掉输入中的所有特殊字符。这样就能消灭绝大部分的XSS攻击。
目前防御XSS主要有如下几种方式:
script
标签)。w.Header().Set("Content-Type","text/javascript")
攻击者成功的向服务器提交恶意的SQL查询代码,程序在接收后错误的将攻击者的输入作为查询语句的一部分执行,导致原始的查询逻辑被改变,额外的执行了攻击者精心构造的恶意代码。
举例:' OR '1'='1
这是最常见的 SQL注入攻击,当我们输如用户名 admin ,然后密码输如' OR '1'=1='1
的时候,我们在查询用户名和密码是否正确的时候,本来要执行的是SELECT * FROM user WHERE username='' and password=''
,经过参数拼接后,会执行 SQL语句 SELECT * FROM user WHERE username='' and password='' OR '1'='1'
,这个时候1=1是成立,自然就跳过验证了。
如下图所示:
但是如果再严重一点,密码输如的是';DROP TABLE user;--
,那么 SQL命令为SELECT * FROM user WHERE username='admin' and password='';drop table user;--'
这个时候我们就直接把这个表给删除了。
'"\尖括号&*
;等)进行转义处理,或编码转换。在上图展示中,使用了Java JDBC中的PreparedStatement
预编译预防SQL注入,可以看到将所有输入都作为了字符串,避免执行恶意SQL。
DDOS:分布式拒绝服务攻击(Distributed Denial of Service),简单说就是发送大量请求是使服务器瘫痪。DDos攻击是在DOS攻击基础上的,可以通俗理解,dos是单挑,而ddos是群殴,因为现代技术的发展,dos攻击的杀伤力降低,所以出现了DDOS,攻击者借助公共网络,将大数量的计算机设备联合起来,向一个或多个目标进行攻击。
在技术角度上,DDoS攻击可以针对网络通讯协议的各层,手段大致有:TCP类的SYN Flood、ACK Flood,UDP类的Fraggle、Trinoo,DNS Query Flood,ICMP Flood,Slowloris类等等。一般会根据攻击目标的情况,针对性的把技术手法混合,以达到最低的成本最难防御的目的,并且可以进行合理的节奏控制,以及隐藏保护攻击资源。
下面介绍一下TCP协议中的SYN攻击。
在三次握手过程中,服务器发送 SYN-ACK
之后,收到客户端的 ACK
之前的 TCP 连接称为半连接(half-open connect)。此时服务器处于 SYN_RCVD
状态。当收到 ACK 后,服务器才能转入 ESTABLISHED
状态.
SYN
攻击指的是,攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送SYN
包,服务器回复确认包,并等待客户的确认。由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN
包将长时间占用未连接队列,正常的SYN
请求被丢弃,导致目标系统运行缓慢,严重者会引起网络堵塞甚至系统瘫痪。
阿里巴巴的安全团队在实战中发现,DDoS 防御产品的核心是检测技术和清洗技术。检测技术就是检测网站是否正在遭受 DDoS 攻击,而清洗技术就是清洗掉异常流量。而检测技术的核心在于对业务深刻的理解,才能快速精确判断出是否真的发生了 DDoS 攻击。清洗技术对检测来讲,不同的业务场景下要求的粒度不一样。
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,也被称为:one click attack/session riding,缩写为:CSRF/XSRF。
你这可以这么理解CSRF攻击:攻击者盗用了你的身份,以你的名义发送恶意请求。CSRF能够做的事情包括:以你名义发送邮件,发消息,盗取你的账号,甚至于购买商品,虚拟货币转账……造成的问题包括:个人隐私泄露以及财产安全。
下图简单阐述了CSRF攻击的思
从上图可以看出,要完成一次CSRF攻击,受害者必须依次完成两个步骤:
看到这里,你也许会说:“如果我不满足以上两个条件中的一个,我就不会受到CSRF的攻击”。是的,确实如此,但你不能保证以下情况不会发生:
下面讲一讲java解决CSRF攻击的方式。
用户名和密码都是admin。
http://localhost:8081/login.html
:
http://localhost:8081/deletePost.html
:
http://localhost:8082/deletePost.html
:
明显看到B网站是8082端口,A网站是8081端口,但是B网站的删除2号帖子功能依然实现。
简单来说,CSRF 就是网站 A 对用户建立信任关系后,在网站 B 上利用这种信任关系,跨站点向网站 A 发起一些伪造的用户操作请求,以达到攻击的目的。
而之所以可以完成攻击是因为B向A发起攻击的时候会把A网站的cookie带给A网站,也就是说cookie已经不安全了。
Synchronizer Tokens: 在表单里隐藏一个随机变化的 csrf_token csrf_token 提交到后台进行验证,如果验证通过则可以继续执行操作。这种情况有效的主要原因是网站 B 拿不到网站 A 表单里的 csrf_token
这种方式的使用条件是PHP和JSP等。因为cookie已经不安全了,因此把csrf_token值存储在session中,然后每次表单提交时都从session取出来放到form表单的隐藏域中,这样B网站不可以得到这个存储到session中的值。
下面是JSP的:<input type="hidden" name="random_form" value=<%=random%>></input>
但是我现在的情况是html,不是JSP,并不能动态的从session中取出csrf_token值。只能采用加密的方式了。
这可能是最简单的解决方案了,因为攻击者不能获得第三方的Cookie(理论上),所以表单中的数据也就构造失败了。
我采用的hash加密方法是JS实现Java的HashCode方法,得到hash值,这个比较简单。也可以采用其他的hash算法。
前端向后台传递hash之后的csrf_token值和cookie中的csrf_token值,后台拿到cookie中的csrf_token值后得到hashCode值然后与前端传过来的值进行比较,一样则通过。
http://localhost:8081/deletePost.html
我们通过UserFilter.java给攻击者返回的是403错误,表示服务器理解用户客户端的请求但拒绝处理。
http://localhost:8082/deletePost.html
:
攻击者不能删除4号帖子。
前端代码:
deletePost.html
|
后台代码:
UserInterceptor.java
package cn.morethink.interceptor; |
注意:
上面一共提到了4种攻击方式,分别是XSS攻击(关键是脚本,利用恶意脚本发起攻击),SQL注入(关键是通过用SQL语句伪造参数发出攻击),DDOS攻击(关键是发出大量请求,最后令服务器崩溃),CSRF攻击(关键是借助本地cookie进行认证,伪造发送请求)。
参考文档:
]]>