使用协程快速获得一个代理池

使用协程快速获得一个代理池

使用协程快速获得一个代理池
前言
在执行 I/O 密集型任务的时候,程序会因为等待 I/O 而阻塞。比如我们使用 requests 库来进行网络爬虫请求的话,如果网站响应速度过慢,程序会一直等待网站响应,*终导致其爬取效率十分低下。本文以爬取 IP 代理池为例,演示 Python 中如何利用异步协程来加速网络爬虫。

协程
协程(Coroutine),又称微线程,纤程,协程是一种用户态的轻量级线程。

协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存,在调度回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,即所有局部状态的一个特定组合。

协程本质上是个单进程,协程相对于多进程来说,无需进程间上下文切换的开销,无需原子操作锁定及同步的开销,编程模型也非常简单。

我们可以使用协程来实现异步操作,比如在网络爬虫场景下,在发出一个请求之后,需要等待一定的时间才能得到响应。其实在这个等待过程中,程序可以干许多其他的事情,等到响应返回之后再切换回来继续处理,这样可以充分利用 CPU 和其他资源,这就是异步协程的优势。

Python 中的协程
从 Python 3.4 开始,Python 中加入了协程的概念,这个版本的协程是通过生成器对象来实现的,在 Python 3.5 中增加了 asyncio 库和 async、await 关键字,使得协程的实现更加方便。

asyncio 库
首先我们先来看一个不使用协程的程序,代码如下:

import time


def job(t):
print(‘Start job {}’.format(t))
time.sleep(t) # 等待 t 秒
print(‘Job {0} takes {0}s’.format(t))


def main():
[job(t) for t in range(1, 3)]


start = time.time()
main()
print(“total time: {}”.format(time.time() – start))
运行结果:

Start job 1
Job 1 takes 1s
Start job 2
Job 2 takes 2s
total time: 3.001577138900757
从运行结果可以看出,我们的 job 是按顺序执行的。必须执行完 job 1 才能开始执行 job 2, job 1 需要 1 秒的执行时间,job 2 需要 2 秒的执行时间,所以总时间是 3 秒多。

如果我们使用协程的方式,job 1 在等待 time.sleep(t) 执行结束的时候(可以看做是等待一个网页的下载成功),是可以切换到 job 2 执行的。

我们再来看一下使用协程改造后的代码:

import time
import asyncio


async def job(t): # 使用 async 关键字将一个函数定义为协程
print(‘Start job {}’.format(t))
await asyncio.sleep(t) # 等待 t 秒, 期间切换执行其他任务
print(‘Job {0} takes {0}s’.format(t))


async def main(loop): # 使用 async 关键字将一个函数定义为协程
tasks = [loop.create_task(job(t)) for t in range(1, 3)] # 创建任务, 不立即执行
await asyncio.wait(tasks) # 执行并等待所有任务完成


start = time.time()
loop = asyncio.get_event_loop() # 建立 loop
loop.run_until_complete(main(loop)) # 执行 loop
loop.close() # 关闭 loop
print(“total time: {}”.format(time.time() – start))
运行结果:

Start job 1
Start job 2
Job 1 takes 1s
Job 2 takes 2s
total time: 2.0033459663391113
从运行结果可以看出,我们没有等待 job 1 执行结束再开始执行 job 2,而是 job 1 触发 await 的时候切换到了 job 2 。 这时 job 1 和 job 2 同时在执行 await asyncio.sleep(t),所以*终程序的执行时间取决于执行时间*长的那个 job,也就是 job 2 的执行时间:2 秒。

aiohttp 库
在对 asyncio 库做了简单了解之后,我们来看一下如何通过协程来改造我们的爬虫程序。

安装 aiohttp 库:

pip install aiohttp
我们先来看一下使用 reqeusts 库实现一个网页的爬取:

import time

import requests

def fetch(url):
r = requests.get(url)
return r.url


def main():
results = [fetch(‘http://www.baidu.com’) for _ in range(2)]
print(results)


start = time.time()
main()
print(“total time: {}”.format(time.time() – start))
运行结果:

[‘http://www.baidu.com/’, ‘http://www.baidu.com/’]
total time: 1.5445010662078857
使用 requests 库,访问两次 http://www.baidu.com,共耗时 1.5 秒

我们用 aiohttp 库来改造上面的代码:

import time
import asyncio

import aiohttp


async def fetch(session, url):
response = await session.get(url) # await 等待网络 IO 并切换协程
return str(response.url)


async def main(loop):
async with aiohttp.ClientSession() as session:
tasks = [
loop.create_task(fetch(session, ‘http://www.baidu.com’))
for _ in range(2)
]
done, pending = await asyncio.wait(tasks) # 执行并等待所有任务完成
results = [r.result() for r in done] # 获取所有返回结果
print(results)


start = time.time()
loop = asyncio.get_event_loop() # 建立 事件循环
loop.run_until_complete(main(loop)) # 在 事件循环 中执行协程
loop.close() # 关闭 事件循环
print(“total time: {}”.format(time.time() – start))
运行结果:

[‘http://www.baidu.com’, ‘http://www.baidu.com’]
total time: 0.10848307609558105
使用 aiohttp 的代码执行时间较使用 reqeusts 的代码有大幅的提升。

上例中,我们使用官方推荐的方式创建 session,并通过 session 执行 get 操作。aiohttp 官方建议一个 application 中共享使用一个 session,不要为每个请求都创建 session。

使用 asyncio 和 aiohttp 快速获得一个代理池
通过爬虫解析免费的代理发布网站页面,来生成代理池。

import os
import re
import time
import asyncio

import aiohttp

HEADERS = {
‘User-Agent’:
‘Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15’
}

OUTPUT_FILE = ‘proxies.txt’ # 代理池输出文件
SITES = [‘http://www.live-socks.net’, ‘http://www.proxyserverlist24.top’] # 代理发布网站
CHECK_URL = ‘http://www.baidu.com’
LOCAL_PROXY = None # 在本地发起请求时的代理


# http get 协程
async def fetch(session, url, proxy=None):
proxy_headers = HEADERS if proxy else None
try:
async with session.get(
url, headers=HEADERS, proxy=proxy,
proxy_headers=proxy_headers,
timeout=aiohttp.ClientTimeout(total=5)) as response:
if response.status == 200:
return await response.text()
else:
return ”
except:
return ”


# 从代理发布网站获取代理发布页面链接
async def get_page_links(loop, session):
tasks = [loop.create_task(fetch(session, url, proxy=LOCAL_PROXY))
for url in SITES] # 创建协程任务
done, _ = await asyncio.wait(tasks) # 执行并等待所有任务完成
htmls = [f.result() for f in done] # 获取所有返回结果

# 解析出 html 页面中的代理发布链接
def parse(html):
return re.findall(r'<h3[\s\S]*?<a.*?(http.*?\.html).*?</a>’, html)

results = map(parse, htmls) # 逐个解析 html 页面

return [y for x in results for y in x]


# 从代理发布页面获取代理 IP
async def get_proxies(loop, session, page_links):
tasks = [loop.create_task(fetch(session, url, proxy=LOCAL_PROXY))
for url in page_links] # 创建协程任务
done, _ = await asyncio.wait(tasks) # 执行并等待所有任务完成
htmls = [f.result() for f in done] # 获取所有返回结果

# 解析出 html 页面中的代理 IP
def parse(html):
return re.findall(r’\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}’, html)

results = map(parse, htmls) # 逐个解析 html 页面

return list(set([y for x in results for y in x]))


# 验证代理 IP
async def check_proxy(session, proxy):
html = await fetch(session, CHECK_URL, proxy=proxy)

# 如果返回通过代理 IP 访问的页面,则说明代理 IP 有效
return proxy if html else ”


# 通过协程批量验证代理 IP,每次同时发起 200 个验证请求
async def check_proxies(loop, session, proxies):
checked_proxies = []
for i in range(0, len(proxies), 200):
_proxies = [proxy.strip() if proxy.strip().startswith(‘http://’)
else ‘http://’ + proxy.strip() for proxy in proxies[i:i + 200]]
tasks = [loop.create_task(check_proxy(session, proxy))
for proxy in _proxies]
done, _ = await asyncio.wait(tasks) # 执行并等待所有任务完成
checked = [f.result() for f in done] # 获取所有返回结果
checked_proxies += [p for p in checked if p] # 获取不为空的返回值,即验证成功的代理 IP

return checked_proxies


# 将代理 IP 逐行保存到文件
def save_proxies(proxies):
# 创建新文件,如果文件已存在,则清空文件内容
with open(OUTPUT_FILE, ‘w’) as f:
f.write(”)

# 通过追加写模式,逐行写入文件
with open(OUTPUT_FILE, ‘a’) as f:
for proxy in proxies:
f.write(proxy + ‘\n’)


async def main(loop):
async with aiohttp.ClientSession() as session:
page_links = await get_page_links(loop, session) # 获得代理发布页面链接
# 从代理发布页面获得代理 IP
proxies = await get_proxies(loop, session, page_links)
print(‘total proxy: {}’.format(len(proxies))) # 解析出的代理 IP 总量
proxies = await check_proxies(loop, session, proxies) # 验证代理 IP

print(‘total checked proxy: {}’.format(len(proxies))) # 验证后的代理 IP 总量
save_proxies(proxies) # 保存代理 IP 到文件


start = time.time()
loop = asyncio.get_event_loop() # 建立 事件循环
loop.run_until_complete(main(loop)) # 在 事件循环 中执行协程
loop.close() # 关闭 事件循环
total_time = time.time() – start
print(f’total time: {total_time}’)
运行结果:

total proxy: 15675
total checked proxy: 4503
total time: 487.2807550430298
更加高效的爬虫
在爬虫程序中,通常有网络请求任务、页面解析任务、数据清洗任务和数据入库任务。

网络请求任务、数据入库任务属于 IO 密集型任务,在 Python 中通常使用多线程模型来提高这类任务的性能,现在还可以通过 aiohttp,Motor(MongoDB 的异步 Python 驱动)等异步框架将性能进一步提升。

页面解析任务、数据清洗任务这类 CPU 密集型的任务我们该如何来提高性能?在 Python 中针对 CPU 密集型任务可以通过 multiprocessing 模块来提升性能,通过 multiprocessing 模块可以使程序运行在多核 CPU 中,增加 CPU 的利用率以提升计算性能。

给代理池爬虫示例增加多核计算支持:

import os
import re
import time
import asyncio
from multiprocessing import Pool

import aiohttp

HEADERS = {
‘User-Agent’:
‘Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0.3 Safari/605.1.15’
}

OUTPUT_FILE = ‘proxies.txt’ # 代理池输出文件
SITES = [‘http://www.live-socks.net’, ‘http://www.proxyserverlist24.top’] # 代理发布网站
CHECK_URL = ‘http://www.baidu.com’
LOCAL_PROXY = ‘http://127.0.0.1:1087′ # •在本地发起请求时的代理


# http get 协程
async def fetch(session, url, proxy=None):
proxy_headers = HEADERS if proxy else None
try:
async with session.get(
url, headers=HEADERS, proxy=proxy,
proxy_headers=proxy_headers,
timeout=aiohttp.ClientTimeout(total=5)) as response:
if response.status == 200:
return await response.text()
else:
return ”
except:
return ”

# 解析出 html 页面中的代理发布链接


def parse_page_link(html):
return re.findall(r'<h3[\s\S]*?<a.*?(http.*?\.html).*?</a>’, html)

# 从代理发布网站获取代理发布页面链接


async def get_page_links(loop, session):
tasks = [loop.create_task(fetch(session, url, proxy=LOCAL_PROXY))
for url in SITES] # 创建协程任务
done, _ = await asyncio.wait(tasks) # 执行并等待所有任务完成
htmls = [f.result() for f in done] # 获取所有返回结果

# 利用多核 CPU 的计算能力提升页面解析性能
with Pool(processes=os.cpu_count() * 2) as pool:
results = pool.map(parse_page_link, htmls)

return [y for x in results for y in x]

# 解析出 html 页面中的代理 IP


def parse_proxy(html):
return re.findall(r’\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{1,5}’, html)

# 从代理发布页面获取代理 IP


async def get_proxies(loop, session, page_links):
tasks = [loop.create_task(fetch(session, url, proxy=LOCAL_PROXY))
for url in page_links] # 创建协程任务
done, _ = await asyncio.wait(tasks) # 执行并等待所有任务完成
htmls = [f.result() for f in done] # 获取所有返回结果

# 利用多核 CPU 的计算能力提升页面解析性能
with Pool(processes=os.cpu_count() * 2) as pool:
results = pool.map(parse_proxy, htmls)

return list(set([y for x in results for y in x]))


# 验证代理 IP
async def check_proxy(session, proxy):
html = await fetch(session, CHECK_URL, proxy=proxy)

# 如果返回通过代理 IP 访问的页面,则说明代理 IP 有效
return proxy if html else ”


# 通过协程批量验证代理 IP,每次同时发起 200 个验证请求
async def check_proxies(loop, session, proxies):
checked_proxies = []
for i in range(0, len(proxies), 200):
_proxies = [proxy.strip() if proxy.strip().startswith(‘http://’)
else ‘http://’ + proxy.strip() for proxy in proxies[i:i + 200]]
tasks = [loop.create_task(check_proxy(session, proxy))
for proxy in _proxies]
done, _ = await asyncio.wait(tasks) # 执行并等待所有任务完成
checked = [f.result() for f in done] # 获取所有返回结果
checked_proxies += [p for p in checked if p] # 获取不为空的返回值,即验证成功的代理 IP

return checked_proxies


# 将代理 IP 逐行保存到文件
def save_proxies(proxies):
# 创建新文件,如果文件已存在,则清空文件内容
with open(OUTPUT_FILE, ‘w’) as f:
f.write(”)

# 通过追加写模式,逐行写入文件
with open(OUTPUT_FILE, ‘a’) as f:
for proxy in proxies:
f.write(proxy + ‘\n’)


async def main(loop):
async with aiohttp.ClientSession() as session:
page_links = await get_page_links(loop, session) # 获得代理发布页面链接
# 从代理发布页面获得代理 IP
proxies = await get_proxies(loop, session, page_links)
print(‘total proxy: {}’.format(len(proxies))) # 解析出的代理 IP 总量
proxies = await check_proxies(loop, session, proxies) # 验证代理 IP

print(‘total checked proxy: {}’.format(len(proxies))) # 验证后的代理 IP 总量
save_proxies(proxies) # 保存代理 IP 到文件


start = time.time()
loop = asyncio.get_event_loop() # 建立 事件循环
loop.run_until_complete(main(loop)) # 在 事件循环 中执行协程
loop.close() # 关闭 事件循环
total_time = time.time() – start
print(f’total time: {total_time}’)
进程间的调度及上下文切换是非常消耗资源的。上面例子中解析任务比较简单,解析量也非常少,增加多核计算支持后,性能几乎没有提升还有可能降低。在实际爬虫项目中需要根据实际情况来衡量和选择。

用 pprint 代替 print 更友好的打印调试信息

用 pprint 代替 print 更友好的打印调试信息

用 pprint 代替 print 更友好的打印调试信息
pprint 是 “pretty printer” 的简写,“pretty” 的含义是 “漂亮的、美观的”,因此 pprint 的含义便是:漂亮的打印。

这是个相当简单却有用的模块,主要用于打印复杂的数据结构对象,例如多层嵌套的列表、元组和字典等。

先看看 print() 打印的一个例子:

mylist = [“Beautiful is better than ugly.”, “Explicit is better than implicit.”, “Simple is better than complex.”, “Complex is better than complicated.”]

print(mylist)

[‘Beautiful is better than ugly.’, ‘Explicit is better than implicit.’, ‘Simple is better than complex.’, ‘Complex is better than complicated.’]
这是一个简单的例子,全部打印在一行里。如果对象中的元素是多层嵌套的内容(例如复杂的 字典 数据),再打印出那肯定是一团糟的,不好阅读。

使用 pprint 模块的 pprint() 替代 print(),可以解决如下痛点:

设置合适的行宽度,作适当的换行
设置打印的缩进、层级,进行格式化打印
判断对象中是否有无限循环,并优化打印内容
基本使用

pprint(object, stream=None, indent=1, width=80, depth=None, *,compact=False)
默认的行宽度参数为 80,当打印的字符小于 80 时,pprint() 基本上等同于内置函数 print(),当字符超出时,它会作美化,进行格式化输出。

import pprint

mylist = [“Beautiful is better than ugly.”, “Explicit is better than implicit.”, “Simple is better than complex.”, “Complex is better than complicated.”]

pprint.pprint(mylist)

# 超出80字符,打印的元素是换行的
[‘Beautiful is better than ugly.’,
‘Explicit is better than implicit.’,
‘Simple is better than complex.’,
‘Complex is better than complicated.’]
设置缩进

pprint.pprint(mylist, indent=4)

[ ‘Beautiful is better than ugly.’,
‘Explicit is better than implicit.’,
‘Simple is better than complex.’,
‘Complex is better than complicated.’]
设置打印行宽

mydict = {‘students’: [{‘name’:’Tom’, ‘age’: 18},{‘name’:’Jerry’, ‘age’: 19}]}

pprint.pprint(mydict)

# 正常打印
{‘students’: [{‘age’: 18, ‘name’: ‘Tom’}, {‘age’: 19, ‘name’: ‘Jerry’}]}

pprint.pprint(mydict, width=20)

# 行宽为 20
{‘students’: [{‘age’: 18,
‘name’: ‘Tom’},
{‘age’: 19,
‘name’: ‘Jerry’}]}

pprint.pprint(mydict, width=70)

# 行宽为 70
{‘students’: [{‘age’: 18, ‘name’: ‘Tom’},
{‘age’: 19, ‘name’: ‘Jerry’}]}
设置打印层级

newlist = [1, [2, [3, [4, [5]]]]]

pprint.pprint(newlist, depth=3)

# 超出的层级会用 … 表示
[1, [2, [3, […]]]]
用 pprint 替换 print
import pprint
print = pprint.pprint

mylist = [“Beautiful is better than ugly.”, “Explicit is better than implicit.”, “Simple is better than complex.”, “Complex is better than complicated.”]

print(mylist)

[‘Beautiful is better than ugly.’,
‘Explicit is better than implicit.’,
‘Simple is better than complex.’,
‘Complex is better than complicated.’]

zookeeper的Leader选举机制详解

分布式开发必须知道的Zookeeper知识及其的Leader选举机制(ZAB原子广播协议)

ZooKeeper是Hadoop下的一个子项目,它是一个针对大型分布式系统的可靠协调系统,提供的功能包括:配置维护、名字服务、分布式同步、组服务等; 它的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

ZooKeeper系统架构

下图就是Zookeeper的架构图:

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

从上面的架构图中,我们需要了解的主要的信息有:

①ZooKeeper分为服务器端(Server)和客户端(Client),客户端可以连接到整个ZooKeeper服务的任意服务器上(Leader除外)。

②ZooKeeper 启动时,将从实例中选举一个Leader,Leader 负责处理数据更新等操作,一个更新操作成功的标志是当且仅当大多数Server在内存中成功修改数据(Quorom机制)。每个Server 在内存中存储了一份数据。

③Zookeeper是可以集群复制的,集群间通过Zab协议(Zookeeper Atomic Broadcast)来保持数据的一致性;

④Zab协议包含两个阶段:Leader Election阶段和Atomic Brodcast阶段。群中将选举出一个Leader,其他的机器则称为Follower,所有的写操作都被传送给Leader,并通过Brodcast将所有的更新告诉给Follower。 当Leader被选举出来,且大多数服务器完成了和leader的状态同步后,Leadder Election 的过程就结束了,就将会进入到Atomic Brodcast的过程。Atomic Brodcast同步Leader和Follower之间的信息,保证Leader和Follower具有形同的系统状态。


Quorom机制简介

在分布式系统中,冗余数据是保证可靠性的手段,因此冗余数据的一致性维护就非常重要。一般而言,一个写操作必须要对所有的冗余数据都更新完成了,才能称为成功结束。比如一份数据在5台设备上有冗余,因为不知道读数据会落在哪一台设备上,那么一次写操作,必须5台设备都更新完成,写操作才能返回。

对于写操作比较频繁的系统,这个操作的瓶颈非常大。Quorum算法可以让写操作只要写完3台就返回。剩下的由系统内部缓慢同步完成。而读操作,则需要也至少读3台,才能保证至少可以读到一个*新的数据。


Zookeeper中的四种角色

①Leader:领导者,负责进行投票的发起和决议,更新系统状态。

②Learner:学习者

③Follower(Learner的子类):跟随者,用于接受客户端请求并向客户端返回结结果,在选主过程中参与投票,Follower可以接收Client请求,如果是写请求将转发给Leader来更新系统状态。

④Observer:观察者,可以接收客户端连接,将写请求转发给Leader节点,但是不参与投票过程,只是同步Leader状态,因为Follower增多会导致投票阶段延迟增大,影响性能。Observer的目的是为了扩展系统,提高读取数据。


为什么Zookeeper中的Server数目一般为基数?

我们知道在Zookeeper中 Leader 选举算法采用了Quorom算法。该算法的核心思想是当多数Server写成功,则任务数据写成功。假设有3个Server,则*多允许一个Server挂掉;如果有4个Server,则同样*多允许一个Server挂掉。既然3个或者4个Server,同样*多允许1个Server挂掉,那么它们的可靠性是一样的,所以选择奇数个ZooKeeper Server即可,这里选择3个Server。


Zookeeper用于Leader选举的算法

①基于UDP的LeaderElection

②基于UDP的FastLeaderElection

③基于UDP和认证的FastLeaderElection

④基于TCP的FastLeaderElection(默认值)


FastLeaderElection机制

接下来要说的就是Zookeeper的Leader选举机制核心算法FastLeaderElection类。FastLeaderElection实现了Election接口,其需要实现接口中定义的lookForLeader(核心的选举算法入口)方法和shutdown方法FastLeaderElection选举算法是标准的Fast Paxos算法实现,可解决LeaderElection选举算法收敛速度慢的问题。

术语介绍

sid(myid)

每个Zookeeper服务器,都需要在数据文件夹下创建一个名为myid的文件,该文件包含整个Zookeeper集群唯一的ID(整数)。例如某Zookeeper集群包含三台服务器,hostname分别为zoo1、zoo2和zoo3,其myid分别为1、2和3,则在配置文件中其ID与hostname必须一一对应,如下所示。在该配置文件中,server.后面的数据即为myid(Leader选举时用的sid或者leader)。

server.1=zoo1:2888:3888
server.2=zoo2:2888:3888
server.3=zoo3:2888:3888

zxid

类似于RDBMS中的事务ID,用于标识一次更新操作的Proposal ID。为了保证顺序性,该zkid必须单调递增。因此Zookeeper使用一个64位的数来表示,高32位是Leader的epoch,从1开始,每次选出新的Leader,epoch加一。低32位为该epoch内的序号,每次epoch变化,都将低32位的序号重置。这样保证了zxid的全局递增性。


Zookeeper节点的四种状态

截图为Zookeeper定义的四种服务器节点状态:

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • LOOKING: 不确定Leader状态。该状态下的服务器认为当前集群中没有Leader,会发起Leader选举。
  • FOLLOWING: 跟随者状态。表明当前服务器角色是Follower,并且它知道Leader是谁。
  • LEADING: 领导者状态。表明当前服务器角色是Leader,它会维护与Follower间的心跳。
  • OBSERVING: 观察者状态。表明当前服务器角色是Observer,与Folower唯一的不同在于不参与选举,也不参与集群写操作时的投票。

FastLeaderElection内部类

FastLeaderElection的内部类的情况如下图:

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • Notification:表示收到的选举投票信息(其他服务器发来的选举投票信息),其包含了被选举者的id、zxid、选举周期等信息。
  • ToSend:表示发送给其他服务器的选举投票信息(其他服务器发来的选举投票信息),其包含了被选举者的id、zxid、选举周期等信息。
  • Messenger:包含了WorkerReceiverWorkerSender两个内部类。WorkerReceiver实现了Runnable接口,是选票接收器。WorkerSender也实现了Runnable接口,为选票发送器

Notification(收到的投票信息)

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • leader:被推选的leader的id。
  • zxid:被推选的leader的事务id。
  • electionEpoch:推选者的选举周期。
  • state:推选者的状态。
  • sid:推选者的id。
  • peerEpoch:被推选者的选举周期。

ToSend(发送的投票信息)

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • leader:被推选的leader的id。
  • zxid:被推选的leader的事务id。
  • electionEpoch:推选者的选举周期。
  • state:推选者的状态。
  • sid:推选者的id。
  • peerEpoch:被推选者的选举周期。

WorkerSender(选票发送器)

WorkerSender也实现了Runnable接口,为选票发送器,其会不断地从sendqueue中获取待发送的选票,并将其传递到底层QuorumCnxManager中。

  • 获取选票

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • 发送选票

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 


WorkerReceiver(选票接收器)

WorkerReceiver实现了Runnable接口,是选票接收器。其会不断地从QuorumCnxManager中获取其他服务器发来的选举消息中。先会从QuorumCnxManager中的pollRecvQueue队列中取出其他服务器发来的选举消息,消息封装在Message数据结构中。然后判断消息中的服务器id是否包含在可以投票的服务器集合中,若不是,则会将本服务器的内部投票发送给该服务器,其流程如下:

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

若包含该服务器,则根据消息(Message)解析出投票服务器的投票信息并将其封装为Notification,然后判断当前服务器是否为LOOKING,若为LOOKING,则直接将Notification放入FastLeaderElection的recvqueue。然后判断投票服务器是否为LOOKING状态,并且其选举周期小于当前服务器的逻辑时钟,则将本(当前)服务器的内部投票发送给该服务器,否则,直接忽略掉该投票。其流程如下:

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

若本服务器的状态不为LOOKING,则会根据投票服务器中解析的version信息来构造ToSend消息,放入sendqueue,等待发送,起流程如下:

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 


核心函数分析

sendNotifications函数

其会遍历所有的参与者投票集合,然后将自己的选票信息发送至上述所有的投票者集合,其并非同步发送,而是将ToSend消息放置于sendqueue中,之后由WorkerSender进行发送。

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

totalOrderPredicate函数

该函数将接收的投票与自身投票进行PK,查看是否消息中包含的服务器id是否更优,其按照epoch、zxid、id的优先级进行PK。

  • 判断消息里的epoch是不是比当前的大,如果大则消息中id对应的服务器就是leader。
  • 如果epoch相等则判断zxid,如果消息里的zxid大,则消息中id对应的服务器就是leader。
  • 如果前面两个都相等那就比较服务器id,如果大,则其就是leader。

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

termPredicate函数

该函数用于判断Leader选举是否结束,即是否有一半以上的服务器选出了相同的Leader,其过程是将收到的选票与当前选票进行对比,选票相同的放入同一个集合,之后判断选票相同的集合是否超过了半数。

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

checkLeader函数

该函数检查是否已经完成了Leader的选举,此时Leader的状态应该是LEADING状态。

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

lookForLeader函数

该函数就是leader选举的核心方法,代码行数有点多,这里仅分析其中的一部分。

  • 更新逻辑时钟、更新选票、发送选票

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • 获取投票数据、连接服务器

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • 选举Leader

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

  • LEADING状态处理

这可能是全网把zookeeper的Leader选举机制讲的*透彻的一篇文章

 

以上就是关于zookeeper的所有基本知识与Leader选举机制的讲解。

Mysql中SQL语句不使用索引的情况

MySQL查询不使用索引汇总

众所周知,增加索引是提高查询速度的有效途径,但是很多时候,即使增加了索引,查询仍然不使用索引,这种情况严重影响性能,这里就简单总结几条MySQL不使用索引的情况

如果MySQL估计使用索引比全表扫描更慢,则不使用索引。例如,如果列key均匀分布在1和100之间,下面的查询使用索引就不是很好:select * from table_name where key>1 and key<90;

如果使用MEMORY/HEAP表,并且where条件中不使用“=”进行索引列,那么不会用到索引,head表只有在“=”的条件下才会使用索引

用or分隔开的条件,如果or前的条件中的列有索引,而后面的列没有索引,那么涉及到的索引都不会被用到,例如:select * from table_name where key1='a' or key2='b';如果在key1上有索引而在key2上没有索引,则该查询也不会走索引

复合索引,如果索引列不是复合索引的*部分,则不使用索引(即不符合*左前缀),例如,复合索引为(key1,key2),则查询select * from table_name where key2='b';将不会使用索引

如果like是以‘%’开始的,则该列上的索引不会被使用。例如select * from table_name where key1 like '%a';该查询即使key1上存在索引,也不会被使用

如果列为字符串,则where条件中必须将字符常量值加引号,否则即使该列上存在索引,也不会被使用。例如,select * from table_name where key1=1;如果key1列保存的是字符串,即使key1上有索引,也不会被使用。

从上面可以看出,即使我们建立了索引,也不一定会被使用,那么我们如何知道我们索引的使用情况呢??在MySQL中,有Handler_read_keyHandler_read_rnd_key两个变量,如果Handler_read_key值很高而Handler_read_rnd_key的值很低,则表明索引经常不被使用,应该重新考虑建立索引。可以通过:show status like 'Handler_read%'来查看着连个参数的值。

关于如何正确创建Mysql的索引,请参考怎样正确创建MySQL索引的方法详解;众所周知,数据表索引可以提高数据的检索效率,也可以降低数据库的IO成本,并且索引还可以降低数据库的排序成本;但索引并不是时时都会生效的,比如以下几种情况,将导致索引失效:

1.如果条件中有or,即使其中有条件带索引也不会使用索引(这也是为什么SQL语句中尽量少用or的原因)

 

%title插图%num

注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引。

2.对于多列索引,不是使用的*部分,则不会使用索引。

3.like查询是以%开头时不会使用索引。

%title插图%num

 

4.如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引。

%title插图%num

 

5.如果 mysql 估计使用全表扫描要比使用索引快,则不使用索引。

6.在索引列上做运算,也会导致不使用索引。

select * from a where b+1 >10;

此外,查看索引的使用情况

?

1 show status like 'Handler_read%';

大家可以注意:

handler_read_key:这个值越高越好,越高表示使用索引查询到的次数。

handler_read_rnd_next:这个值越高,说明查询越低效。

iOS app生命周期方法执行探究

使用一个新创建的没有后台运行的app测试:
[LogInfo] 2018-11-06 10:49:25.444
File:AppDelegate Line:30
Function:-[AppDelegate applicationWillResignActive:]
applicationWillResignActive
[LogInfo] 2018-11-06 10:49:26.030
File:AppDelegate Line:38
Function:-[AppDelegate applicationDidEnterBackground:]
applicationDidEnterBackground
[LogInfo] 2018-11-06 10:50:27.345
File:AppDelegate Line:22
Function:-[AppDelegate application:didFinishLaunchingWithOptions:]
didFinishLaunchingWithOptions
[LogInfo] 2018-11-06 10:50:27.588
File:AppDelegate Line:50
Function:-[AppDelegate applicationDidBecomeActive:]
applicationDidBecomeActive
[LogInfo] 2018-11-06 10:51:08.119
File:AppDelegate Line:30
Function:-[AppDelegate applicationWillResignActive:]
applicationWillResignActive
[LogInfo] 2018-11-06 10:51:09.153
File:AppDelegate Line:38
Function:-[AppDelegate applicationDidEnterBackground:]
applicationDidEnterBackground
[LogInfo] 2018-11-06 10:51:09.160
File:AppDelegate Line:55
Function:-[AppDelegate applicationWillTerminate:]
applicationWillTerminate

测试步骤:

切后台,杀应用
应用前台杀应用
可以看到步骤2调用了applicationWillTerminate,步骤1则不会。

但我们在一个支持后台运行的应用中进行同样的测试则发现,applicationWillTerminate总是会被调用;当切后台的时候applicationDidEnterBackground也会正常触发。

从这样的结果来看,苹果的注释有些让人难以理解:

(void)applicationDidEnterBackground:(UIApplication *)application {
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}

(void)applicationWillTerminate:(UIApplication *)application {
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
}

很明显并非只有我们关心这一点:
https://stackoverflow.com/questions/29416375/when-will-applicationwillterminate-be-called
很值得一看。

总结:applicationWillTerminate不总是被执行,如果要保证自己的任务,需要使用beginBackgroundTaskWithExpirationHandler进行相关处理。而其具体的行为需要看你的应用是否支持后台运行(*好用实际应用的真机测试结果来支持你的观点,然后做好逻辑处理!)。

Java并发编程之锁机制之LockSupport工具

LockSupport类

了解线程的阻塞和唤醒,我们需要查看LockSupport类。具体代码如下:

  1. public class LockSupport {
  2. private LockSupport() {} // Cannot be instantiated.
  3. private static void setBlocker(Thread t, Object arg) {
  4. U.putObject(t, PARKBLOCKER, arg);
  5. }
  6. public static void unpark(Thread thread) {
  7. if (thread != null)
  8. U.unpark(thread);
  9. }
  10. public static void park(Object blocker) {
  11. Thread t = Thread.currentThread();
  12. setBlocker(t, blocker);
  13. U.park(false, 0L);
  14. setBlocker(t, null);
  15. }
  16. public static void parkNanos(Object blocker, long nanos) {
  17. if (nanos > 0) {
  18. Thread t = Thread.currentThread();
  19. setBlocker(t, blocker);
  20. U.park(false, nanos);
  21. setBlocker(t, null);
  22. }
  23. }
  24. public static void parkUntil(Object blocker, long deadline) {
  25. Thread t = Thread.currentThread();
  26. setBlocker(t, blocker);
  27. U.park(true, deadline);
  28. setBlocker(t, null);
  29. }
  30. public static Object getBlocker(Thread t) {
  31. if (t == null)
  32. throw new NullPointerException();
  33. return U.getObjectVolatile(t, PARKBLOCKER);
  34. }
  35. public static void park() {
  36. U.park(false, 0L);
  37. }
  38. public static void parkNanos(long nanos) {
  39. if (nanos > 0)
  40. U.park(false, nanos);
  41. }
  42. public static void parkUntil(long deadline) {
  43. U.park(true, deadline);
  44. }
  45. //省略部分代码
  46. private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
  47. private static final long PARKBLOCKER;
  48. private static final long SECONDARY;
  49. static {
  50. try {
  51. PARKBLOCKER = U.objectFieldOffset
  52. (Thread.class.getDeclaredField(“parkBlocker”));
  53. SECONDARY = U.objectFieldOffset
  54. (Thread.class.getDeclaredField(“threadLocalRandomSecondarySeed”));
  55. } catch (ReflectiveOperationException e) {
  56. throw new Error(e);
  57. }
  58. }
  59. }
  60. 复制代码

从上面的代码中,我们可以知道LockSupport中的对外提供的方法都是静态方法。这些方法提供了*基本的线程阻塞和唤醒功能,在LockSupport类中定义了一组以park开头的方法用来阻塞当前线程。以及unPark(Thread thread)方法来唤醒一个被阻塞的线程。关于park开头的方法具体描述如下表所示:

 

park.png

 

 

其中park(Object blocker)parkNanos(Object blocker, long nanos)parkUntil(Object blocker, long deadline)三个方法是Java 6中新增加的方法。其中参数blocker是用来标识当前线程等待的对象(下文简称为阻塞对象),该对象主要用于问题排查和系统监控

由于在Java 5之前,当线程阻塞时(使用synchronized关键字)在一个对象上时,通过线程dump能够查看到该线程的阻塞对象。方便问题定位,而Java 5退出的Lock等并发工具却遗漏了这一点,致使在线程dump时无法提供阻塞对象的信息。因此,在Java 6中,LockSupport新增了含有阻塞对象的park方法。用以替代原有的park方法。

LockSupport中的blocker

可能有很多读者对Blocker的原理有点好奇,既然线程都被阻塞了,是通过什么办法将阻塞对象设置到线程中去的呢? 不急不急,我们继续查看含有阻塞对象(Object blocker)的park方法。 我们发现内部都调用了setBlocker(Thread t, Object arg)方法。具体代码如下所示:

  1. private static void setBlocker(Thread t, Object arg) {
  2. U.putObject(t, PARKBLOCKER, arg);
  3. }
  4. 复制代码

其中 U为sun.misc.包下的Unsafe类。而其中的PARKBLOCKER是在静态代码块中进行赋值的,也就是如下代码:

  1. private static final sun.misc.Unsafe U = sun.misc.Unsafe.getUnsafe();
  2. static {
  3. try {
  4. PARKBLOCKER = U.objectFieldOffset
  5. (Thread.class.getDeclaredField(“parkBlocker”));
  6. //省略部分代码
  7. } catch (ReflectiveOperationException e) {
  8. throw new Error(e);
  9. }
  10. }
  11. 复制代码

Thread.class.getDeclaredField("parkBlocker")方法其实很好理解,就是获取线程中的parkBlocker字段。如果有则返回其对应的Field字段,如果没有则抛出NoSuchFieldException异常。那么关于Unsafe中的objectFieldOffset(Field f)方法怎么理解呢?

在描述该方法之前,需要给大家讲一个知识点。在JVM中,可以自由选择如何实现Java对象的"布局",也就Java对象的各个部分分别放在内存那个地方,JVM是可以感知和决定的。 在sun.misc.Unsafe中提供了objectFieldOffset()方法用于获取某个字段相对 Java对象的“起始地址”的偏移量,也提供了getInt、getLong、getObject之类的方法可以使用前面获取的偏移量来访问某个Java 对象的某个字段。

有可能大家理解起来比较困难,这里给大家画了一个图,帮助大家理解,具体如下图所示:

blocker.png

 

 

在上图中,我们创建了两个Thread对象,其中Thread对象1在内存中分配的地址为0x10000-0x10100,Thread对象2在内存中分配的地址为0x11000-0x11100,其中parkBlocker对应内存偏移量为2(这里我们假设相对于其对象的“起始位置”的偏移量为2)。那么通过objectFieldOffset(Field f)就能获取该字段的偏移量。需要注意的是某字段在其类中的内存偏移量总是相同的,也就是对于Thread对象1与Thread对象2,parkBlocker字段在其对象所在的内存偏移量始终是相同的。

那么我们再回到setBlocker(Thread t, Object arg)方法,当我们获取到parkBlocker字段在其对象内存偏移量后, 接着会调用U.putObject(t, PARKBLOCKER, arg);,该方法有三个参数,*个参数是操作对象,第二个参数是内存偏移量,第三个参数是实际存储值。该方法理解起来也很简单,就是操作某个对象中某个内存地址下的数据。那么结合我们上面所讲的。该方法的实际操作结果如下图所示:

blocker_set.png

 

 

到现在,我们就应该懂了,尽管当前线程已经阻塞,但是我们还是能直接操控线程中实际存储该字段的内存区域来达到我们想要的结果。

LockSupport底层代码实现

通过阅读源代码我们可以发现,LockSupport中关于线程的阻塞和唤醒,主要调用的是sun.misc.Unsafe 中的park(boolean isAbsolute, long time)unpark(Object thread)方法,也就是如下代码:

  1. private static final jdk.internal.misc.Unsafe theInternalUnsafe =
  2. jdk.internal.misc.Unsafe.getUnsafe();
  3. public void park(boolean isAbsolute, long time) {
  4. theInternalUnsafe.park(isAbsolute, time);
  5. }
  6. public void unpark(Object thread) {
  7. theInternalUnsafe.unpark(thread);
  8. }
  9. 复制代码

查看sun.misc.包下的Unsafe.java文件我们可以看出,内部其实调用的是jdk.internal.misc.Unsafe中的方法。继续查看jdk.internal.misc.中的Unsafe.java中对应的方法:

  1. @HotSpotIntrinsicCandidate
  2. public native void unpark(Object thread);
  3. @HotSpotIntrinsicCandidate
  4. public native void park(boolean isAbsolute, long time);
  5. 复制代码

通过查看方法,我们可以得出*终调用的是JVM中的方法,也就是会调用hotspot.share.parims包下的unsafe.cpp中的方法。继续跟踪。

  1. UNSAFE_ENTRY(void, Unsafe_Park(JNIEnv *env, jobject unsafe, jboolean isAbsolute, jlong time)) {
  2. //省略部分代码
  3. thread->parker()->park(isAbsolute != 0, time);
  4. //省略部分代码
  5. } UNSAFE_END
  6. UNSAFE_ENTRY(void, Unsafe_Unpark(JNIEnv *env, jobject unsafe, jobject jthread)) {
  7. Parker* p = NULL;
  8. //省略部分代码
  9. if (p != NULL) {
  10. HOTSPOT_THREAD_UNPARK((uintptr_t) p);
  11. p->unpark();
  12. }
  13. } UNSAFE_END
  14. 复制代码

通过观察代码我们发现,线程的阻塞和唤醒其实是与hotspot.share.runtime中的Parker类相关。我们继续查看:

  1. class Parker : public os::PlatformParker {
  2. private:
  3. volatile int _counter ;//该变量非常重要,下文我们会具体描述
  4. //省略部分代码
  5. protected:
  6. ~Parker() { ShouldNotReachHere(); }
  7. public:
  8. // For simplicity of interface with Java, all forms of park (indefinite,
  9. // relative, and absolute) are multiplexed into one call.
  10. void park(bool isAbsolute, jlong time);
  11. void unpark();
  12. //省略部分代码
  13. }
  14. 复制代码

在上述代码中,volatile int _counter该字段的值非常重要,一定要注意其用volatile修饰(在下文中会具体描述,接着当我们通过SourceInsight工具(推荐大家阅读代码时,使用该工具)点击其park与unpark方法时,我们会得到如下界面:

 

parker.png

 

 

从图中红色矩形中我们可也看出,针对线程的阻塞和唤醒,不同操作系统有着不同的实现。众所周知Java是跨平台的。针对不同的平台,做出不同的处理。也是非常理解的。因为作者对windows与solaris操作系统不是特别了解。所以这里我选择对Linux下的平台下进行分析。也就是选择hotspot.os.posix包下的os_posix.cpp文件进行分析。

Linux下的park实现

为了方便大家理解Linux下的阻塞实现,在实际代码中我省略了一些不重要的代码,具体如下图所示:

  1. void Parker::park(bool isAbsolute, jlong time) {
  2. //(1)如果_counter的值大于0,那么直接返回
  3. if (Atomic::xchg(0, &_counter) > 0) return;
  4. //获取当前线程
  5. Thread* thread = Thread::current();
  6. JavaThread *jt = (JavaThread *)thread;
  7. //(2)如果当前线程已经中断,直接返回。
  8. if (Thread::is_interrupted(thread, false)) {
  9. return;
  10. }
  11. //(3)判断时间,如果时间小于0,或者在*对时间情况下,时间为0直接返回
  12. struct timespec absTime;
  13. if (time < 0 || (isAbsolute && time == 0)) { // don’t wait at all
  14. return;
  15. }
  16. //如果时间大于0,判断阻塞超时时间或阻塞截止日期,同时将时间赋值给absTime
  17. if (time > 0) {
  18. to_abstime(&absTime, time, isAbsolute);
  19. }
  20. //(4)如果当前线程已经中断,或者申请互斥锁失败,则直接返回
  21. if (Thread::is_interrupted(thread, false) ||
  22. pthread_mutex_trylock(_mutex) != 0) {
  23. return;
  24. }
  25. //(5)如果是时间等于0,那么就直接阻塞线程,
  26. if (time == 0) {
  27. _cur_index = REL_INDEX; // arbitrary choice when not timed
  28. status = pthread_cond_wait(&_cond[_cur_index], _mutex);
  29. assert_status(status == 0, status, “cond_timedwait”);
  30. }
  31. //(6)根据absTime之前计算的时间,阻塞线程相应时间
  32. else {
  33. _cur_index = isAbsolute ? ABS_INDEX : REL_INDEX;
  34. status = pthread_cond_timedwait(&_cond[_cur_index], _mutex, &absTime);
  35. assert_status(status == 0 || status == ETIMEDOUT,
  36. status, “cond_timedwait”);
  37. }
  38. //省略部分代码
  39. //(7)当线程阻塞超时,或者到达截止日期时,直接唤醒线程
  40. _counter = 0;
  41. status = pthread_mutex_unlock(_mutex);
  42. //省略部分代码
  43. }
  44. 复制代码

从整个代码来看其实关于Linux下的park方法分为以下七个步骤:

  • (1)调用Atomic::xchg方法,将_counter的值赋值为0,其方法的返回值为之前_counter的值,如果返回值大于0(因为有其他线程操作过_counter的值,也就是其他线程调用过unPark方法),那么就直接返回。
  • (2)如果当前线程已经中断,直接返回。也就是说如果当前线程已经中断了,那么调用park()方法来阻塞线程就会无效。
  • (3) 判断其设置的时间是否合理,如果合理,判断阻塞超时时间阻塞截止日期,同时将时间赋值给absTime
  • (4) 在实际对线程进行阻塞前,再一次判断如果当前线程已经中断,或者申请互斥锁失败,则直接返回
  • (5) 如果是时间等于0(时间为0,表示一直阻塞线程,除非调用unPark方法唤醒),那么就直接阻塞线程,
  • (6)根据absTime之前计算的时间,并调用pthread_cond_timedwait方法阻塞线程相应的时间。
  • (7) 当线程阻塞相应时间后,通过pthread_mutex_unlock方法直接唤醒线程,同时将_counter赋值为0。

因为关于Linux的阻塞涉及到其内部函数,这里将用到的函数都进行了声明。大家可以根据下表所介绍的方法进行理解。具体方法如下表所示:

linux方法.png

 

 

Linux下的unpark实现

在了解了Linux的park实现后,再来理解Linux的唤醒实现就非常简单了,查看相应方法:

  1. void Parker::unpark() {
  2. int status = pthread_mutex_lock(_mutex);
  3. assert_status(status == 0, status, “invariant”);
  4. const int s = _counter;
  5. //将_counter的值赋值为1
  6. _counter = 1;
  7. // must capture correct index before unlocking
  8. int index = _cur_index;
  9. status = pthread_mutex_unlock(_mutex);
  10. assert_status(status == 0, status, “invariant”);
  11. //省略部分代码
  12. }
  13. 复制代码

其实从代码整体逻辑来讲,*终唤醒其线程的方法为pthread_mutex_unlock(_mutex)(关于该函数的作用,我已经在上表进行介绍了。大家可以参照Linux下的park实现中的图表进行理解)。同时将_counter的值赋值为1, 那么结合我们上文所讲的park(将线程进行阻塞)方法,那么我们可以得知整个线程的唤醒与阻塞,在Linux系统下,其实是受到Parker类中的_counter的值的影响的

LockSupport的使用

现在我们基本了解了LockSupport的基本原理。现在我们来看看它的基本使用吧。在例子中,为了方便大家顺便弄清blocker的作用,这里我调用了带blocker的park方法。具体代码如下所示:

  1. class LockSupportDemo {
  2. public static void main(String[] args) throws InterruptedException {
  3. Thread a = new Thread(new Runnable() {
  4. @Override
  5. public void run() {
  6. LockSupport.park(“线程a的blocker数据”);
  7. System.out.println(“我是被线程b唤醒后的操作”);
  8. }
  9. });
  10. a.start();
  11. //让当前主线程睡眠1秒,保证线程a在线程b之前执行
  12. Thread.sleep(1000);
  13. Thread b = new Thread(new Runnable() {
  14. @Override
  15. public void run() {
  16. String before = (String) LockSupport.getBlocker(a);
  17. System.out.println(“阻塞时从线程a中获取的blocker——>” + before);
  18. LockSupport.unpark(a);
  19. //这里睡眠是,保证线程a已经被唤醒了
  20. try {
  21. Thread.sleep(1000);
  22. String after = (String) LockSupport.getBlocker(a);
  23. System.out.println(“唤醒时从线程a中获取的blocker——>” + after);
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. }
  28. });
  29. b.start();
  30. }
  31. }
  32. 复制代码

代码中,创建了两个线程,线程a与线程b(线程a优先运行与线程b),在线程a中,通过调用LockSupport.park("线程a的blocker数据");给线程a设置了一个String类型的blocker,当线程a运行的时候,直接将线程a阻塞。在线程b中,先会获取线程a中的blocker,打印输出后。再通过LockSupport.unpark(a);唤醒线程a。当唤醒线程a后。*后输出并打印线程a中的blocker。 实际代码运行结果如下:

  1. 阻塞时从线程a中获取的blocker——>线程a的blocker数据
  2. 我是被线程b唤醒后的操作
  3. 唤醒时从线程a中获取的blocker——>null
  4. 复制代码

从结果中,我们可以看出,线程a被阻塞时,后续就不会再进行操作了。当线程a被线程b唤醒后。之前设置的blocker也变为null了。同时如果在线程a中park语句后还有额外的操作。那么会继续运行。关于为毛之前的blocker之前变为null,具体原因如下:

  1. public static void park(Object blocker) {
  2. Thread t = Thread.currentThread();
  3. setBlocker(t, blocker);
  4. U.park(false, 0L);//当线程被阻塞时,会阻塞在这里
  5. setBlocker(t, null);//线程被唤醒时,会将blocer置为null
  6. }
  7. 复制代码

通过上述例子,我们完全知道了blocker可以在线程阻塞的时候,获取数据。也就证明了当我们对线程进行问题排查和系统监控的时候blocker的有着非常重要的作用。

苹果IOS手机设置BurpSuite抓包(详细步骤)

0×01 添加BP的Proxy Listeners
点击Add添加

%title插图%num

端口填写8080,然后选择Specific address,选手机和电脑同时在的那个IP区段

IP具体查询方式使用Win+R

%title插图%num

输入ipconfig

然后看这个

%title插图%num

在Specific address中选择这个IP即可,配置完之后记得勾选

 

0×02 在Intercept Client Requests中勾选
Intercept requests based on the following rules和Automatically……edited,截图如下

%title插图%num

至此电脑端配置完成

0×03 在Safari浏览器中打开网址http://burp下载证书
打开网页后点击右上角的CA证书进行下载,省去苹果手机的邮箱操作。

 

0×04 手机管理证书
首先打开设置->通用->关于本机->描述文件

在里面配置好下载的证书

%title插图%num

然后在关于本机->证书信任设置里面开启信任

%title插图%num

0×05 手机设置代理
点开无线局域网(注意手机和电脑处于同一网络)

点击连接到的WIFI的旁边蓝色圆框(里面是i)

%title插图%num

配置代理改为手动

%title插图%num

服务器填写IP地址

IP地址和端口就是刚刚在电脑上用ipconfig查找到的

%title插图%num

至此就可以成功抓包了

 

iOS下的实际网络连接状态检测:RealReachability

序言
网络连接状态检测对于我们的iOS app开发来说是一个非常通用的需求。为了更好的用户体验,我们会在无网络时展现本地或者缓存的内容,并对用户进行合适的提示。对*大部分iOS开发者来说,从苹果示例代码改变而来的各种Reachablity框架是实现这个需求的普遍选择,比如这个库。但事实上,基于此方案的所有实现,都无法帮助我们检测真正的网络连接状态,它们能检测的只是本地连接状态;这种情况包括但不限于如下场景:
1.现在很流行的公用wifi,需要网页鉴权,鉴权之前无法上网,但本地连接已经建立;
2.存在了本地网络连接,但信号很差,实际无法连接到服务器;
3.iOS连接的路由设备本身没有连接外网。
cocoachina上已有很多网友对此进行提问和吐槽,比如:
如何判断设备是否真正连上互联网?而不是只有网络连接
[Reachability reachabilityWithHostName:]完全没用!

苹果的Reachability示例中有如下说明,告诉我们其能力受限于此:
“Reachability cannot tell your application if you can connect to a particular host, only that an interface is available that might allow a connection, and whether that interface is the WWAN.”
苹果的SCNetworkReachability API则告诉了我们更多:
“Reachability does not guarantee that the data packet will actually be received by the host. ”
而Reachability相关的所有框架在底层实现都是通过SCNetworkReachability进行检测,所以无法检测实际网络连接情况。
有鉴于此,笔者希望打造一个通用、简单、可靠的实际网络连接状态检测框架,于是RealReachability诞生了。

RealReachability简单介绍
RealReachability是笔者几个月前发布到github的开源库,目前有1000多个star,200多个fork,几经修改完善后,当前pod版本为1.1.5。
项目地址如下:
https://github.com/dustturtle/RealReachability。
此框架开发的初衷来源于项目实际需求,离线模式对网络连接状态的要求比较苛刻,且实际场景经常会遇到“伪连接”的情况,Reachability面对此场景力不从心。多方研究后引入了ping能力(此方案流量开销*小,也*简单),实现了简单的实际网络连接监测;后面经过提炼和优化,就有了这个框架。可以告诉大家的是,这个框架在appstore上架应用中已经经受了考验,目前也不断完善中,追求稳定的朋友可以使用*新的pod版本(修复了已知的*大部分问题,参考demo的使用方式即可)。

集成和使用介绍
集成
*简便的集成方法当属pod: pod ‘RealReachability’。
手动集成:将RealReachability文件夹加入到工程即可。
依赖:Xcode5.0+,支持ARC, iOS6+.项目需要引入SystemConfiguration.framework.
使用介绍
其接口的设计和调用方法和Reachability非常相似,大家可以无缝上手,非常方便。
开启网络监听(建议在didFinishLaunchingWithOptions中进行监听):

– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GLobalRealReachability startNotifier];
return YES;
}

监听网络变化通知:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(networkChanged:)
name:kRealReachabilityChangedNotification
object:nil];

通知回调代码示例:
– (void)networkChanged:(NSNotification *)notification
{
RealReachability *reachability = (RealReachability *)notification.object;
ReachabilityStatus status = [reachability currentReachabilityStatus];
NSLog(@”currentStatus:%@”,@(status));
}

触发实时网络状态查询代码示例:
[GLobalRealReachability reachabilityWithBlock:^(ReachabilityStatus status) {
switch (status)
{
case NotReachable:
{
// case NotReachable handler
break;
}

case ReachableViaWiFi:
{
// case ReachableViaWiFi handler
break;
}

case ReachableViaWWAN:
{
// case ReachableViaWWAN handler
break;
}

default:
break;
}
}];

查询当前实际网络连接状态:
ReachabilityStatus status = [reachability currentReachabilityStatus];

设置ping检测用的host服务器地址(可选):
注意:这里你需要确保该服务器支持ping操作。不设置的情况下我们默认使用www.apple.com作为ping服务器。
可以使用自己的服务器,或者使用稳定、可靠的网络地址(比如百度、qq等)。设置示例如下:

GLobalRealReachability.hostForPing = @”www.apple.com”;

获取当前的数据网络连接类型(高级功能):
WWANAccessType accessType = [GLobalRealReachability currentWWANtype];

该类型可以被用来优化应用程序的体验,比如不同的网络类型下设置不同的网络超时时间等,关于此方面的优化方案,可以参考携程给出的分享:http://www.infoq.com/cn/articles/how-ctrip-improves-app-networking-performance

Demo:
我们在github的repository中已经包含了简单的Demo工程,直接下载运行即可。相关的Api调用也可以参考demo中的实现。
demo截图:

RealReachability的实现原理
RealReachability架构图:

RealReachability主要包含3大模块:connection、ping、FSM;
其中Ping模块通过对同样是苹果提供的ping样例代码进行了封装,connection模块实现了基于SCNetworkReachability的本地状态检测,FSM模块是有限状态机。通过FSM的状态管理控制connection模块和Ping模块协同工作,并通过可配置的定时策略等业务逻辑优化,*终得到了我们的实现。
PS:其中connection模块和ping模块也可独立使用,分别提供本地网络检测和ping的能力,感兴趣的读者也可以尝试(调用方式请参考RealReachability开源代码)。

结束语
希望这个框架能够帮助到大家的iOS开发! 遇到任何疑问或者使用上的问题,都可以联系我,期待与您交流iOS开发技术(可以直接在我的博客提问或者email给我).

更新:github上的master版本已经支持IPV6,请开发者尽快升级。

什么是端口转发?什么是端口映射?如何设置端口映射?

很多朋友多次提到端口映射与端口转发的区别?这二者如果不细区分的,是容易弄混,今天我们来看下。

一、什么是端口映射

端口映射:端口映射就是将内网中的主机的一个端口映射到外网主机的一个端口,提供相应的服务。当用户访问外网IP的这个端口时,服务器自动将请求映射到对应局域网内部的机器上。

比如:

我们在内网中有一台Web服务器,但是外网中的用户是没有办法直接访问该服务器的。于是我们可以在路由器上设置一个端口映射,只要外网用户访问路由器ip的80端口,那么路由器会把自动把流量转到内网Web服务器的80端口上。并且,在路由器上还存在一个Session,当内网服务器返回数据给路由器时,路由器能准确的将消息发送给外网请求用户的主机。在这过程中,路由器充当了一个反向代理的作用,他保护了内网中主机的安全。

 

什么是端口转发?什么是端口映射?如何设置端口映射?

 

 

 

二、什么是端口转发

端口转发:端口转发(Port forwarding),有时被叫做隧道,是安全壳(SSH) 为网络安全通信使用的一种方法。

比如,我们现在在内网中,是没有办法直接访问外网的。但是我们可以通过路由器的NAT方式访问外网。假如我们内网现在有100台主机,那么我们现在都是通过路由器的这一个公网IP和外网通信的。那么,当互联网上的消息发送回来时,路由器是怎么知道这个消息是给他的,而另外消息是给你的呢?这就要我们的ip地址和路由器的端口进行绑定了,这时,在路由器中就会有一个内网ip和路由器端口对应的一张表。当路由器的10000端口收到消息时,就知道把消息发送给他,而当20000端口收到消息时,就知道把消息发送给你。这就是端口转发,其转发一个端口收到的流量,给另一个主机。

 

什么是端口转发?什么是端口映射?如何设置端口映射?

 

 

三、端口映射与端口转发的区别

这里举个例子方便大家就理解了:

端口转发:转发的重点在“转”上面。送快递的过来,把所有快递都交给门口保安,保安再根据每件快递上的收件人来分发。

端口映射:就是在大门口给每个人装个柜子,送快递的直接按收件人名字,把快递放到对应的柜子里面。

四、如何利用花生壳进行端口映射?实现外网访问本地服务器

映射实现外网访问本地服务器,当你的内外的内容需要外网的用户进行访问时,可以进行端口映射。

 

下载花生壳,注册后然后会分配到一个二级域名,设置内外网端口,或内网主机。

 

什么是端口转发?什么是端口映射?如何设置端口映射?

 

 

什么是端口转发?什么是端口映射?如何设置端口映射?

 

 

其中内网主机IP地址是你的内网同网段的ip地址,怎么查看自己的内网IP?请win键搜索框中输入cmd,打开命令行输入ipconfig,就可以查看到内网的ip段了。

然后设置路由器的端口映射功能。

 

什么是端口转发?什么是端口映射?如何设置端口映射?

 

 

有的路由器是直接在虚拟服务器中设置。

 

什么是端口转发?什么是端口映射?如何设置端口映射?

 

 

设置后,重启路由器,当然,如果端口被其它的占用了,可以使用其它的端口。

可以使用域名 25c8898k47.zicp.vip:55694 进行访问内网服务器。

五、路由器在网关中设置了端口映射,但外网访问不了的原因

1、首先检查您设置的端口映射是否正确映射到您内网的服务器。即您设置的转发规则的IP地址是否为您服务器的IP地址。

2、无法访问内网的服务器,请检查服务器是否开启了。您可以在局域网内使用其他的计算机访问一下您的服务器,确认服务器是否开启,并能成功访问。

3、请您检查您是否映射了服务器所需的所有端口,有可能没有完全设置访问服务器所需的端口,导致访问失败。您可以通过开启“DMZ主机”来检测一下,是否端口没有映射完全。若开启DMZ主机后,能正常访问服务器,则可能是端口添加不完全。

4、网关上是否设置了“防火墙”,是否将服务器通信的端口过滤,导致访问失败。

5、本机防火墙:因本地网络为信任区域,防火墙并不会阻止本地网络的访问,若访问源地址为外网地址(非信任区域地址),则可能会被防火墙拦截而无法访问。

什么是端口转发?什么是端口映射?如何设置端口映射?

 

 

6、服务商将相应端口屏蔽:在网关WAN口接一台电脑模拟内外网环境,若WAN口所接电脑可以正常访问内网服务器,但在Internet上无法访问,则可能是服务器将相应端口屏蔽导致虚拟服务器无法访问;

*后补充下:

1、若是80端口的WEB服务器,一定要注意网关的管理端口是否更改,否则两者会冲突;

2、检查虚拟服务器是否映射成功一定要从外网进行访问,不要从内网用WAN口IP去访问,这样在测试的时候是成功的,但实际外网并不能成功访问。

3、特殊的服务器,如网络摄像机及特殊的应用软件等,如果客户自己都不清楚通讯的端口,可先尝试DMZ主机以确认是否可以使用,同时建议客户咨询软件提供商。

iOS通过dSYM文件分析crash

我们在ios开发中会碰到的很多crash问题,如果Debug调试模式的话,我们可以往往很容易的根据log的输出定位到导致crash的原因,但对于已经上线的应用,或者是release环境包导致的crash,我们就需要一些特殊的手段来通过crash log进行分析定位了。

通过参考网上的一些资料,总结了一下,下面介绍一下通过dSYM文件以及crash log分析定位的方法。

1.导出crash log

通过Xcode的Organizer查看某台iphone设备的DeviceLog,选择需要的crash log,导出XXX.crash文件。

2.找到对应的app文件

找到当前iphone设备上安装的ipa文件,更改文件后缀名为zip,解压后得到Payload文件夹,你需要的app文件就在其中了。

3.找到对应build版本的dSYM文件

dSYM文件是iOS编译后保存16进制函数地址映射信息的文件,每次应用程序build后,都会生成对应的xxx.app, xxx.app.dSYM文件。

4.确定dSYM、app以及crash文件的关系

每一个xx.app, xxx.app.dSYM文件都拥有相应的uuid,crash文件也有uuid,只有三者uuid一至才表明之三者可以解析出正确的日志文件。
查看xx.app文件的uuid的方法,在terminal中输入命令:

dwarfdump –uuid xxx.app/xxx (xxx工程名)

查看xx.app.dSYM文件的uuid的方法,在terminal中输入命令:

dwarfdump –uuid xxx.app.dSYM (xxx工程名)

而.crash的uuid位于,crash日志中的Binary Images:中的*行尖括号内。如:

armv7 <8bdeaf1a0b233ac199728c2a0ebb4165>

将对应的xxx.app.dSYM文件以及xxx.app文件以及xxx.crash文件拷贝到同一文件夹中,如:~/Desktop/DebugLog。

5.通过symbolicatecrash分析crash文件

Xcode有自带的symbolicatecrash工具,可以通过dSYM文件将crash文件中的16进制地址转换成可读的函数地址。symbolicatecrash工具位于:
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKit.framework/Versions/A/Resources/symbolicatecrash(Xcode 4.5)
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/symbolicatecrash(Xcode 5.0)

该文件是隐藏文件,可以通过如下命令查找并拷贝到系统目录下,并建立快捷方式。

1)打开终端,进入到symbolicatecrash工具所在的文件夹目录

cd /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/PrivateFrameworks/DTDeviceKitBase.framework/Versions/A/Resources/

2)查找确认是否存在symbolicatecrash

ls -al | grep symbolicatecrash

3)将symbolicatecrash工具拷贝到/usr/bin目录下

sudo cp symbolicatecrash /usr/bin/symbolicatecrash

4)设置DEVELOPER_DIR系统变量

cd ~/

vi .bash_profile

并输入如下内容
export DEVELOPER_DIR=”/Applications/Xcode.app/Contents/Developer”

保存并退出

source .bash_profile

5)重启终端,确认是否已正确设置DEVELOPER_DIR系统变量

echo $DEVELOPER_DIR

查看输出结果是否为/Applications/Xcode.app/Contents/Developer

6)查看PATH系统变量是否存在如下路径/usr/bin

echo $PATH

7)如果PATH不存在如下路径/usr/bin,可在~/.bash_profile中添加如下代码

export PATH=”/usr/bin:$PATH”

保存并退出

source .bash_profile

8)上述准备工作完成后,进入dSYM和crash文件对应的文件夹目录,如

cd ~/Desktop/DebugLog

9)执行如下命令,即可正确解析crash文件

symbolicatecrash xxx.crash xxx.app.dSYM > test.txt

注意:symbolicatecrash的参数顺序,否则会报类似如下错误

Use of uninitialized value $data in substitution (s///) at /usr/bin/symbolicatecrash line 678.

Use of uninitialized value $data in substitution (s///) at /usr/bin/symbolicatecrash line 681.

Use of uninitialized value $data in substitution (s///) at /usr/bin/symbolicatecrash line 685.

Use of uninitialized value in pattern match (m//) at /usr/bin/symbolicatecrash line 404.

Use of uninitialized value in scalar assignment at /usr/bin/symbolicatecrash line 418.

No crash report version in XXX.app.dSYM/ at /usr/bin/symbolicatecrash line 954.

今天就先到这里,希望对大家有所帮助。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速