Quantcast
Channel: IT社区推荐资讯 - ITIndex.net
Viewing all 11805 articles
Browse latest View live

微信浏览器webview调试

$
0
0

头图

生命不息,折腾不止!

QQ浏览器提供微信调试的插件,本来应该是一件很值得高兴的事情,但是TX改不了一贯的作风,产品狗非要设计为强制设置默认浏览器且QQ所有链接都必须使用QQ浏览器打开,并且无法修改(老版本QQ可以设置)。

说实在的,其实体验蛮好的,默认导入书签(你TM经过我允许了?说不定密码也导入了,当初360浏览器就这么干了),提供IE、Chrome和Edge三个内核(三核浏览器从此诞生,吓尿了),不对,你自己的X5内核呢?不然就四核了。太多我不想吐槽……

于是我已经不打算用了那个好用的调试工具了(但是开发的这个工具还是很好的),然后想到之前的一篇文章: 屌爆了,完美调试 微信webview(x5),再次回顾了一下,QQ浏览器应该用的是同一招,细心的话会发现这应该就是调试工具的雏形。

TBS安装

  1. 可以先试试上面提到的文章中的办法(我没安装成功)

  2. 微信中打开网址 http://debugtbs.cc,安装本地TBS内核,不成功则安装线上TBS内核

  3. 微信调试工具提供的办法

ADB安装与启动

这是用于连接android手机的,具体安装参考文章中已经提到。 Android-SDK下载,设置环境变量可根据 window设置环境变量

  • 查看连接设备
    然后进入 D:\Program Files\AndroidSDK\platform-tools,打开cmd,执行

D:\Program Files\AndroidSDK\platform-tools>adb.exe devices
List of devices attached
95CANR4H6T9S7HPJ        device
  • 启动和停止ADB
    不需要配置指定设备,执行

D:\Program Files\AndroidSDK\platform-tools>adb.exe start-server
* daemon not running. starting it now on port 5037 *
* daemon started successfully *
D:\Program Files\AndroidSDK\platform-tools>adb.exe kill-server

调试服务

需要安装python(hint:2.7版本会出现常见的编码问题),下载参考文档中的 指定文件,打开手机端USB调试。

解压后进入 D:\Tools\wx_sq_webview_debug\inspector_client20150401打开cmd执行

python ./inspector.py --adb "D:\Program Files\AndroidSDK\platform-tools\adb.exe"

然后浏览器打开 http://localhost:9222/即可显示微信中打开的需要调试的页面。

一键执行

每次执行这些命令太麻烦,写个 bat文件一次性执行即可,第一次写,写的很丑:

@echo off
cd "D:\Program Files\AndroidSDK\platform-tools\"
start adb start-server
start %HOMEDRIVE%\Python27\pythonw "D:\Tools\wx_sq_webview_debug\inspector_client20150401\inspector.py" --adb "D:\Program Files\AndroidSDK\platform-tools\adb.exe"
echo begin server at:http://localhost:9222/
:begin
set /p INPUT= Stop server(Y/N)?:
if /i "%INPUT%"=="y" (goto stop
) else (
    goto begin
)
:stop
echo stop server
start adb kill-server
exit

自己修改对应的Android-SDK、Python和chrome inspector的路径即可。

Finally

当然是卸载QQ浏览器啊,瞬间清净了,不弹窗口了,手机也不会动不动安装各种应用了,不会默认安装手机QQ浏览器了,开开心心睡觉!

原文链接


说说下载劫持那些事儿

$
0
0

本文转载自「给产品经理讲技术」公号,已经过原作者授权转载。

今年的双十一,想必广大千手观音们又狠狠的剁了几只手。然而,剁手换来的宝贝在漫漫快递路上也是命途多舛,轻者磕磕碰碰包装损毁,重者与快递货车一起被付之一炬。这些“不可抗力”造成的问题屡见不鲜,碰到了也只能自认倒霉。不过,有的网友看着苹果6代的订单,却啃着寄过来的6袋苹果,个中滋味大家就自行脑补吧…。其实,在安卓应用分发领域,这种“苹果6代”变“6袋苹果”的情况也屡见不鲜:

为什么会出现这种情况呢?其实一次网络下载的过程,就像一次“网购”,当我们点击下载按钮时,就跟下载服务器下了一份“订单”,“订购”了一个文件(当然大部分是免费的),服务器确认“订单”后,就会将文件在网络中“快递”(传输)到用户的终端(手机、PC等)。下载劫持一般出现在“下订单”的过程中。

举个栗子,假设我们通过微信官网的链接http://dldir1.qq.com/weixin/android/weixin637android660.apk下载微信安卓版本的客户端,整个流程大概是这个样子:

当点击了下载按钮后,客户端会通过url中的“域名”“dldir1.qq.com”来向DNS服务器获取确认“订单(下载)服务器”的IP地址,IP地址在互联网中相当于日常生活中“电话号码”,有了它,就可以连接到这台“订单(下载)服务器”,而DNS服务器就像一个存贮着大量“姓名”(域名)和“电话号码”(IP地址)的黄页。

当客户端获得了“订单(下载)服务器”的“电话号码”(ip地址)后,就会连接“订单(下载)服务器”,并告知“订单(下载)服务器”客户端需要获取服务器上的“微信安卓版”apk文件,一般情况下,服务器在这个阶段确认了“订单”后,就会向客户端“快递”(传输)对应的apk文件,当客户端将文件下载完毕后,这次“网购”也就完成了。

下面,我们引入运营商(电信、联通等)网关的概念。运营商网关可以类比成日常生活中的“总机”,接入运营商的互联网设备想要能够“上网”,都需要经过“总机”(运营商网关)的转接。也就是说,在上图中的第二步,我们并不能直接通过“订单(下载)服务器”的“电话号码”(IP地址)联系到“订单(下载)服务器”,而是需要先连接到“总机”(网关),并且告诉它,我们要向某某某服务器下“订单”,经过“总机”的转接,我们才能真正连接到“订单(下载)服务器”。整个过程如下图:

可以发现,DNS服务器和网关的决策,确定了客户端“订单”(下载请求)的走向。而“下载劫持”也就发生在这两个关键节点上。

假设客户端获取下载服务器“电话号码”的DNS服务器被篡改,那么客户端可能会通过“dldir1.qq.com”查询到一个“骗子服务器”的“电话号码”(IP地址),当我们联系到这个“骗子服务器”时,我们的“订单”(下载请求)可能会换来一些奇奇怪怪的“商品”。


当我们遇到这种情况时,可以手动修改DNS服务器IP(具体方法请问度娘)来解决。然而当运营商的“总机”(网关)“出了问题”(这些“问题”一般是运营商主动造成的)时,就没那么容易解决了。

假设当客户端拿着“订单(下载)服务器”的电话号码要求“总机”(网关)转接到我们指定的“订单(下载)服务器”时,“总机”(网关)对客户端说“哎呀,不要去A家下载微信了,你去我给你介绍的B家下载“XX助手”吧,比微信好用”(这个过程在技术上是被一个叫做302跳转的机制完成的,如果你不知道什么是302,出门左转,查询我们星期一的文章)。客户端是个实在人,就这样被“引诱”到B家的服务器上下载了。

“总机”(网关)和服务器B就这样沆瀣一气,来骗客户端的下载量。

刚刚给大家从技术层面简单介绍了下“下载劫持”的“饭醉手段”,至于为什么有人要做“下载劫持”,想必产品同学们应该比我更能知晓其中的奥妙,我就不班门弄斧了。就写到这里吧,我去洗水果了,双十一寄来的六袋苹果,再不吃就烂了….

(文中“XX助手”和服务器IP地址均属臆造,如有雷同,我也不知道是咋回事儿)

原文链接: 点击查看

相关日志

APP的推送是咋回事

$
0
0

本文转载自「给产品经理讲技术」公号,已经过原作者授权转载。

相信大家对推送这项技术并不陌生。如果没听说过,那么作为一个充满好奇心的孩子,你一定想过这个问题:睡觉前我明明关闭了淘宝、网易新闻等app,为什么第二天他们又自动出现在我手机的通知栏上呢?这其实就是推送系统干的好事:在你睡觉的时候,服务器悄悄的向你的手机推送了一个消息,然后唤醒了你已经关闭的app。事实上,无论你愿意与否,现在大多数‘有节操’的app,都已经内置了推送系统,并时刻准备着登上你的通知栏的‘头条’。

传统的app架构里,通常是app主动向服务器请求数据,服务器被动的提供数据。以新闻客户端app为例:app被用户打开的时候,会通过网络(无论3g、4g还是wifi)连接到服务器上,向服务器请求最新的新闻。服务器收到请求,从自己的数据库里查询最新的新闻,返回给app。app收到服务器返回的数据,经过一系列的解析处理操作,最终把最新的新闻呈现给用户。一次通信就完成了。然而如果此时服务器上又有了新的新闻,无论多么重要,在用户没有主动刷新的情况下,是没有办法让用户看到的。推送就是为了解决这样的困境的,它给了服务器一个展示自我的机会,主动连接上所有的app,告诉他们我有新的新闻了,你们再来请求一次吧,于是收到推送的app(即时此时已经被用户关闭了)又去服务器请求最新的新闻,这样用户就能看到最新的新闻了。

从技术上来讲,实现一个推送系统需要服务器端和终端的配合。一种方法是轮询,也就是不停的向服务器发起请求。这其实很好理解,作为app,我既然不知道什么时候会发生新的新闻,那我一遍一遍的问好了,而且我知道这样一定会成功的。显而易见,这种方法app端费时费力不说,电量流量也扛不住啊,服务器要处理如此量大的请求,必然也是非常头疼的。另一种方法是服务器和app建立一个长时间连接的通道,通过这个通道,不仅app可以向服务器请求数据,服务器也可以向app发送数据,看起来非常完美,但是如果app被用户关闭的话,通道就断掉了。好在android系统给app提供了一个这样的环境,app可以启动一个后台服务来维持这个通道,即使app被关掉了,服务依然可以运行,通道依然还在工作(ios后面会讲)。回到前面的例子,你在睡觉前关掉了淘宝,但是并没有关闭淘宝的后台服务,淘宝依然可以接收服务器推送来的指令,把自己的唤醒。

那么如何维持这样的一条长时间连接的通道呢?就好比两个人打电话,一开始聊的热情有来有往,后来慢慢沉默下来了,几分钟之后,电话的另一头没有任何动静,如何知道那边的人还在呢?很简单,只需要另一头的人每隔几分钟说一个字就行。同样的道理,app会每隔一段时间向服务器报告自己还活着,就像心跳一样,服务器收到后,就知道这个通道是可以继续使用的了。然而天下没有免费的午餐,发送心跳是有代价的,一般手机锁屏之后,为了省电CPU是出于休眠状态的,然而发送心跳就会唤醒CPU,必然会增加电量的消耗。这还只是一个长连接通道的情况,如果手机里装了2、30个带有推送的app呢?先别急着抱怨,聪明的android工程师和ios工程师早就想到了这一点,他们分别设计了GCM和apns来解决多个app有多个长连接通道的问题。以apns为例,ios开通了一条系统级别的长连接通道,通道的一端是手机的所有app,另一端是苹果的服务器。app的服务器如果有新的消息需要推送的话,先把消息发送到苹果的服务器上,再利用苹果的服务器通过长连接通道发送到用户手机,然后通知具体的app。这样就做到了即使手机安装了100个app,也只需要向一条通道里发送心跳。

回到Android,系统提供的GCM只能在Android2.2以上才能使用,3.0以下必须要安装Googleplay并登陆了Google账号才能支持。而国内发行的手机大多是阉割掉了google 服务的。因此,对于Android系统来说,各家app只能各显神通,开发自己的专用长连接通道了。然而这时候他们遇到了app的天敌:管家和卫士们。前文说了,app想要及时收到服务器推送的消息,关键在于自己与服务器的长连接通道不被关闭,也就是自己的后台服务可以一直在后台运行,而管家和卫士们的一键清理功能就是专治这种“毒瘤”的。道高一尺魔高一丈,app在与管家和斗士们的长期斗争中,总结了一系列躲避被清理掉的方法,什么定时自启能力、什么相互唤醒、什么前台进程等等,当然这就是另一个话题了,我们后面会讲到。

总结起来,app和后台的连接方式有两种。一种叫pull,也叫轮询,就是定期的不断向后台请求,缺点是耗电,费流量,不环保。对于一名有追求的程序员,他应该会比较恶心这种方式的,你千万不要对他说,我不管你怎么实现,我就要这种效果这种傻逼话了,凡事应该找到最优路径。另一种叫push,app和后台一直维持了一条通信通道,两端不定期的就会偷摸的约会,告诉对方“I‘m Here”,也能顺带把信息互相携带了。缺点是要维持一条长连接通道,这条通道容易被其他程序杀死,要多想复活办法。

原文链接, 点击查看

相关日志

让我们来谈谈分工

$
0
0

Division of Labour昨天,我看到 一个新闻——雅虎取消了QA团队,工程师必须自己负责代码质量,并使用持续集成代替QA。 同时,也听到网友说,“听微软做数据库运维的工程师介绍,他们也是把运维工程师和测试工程师取消了,由开发全部完成。每个人都是全栈工程师”。于是,我顺势引用了几年前写过一篇文章《 我们需要专职的QA吗?》,并且又鼓吹了一下全栈。当然,一如既往的得到了一些的争议和嘲弄;-)。

有人认为取消QA基本上是公司没钱的象征,这个观点根本不值一驳,属于井底之蛙。有人认为,社会分工是大前提,并批评我说怎么不说把所有的事全干的,把我推向了另外一个极端。另外,你千万不要以为有了分工,QA的工作就保得住了。

就像《乔布斯传》中乔布斯质疑财务制度的时候说的,有时候,很多人都不问为什么,觉得存在的东西都是理所应当的东西。让我们失去了独立思考的机会。分工也是一样。

所以,为了说完整分工这个逻辑。请大家耐住性子,让我就先来谈谈“分工的优缺点”吧。

分工的优点和缺点

首先,分工(Division of Labour)应该是由 Adam Smith在1776年的《 国富论》中提出来的,Adam在那时候就观察到分工对于手工业生产效率的提高。他将效率提高的原因归结于三点:

  • 熟练程度的增加。当一个工人单纯地重复同一道工序时,其对这道工序的熟练程度会大幅增加。 表现为产量和质量的提高
  • 如果没有分工,由一道工序转为另一道工序时会损失时间,而分工避免了这中间的损失。
  • 由于对于工序的了解和熟练度的增加, 更有效率的机械和工具被发明出来,从而提高了产量

分工的确是提高生产力。我想到了福特公司一开始做出来的汽车几乎买不出去,原因有两个,一个是成本太高,另外是生产太复杂,产能太低。于是福特公司开始把制造一辆汽车的工序分解开来,进行分工,分工给福特公司带来的好处是:

  1. 很多工作可以并行了,而且 因为事情变得简单后,执行力也变强了
  2. 一个非常复杂和高深的汽车制造因为分工后, 很多工作不需要很NB的人来干了,只需要一般劳动者经过简单的培训就可以干了。而且,越干越熟练,越干越专业,最终可能让合适的人合适的事。
  3. 分工后导致了很多重复劳动可以用技术来解决,于是福特公司出现生产流水线的技术(你是否还记得卓别林《摩登时代》里的工业生产流水线的场景,那取自福特公司)。

于是,福特公司的生产效率大大提高,最终实现了让每个美国家庭都能买得起汽车的理想,同时让美国成为了轮子上的国家。

不过,我们需要注意的是,在《国富论》中,Adam他同时也提到,分工如果过细,同样会带来问题—— 简单重复的劳动会让人变成一个不会思考的机器,从而越来越笨,进而变成平庸的无技能的人。自“分工”出现以后,争论就没有停止过。

Karl Max同样认为 分工越来越细,会导致人的技术越来越差,同时,大量的重复劳动也会导致人对工作的失出热情,产生厌倦和抵触心理,最终会导致生产力的下降

同时,还有一些经济学家也同样表明分工的一些缺点:

  • 导致人只关注整个事情中的一小块,缺乏全局视角,导致视野受限,没有完全领会工作的意义和目标,从而导致各种返工
  • 对于组织而言,分工也会导致出现大量的沟通协同成本,并出现碎片的生产方式,以及组织的孤岛形式,并不利于提高生产力

当然,奥地利经济学家 Ludwig von Mises 并不这么认为,他认为,在分工所得到的好处面前,这些副作用不算什么。并且,他认为在资本主义的制度下,完全是可以平衡分工的各种优点和各种缺点,从而可以达到提高生产力和提高人员素质的双赢解的。

比如说, 分工中的各种沟通问题是可以通过一个标准协议来解的,造灯泡的,造开关的,造灯座的完全不知道对方的存在,他们只所以可以让做出来的东西拼在一起,完全是通过了一种标准协议完成的。 这也是为什么这个世界上有各种各样的标准化的组织

还有很多经济学家对分工都有自己的见解和想法。不过基本上就是上面这些Pros和Cons了。下图是一个PPT的两个slids,可以点击看大图( 来源

lecture-5-10-728lecture-5-11-728

全球化下的分工

分工带来问题在全球化的浪潮下变得尤为突出。其委婉地被讲成是比较优势( Comparative Advantage

比较优势(又叫 相对优势)是经济学的概念,解释了为何在拥有相对的机会成本的优势下生产,贸易对双方都有利。当一方(一个人,一间公司,或一国)进行一项生产时所付出的机会成本比另一方低,这一方面拥有了进行这项生产的比较优势。于是,一个国家倘若专门生产自己相对优势较大的产品,并通过国际贸易换取自己不具有相对优势的产品就能获得利益。

于是乎,分工本来想要的是——合适的人干合适的事, 但是在比较优势的情况下,商业社会把分工变成了—— 不是选择合适的人、公司或国家,而是选择成本低的人、公司或国家

经济合作与发展组织 OECD最近(2015年6月28日)对全球化这样建议的——

“有效率的政策的本质不是阻止失业而是鼓励就业,如果各个国家都在收获全球化的利益而不是开放贸易的话,那么一些地方就会失去工作机会,当然也伴随着在另一些地方出现新的工作机会,这是全球化进程不可避免的,而我们面对的挑战是怎么能流畅调整我们的流程,能为那些新出现的工作机会找到合适的技能匹配的工人”。

通过上面的说明,我想你可以知道,为什么中国成为了世界劳动力大国,而为什么当初美国科技公司进入中国的时候,首先把测试的工作放到了中国。这就是所谓的全球化分工。同时我们也可以看到,像我们中国这样技术能力的确非常不足的国家,的确是可以通过分工这种形式,让我们这些技能一般的技术人员参与一个复杂的有技术含量的项目当中。这其中就是分工的光明面和阴暗面。

那么,我们想一想, 随着中国的人力成本的越来越大,国际化的分工因为商业资本的因素,必然不会选择中国,只会选择人力成本更低的国家,比如印度、越南、甚至人力成本更低的国家。美国雅虎和Adobe不是离开中国了么?再看看中国因为人民币的汇率或是人力成本的上升,我们在早几年关了多少个Made in China的工厂,这就是全球化的分工,商业上来说,他不是找最合适的人,而是找成本最低的人。

所以, 你千万不要以为我一提倡全栈了,你QA的工作就保不住了,就算没有全栈,就算是你还在坚持的社会化的分工,也可能让你的QA的工作就保不住了,除非,你能提供更低的价格。(想想这其中的逻辑吧,人家美国人把一些技术工作(比如测试)外包到中国的原因不是因为中国人聪明,想得周全,适合干这个测试这个事,而是因为中国人廉价,所以,当中国不在廉价了,自然就会找更廉价的地方了)

为什么国家要从Made in China转型?不就是因为中国早期拿到的国际化分工就是这些没有技术含量的支持性的分工么?也因此而造就了大量的技能很一般的工人。为了能在全球化分工中能拿到更有质量的工作, 我们必然要从劳动密集型转向成知识密集型,必然要从支持性的工作转变为产出性的工作,必然需要单一技能型的技工转变为复合型的人才

分工的温床和天敌

分工的温床主要有两个

  • 一个是成本和效率,资本家或企业主或一个国家为了追求更快成本更底的生产方式,他们必然会进行大规模的分工,伴随着分工,他们也会把一些知识或技术密集型的工作生生地变成劳动密集型的工作。然后层层外包。
  • 一个是组织的大小,当一个组织的人数不断的变大,那么,你只能把工作和任务分得更细。这是被人数逼的,而不是实际需要的。这就是为什么我们可以看到很多大公司里要么人浮于事,要么瞎忙。

分工的天敌主要有一个——那就是技术

每当新技术出现的时候,一些复杂的工序会被一台机器或是一种高超的技术所取代,不管是被技术自动化,还是被技术所简化 总之,以前本来需要数十人或是数百人才能干的事,突然之间只需要一个人就可以干完了。生产力得到了巨大的释放。所以,你这就是我们常听的—— 科技是第一生产力!

所以, 当你面对一些难题的时候,比如线上的故障,或是一个复杂的软件生产活动,你是要加更多的流程更多的人呢,还是要用技术解决问题呢?一边是温床,一边是天敌,你想好了吗?

什么样分工才是好的

分工是必然的,因为很简单,你不可能一个人干完所有的事情,所以必需要分工, 分工不是问题,而问题则变成了——什么样的分工是理想的,是优雅的,是有效率的?

华君武漫画《科学分工?》

华君武漫画《科学分工?》

对于分工来说,一般来是一种组织和管理形为。就目前来说,现代的公司有两种分工模式,分别是 ControlCommitment这两种分工。

  • Control就是控制型的管理,它是一种是基于工作技能的分工,于是员工会被这种分工分配到一个比较窄的技能里去完成一个非常明确的工作
  • 而Commitment则是面向员工的责任心和所承担的目标来分工并完成工作的。相比起前者来说,这样的分工在完成工作时,需要的不仅仅是技能,还需要更多的责任感

这么说吧,

  • 对于基于工作技能的分工,你会看到,这样的公司会把技术人员按编程语言来分,比如:Java、PHP、C/C++,或是分成:Web端、iOS端、Android端、后端、算法、数据。或是分成:开发,测试,运维。
  • 对于基于Commitment的分工,你会看到他们这样分的,软件工程师(不分前后端,不分语言,不分运维,测试),因为这样的公司认为,他招的不是只有特定语言技能的Coder,而是而学多种语言多种技术能保证软件质量以及能对软件维护的软件工程师。这种公司的软件工程师是各种团队都可以去的,而他们的分工更多的是按软件的功能,软件的模块,或是软件的产品线来分工。

基于技能的分工已是过去时,而基于 Commitment 的分工是更有效率的分工的未来。你可以参看McAlister-Kizzier, Donna. 的文献 “ Division of Labor.” 。

小结

我说了这么多,不知道你看懂了我想表达什么没有?我不强加我的价值观,只希望你自己问自己几个问题:

1)作为工作的人,在分工中你会怎样选择?是成为一颗棋子,一颗螺丝钉,还是成为一个多面手?

2)作为工作的人,当你选择工作或任务的时候,你是选择做支持性的工作,还是做产出性的工作?你是选择做劳动密集型重复工作,还是做知识密集型的创新性的工作?

3)作为老板,你是想要什么样的员工?听话的只会加班和干重复工作的劳动力,还是有责任心的为企业和产品负责的员工?

4)作为老板,你是想通过分工释放低端员工的生产力,还是通过科技或技术去创造更NB的生产力?

5)作为老板,分工中的问题,你找到比较优的解了吗?比如,对于不同团队间的协议,你找到了吗?

可能,在不同的情况下你会有不同的答案。但是对我来说呢,无论是什么情况,我都只会有一个答案。

(全文完)


欢迎关注CoolShell微信公众账号

(转载本站文章请注明作者和出处 酷 壳 – CoolShell.cn,请勿用于任何商业用途)

——=== 访问 酷壳404页面寻找遗失儿童。 ===——

相关文章

让PHP7达到最高性能的几个Tips

$
0
0

标签:   PHP7

  PHP7已经发布了,  作为PHP10年来最大的版本升级, 最大的性能升级, PHP7在多放的测试中都表现出很明显的性能提升, 然而, 为了让它能发挥出最大的性能, 我还是有几件事想提醒下.

PHP7 VS PHP5.6

1. Opcache

记得启用Zend Opcache, 因为PHP7即使不启用Opcache速度也比PHP-5.6启用了Opcache快, 所以之前测试时期就发生了有人一直没有启用Opcache的事情. 启用Opcache非常简单, 在php.ini配置文件中加入:

zend_extension=opcache.so
opcache.enable=1
opcache.enable_cli=1"

2. 使用新的编译器

  使用新一点的编译器, 推荐GCC 4.8以上, 因为只有GCC 4.8以上PHP才会开启Global Register for opline and execute_data支持, 这个会带来5%左右的性能提升(Wordpres的QPS角度衡量)

  其实GCC 4.8以前的版本也支持, 但是我们发现它支持的有Bug, 所以必须是4.8以上的版本才会开启这个特性.

3.  HugePage

  我之前的文章也介绍过: 让你的PHP7更快之Hugepage , 首先在系统中开启HugePages, 然后开启Opcache的huge_code_pages.

  以我的CentOS 6.5为例, 通过:

$sudo sysctl vm.nr_hugepages=512

分配512个预留的大页内存:

$ cat /proc/meminfo  | grep Huge
AnonHugePages:    106496 kB
HugePages_Total:     512
HugePages_Free:      504
HugePages_Rsvd:       27
HugePages_Surp:        0
Hugepagesize:       2048 kB

  然后在php.ini中加入:

 opcache.huge_code_pages=1

  这样一来, PHP会把自身的text段, 以及内存分配中的huge都采用大内存页来保存, 减少TLB miss, 从而提高性能.

4. Opcache file cache

开启Opcache File Cache(实验性),  通过开启这个, 我们可以让Opcache把opcode缓存缓存到外部文件中, 对于一些脚本, 会有很明显的性能提升.
  在php.ini中加入:

opcache.file_cache=/tmp

  这样PHP就会在/tmp目录下Cache一些Opcode的二进制导出文件, 可以跨PHP生命周期存在.

5. PGO

我之前的文章: 让你的PHP7更快(GCC PGO)也介绍过, 如果你的PHP是专门为一个项目服务, 比如只是为你的Wordpress, 或者drupal, 或者其他什么, 那么你就可以尝试通过PGO, 来提升PHP, 专门为你的这个项目提高性能.

具体的,  以wordpress 4.1为优化场景.. 首先在编译PHP的时候首先:

$ make prof-gen

然后用你的项目训练PHP, 比如对于Wordpress:

$ sapi/cgi/php-cgi -T 100 /home/huixinchen/local/www/htdocs/wordpress/index.php >/dev/null

也就是让php-cgi跑100遍wordpress的首页, 从而生成一些在这个过程中的profile信息.

最后:

$ make prof-clean
$ make prof-use && make install

这个时候你编译得到的PHP7就是为你的项目量身打造的最高性能的编译版本.

暂时就这么多吧, 以后想起来再加, 欢迎大家尝试, thanks


Comments

Copyright © 2010 风雪之隅版权所有, 转载务必注明. 该Feed只供个人使用, 禁止未注明的转载或商业应用. 非法应用的, 一切法律后果自负. 如有问题, 可发E-mail至my at laruence.com.(Digital Fingerprint: 73540ba0a1738d7d07d4b6038d5615e2)


您可能还对下面的文章感兴趣:

  1. PHP7 VS HHVM (WordPress) [2014-12-29 00:02:16]

基于Hadoop datajoin包开发Reduce join及针对MRV2优化

$
0
0

        编写不易,转载请注明(http://shihlei.iteye.com/blog/2263757)! 

 

        最近项目,需要对两个文件进行连接查询,从文件2中提取在文件1中选线id的记录。
主要问题:两个文件都很大【 文件1:1亿记录 ; 文件2:8亿记录 】 
方案:

  • 方案1:Map启动将文件1表示读取bloomfilter,map处理文件2,发现存在即输出。
    问题:文件1过大,读取时间长,task直接timeout被kill。

  • 方案2:使用Reduce端join,使用Hadoop data-join包的api进行连接

一 Hadoop Reduce Join

1思想

        根据输入标记数据源,根据提供的Group key 分组,在Reduce侧处理组内容,完成连接。

2 实现

(1)定义可标记的输出类型
	/**
	 * 
	 * 1 根据文件名作为tag,区分数据来源 2 将数据封装成TaggedMapOutput 对象,并打上必要的tag 3 生成group
	 * by的分组key,作为依据
	 * 
	 * */
	public static class JoinMapper extends DataJoinMapperBase {

		/**
		 * 读取输入的文件路径
		 * 
		 * **/
		protected Text generateInputTag(String inputFile) {
			// 取文件名的A和B作为来源标记
			String datasource = StringUtils.splitByWholeSeparatorPreserveAllTokens(inputFile, ".", 2)[0];
			return new Text(datasource);
		}

		/***
		 * 分组的Key
		 * 
		 * **/
		protected Text generateGroupKey(TaggedMapOutput aRecord) {
			String line = ((Text) aRecord.getData()).toString();
			if (StringUtils.isBlank(line)) {
				return null;
			}
			// 去每个文件的第一个字段作为连接key
			String groupKey = StringUtils.splitByWholeSeparatorPreserveAllTokens(line, ",", 2)[0];
			return new Text(groupKey);
		}

		/**
		 * 对文件打上标记
		 */
		protected TaggedMapOutput generateTaggedMapOutput(Object value) {
			TaggedWritable retv = new TaggedWritable((Text) value);
			retv.setTag(this.inputTag);
			return retv;
		}
	}

	// 自定义输出类型=====================================================
	public static class TaggedWritable extends TaggedMapOutput {
		private Writable data;

		// 需要定义默认构造函数,否则报错
		public TaggedWritable() {
			this.tag = new Text();
		}

		public TaggedWritable(Writable data) {
			this.tag = new Text("");
			this.data = data;
		}

		public Writable getData() {
			return data;
		}

		public void setData(Writable data) {
			this.data = data;
		}

		public void write(DataOutput out) throws IOException {
			this.tag.write(out);
			// 由于定义类型为WriteAble 所以不好使
			out.writeUTF(this.data.getClass().getName());
			this.data.write(out);
		}

		public void readFields(DataInput in) throws IOException {
			this.tag.readFields(in);
			this.data.readFields(in);
			String dataClz = in.readUTF();
			try {
				if (this.data == null || !this.data.getClass().getName().equals(dataClz)) {
					this.data = (Writable) ReflectionUtils.newInstance(Class.forName(dataClz), null);
				}
				this.data.readFields(in);
			} catch (ClassNotFoundException cnfe) {
				System.out.println("Problem in TaggedWritable class, method readFields.");
			}
		}
	}

遇到的坑:——跟Hadoop实战的区别

 

a)没有默认构造函数


报错:

java.lang.Exception: java.lang.RuntimeException: java.lang.NoSuchMethodException: x.bd.hadoop.join.MR1ReduceJoinJob$TaggedWritable.<init>()
  at org.apache.hadoop.mapred.LocalJobRunner$Job.runTasks(LocalJobRunner.java:462)
  at org.apache.hadoop.mapred.LocalJobRunner$Job.run(LocalJobRunner.java:529)
Caused by: java.lang.RuntimeException: java.lang.NoSuchMethodException: x.bd.hadoop.join.MR1ReduceJoinJob$TaggedWritable.<init>()
  at org.apache.hadoop.util.ReflectionUtils.newInstance(ReflectionUtils.java:131)
  at org.apache.hadoop.io.serializer.WritableSerialization$WritableDeserializer.deserialize(WritableSerialization.java:66)
  at org.apache.hadoop.io.serializer.WritableSerialization$WritableDeserializer.deserialize(WritableSerialization.java:42)
  at org.apache.hadoop.mapred.Task$ValuesIterator.readNextValue(Task.java:1421)
  at org.apache.hadoop.mapred.Task$ValuesIterator.next(Task.java:1361)
  at org.apache.hadoop.mapred.ReduceTask$ReduceValuesIterator.moveToNext(ReduceTask.java:220)
  at org.apache.hadoop.mapred.ReduceTask$ReduceValuesIterator.next(ReduceTask.java:216)
  at org.apache.hadoop.contrib.utils.join.DataJoinReducerBase.regroup(DataJoinReducerBase.java:106)
  at org.apache.hadoop.contrib.utils.join.DataJoinReducerBase.reduce(DataJoinReducerBase.java:129)
  at org.apache.hadoop.mapred.ReduceTask.runOldReducer(ReduceTask.java:444)
  at org.apache.hadoop.mapred.ReduceTask.run(ReduceTask.java:392)
  at org.apache.hadoop.mapred.LocalJobRunner$Job$ReduceTaskRunnable.run(LocalJobRunner.java:319)
  at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
  at java.util.concurrent.FutureTask.run(FutureTask.java:266)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
  at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.NoSuchMethodException: x.bd.hadoop.join.MR1ReduceJoinJob$TaggedWritable.<init>()
  at java.lang.Class.getConstructor0(Class.java:3082)
  at java.lang.Class.getDeclaredConstructor(Class.java:2178)
  at org.apache.hadoop.util.ReflectionUtils.newInstance(ReflectionUtils.java:125)

解决:

添加默认构造器。

  

b)反序列化readFields空指针

 

报错:

java.lang.Exception: java.lang.NullPointerException
  at org.apache.hadoop.mapred.LocalJobRunner$Job.runTasks(LocalJobRunner.java:462)
  at org.apache.hadoop.mapred.LocalJobRunner$Job.run(LocalJobRunner.java:529)
Caused by: java.lang.NullPointerException
  at x.bd.hadoop.join.MR1ReduceJoinJob$TaggedWritable.readFields(MR1ReduceJoinJob.java:160)
  at org.apache.hadoop.io.serializer.WritableSerialization$WritableDeserializer.deserialize(WritableSerialization.java:71)
  at org.apache.hadoop.io.serializer.WritableSerialization$WritableDeserializer.deserialize(WritableSerialization.java:42)
  at org.apache.hadoop.mapred.Task$ValuesIterator.readNextValue(Task.java:1421)
  at org.apache.hadoop.mapred.Task$ValuesIterator.next(Task.java:1361)
  at org.apache.hadoop.mapred.ReduceTask$ReduceValuesIterator.moveToNext(ReduceTask.java:220)
  at org.apache.hadoop.mapred.ReduceTask$ReduceValuesIterator.next(ReduceTask.java:216)
  at org.apache.hadoop.contrib.utils.join.DataJoinReducerBase.regroup(DataJoinReducerBase.java:106)
  at org.apache.hadoop.contrib.utils.join.DataJoinReducerBase.reduce(DataJoinReducerBase.java:129)
  at org.apache.hadoop.mapred.ReduceTask.runOldReducer(ReduceTask.java:444)
  at org.apache.hadoop.mapred.ReduceTask.run(ReduceTask.java:392)
  at org.apache.hadoop.mapred.LocalJobRunner$Job$ReduceTaskRunnable.run(LocalJobRunner.java:319)
  at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
  at java.util.concurrent.FutureTask.run(FutureTask.java:266)
  at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
  at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
  at java.lang.Thread.run(Thread.java:745) 

原因:

在反序列化时有如下(《Hadoop实战》中):

 

public void readFields(DataInput in) throws IOException {
	this.tag.readFields(in);
	this.data.readFields(in);
}
 这时两个成员变量还没有值

解决:
1 在构造是创建Text 类型 Tag对象。

2 由于Data 对象无法是Writeable类型,无法创建,所以只能在序列化时多记录类型,在readRields时反射出来。

 

详细见上代码。

 

(2)继承DataJoinMapperBase 实现记录标记

 

主要实现三个方法:

  /**
   * 根据文件名实现找打标记
   */
  protected abstract Text generateInputTag(String inputFile);

  /**
   * 对本行记录打上标记,生成TaggedMapOutput
   */
  protected abstract TaggedMapOutput generateTaggedMapOutput(Object value);

  /**
   * 生成分组key,其实就是Reduce的key
   */
  protected abstract Text generateGroupKey(TaggedMapOutput aRecord);

代码: 

/**
   * 
   * 1 根据文件名作为tag,区分数据来源 2 将数据封装成TaggedMapOutput 对象,并打上必要的tag 3 生成group
   * by的分组key,作为依据
   * 
   * */
  public static class JoinMapper extends DataJoinMapperBase {

    /**
     * 读取输入的文件路径
     * 
     * **/
    protected Text generateInputTag(String inputFile) {
      // 取文件名的A和B作为来源标记
      String datasource = StringUtils.splitByWholeSeparatorPreserveAllTokens(inputFile, ".", 2)[0];
      return new Text(datasource);
    }

    /***
     * 分组的Key
     * 
     * **/
    protected Text generateGroupKey(TaggedMapOutput aRecord) {
      String line = ((Text) aRecord.getData()).toString();
      if (StringUtils.isBlank(line)) {
        return null;
      }
      // 去每个文件的第一个字段作为连接key
      String groupKey = StringUtils.splitByWholeSeparatorPreserveAllTokens(line, ",", 2)[0];
      return new Text(groupKey);
    }

    /**
     * 对文件打上标记
     */
    protected TaggedMapOutput generateTaggedMapOutput(Object value) {
      TaggedWritable retv = new TaggedWritable((Text) value);
      retv.setTag(this.inputTag);
      return retv;
    }
  }
(3)继承DataJoinReducerBase 根据条件数据数据

 

主要实现combin()方法,完成连接操作

public static class JoinReducer extends DataJoinReducerBase {

		/**
		 * tags,标签集合,且有顺序通常按照文件读取顺序 values,标签值,
		 * 
		 * 本方法被调一次,会传递一组要连接的记录,文件1的一条,文件2的一条
		 */
		protected TaggedMapOutput combine(Object[] tags, Object[] values) {
			// 按照需求,非left join 或 right join所以要求
			if (tags.length < 2)
				return null;

			String joinedStr = "";
			for (int i = 0; i < values.length; i++) {
				// 设置拼接符
				if (i > 0)
					joinedStr += ",";
				TaggedWritable tw = (TaggedWritable) values[i];
				String line = ((Text) tw.getData()).toString();
				String[] tokens = line.split(",", 2);
				joinedStr += tokens[1];
			}
			// 写出
			TaggedWritable retv = new TaggedWritable(new Text(joinedStr));
			retv.setTag((Text) tags[0]);
			return retv;
		}
	}

 

(4)整体调用代码
public class MR1ReduceJoinJob {
	public static void main(String[] args) throws Exception {
		String in = "/Test/demo/in";
		String out = "/Test/demo/out";

		Configuration conf = new Configuration();
		FileSystem fs = FileSystem.get(conf);

		JobConf job = buildJob(new Path(in), new Path(out), fs, conf);
		RunningJob runningJob = JobClient.runJob(job);
		// 等待结束
		runningJob.waitForCompletion();
		if (runningJob.isSuccessful()) {
			System.out.println("success ! ");
		} else {
			System.out.println(runningJob.getFailureInfo());
		}
	}

	public static JobConf buildJob(Path in, Path out, FileSystem fs, Configuration conf) throws IOException {
		fs.delete(out, true);

		JobConf job = new JobConf(new Configuration(), MR1ReduceJoinJob.class);
		job.setJobName("MR1ReduceJoinJob");

		job.setJarByClass(MR1ReduceJoinJob.class);

		job.setMapperClass(JoinMapper.class);
		job.setReducerClass(JoinReducer.class);

		job.setNumReduceTasks(1);

		job.setInputFormat(TextInputFormat.class);
		job.setOutputFormat(TextOutputFormat.class);

		job.setOutputKeyClass(Text.class);
		job.setOutputValueClass(TaggedWritable.class);

		job.set("mapred.textoutputformat.separator", ",");

		// 解决死循环问题
		job.setLong("datajoin.maxNumOfValuesPerGroup", Long.MAX_VALUE);

		FileInputFormat.addInputPath(job, in);
		FileOutputFormat.setOutputPath(job, out);
		return job;
}
}
 

 

输入文件:

A:

 

1,a
2,b
3,c

 

B:

 

1,11
1,111
2,22
2,222
4,44

 

输出文件:

1,a,111
1,a,11
2,b,222
2,b,22

 

其他的坑:

 

大文件执行是一直出这个:

2-13 17:54:45 -2924 [localfetcher#5] INFO    - closeInMemoryFile -> map-output of size: 215770, inMemoryMapOutputs.size() -> 2, commitMemory -> 829095, usedMemory ->1044865
2015-12-13 17:54:45 -2925 [localfetcher#5] INFO    - localfetcher#5 about to shuffle output of map attempt_local647573184_0001_m_000009_0 decomp: 205864 len: 205868 to MEMORY
2015-12-13 17:54:45 -2925 [localfetcher#5] INFO    - Read 205864 bytes from map-output for attempt_local647573184_0001_m_000009_0
2015-12-13 17:54:45 -2925 [localfetcher#5] INFO    - closeInMemoryFile -> map-output of size: 205864, inMemoryMapOutputs.size() -> 3, commitMemory -> 1044865, usedMemory ->1250729
2015-12-13 17:54:45 -2926 [localfetcher#5] INFO    - localfetcher#5 about to shuffle output of map attempt_local647573184_0001_m_000007_0 decomp: 211843 len: 211847 to MEMORY
2015-12-13 17:54:45 -2926 [localfetcher#5] INFO    - Read 211843 bytes from map-output for attempt_local647573184_0001_m_000007_0
2015-12-13 17:54:45 -2926 [localfetcher#5] INFO    - closeInMemoryFile -> map-output of size: 211843, inMemoryMapOutputs.size() -> 4, commitMemory -> 1250729, usedMemory ->1462572
2015-12-13 17:54:45 -2927 [localfetcher#5] INFO    - localfetcher#5 about to shuffle output of map attempt_local647573184_0001_m_000000_0 decomp: 851861 len: 851865 to MEMORY
2015-12-13 17:54:45 -2929 [localfetcher#5] INFO    - Read 851861 bytes from map-output for attempt_local647573184_0001_m_000000_0
2015-12-1

 

根据源码及别人方案,要改成

job.setLong("datajoin.maxNumOfValuesPerGroup", Long.MAX_VALUE);

后来问题又不出现了。

 

4 不足

  • 基于mrv1 实现, API 交旧
  • Map的问题:
    (1)只能基于文件名分组,要求连接的文件名必须能区分
    (2)generateTaggedMapOutput()还需要手动绑定Tag

  • Reduce 问题:
    (1)迭代输出时不能灵活控制,框架给分组,输出单个文件的字段会重,需要自己处理。

  • 排序相同Group key的记录,也不适合处理超大的记录,可以通过二次排序改进。

  • 我这种需要查找exist的需求不好实现

二 基于MR V2 重写并改进

1 TaggedValue

 

package x.bd.hadoop.join.base;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.io.WritableUtils;
import org.apache.hadoop.util.ReflectionUtils;

/**
 * 可标记的结果
 * 
 * @author shilei
 *
 */
public class TaggedValue implements Writable {
	private Text tag;
	private Writable data;

	public TaggedValue() {
		tag = new Text();
	}
	
	public TaggedValue(Writable data) {
		tag = new Text();
		this.data = data;
	}

	@Override
	public void write(DataOutput out) throws IOException {
		// 写出内容
		this.tag.write(out);
		out.writeUTF(this.data.getClass().getName());
		this.data.write(out);
	}

	@Override
	public void readFields(DataInput in) throws IOException {
		this.tag.readFields(in);
		String dataClz = in.readUTF();
		try {
			if (this.data == null || !this.data.getClass().getName().equals(dataClz)) {
				this.data = (Writable) ReflectionUtils.newInstance(Class.forName(dataClz), null);
			}
			this.data.readFields(in);
		} catch (ClassNotFoundException cnfe) {
			System.out.println("Problem in TaggedWritable class, method readFields.");
		}
	}

	public Text getTag() {
		return tag;
	}

	public void setTag(Text tag) {
		this.tag = tag;
	}

	public Writable getData() {
		return data;
	}

	public void setData(Writable data) {
		this.data = data;
	}

	/**
	 * clone克隆 一个 对象数据
	 * 
	 * @param conf
	 * @return
	 */
	public TaggedValue clone(Configuration conf) {
		return (TaggedValue) WritableUtils.clone(this, conf);
	}

	public static void main(String[] args) {
		System.out.println(TaggedValue.class.getName());
	}

}

 

2 DataJoinMapBase

 

package x.bd.hadoop.join.base;

import java.io.IOException;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.lib.input.FileSplit;

/**
 * 连接查询 Mapper
 * 
 * @author shilei
 *
 * @param <KEYIN>
 * @param <VALUEIN>
 */
public abstract class DataJoinMapperBase<KEYIN, VALUEIN> extends Mapper<KEYIN, VALUEIN, Text, TaggedValue> {
	// 类型标记
	protected Text inputTag;
	// 输入文件路径,文件名
	protected String inputFilePath, inputFileName;

	/**
	 * 根据数据的文件名确定输入标签
	 * 
	 */
	protected abstract Text generateInputTagByFile(String inputFilePath, String inputFileName);

	/**
	 * 根据行内容处理Tag
	 * 
	 * @param inputFilePath
	 * @param inputFileName
	 * @return
	 */
	protected Text generateInputTagByLine(Text tag, KEYIN key, VALUEIN value, Context context) {
		return inputTag;
	}

	/**
	 * 封装待标签的输出
	 */
	protected abstract TaggedValue generateTaggedMapValue(VALUEIN value);

	/**
	 * 生成group by的列
	 */
	protected abstract Text generateGroupKey(TaggedValue tagValue);

	@Override
	protected void setup(Mapper<KEYIN, VALUEIN, Text, TaggedValue>.Context context) throws IOException, InterruptedException {
		FileSplit inputSplit = (FileSplit)context.getInputSplit();
		this.inputFilePath = inputSplit.getPath().getParent().getName();
		this.inputFileName = inputSplit.getPath().getName();
		this.inputTag = generateInputTagByFile(this.inputFilePath, this.inputFileName);
	}

	@Override
	public void map(KEYIN key, VALUEIN value, Context context) throws IOException, InterruptedException {
		// 根据本行情况,处理Tag
		this.inputTag = generateInputTagByLine(this.inputTag, key, value, context);
		
		// 生成带标签的value
		TaggedValue taggedValue = generateTaggedMapValue(value);
		if (taggedValue == null) {
			context.getCounter("DataJoinMapper", "discardedCount").increment(1);
			return;
		}

		// 生成分组健
		Text groupKey = generateGroupKey(taggedValue);
		if (groupKey == null) {
			context.getCounter("DataJoinMapper", "nullGroupKeyCount").increment(1);
			return;
		}

		// 输出内容绑定标签
		taggedValue.setTag(this.inputTag);
		// key : group key , value : taggedValue
		context.write(groupKey, taggedValue);
		context.getCounter("DataJoinMapper", "outCount").increment(1);
	}
}

 

3 DataJoinReduceJoin

 

package x.bd.hadoop.join.base;

import java.io.IOException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;

/**
 * 连接查询Reduce
 * 
 * @author shilei
 *
 * @param <KEYOUT>
 * @param <VALUEOUT>
 */
public abstract class DataJoinReducerBase<KEYOUT, VALUEOUT> extends Reducer<Text, TaggedValue, KEYOUT, VALUEOUT> {

	@Override
	protected void setup(Reducer<Text, TaggedValue, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
		super.setup(context);
	}

	/**
	 * 合并结果
	 */
	protected abstract void combine(SortedMap<Text, List<TaggedValue>> valueGroups, Reducer<Text, TaggedValue, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException;

	@Override
	protected void reduce(Text key, Iterable<TaggedValue> values, Reducer<Text, TaggedValue, KEYOUT, VALUEOUT>.Context context) throws IOException, InterruptedException {
		// 获得一个sort map ,key 是tab ,value 是结果的集合
		SortedMap<Text, List<TaggedValue>> groups = regroup(key, values, context);
		combine(groups, context);
		context.getCounter("DataJoinReucer", "groupCount").increment(1);
	}

	/**
	 * 按照Tag 对value 进行充分组
	 * 
	 * @param key
	 * @param arg1
	 * @param reporter
	 * @return
	 * @throws IOException
	 */
	private SortedMap<Text, List<TaggedValue>> regroup(Text key, Iterable<TaggedValue> values, Reducer<Text, TaggedValue, KEYOUT, VALUEOUT>.Context context) throws IOException {
		/*
		 * key: tag; value : TaggedValue
		 */
		SortedMap<Text, List<TaggedValue>> valueGroup = new TreeMap<Text, List<TaggedValue>>();

		// 遍历Value
		Iterator<TaggedValue> iter = values.iterator();
		while (iter.hasNext()) {

			// TODO 为什么需要克隆?
			TaggedValue taggedValue = ((TaggedValue) iter.next()).clone(context.getConfiguration());
			// 获得记录的 tag
			Text tag = taggedValue.getTag();
			// 从map 中获取一个iterator,如果已经创建,就做一个情况

			List<TaggedValue> datas = valueGroup.get(tag);
			if (datas == null) {
				datas = new LinkedList<TaggedValue>();
				valueGroup.put(tag, datas);
			}
			datas.add(taggedValue);

			// System.out.println("reduce : " + taggedValue + "|" +
			// tag.toString() + "|" + taggedValue.getData().toString());
			taggedValue = null;
		}
		return valueGroup;
	}

} 

 

4 整体调用

package x.bd.hadoop.join;

import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.SortedMap;

import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.Writable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

import x.bd.hadoop.join.base.DataJoinMapperBase;
import x.bd.hadoop.join.base.DataJoinReducerBase;
import x.bd.hadoop.join.base.TaggedValue;

/**
 * 使用Hadoop API 对数据进行 Reduce 连接</br>
 * 
 * 文件1:A.txt 1,a</br> 2,b </br> 3,c
 * 
 * 文件2:B.txt 1,11</br> 1,111</br> 2,22</br> 2,222</br>4,44
 * 
 * 关联查询(要求inner join): 1,a,11</br> 1,a,111</br> 2,b,22 </br> 2,b,222</br>
 * 
 * 
 * @author shilei
 *
 */
public class MR2SelfReduceJoinJob extends Configured implements Tool {

	public static void main(String[] args) throws Exception {
		Configuration conf = new Configuration();
		int res = ToolRunner.run(conf, new MR2SelfReduceJoinJob(), args);
		if (res == 0) {
			System.out.println("MR2SelfReduceJoinJob  success !");
		} else {
			System.out.println("MR2SelfReduceJoinJob  error ! ");

		}
		System.exit(res);
	}

	@Override
	public int run(String[] args) throws Exception {
		String in = "/Test/demo/in";
		String out = "/Test/demo/out";

		Path inPath = new Path(in);
		Path outPath = new Path(out);

		Configuration conf = getConf();
		FileSystem fs = FileSystem.get(conf);

		fs.delete(outPath, true);

		Job job = Job.getInstance(conf, "MR2SelfReduceJoinJob");

		job.setJarByClass(MR2SelfReduceJoinJob.class);
		job.setMapperClass(JoinMapper.class);
		job.setReducerClass(JoinReducer.class);

		job.setNumReduceTasks(1);

		// 处理map的输出
		job.setMapOutputKeyClass(Text.class);
		job.setMapOutputValueClass(TaggedValue.class);

		job.setOutputKeyClass(Writable.class);
		job.setOutputValueClass(NullWritable.class);

		FileInputFormat.addInputPath(job, inPath);
		FileOutputFormat.setOutputPath(job, outPath);

		if (job.waitForCompletion(true)) {
			return 0;
		} else {
			return 1;
		}
	}

	// Map=======================================
	/**
	 * 
	 * 1 根据文件名作为tag,区分数据来源 2 将数据封装成TaggedMapOutput 对象,并打上必要的tag 3 生成group
	 * by的分组key,作为依据
	 * 
	 * */
	public static class JoinMapper extends DataJoinMapperBase<Object, Text> {
		/**
		 * 读取输入的文件路径
		 * 
		 */
		@Override
		protected Text generateInputTagByFile(String inputFilePath, String inputFileName) {
			// 取文件名的A和B作为来源标记
			String datasource = StringUtils.splitByWholeSeparatorPreserveAllTokens(inputFileName, ".", 2)[0];
			return new Text(datasource);
		}

		/**
		 * 按需峰值要处理的记录,这里只需要原样输出
		 */
		@Override
		protected TaggedValue generateTaggedMapValue(Text value) {
			return new TaggedValue(value);
		}

		/**
		 * 数据的第一个字段作为分组key
		 */
		@Override
		protected Text generateGroupKey(TaggedValue tagValue) {
			String line = ((Text) tagValue.getData()).toString();
			if (StringUtils.isBlank(line)) {
				return null;
			}
			// 去每个文件的第一个字段作为连接key
			String groupKey = StringUtils.splitByWholeSeparatorPreserveAllTokens(line, ",", 2)[0];
			return new Text(groupKey);
		}
	}

	// Reduce============================================
	public static class JoinReducer extends DataJoinReducerBase<Writable, NullWritable> {
		private Text key = new Text("B");

		@Override
		protected void combine(SortedMap<Text, List<TaggedValue>> valueGroups, Reducer<Text, TaggedValue, Writable, NullWritable>.Context context) throws IOException, InterruptedException {
			// 必须能连接上
			if (valueGroups.size() < 2) {
				return;
			}

			// 这里只输出文件2的字段
			// 写出
			List<TaggedValue> cookieValues = valueGroups.get(key);
			Iterator<TaggedValue> iter = cookieValues.iterator();
			while (iter.hasNext()) {
				TaggedValue value = iter.next();
				if (value == null) {
					continue;
				}
				context.write(value.getData(), NullWritable.get());
			}
		}
	}
}

三 源码包

 





已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



netty实现tcp长连接和心跳检测

$
0
0

       通过netty实现服务端与客户端的长连接通讯,及心跳检测。

       基本思路:netty服务端通过一个Map保存所有连接上来的客户端SocketChannel,客户端的Id作为Map的key。每次服务器端如果要向某个客户端发送消息,只需根据ClientId取出对应的SocketChannel,往里面写入message即可。心跳检测通过IdleEvent事件,定时向服务端放送Ping消息,检测SocketChannel是否终断。

        环境JDK1.8 和netty5

        以下是具体的代码实现和介绍:

1公共的Share部分(主要包含消息协议类型的定义)

设计消息类型:

public enum  MsgType {
    PING,ASK,REPLY,LOGIN
}

 Message基类:

 

//必须实现序列,serialVersionUID 一定要有,否者在netty消息序列化反序列化会有问题,接收不到消息!!!
public abstract class BaseMsg  implements Serializable {
    private static final long serialVersionUID = 1L;
    private MsgType type;
    //必须唯一,否者会出现channel调用混乱
    private String clientId;
 
    //初始化客户端id
    public BaseMsg() {
        this.clientId = Constants.getClientId();
    }
 
    public String getClientId() {
        return clientId;
    }
 
    public void setClientId(String clientId) {
        this.clientId = clientId;
    }
 
    public MsgType getType() {
        return type;
    }
 
    public void setType(MsgType type) {
        this.type = type;
    }
}

 常量设置:

 

public class Constants {
    private static String clientId;
    public static String getClientId() {
        return clientId;
    }
    public static void setClientId(String clientId) {
        Constants.clientId = clientId;
    }
}

 登录类型消息:

 

public class LoginMsg extends BaseMsg {
    private String userName;
    private String password;
    public LoginMsg() {
        super();
        setType(MsgType.LOGIN);
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

 心跳检测Ping类型消息:

public class PingMsg extends BaseMsg {
    public PingMsg() {
        super();
        setType(MsgType.PING);
    }
}

 请求类型消息:

public class AskMsg extends BaseMsg {
    public AskMsg() {
        super();
        setType(MsgType.ASK);
    }
    private AskParams params;
    public AskParams getParams() {
        return params;
    }
    public void setParams(AskParams params) {
        this.params = params;
    }
}
//请求类型参数
//必须实现序列化接口
public class AskParams implements Serializable {
    private static final long serialVersionUID = 1L;
    private String auth;
 
    public String getAuth() {
        return auth;
    }
 
    public void setAuth(String auth) {
        this.auth = auth;
    }
}

 响应类型消息:

public class ReplyMsg extends BaseMsg {
    public ReplyMsg() {
        super();
        setType(MsgType.REPLY);
    }
    private ReplyBody body;
    public ReplyBody getBody() {
        return body;
    }
    public void setBody(ReplyBody body) {
        this.body = body;
    }
}
//相应类型body对像
public class ReplyBody implements Serializable {
    private static final long serialVersionUID = 1L;
}
public class ReplyClientBody extends ReplyBody {
    private String clientInfo;
 
    public ReplyClientBody(String clientInfo) {
        this.clientInfo = clientInfo;
    }
 
    public String getClientInfo() {
        return clientInfo;
    }
 
    public void setClientInfo(String clientInfo) {
        this.clientInfo = clientInfo;
    }
}
public class ReplyServerBody extends ReplyBody {
    private String serverInfo;
    public ReplyServerBody(String serverInfo) {
        this.serverInfo = serverInfo;
    }
    public String getServerInfo() {
        return serverInfo;
    }
    public void setServerInfo(String serverInfo) {
        this.serverInfo = serverInfo;
    }
}

 2 Server端:主要包含对SocketChannel引用的Map,ChannelHandler的实现和Bootstrap.

Map:

public class NettyChannelMap {
    private static Map<String,SocketChannel> map=new ConcurrentHashMap<String, SocketChannel>();
    public static void add(String clientId,SocketChannel socketChannel){
        map.put(clientId,socketChannel);
    }
    public static Channel get(String clientId){
       return map.get(clientId);
    }
    public static void remove(SocketChannel socketChannel){
        for (Map.Entry entry:map.entrySet()){
            if (entry.getValue()==socketChannel){
                map.remove(entry.getKey());
            }
        }
    }
}

 Handler

public class NettyServerHandler extends SimpleChannelInboundHandler<BaseMsg> {
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        //channel失效,从Map中移除
        NettyChannelMap.remove((SocketChannel)ctx.channel());
    }
    @Override
    protected void messageReceived(ChannelHandlerContext channelHandlerContext, BaseMsg baseMsg) throws Exception {
 
        if(MsgType.LOGIN.equals(baseMsg.getType())){
            LoginMsg loginMsg=(LoginMsg)baseMsg;
            if("robin".equals(loginMsg.getUserName())&&"yao".equals(loginMsg.getPassword())){
                //登录成功,把channel存到服务端的map中
                NettyChannelMap.add(loginMsg.getClientId(),(SocketChannel)channelHandlerContext.channel());
                System.out.println("client"+loginMsg.getClientId()+" 登录成功");
            }
        }else{
            if(NettyChannelMap.get(baseMsg.getClientId())==null){
                    //说明未登录,或者连接断了,服务器向客户端发起登录请求,让客户端重新登录
                    LoginMsg loginMsg=new LoginMsg();
                    channelHandlerContext.channel().writeAndFlush(loginMsg);
            }
        }
        switch (baseMsg.getType()){
            case PING:{
                PingMsg pingMsg=(PingMsg)baseMsg;
                PingMsg replyPing=new PingMsg();
                NettyChannelMap.get(pingMsg.getClientId()).writeAndFlush(replyPing);
            }break;
            case ASK:{
                //收到客户端的请求
                AskMsg askMsg=(AskMsg)baseMsg;
                if("authToken".equals(askMsg.getParams().getAuth())){
                    ReplyServerBody replyBody=new ReplyServerBody("server info $$$$ !!!");
                    ReplyMsg replyMsg=new ReplyMsg();
                    replyMsg.setBody(replyBody);
                    NettyChannelMap.get(askMsg.getClientId()).writeAndFlush(replyMsg);
                }
            }break;
            case REPLY:{
                //收到客户端回复
                ReplyMsg replyMsg=(ReplyMsg)baseMsg;
                ReplyClientBody clientBody=(ReplyClientBody)replyMsg.getBody();
                System.out.println("receive client msg: "+clientBody.getClientInfo());
            }break;
            default:break;
        }
        ReferenceCountUtil.release(baseMsg);
    }
}

 ServerBootstrap:

public class NettyServerBootstrap {
    private int port;
    private SocketChannel socketChannel;
    public NettyServerBootstrap(int port) throws InterruptedException {
        this.port = port;
        bind();
    }
    private void bind() throws InterruptedException {
        EventLoopGroup boss=new NioEventLoopGroup();
        EventLoopGroup worker=new NioEventLoopGroup();
        ServerBootstrap bootstrap=new ServerBootstrap();
        bootstrap.group(boss,worker);
        bootstrap.channel(NioServerSocketChannel.class);
        bootstrap.option(ChannelOption.SO_BACKLOG, 128);
        //通过NoDelay禁用Nagle,使消息立即发出去,不用等待到一定的数据量才发出去
        bootstrap.option(ChannelOption.TCP_NODELAY, true);
        //保持长连接状态
        bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);
        bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                ChannelPipeline p = socketChannel.pipeline();
                p.addLast(new ObjectEncoder());
                p.addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
                p.addLast(new NettyServerHandler());
            }
        });
        ChannelFuture f= bootstrap.bind(port).sync();
        if(f.isSuccess()){
            System.out.println("server start---------------");
        }
    }
    public static void main(String []args) throws InterruptedException {
        NettyServerBootstrap bootstrap=new NettyServerBootstrap(9999);
        while (true){
            SocketChannel channel=(SocketChannel)NettyChannelMap.get("001");
            if(channel!=null){
                AskMsg askMsg=new AskMsg();
                channel.writeAndFlush(askMsg);
            }
            TimeUnit.SECONDS.sleep(5);
        }
    }
}

 3 Client端:包含发起登录,发送心跳,及对应消息处理

handler

public class NettyClientHandler extends SimpleChannelInboundHandler<BaseMsg> {
    //利用写空闲发送心跳检测消息
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent e = (IdleStateEvent) evt;
            switch (e.state()) {
                case WRITER_IDLE:
                    PingMsg pingMsg=new PingMsg();
                    ctx.writeAndFlush(pingMsg);
                    System.out.println("send ping to server----------");
                    break;
                default:
                    break;
            }
        }
    }
    @Override
    protected void messageReceived(ChannelHandlerContext channelHandlerContext, BaseMsg baseMsg) throws Exception {
        MsgType msgType=baseMsg.getType();
        switch (msgType){
            case LOGIN:{
                //向服务器发起登录
                LoginMsg loginMsg=new LoginMsg();
                loginMsg.setPassword("yao");
                loginMsg.setUserName("robin");
                channelHandlerContext.writeAndFlush(loginMsg);
            }break;
            case PING:{
                System.out.println("receive ping from server----------");
            }break;
            case ASK:{
                ReplyClientBody replyClientBody=new ReplyClientBody("client info **** !!!");
                ReplyMsg replyMsg=new ReplyMsg();
                replyMsg.setBody(replyClientBody);
                channelHandlerContext.writeAndFlush(replyMsg);
            }break;
            case REPLY:{
                ReplyMsg replyMsg=(ReplyMsg)baseMsg;
                ReplyServerBody replyServerBody=(ReplyServerBody)replyMsg.getBody();
                System.out.println("receive client msg: "+replyServerBody.getServerInfo());
            }
            default:break;
        }
        ReferenceCountUtil.release(msgType);
    }
}

 bootstrap

public class NettyClientBootstrap {
    private int port;
    private String host;
    private SocketChannel socketChannel;
    private static final EventExecutorGroup group = new DefaultEventExecutorGroup(20);
    public NettyClientBootstrap(int port, String host) throws InterruptedException {
        this.port = port;
        this.host = host;
        start();
    }
    private void start() throws InterruptedException {
        EventLoopGroup eventLoopGroup=new NioEventLoopGroup();
        Bootstrap bootstrap=new Bootstrap();
        bootstrap.channel(NioSocketChannel.class);
        bootstrap.option(ChannelOption.SO_KEEPALIVE,true);
        bootstrap.group(eventLoopGroup);
        bootstrap.remoteAddress(host,port);
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                socketChannel.pipeline().addLast(new IdleStateHandler(20,10,0));
                socketChannel.pipeline().addLast(new ObjectEncoder());
                socketChannel.pipeline().addLast(new ObjectDecoder(ClassResolvers.cacheDisabled(null)));
                socketChannel.pipeline().addLast(new NettyClientHandler());
            }
        });
        ChannelFuture future =bootstrap.connect(host,port).sync();
        if (future.isSuccess()) {
            socketChannel = (SocketChannel)future.channel();
            System.out.println("connect server  成功---------");
        }
    }
    public static void main(String[]args) throws InterruptedException {
        Constants.setClientId("001");
        NettyClientBootstrap bootstrap=new NettyClientBootstrap(9999,"localhost");
        LoginMsg loginMsg=new LoginMsg();
        loginMsg.setPassword("yao");
        loginMsg.setUserName("robin");
        bootstrap.socketChannel.writeAndFlush(loginMsg);
        while (true){
            TimeUnit.SECONDS.sleep(3);
            AskMsg askMsg=new AskMsg();
            AskParams askParams=new AskParams();
            askParams.setAuth("authToken");
            askMsg.setParams(askParams);
            bootstrap.socketChannel.writeAndFlush(askMsg);
        }
    }
}

具体的例子和相应pom.xml 见  https://github.com/WangErXiao/ServerClient

转发请注明来源: http://my.oschina.net/robinyao/blog/399060

总结:

java实现tcp的序列化和反序列化的时候,最好使用json字符串传递数据。这样确保客户端反序列化成功。如果客户端和服务端都是使用java实现,则使用对象序列化和反序列化可以的。如果客户端使用非java语言实现的,不能使用java的对象序列化和反序列化。最好使用字符串传递数据。推荐使用json字符串。

 



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



互联网推送服务原理:长连接+心跳机制(MQTT协议)

$
0
0

互联网推送消息的方式很常见,特别是移动互联网上,手机每天都能收到好多推送消息,经过研究发现,这些推送服务的原理都是维护一个长连接(要不不可能达到实时效果),但普通的socket连接对服务器的消耗太大了,所以才会出现像MQTT这种轻量级低消耗的协议来维护长连接,那么要如何维护长连接呢:

 

 

       在写之前,我们首先了解一下为什么android维护长连接需要心跳机制,首先我们知道,维护任何一个长连接都需要心跳机制,客户端发送一个心跳给服务器,服务器给客户端一个心跳应答,这样就形成客户端服务器的一次完整的握手,这个握手是让双方都知道他们之间的连接是没有断开,客户端是在线的。如果超过一个时间的阈值,客户端没有收到服务器的应答,或者服务器没有收到客户端的心跳,那么对客户端来说则断开与服务器的连接重新建立一个连接,对服务器来说只要断开这个连接即可。那么在智能手机上的长连接心跳和在Internet上的长连接心跳有什么不同的目的呢?原因就在于智能手机使用的是移动无线网络,那么我们在讲长连接之前我们首先要了解无线移动网络的特点。

1.无线移动网络的特点:

        当一台智能手机连上移动网络时,其实并没有真正连接上Internet,运营商分配给手机的IP其实是运营商的内网IP,手机终端要连接上Internet还必须通过运营商的网关进行IP地址的转换,这个网关简称为NAT(NetWork Address Translation),简单来说就是手机终端连接Internet 其实就是移动内网IP,端口,外网IP之间相互映射。相当于在手机终端在移动无线网络这堵墙上打个洞与外面的Internet相连。原理图如下:(来源网络)

 

        GGSN(GateWay GPRS Support Note 网关GPRS支持节点)模块就实现了NAT功能,由于大部分的移动无线网络运营商为了减少网关NAT映射表的负荷,如

果一个链路有一段时间没有通信时就会删除其对应表,造成链路中断,正是这种刻意缩短空闲连接的释放超时,原本是想节省信道资源的作用,没想到让互联网

的应用不得以远高于正常频率发送心跳来维护推送的长连接。这也是为什么会有之前的信令风暴,微信摇收费的传言,因为这类的应用发送心跳的频率是很短的,

既造成了信道资源的浪费,也造成了手机电量的快速消耗。

2.android系统的推送和IOS的推送有什么区别:

        首先我们必须知道,所有的推送功能必须有一个客户端和服务器的长连接,因为推送是由服务器主动向客户端发送消息,如果客户端和服务器之间不存在一个长连接那么服务器是无法来主动连接客户端的。因而推送功能都是基于长连接的基础是上的。

        IOS长连接是由系统来维护的,也就是说苹果的IOS系统在系统级别维护了一个客户端和苹果服务器的长链接,IOS上的所有应用上的推送都是先将消息推送到苹果的服务器然后将苹果服务器通过这个系统级别的长链接推送到手机终端上,这样的的几个好处为:1.在手机终端始终只要维护一个长连接即可,而且由于

这个长链接是系统级别的不会出现被杀死而无法推送的情况。2.省电,不会出现每个应用都各自维护一个自己的长连接。3.安全,只有在苹果注册的开发者才能够进行推送,等等。

       android的长连接是由每个应用各自维护的,但是google也推出了和苹果技术架构相似的推送框架,C2DM,云端推送功能,但是由于google的服务器不在中国境内,其他的原因你懂的。所以导致这个推送无法使用,android的开发者不得不自己去维护一个长链接,于是每个应用如果都24小时在线,那么都得各自维

护一个长连接,这种电量和流量的消耗是可想而知的。虽然国内也出现了各种推送平台,但是都无法达到只维护一个长连接这种消耗的级别。

3.推送的实现方式:

一:客户端不断的查询服务器,检索新内容,也就是所谓的pull 或者轮询方式

二:客户端和服务器之间维持一个TCP/IP长连接,服务器向客户端push

三:服务器又新内容时,发送一条类似短信的信令给客户端,客户端收到后从服务器中下载新内容,也就是SMS的推送方式

苹果的推送系统和googleC2DM推送系统其实都是在系统级别维护一个TCP/IP长连接,都是基于第二种的方式进行推送的。第三种方式由于运营商没有免费开放

这种信令导致了这种推送在成本上是无法接受的,虽然这种推送的方式非常的稳定,高效和及时。

如果想了解android中各种推送方式请参考这个链接: Android实现推送方式解决方案 这篇博客已经介绍的非常好了。



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐




网页链接触发原生Intent

$
0
0

人们每天都要访问大量的手机网页, 如果把手机网页(Web)和应用(App)紧密地联系起来, 就可以增大用户的访问量, 也有其他应用场景, 如 网页中调用支付链接, 新闻中启动问诊界面, 提供优质的原生功能等等.

如何在网页(Web)中, 通过Intent直接启动应用(App)的Activity呢?

本文主要有以下几点:
(1) 如何在Web中发送原生的Intent消息.
(1) 如何加载本地的HTML页面到浏览器.
(2) 如何创建半透明的Activity页面.

展示

1. 配置项目

新建HelloWorld工程. 添加ButterKnife支持.

compile 'com.jakewharton:butterknife:7.0.1'

2. BottomSheet

逻辑, 添加ShareIntent的监听, 即网页链接触发的Intent, 提取Link和Title信息, 底部出现或消失的动画.

/**
 * 网页Activity
 * <p/>
 * Created by wangchenlong on 15/12/7.
 */
public class WebIntentActivity extends Activity {

    @Bind(R.id.web_intent_et_title) EditText mEtTitle;
    @Bind(R.id.web_intent_et_link) EditText mEtLink;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_bottom_sheet);
        ButterKnife.bind(this);

        // 获取WebIntent信息
        if (isShareIntent()) {
            ShareCompat.IntentReader intentReader = ShareCompat.IntentReader.from(this);
            mEtLink.setText(intentReader.getText());
            mEtTitle.setText(intentReader.getSubject());
        }
    }

    @Override protected void onResume() {
        super.onResume();
        // 底部出现动画
        overridePendingTransition(R.anim.bottom_in, R.anim.bottom_out);
    }

    // 判断是不是WebIntent
    private boolean isShareIntent() {
        return getIntent() != null && Intent.ACTION_SEND.equals(getIntent().getAction());
    }

    @Override public void overridePendingTransition(int enterAnim, int exitAnim) {
        super.overridePendingTransition(enterAnim, exitAnim);
    }
}

动画属性, 沿Y轴变换.

<set xmlns:android="http://schemas.android.com/apk/res/android"><translate
        android:duration="300"
        android:fromYDelta="100%p"
        android:toYDelta="0%p"/></set>
<set xmlns:android="http://schemas.android.com/apk/res/android"><translate
        android:duration="300"
        android:fromYDelta="0%p"
        android:toYDelta="100%p"/></set>

BottomSheet页面, 由两个EditText组成.

<?xml version="1.0" encoding="utf-8"?><LinearLayout
    android:id="@+id/web_intent_ll_popup_window"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|center"
    android:background="@android:color/white"
    android:orientation="vertical"
    android:padding="10dp"><TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:text="发送网页内容到应用"
        android:textSize="20sp"/><android.support.design.widget.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginStart="12dp"
        android:layout_marginTop="8dp"><EditText
            android:id="@+id/web_intent_et_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Post Title"
            android:inputType="textCapWords"/></android.support.design.widget.TextInputLayout><android.support.design.widget.TextInputLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginEnd="16dp"
        android:layout_marginStart="12dp"
        android:layout_marginTop="8dp"><EditText
            android:id="@+id/web_intent_et_link"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Link"
            android:inputType="textCapWords"/></android.support.design.widget.TextInputLayout></LinearLayout>

注意
设置LinearLayout的 android:layout_gravity="bottom|center"属性,
配合样式(Styles)的 <item name="android:windowIsFloating">false</item>属性,
可以在底部显示页面.

效果

声明, 添加 SEND的Action, BROWSABLE的Category, text/plain的文件类型.
主题设置透明主题. 启动时, 会保留上部半透明, 用于显示网页信息.

<activity
            android:name=".WebIntentActivity"
            android:theme="@style/Theme.Transparent"><intent-filter><action android:name="android.intent.action.SEND"/><category android:name="android.intent.category.DEFAULT"/><category android:name="android.intent.category.BROWSABLE"/><data android:mimeType="text/plain"/></intent-filter></activity>

透明主题, 注意一些关键属性, 参考注释, 不一一列举.

<style name="Theme.Transparent" parent="AppTheme.NoActionBar"><!--背景色--><item name="android:windowBackground">@color/page_background</item><!--不使用背景缓存--><item name="android:colorBackgroundCacheHint">@null</item><!--控制窗口位置, 非流窗口, 固定位置, 用于非全屏窗口--><item name="android:windowIsFloating">false</item><!--窗口透明--><item name="android:windowIsTranslucent">true</item><!--窗口无标题--><item name="android:windowNoTitle">true</item></style>

背景颜色 windowBackground非常重要, 不是常规颜色, 也可以设置为透明.

<!--最前两位是颜色厚度, 00透明, FF全黑--><color name="page_background">#99323232</color>

3. 主页面

本地HTML文件存放在 assets中, 提供在浏览器打开功能.
浏览器打开Web链接非常简单, 打开本地HTML有很多难点.

/**
 * 测试WebIntent的Demo
 *
 * @author C.L.Wang
 */
public class MainActivity extends AppCompatActivity {

    @SuppressWarnings("unused")
    private static final String TAG = "DEBUG-WCL: " + MainActivity.class.getSimpleName();

    private static final String FILE_NAME = "file:///android_asset/web_intent.html";

    @Bind(R.id.main_wv_web) WebView mWvWeb; // WebView

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 跳转WebIntentActivity
                startActivity(new Intent(MainActivity.this, WebIntentActivity.class));
            }
        });

        mWvWeb.loadUrl(FILE_NAME);
    }

    @Override public void onBackPressed() {
        // 优先后退网页
        if (mWvWeb.canGoBack()) {
            mWvWeb.goBack();
        } else {
            finish();
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        // 打开浏览器选项
        if (id == R.id.action_open_in_browser) {
            // 获取文件名, 打开assets文件使用文件名
            String[] as = FILE_NAME.split("/");
            openUrlInBrowser(as[as.length - 1]);
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    /**
     * 在浏览器中打开
     *
     * @param url 链接(本地HTML或者网络链接)
     */
    private void openUrlInBrowser(String url) {
        Uri uri;
        if (url.endsWith(".html")) { // 文件
            uri = Uri.fromFile(createFileFromInputStream(url));
        } else { // 链接
            if (!url.startsWith("http://") && !url.startsWith("https://")) {
                url = "http://" + url;
            }
            uri = Uri.parse(url);
        }

        try {
            Intent intent = new Intent(Intent.ACTION_VIEW, uri);
            // 启动浏览器, 谷歌浏览器, 小米手机浏览器支持, 其他手机或浏览器不支持.
            intent.setClassName("com.android.browser", "com.android.browser.BrowserActivity");
            startActivity(intent);
        } catch (ActivityNotFoundException e) {
            Toast.makeText(this, "没有应用处理这个请求. 请安装浏览器.", Toast.LENGTH_LONG).show();
            e.printStackTrace();
        }
    }

    /**
     * 存储assets内的文件
     *
     * @param url 文件名
     * @return 文件类(File)
     */
    private File createFileFromInputStream(String url) {
        try {
            // 打开Assets内的文件
            InputStream inputStream = getAssets().open(url);
            // 存储位置 /sdcard
            File file = new File(
                    Environment.getExternalStorageDirectory().getPath(), url);
            OutputStream outputStream = new FileOutputStream(file);
            byte buffer[] = new byte[1024];
            int length;
            while ((length = inputStream.read(buffer)) > 0) {
                outputStream.write(buffer, 0, length);
            }
            outputStream.close();
            inputStream.close();
            return file;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

注意:
(1) 浏览器打开 assets内文件的方式, 与 WebView有所不同,
具体参考 createFileFromInputStream函数.
(2) 在浏览器打开时, 需要指定包名, 而且各自浏览器的模式也不一样,
小米支持Google原生调用, 参考 openUrlInBrowser函数.
(3) 回退事件的处理方式, 参考 onBackPressed函数.

动画

Github 下载地址

就这些了, 在浏览器的HTML5页面中, 可以添加更多和本地应用的交互.
OK, Enjoy It.

作者:u012515223 发表于2015/12/14 17:05:44 原文链接
阅读:0 评论:0 查看评论

python之记录一次内存溢出

$
0
0

问题现象

手头一个系统上线后,节点机中agent应用在运行10天后,占用系统内存居然高达10GB以上,这显然是发生了严重内存泄露。

 

问题原因

python是动态语言,对用动态语言的内存分析不是很容易,尝试了一下比较经典的内存分析工具meliae,但是发现不是很好用。查了很多资料后,发现了 https://github.com/pympler/pympler 这个工具,官方文档地址为:

http://pythonhosted.org/Pympler/tutorials/muppy_tutorial.html

具体的分析过程我就不在这里描述了,大家可以通过pympler的官方文档去尝试。

 

在跟踪分析后,发现agent代码中的ProgressThread在一直增长,没有被释放。相关代码如下:

        progress_thread = ProgressThread(vid, segments, master_urls, self.hostname)
        progress_thread.setDaemon(True)
        progress_thread.start()
        self.progress_threads.append(progress_thread)

 可以看到,在类对象中,progress_threads这个列表,每次创建一个ProgressThread线程对象时,就会把对象插入到progress_threads列表中。

 

python的垃圾回收机制中,会自动对引用计数为0的对象回收。这里每次创建的线程对象,都被插入到progress_threads中,这导致了即使线程运行完毕了,其引用计数一直为1,导致所有的线程对象无法被回收。

 

问题解决

知道了问题原因,就很好解决了。在线程退出的地方,加入如下一行代码

            if progress_thread.if_stop == True:
                self.progress_threads.remove(progress_thread)

 让线程对象的引用计数为0,就解决了这个问题。

 



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



P2P网站应用安全报告

$
0
0

0x00 前言


有关e租宝公司被调查的新闻在微博、朋友圈被引爆刷屏。许多人看中P2P理财的高收益,却忽视其中的风险。猎豹移动安全实验室监测发现,P2P网站已成钓鱼欺诈网站的重灾区,大量P2P手机理财软件也存在安全隐患。网民须小心选择P2P类理财产品。

0x01 P2P行业现状


P2P网贷在2007开始传入国内,2015年呈现爆发态势,成交规模已进入万亿元时代。由于行业监管未出台,P2P行业处于野蛮生长阶段,鱼龙混杂,平台上线和跑路司空见惯。

据统计,截止今年,纳入中国P2P网贷指数统计的网贷平台有超过2500家,其中问题平台近1000家。从全国范围内看:广东、山东的问题平台数量最多,数量分别达到了163家和198家。从平台性质来看,问题平台无一例外都是民营系的。

p1图1 截止11月全国各省正常平台和问题平台数量统计

问题平台中29%出现提现困难,56%的问题平台选择了跑路,有的平台跑路后甚至连公司员工都不知情。

p2图2 问题平台状态比例

一般来讲,P2P平台运营出现跑路的有两种,一种是经营不善出现资金链断裂的;还有一种是纯诈骗性质的网站,骗到投资者钱财后就立马关闭网站跑路。即使是今天正常运营的平台明天就有可能倒闭跑路,那么如何识别诈骗和即将跑路的平台呢?这就先要弄清楚它的诈骗流程。

0x02 P2P网站诈骗流程


很多平台上线前期会以高利率为诱饵,发布大量虚假标的,通过虚假宣传、注册返利、秒标等形式,吸引普通投资者大量资金,资金到账后便卷款而逃。网站平台突然无法登陆,公司高管失踪,办公地点人去楼空。

也有部分网贷平台,宣称出现投资未按时收回,说是提现困难,让投资者继续投资支持平台。而在投资者交流群,会有一些人以低价收购无法提现的账号余额,业内称之为“收草”。而实际上,低价“收草”的人和欺诈平台是合谋诈骗。

p3图3 典型网贷诈骗流程图

0x03 诈骗手法


P2P诈骗网站吸收资金一般有以下几种手法。

1、高利率吸引投资者资金:

一般的P2P网贷平台年化收益率在10%左右,而超过20%,甚至接近30%都是需要高度警惕。监测发现,有的平台网站赫然宣称有700%以上的收益率。

p4图4 诈骗平台通过极高利率吸引投资者

2、高回报加奖励

某台上项目的年化收益率普遍超过22%,同时平台给予投资者以3%-5%不等的投标奖励。部分存在投机和侥幸心理的投资者很快就上钩被套牢。

p5图5 高利率、高奖励的借款项目

3、设立虚标

伪造借款项目和虚构借款人信息,并标出可观的收益率,吸引缺乏风险意识的投资者。如下图:某平台的借款项目信息说明含糊其辞,项目图片一模一样,明显是虚标或拆标。

p6图6 虚标或拆标的项目

4、庞氏骗局

利用新投资者的资金来向老投资者支付利息和短期回报,制造一种高盈利的假象,进一步骗取更多投资者的投资。一旦平台没有持续的投资来源,整个资金链就会断裂,平台就会跑路。前段时间风靡朋友圈的“MMM金融互助社区”就是典型例子。

0x04 P2P网站的安全性


除了诈骗平台蓄意骗取投资者钱财之外,P2P网贷网站广泛存在安全漏洞,极易导致黑客攻击。资金安全是每一个网贷平台应当首先保障的,而保障资金安全的首要前提是保障网站的安全。

P2P网站由于直接牵涉投资者的资金、个人信息、银行账户等敏感信息,故其危险性比一般网站的漏洞更高。

p7图7P2P平台存在的一些安全性问题

我们对部分P2P网站进行了抽样安全监测,目前发现有131家网站存在不同类型的安全漏洞。其中撞库攻击(40%)、信息泄漏(24%)、后台地址暴露(24%)是3个主要漏洞类型,严重危及网站的用户数据安全和资金安全。

p8图8部分P2P网站漏洞类型分布

0x05 P2P应用的安全性


由于智能手机的普及,很多平台开发了自己的手机P2P理财应用,方便投资者随时随地投资理财;有的平台甚至只能在手机应用上使用充值、投资、提现。

我们抽样审计了104款理财应用,约37%存在数据明文传输问题,8%的短信校验码在客户端校验,只有24%使用了加密传输,剩下31%由于部分平台倒闭跑路或其他原因,无法访问服务器。

p9图9P2P手机应用安全问题类型分布

  • 密码明文传输

    104款应用中,有部分应用直接明文发送密码、支付密码,或者仅仅只是简单的base64编码一下。

    p10图10某P2P手机应用明文传输密码及金额

  • 短信验证码客户端校验

    少部分应用中的手机短信验证码居然在客户端验证(HTTP回包中带有短信验证码),这样可以造成恶意注册,刷红包,修改任意用户的密码等严重问题。

    p11图11某P2P手机应用本地验证短信校验码

显而易见的风险存在于P2P手机应用中,正规P2P网贷平台对安全十分重视,那些小平台和诈骗平台根本没有实力、或者根本没花心思去提升网站安全性。以下是猎豹移动安全实验室对部分P2P类手机应用的分析结果:

APP产品名安全问题
爱贷网理财明文传输
互贷网理财明文上报
合信明文传输
合盘贷明文传输
汉金所明文密码
国诚金融明文传输
众可贷明文传输
财加网密码MD5加密,短信验证码客户端校验
得利宝明文密码
道口贷明文密码,短信验证码客户端校验
励国理财明文密码
力帆善融明文密码,短信验证码客户端校验。
聚诚财富明文密码
兢业贷明文密码
卓安e贷明文密码
中金贷明文密码
银票网明文密码
鑫合汇理财明文密码
小油菜理财明文密码
三益宝明文密码
融贝网明文密码
钱内助明文发送登录密码,身份证信息明文
票据客明文传输,个人信息验证明文,支付明文
胖胖猪明文密码,短信验证码客户端校验
理财乐钱包base64编码密码
骑士贷APK无法下载
宁创金融app无法下载(http://www.0086cf.com/app/index.html)
你我贷理财app无法下载
智信创富app无法下载
芝麻金融总是返回服务器太忙
携银理财无法运行
温商贷理财app崩溃
苏融贷无法注册
拍拍贷连接不上服务器
诺诺镑客理财连接不上服务器
麻袋理财连接不上服务器
链家理财无法注册
九斗鱼总是返回服务器太忙
金联储无法注册
黄河金融无法注册
短融网无法注册
城城理财无法注册
诚投在线无法注册
爱利是无法注册

0x06 以P2P网贷为噱头人钓鱼网站


根据监测数据,2015年平均每月新增195家P2P理财钓鱼网站。这些网站生存周期较短,为了逃避拦截,通常会设置多个域名指向同一个IP地址。

p12图12 2015年每月新增P2P理财钓鱼网站数量

根据最近两月监测显示,P2P理财钓鱼网站的访问量呈锯齿状波动:其原因是P2P类钓鱼网站打一枪换一个地方,短短几天就完成建站上线->欺骗->关站->建新站的循环。

p13图13 十月和十一月P2P理财钓鱼诈骗网站访问量

0x07 如何识别诈骗平台


知道了P2P的诈骗流程和手法,就可以识别一个平台是否为诈骗平台了,通常有以下几种方法。

第一,诈骗平台的界面设计相对比较粗糙。很多诈骗平台基本是几千块钱购买一个模板,再租一个主机空间就上线了,并且通常IP地址位于境外。

p14图14套用同一个模板的理财诈骗网站

第二,宣传的收益率很高,甚至远远高于行业平均水平。

p15图15诈骗网站高利率的虚假项目

第三,公司介绍造假,备案和注册信息造假,办公地址较为偏远,甚至根本不存在。

p16图16某P2P曝光群曝光的某诈骗平台的虚假注册信息

第四,平台活动不断,常见日标、秒标,但标的信息含糊其辞,如资金周转等。甚至虚构借款人信息,设立虚标。

第五,诈骗平台基本没有第三方资金托管平台。投资者注册平台帐号后可以直接投资,不要求注册第三方支付机构帐号的,可确定是没有资金托管的。

第六,平台负责人曾有过失信记录,可登录最高人民法院网站(shixin.court.gov.cn)查询。

第七,平台业务是否公开透明,过往业务记录是否可查询调阅。

第八,平台涉及自融,如果平台资金被平台本身或股东挪作他用,那就是自融,涉嫌非法集资、诈骗等违法犯罪行为。

0x08 真实案例


11月23日,宏量财富将网站<www.hongliangcf.com>关闭, 并把群里的一千多用户踢得一干二净。

这家名为宏量资产管理有限公司的平台,成立时间不足三个月。该公司各种注册和资质手续均齐备,且网站也有ICP备案。注册资金为两千万元。

据受害者称,10月份时,经过各项考察,认为平台可信,于10月23日在平台投资1万元,随后被告知该平台三名高管于11月23日凌晨跑路,大概有十几个投资者以及公司10名员工均被蒙在鼓里。据该平台同为受害者的客服主管说,至少有4000投资者,涉及金额高达2400万以上。

p17图17宏量财富跑路爆料帖

即使是实地考察过的,平台有正规备案的也可能因为经营不善,资金链断裂而跑路;部分平台在经营正常的情况下,负责人也可能由于贪婪而卷款跑路,甚至连平台自身工作人员都不知情。目前宏量财富的受害者们已经建立维权群并报案。

0x09 正规平台运作流程


除了识别一个平台是否为问题平台,还要知道正规平台是如何运作的。像红岭创投、宜人贷、陆金所等大型正规网贷平台都会有严格的运作流程,用户的信息和资金安全都有充分保障。

1、严格的贷前审核

正规平台针对借款人会有严格的贷前审查,通过背景调查、借款用途调查以及个人信用风险评估等审核借款人提出的借贷需求,避免不良客户的欺诈风险。

2、完善的贷后管理

借款项目遇到逾期未归还借款的,平台会采取充分手段催促借款人还款,甚至采取法律手段。并且对投资者完全公开透明。

3、充分的风险准备金

如果投资者的投资的某笔借款出现严重逾期,平台应会通过风险准备金对投资者偿付本金和利息,分散投资者投资行为所带来的信用风险。

4、完善的法律和政策保障

正规平台从事业务应当是合法合规的,不进行拆标和虚标行为,每个借款项目都有合法的电子合同、财务抵押凭证等必须的文件文书。

5、第三方资金托管和担保

正规平台采取和第三方合作托管用户资金,不私设资金池。严格规范资金管理,并有第三方担保交易。

6、重视平台自身和用户信息的安全

平台网站建设充分重视安全问题,通过加密连接、防火墙、二次验证等技术手段保证数据和信息的安全。并有严格的IT管理规范,防止出现人为的安全事故。

0x0A 结语


P2P网贷是伴随“互联网+”兴起的新生行业,目前行业监管不明,P2P行业在全国处于野蛮生长阶段。由于P2P的特性,存在投资者分散,平台不透明,资金监管缺失,借款人信息难以核实等问题,使得部分平台借机诈骗敛财,卷款跑路事件屡屡发生。另一方面,由于平台运营方对安全缺乏普遍的重视,网站的安全漏洞层出不穷,黑客攻击造成的系统瘫痪、数据恶意篡改、资金盗取等时有发生。

对于投资者而言,面对高利率和高回报要保持理性,认真考察评估平台的真实性、安全性、专业性以及可持续性,选择可靠平台并分散投资。随时关注平台及借贷项目的最新情况,保存充值记录、借贷项目合同、客服记录等证据,方便及时维权。

对于网贷平台方,要充分重视用户信息和资金安全,及时修复网站和应用存在的各种安全漏洞,并且对资金进行第三方托管,遭遇黑客攻击要及时联系警方处理,不能姑息和纵容。

实现歌词同步源码

$
0
0

实现歌词同步

介绍:

利用TextView实现歌词同步显示,这是一个简单的利用TextView实现滚动实时显示歌词的。
里面的内容都已经写上了详细的注释。里面播放音乐的时候歌词同步展示。
做媒体这块的朋友可以学习一下,练练歌词同步实现方法。
歌词实现展示类TextShow,

项目源码来源: http://www.itlanbao.com/code/20151214/10000/100695.html

效果截图:

 

主要代码如下:

package com.example.textviewchange;

import java.io.BufferedReader;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import android.app.Activity;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.widget.TextView;

public class MainActivity extends Activity {
	private MediaPlayer player;
	private TextView tv;
	private boolean flag=false;//用来停止和判断线程
	private int i;
	private TextShow ts;
	private Map<String,String> map,map1;
	private int count=5;
	private List<String> list;
	private String s="";
	private boolean change=false;
	Looper looper=Looper.getMainLooper();
	Handler handler=new Handler(looper){
		public void handleMessage(android.os.Message msg) {
			if(msg.what==10&&flag==false){
				if(i==map1.size()){//判断歌词是不是已经全部显示
					flag=true;
			//如果销毁了activity则,不执行下面的方法
					return;
				}
				Changetext();
			}
		};
	};
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		tv=(TextView)findViewById(R.id.tv);
		//获取assert中的歌曲文件以及歌词文件
		AssetManager am=getAssets();
		AssetFileDescriptor afd;
		//使用媒体播放器
		player=new MediaPlayer();
		//设置播放的音频类型
		player.setAudioStreamType(AudioManager.STREAM_MUSIC);
		try {
			afd = am.openFd("apple.mp3");
			FileDescriptor fd=afd.getFileDescriptor();
			player.setDataSource(fd);
			//获取sd卡中的歌曲文件
			//player.setDataSource("/sdcard/Music/apple.mp3");
			player.prepare();
		} catch (IOException e1) {
			e1.printStackTrace();
		}
		//开始播放
		player.start();
		//测试,这里应该用输入输出流读取文件
		map1=new HashMap<>();
		list=new  ArrayList<>();
		//加载歌词文件
		read();
		
		//保留插入顺序
		//设置给显示界面刚开始显示5行
				map=new LinkedHashMap<String,String>();
				for(int i=0;i<5;i++){
					map.put(list.get(i), map1.get(list.get(i)));
				}
				ts=new TextShow(map);
		new Thread(){
			public void run() {
				while(!flag){
					try {
						sleep(500);
						Message mes=new Message();
						mes.what=10;
						handler.sendMessage(mes);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			};
		}.start();
	}
	public void Changetext(){
		//给正在播放的歌词添加样式
		if(map!=null&&map.get(conTime(player.getCurrentPosition()))!=null){
			String s1="";
			tv.setText(ts.Add(conTime(player.getCurrentPosition()).toString()));
			s1=conTime(player.getCurrentPosition()).toString();
			//判断在相同的时间段内,Changetext方法是不是执行了两次
			if(!s.equals(s1)){
				change=true;
				s=s1;
			}else{
				change=false;
			}
			if(change){
			//设置正在播放的歌词的上方的行数
			if(i>5){
				//移除最顶部的一行
				map.remove(list.get(i-6));
			}
				//增加一行在最底部
			if(count<map1.size()){
			map.put(list.get(count), map1.get(list.get(count)));
			}
			i++;
			count++;
			}
		}
	}
	//读取LRC的歌词文件
	public void read(){
		try {
			InputStream in=getAssets().open("c.lrc");
			BufferedReader bf=new BufferedReader(new InputStreamReader(in));
			String msg="";
			while((msg=bf.readLine())!=null){
				if(msg.length()!=0){
					//保存歌词的时间以及歌词内容
					map1.put(msg.substring(1,6), msg.substring(msg.lastIndexOf("]")+1)+"\n");
					//保存每一行歌词的时间
					list.add(msg.substring(1,6));
				}
			}
			bf.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	@Override
	protected void onDestroy() {
		super.onDestroy();
		if(player!=null){
			player.stop();
			player.release();
			player=null;
		}
		flag=true;
	}
	//将音频的播放时间转换成00:00的格式
	public CharSequence conTime(int duration) {
		
		int second=duration/1000;
		int miunte=second/60;
		second=second%60;
		String miunte1 = String.valueOf(miunte);
		String second1 = String.valueOf(second);
		if(miunte<10){
			miunte1="0"+miunte;
		}if(second<10){
			second1="0"+second;
		}
		return miunte1+":"+second1;
	}
}

 

 特殊文字显示 (歌词同步显示)

package com.example.textviewchange;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import android.graphics.Color;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.ForegroundColorSpan;
/*
 *	特殊文字显示 (歌词同步显示)
 * */
public class TextShow {
	private SpannableStringBuilder ssb;
	private SpannableString ss;
	//private String str;
	private Map<String,String> map;
	//传入的map必须为LinkedHashMap
	public TextShow(Map<String,String> map) {
		this.map=map;
		ssb=new SpannableStringBuilder();
	}
	//设置字符串的样式(如果要分行显示,在传入的字符串中添加"\n")
	//根据字符串从map中取值
	public SpannableString changeText(String str){
		ss=new SpannableString(str);
		//设置字符串的大小
		ss.setSpan(new AbsoluteSizeSpan(50),0,ss.length(),Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
		//设置字符串的颜色
		ss.setSpan(new ForegroundColorSpan(Color.MAGENTA), 0, ss.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
		return ss;
	}
	//追加到SpannableStringBuilder中某一行
	
	//ss 需要追加的有样式的字符串
	public SpannableStringBuilder Add(String str){
		//清除上一次的数据
		ssb.clear();
		//遍历map
		Iterator<Entry<String, String>> iter = map.entrySet().iterator();
		while (iter.hasNext()) {
			Map.Entry<String,String> entry = (Map.Entry<String,String>) iter.next();
				String key = entry.getKey();
				String val = entry.getValue();
		if(key.equals(str)){
			ssb.append(changeText(val));
			}
		else{
			SpannableString ss1=new SpannableString(val);
			ssb.append(ss1);
		}
		}
		/* 也可以用下面的方法
		 * for(String string:map.values()){
			if(string.equals(map.get(str))){
				ssb.append(changeText(str));
			}else{
			SpannableString ss1=new SpannableString(string);
			ssb.append(ss1);
			
			}
			
		}*/
		return ssb;
	}
}

 



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



【数读】中国房地产投资增速跌至1%,过剩库存有增无减

$
0
0

【数读】中国房地产投资增速跌至1%,过剩库存有增无减

中国房地产投资增速跌至1%,过剩库存有增无减

中国房地产投资增速跌至1%,过剩库存有增无减

近日,中国社科院发布了一份《中国住房报告(2015-2016)》,称商品住房过剩总库存高达21亿平方米,楼市已进入“结构性过剩”时代。

在过去的十几年中,涌向房地产市场的资本增速平均保持在20-30%之间,即便是金融危机后一年也有超过16%的增速,但自2014年下半年以后,中国房地产市场的投资增速急剧下滑,直至今年11月同比增长跌至1.3%,住房投资增速更是跌到0.7%,几乎是住房市场化以来的最低记录,这也意味着房地产投资对今年GDP增长的直接贡献几近于零。

投资减缓的同时,商品房新开工面积自去年以来也进入负增长状态,施工面积增速一路降至1.8%,但投资和建设速度的大幅减缓并未使得住房过剩的情况有所好转。截至11月,中国商品房待售(空置)面积达7亿平方米,增速超过16%。如果算上期房待售面积,则2015年中国商品房总库存预计高达39亿平方米,去化周期达4.5年。

这些在建或已完工的面积有7成分布在三四线城市,因此相比北上广等一线城市,住房供应过剩的情况在那些最不发达的城市里更为突出。国际货币基金组织IMF在今年4月的一份报告里估计,在北京、上海、深圳等一线城市,过度建设导致房屋存量超过11个月的销售量,但由于外地人口的持续涌入,尚在可控范围内。二线城市的新房存量接近1.5年的销售量,而在三四线城市,待售房屋存量则相当于3年的销售量。

积压卖不出去的房子并没有意味着房地产开发商的好日子也跟着到头了。尽管如钢铁、水泥等下游行业一样靠举债支撑的开放商并不少见,但银行业对他们却格外施恩,比如一家负债水平已经高达292%的大型地产公司,其拥有的土地供应需要八年才能消化,却能继续从银行那里借到1000万人民币的贷款,负债累累的地产开发商们仍然相信中国经济不会放弃他们。

至于这个牵动影响中国经济活动四分之一的行业接下来该如何应对,社科院在报告中建议,2016-2017房地产市场调控应促进投资恢复到10%的增速,并且实现在销售大幅增长的同时,保持房价不要出现大起大落,以免开发商出现债务违约。显然,专家们在为振兴房地产业深谋远虑时,似乎并没有考虑到底谁要来买单的问题。

更多有态度内容请下载网易新闻  http://www.163.com/newsapp/

数读栏目招聘实习生:http://data.163.com/12/1219/17/8J3SUKVK00014MTN.html

[详细]

氪纪2015|如果免费能做成,有 BAT 就够了,还要你们做什么?

$
0
0

编者按:都说2015年是企业级服务市场的元年,说来说去基本分布在5大类:客户关系管理、企业资源计划、工程管理、生产运营、供应链管理。其中,由于CRM(客户关系管理)涉及销售,作为企业营收的根本,是最早进入红海之争的门类,那就不妨借CRM为主线讨论下企业级市场,穿联其中的基本逻辑和内在联系。另外,这也是 36氪 “氪纪2015”系列第一篇行业梳理文章,接下来各领域梳理也会陆续放出。

IDG 资本副总裁张海涛说过:“ 凡是大市场一定有很多人在服务,我作为一个新来的,一穷二白,凭什么从这些人嘴里讨到一碗饭,所以切入模式很重要。”现如今的SaaS概念无非就是在用新的技术去解决老的问题,依旧从切入模式看起。

站在老板角度还是员工角度?

为 B 端提供企业服务,产品设计理念在很大一定程度决定了其盈利模式,甚至决定了其是否盈利。

因为 B 端的付费是企业老板,所以一般的企业服务工具,为了让老板买单,都是站在老板的立场开发设计。拿CRM举例,首先,很强调签到、日报等功能,因为老板不知道员工每天在做些什么;其次,很强调客户资料的留存,因为员工离职的同时也带走了企业的客户资源,所以老板比较担心;再次,为了帮老板对公司做出更好的分析、预测等,员工需要额外录入很多没用的信息,导致了产品的实用性很低。

这样一来,产品恰恰忽略了工具的使用者——员工, 员工才是工作的主体,他们的用户体验才能决定产品的使用情况。而产品设计与日常工作的脱节导致员工抵触心理,领导层需要用 “胡萝卜+大棒” 的方式,工具却依然推广不畅。

所以, 企业级市场的产品,应该站在员工的角度开发,能真正帮助员工解决痛点、提高工作效率,让员工“用着爽了”,老板自然也开心的付费了。这种理念的践行者,我写过的典型有 Teambition百销帮等。

但这里也存在一些博弈。例如, 在中国,谁愿意自发的被人管理?这是人本性的问题,所以“自下而上”这种推行政策是否真的有效可行?对于产品推广的早期,创业者还是需要稍作权衡,例如 Worktile的CEO王涛曾表示,“自上而下推广会相对顺畅,自下而上的用户活跃度会更高”的结论。销售易CEO史彦泽也表示过“若是协同类、工具类或许不一样,但管理类软件从上而下最有效”。

但可以肯定的是,未来的企业, “管人” 的理念会越来越淡化,更多的会靠团队自己的驱动力。所以要想做的长远,产品好用才是王道。

做企业级市场该不该收费?

最近还有创业者在疑惑这个问题,在这里我比较喜欢一句话: “如果免费能做成,有BAT就够了,还要你们做什么。”

我们首先要有认知,B端市场和C端市场是不一样的,免费、补贴略有效果,但最终改变不了什么。企业级客户绝对不会因为你给这个公司贴钱,他就用这个产品,所以也请不要指望通过撒很多的广告一下子铺开市场。

其次,就是那句老话, 一分钱一分货。

举个例子,SugarCRM,美国著名的客户关系管理系统,苹果、IBM都是其客户。它一开始在做开源,一款完全开放源代码的商业开源软件,产品对外免费,通过培训和售后收费。结果呢?公司研发成本过高,没钱导致没有资本去提高产品。后来及时改变策略才作出了一些成绩。

再举个例子,我写过一个 法律服务平台“米律”,也算是企业服务之一,创始人郑明龙的观点我就很赞同,第一,平台做低价格是在自我贬低律师的价值,这样就很难吸引优秀的人才跟你一起玩。第二, 免费在一定程度上就意味着免责。

说到这里,扯出行业一个普遍现象——“ 羊毛出在猪身上”——产品使用我不收你钱,但我拿企业的数据做文章。现在的企业都不傻,若不收费,用户的信任是要溃败的,因为他不知道你拿他企业的数据干什么样的事情,出了事情,反正不收费也可以不担责任。其实很多财务软件就是在这么玩。

当然,早期低价做一些推广,是无可厚非的。不过创业者要明白, 低价推广绝不等于没有商业模式。其实企业级产品的商业逻辑非常简单: 产品好用,为企业创造了价值,客户买单。

收谁的费?怎么收费?

最大的收益是公司,所以买单的应该是老板。

在中国,让一个C端员工为企业软件付费,确实不符合逻辑。不过 这里有一个切入点:我只做一款面向C端的产品,收C端的费用。我写过一个 保险行业的产品“宜保通”,他就是通过跟大保险公司谈妥,由保险公司向手下员工推广产品,产品对企业免费,对员工收费,但保证提高业绩。还有之前写过的 百销帮纷享销客投资的客脉,也是直接售卖给员工使用,百销帮自去年11月份已经累积10 万多个人用户,客脉上线两个月累积了1万个人用户。

不过也有质疑: 我做一款产品,既能覆盖这些单纯C端产品的功能,又加入了B端管理层想要看到的数据统计,再让公司买单,岂不是更好?所以,直接做C端产品是否大有可为,没办法下定论,但我个人觉得不失为一种方向。

接下来的问题就是怎样收费?

目前普遍的一种方式就是按照企业内部的使用人头收费。这里就有问题,对中小型企业来说,购买成本是一个问题,按照人头来买服务还是偏贵的,拿ERP举例,企业为了省钱,只买了部分名额,导致软件在企业中的渗透率不过5%左右,公司内部只有部分的流程可以通过系统来进行标准化管理。然后事态发展就变成这样: 为了省钱,用的人少,用的人越少,越没有效果,既然没有效果,那就不必使用。

这里有一个创新点就是 按照效果收费,我写过的 ERP产品“互道”就是这个思路。逻辑上非常合理,我帮你多赚了钱,你给我分成,赚多少分多少,大家都开心。但是有一个很大的难点,一个企业业绩增长的原因是来自多方面的,多少增长是来自软件的,基本不可能被量化。并且,就全球排名第一的CRM软件Salesforce来说,帮企业提高的效率平均在30%。初创公司没有信心去按照效果收费也是情理之中。

所以,按照时间单位收取服务费貌似是一种比较折中的方式,当然,如果有效果量化机制,自然是极好的。

目标客户应该为大企业、中企业、还是小企业?

目标客户的定位很重要。

大企业客户是营收的主要来源。像Salesforce,80%的营收来自顶层的2000家客户。但是, 做大客户是一件很苦逼的事情,因为大公司的诉求太多,不能满足在一个标准平台,基本都需要个性化定制,那如果都跑去给大企业一个一个的做项目,耗费精力不说、对于行业的意义貌似也并不大。并且大企业已经有传统的大服务商在做,要挤进去,也是有些困难的。

再说小微企业通常没有黏性,由于公司体量不大,本身就没有流程,工作场景下需要的统计用Excel可能差不多就够用了,更不用谈论专业性的需求。这种企业有一个特点:你的软件今天免费,我就用,等你开始收费了,我就找别的免费的用。所以,小微企业的故事基本不讲了。不过,这里也有博弈,有一个逻辑是说,小企业总有一天会长大,他们更是需要专业的管理软件来帮他们成长。并且等他们长大之后,便是软件的忠实用户。

只能说,相对而言,定位中小企业还是比较靠谱的。

在企业服务中,移动市场真的诉求很大?

接触了很多创业者发现,大家开始强调移动端市场。但我一直有一个疑问,办公场景中有多少是基于移动端完成的?莫非大家都在用手机记账?用手机记录销售客户?用手机管理企业资源?

这个问题 销售易CTO周然的观点我比较认同,手机作为一个纪录工具是很方便的,但应该侧重在户外场景下,比如说根据定位发现附近有哪些客户可以拜访,从而基于地理位置进行推荐。在北美,移动端已达到足够智能化的程度,可以一搜索就发现附近所有的公开信息。

还有近期 拿到 A 轮融资的“天焱微企”,产品iWORK365只是基于微信公众号的协作平台。其CEO吴泳涛坦言,基于移动端确实只能解决一些相对轻量的工作,比较重度的工作不适合在微信完成。

不过,这里有一个不得不提的案例—— 和创科技,原名图搜天下,只做移动CRM,产品名红圈营销。该公司已在今年11月13日挂牌上市,股票代码 834218。他算是新时代CRM行业上市的第一股,为什么只做移动端的可以上市?我们来看下他的标杆客户,有三元、新希望集团、复星、依文和居然之家等。和创的打法,就是每个行业先攻下前几家企业,然后用这些标杆企业的去谈妥行业其他家。不难发现,这些客户基本是传统产业,他们的销售场景还停留在很原始、相对简单记录的阶段,并不需要多复杂。

所以说,移动是不是刚需,要看场景、看定位。

企业级市场CRM、团队协作等的逻辑应该是怎样?

企业级应用CRM、OA、ERP、团队协作等都是分开在做的。现在很多团队协作应用开始在自己的平台上串联CRM、OA、ERP等,他们的逻辑是,用团队协作可以链接整个公司的业务,但若垂直在CRM、ERP等一个门类做协作,只能局限成销售团队的协作、管理团队的协作,不能动员公司全局。

这个逻辑是没问题的,不过当创业者开辟一条通往市场的丝绸之路时,大厂往往就沿着方向进入了。还拿CRM举例,大家总是对比销售易和纷享销客,但销售易只做CRM,纷享销客更多是基于协作平台突出了CRM,后来阿里的 钉钉就寻迹切入市场,导致我们经常看到同一个楼宇里,钉钉的广告放一天,纷享销客的广告放一天。

既然谈到大厂,这里顺便扯句, 当大厂进来的时候,专注就是壁垒。说到专注,各行各业都有自己的产业逻辑,制奶业的链条不可能无缝应用在建筑业。每个行业都做定制,成本太高,但若不做定制,产品又不够专业——这又是一个博弈。所以,开放接口,让产品可以模块化定制,更灵活的适应各行业是一条不错的路子。

总结

美国也是做了 10 多年才起来的企业级市场,在中国必不会太短期内看到成效,但可以肯定的是,中国的发展速度会快于美国。未来,基于各行业的SaaS产品或成为投资人的看点。

P.S.  2015 ,是冰火两重天的 2015。从投融资的轰轰烈烈,到热潮迅疾褪去,一切发生也不过转瞬。而越是这般在激变的年代里,所历之事便越值得记录,因为其中往往不乏激流与暗涌,光环与落败,于经历者而言是大彻大悟,于旁观者则是趣味盎然,因此要做成一本 “互联网创业史记” 的 36 氪媒体自然不会错过写下他们的机会,以是我们写成 “氪纪 2015” 系列,将这一年之事总结写成后端至诸位面前,供诸位品判。 

hadoop调度算法

$
0
0

 

 

1 hadoop目前支持以下三种调度器:

 

FifoScheduler:最简单的调度器,按照先进先出的方式处理应用。只有一个队列可提交应用,所有用户提交到这个队列。没有应用优先级可以配置。

CapacityScheduler:可以看作是FifoScheduler的多队列版本。每个队列可以限制资源使用量。但是,队列间的资源分配以使用量作排列依据,使得容量小的队列有竞争优势。集群整体吞吐较大。延迟调度机制使得应用可以放弃跨机器或者跨机架的调度机会,争取本地调度。
      详情见官网 http://hadoop.apache.org/docs/r1.2.1/capacity_scheduler.html

FairScheduler:多队列,多用户共享资源。特有的客户端创建队列的特性,使得权限控制不太完美。根据队列设定的最小共享量或者权重等参数,按比例共享资源。延迟调度机制跟CapacityScheduler的目的类似,但是实现方式稍有不同。资源抢占特性,是指调度器能够依据公平资源共享算法,计算每个队列应得的资源,将超额资源的队列的部分容器释放掉的特性。
     详情见官网 http://hadoop.apache.org/docs/r1.2.1/fair_scheduler.html

 

 

2 比较:


 

3 配置FairScheduler

修改mapred-site.xml,然后重启集群
更多配置见conf/fair-scheduler.xml



 

 

 

4 配置CapacityScheduler
 

修改mapred-site.xml,然后重启集群
更多配置见conf/capacity-scheduler.xml


 



 

 

 

 



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐




社会化海量数据采集爬虫框架搭建

$
0
0

转载自:  http://www.lanceyan.com/tech/arch/snscrawler.html

 

 

随着BIG DATA大数据概念逐渐升温,如何搭建一个能够采集海量数据的架构体系摆在大家眼前。如何能够做到所见即所得的无阻拦式采集、如何快速把不规则页面结构化并存储、如何满足越来越多的数据采集还要在有限时间内采集。这篇文章结合我们自身项目经验谈一下。

我们来看一下作为人是怎么获取网页数据的呢?

1、打开浏览器,输入网址url访问页面内容。
2、复制页面内容的标题、作者、内容。
3、存储到文本文件或者excel。

从技术角度来说整个过程主要为 网络访问、扣取结构化数据、存储。我们看一下用java程序如何来实现这一过程。

import java.io.IOException;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.lang.StringUtils;

public class HttpCrawler {
       public static void main( String[] args) {

             String content = null ;
             try {
                  HttpClient httpClient = new HttpClient();
                   //1、网络请求
                  GetMethod method = new GetMethod("http://www.baidu.com" );
                   int statusCode = httpClient.executeMethod(method);
                   if (statusCode == HttpStatus. SC_OK) {
                        content = method.getResponseBodyAsString();
                         //结构化扣取
                         String title = StringUtils.substringBetween(content, "<title>" , "</title>" );
                         //存储
                         System. out .println(title);
                  }

            } catch (HttpException e) {
                  e.printStackTrace();
            } catch ( IOException e) {
                  e.printStackTrace();
            } finally {
            }
      }
}

通过这个例子,我们看到通过httpclient获取数据,通过字符串操作扣取标题内容,然后通过system.out输出内容。大家是不是感觉做一个爬虫也还是蛮简单呢。这是一个基本的入门例子,我们再详细介绍怎么一步一步构建一个分布式的适用于海量数据采集的爬虫框架。

整个框架应该包含以下部分,资源管理、反监控管理、抓取管理、监控管理。看一下整个框架的架构图:

社会化海量数据抓取组件图

    • 资源管理指网站分类体系、网站、网站访问url等基本资源的管理维护;
    • 反监控管理指被访问网站(特别是社会化媒体)会禁止爬虫访问,怎么让他们不能监控到我们的访问时爬虫软件,这就是反监控机制了;

一个好的采集框架,不管我们的目标数据在哪儿,只要用户能够看到都应该能采集到。所见即所得的无阻拦式采集,无论是否需要登录的数据都能够顺利采集。现在大部分社交网站都需要登录,为了应对登录的网站要有模拟用户登录的爬虫系统,才能正常获取数据。不过社会化网站都希望自己形成一个闭环,不愿意把数据放到站外,这种系统也不会像新闻等内容那么开放的让人获取。这些社会化网站大部分会采取一些限制防止机器人爬虫系统爬取数据,一般一个账号爬取不了多久就会被检测出来被禁止访问了。那是不是我们就不能爬取这些网站的数据呢?肯定不是这样的,只要社会化网站不关闭网页访问,正常人能够访问的数据,我们也能访问。说到底就是模拟人的正常行为操作,专业一点叫“反监控”。

那一般网站会有什么限制呢?

一定时间内单IP访问次数,没有哪个人会在一段持续时间内过快访问,除非是随意的点着玩,持续时间也不会太长。可以采用大量不规则代理IP来模拟。

一定时间内单账号访问次数,这个同上,正常人不会这么操作。可以采用大量行为正常的账号,行为正常就是普通人怎么在社交网站上操作,如果一个人一天24小时都在访问一个数据接口那就有可能是机器人了。

如果能把账号和IP的访问策略控制好了,基本可以解决这个问题了。当然对方网站也会有运维会调整策略,说到底这是一个战争,躲在电脑屏幕后的敌我双方,爬虫必须要能感知到对方的反监控策略进行了调整,通知管理员及时处理。未来比较理想应该是通过机器学习算法自动完成策略调整,保证抓取不间断。

    • 抓取管理指通过url,结合资源、反监控抓取数据并存储;我们现在大部分爬虫系统,很多都需要自己设定正则表达式,或者使用htmlparser、jsoup等软件来硬编码解决结构化抓取的问题。不过大家在做爬虫也会发现,如果爬取一个网站就去开发一个类,在规模小的时候还可以接受,如果需要抓取的网站成千上万,那我们不是要开发成百上千的类。为此我们开发了一个通用的抓取类,可以通过参数驱动内部逻辑调度。比如我们在参数里指定抓取新浪微博,抓取机器就会调度新浪微博网页扣取规则抓取节点数据,调用存储规则存储数据,不管什么类型最后都调用同一个类来处理。对于我们用户只需要设置抓取规则,相应的后续处理就交给抓取平台了。

整个抓取使用了 xpath、正则表达式、消息中间件、多线程调度框架(参考)。 xpath 是一种结构化网页元素选择器,支持列表和单节点数据获取,他的好处可以支持规整网页数据抓取。我们使用的是google插件  XPath Helper,这个玩意可以支持在网页点击元素生成xpath,就省去了自己去查找xpath的功夫,也便于未来做到所点即所得的功能。 正则表达式补充xpath抓取不到的数据,还可以过滤一些特殊字符。 消息中间件,起到抓取任务中间转发的目的,避免抓取和各个需求方耦合。比如各个业务系统都可能抓取数据,只需要向消息中间件发送一个抓取指令,抓取平台抓完了会返回一条消息给消息中间件,业务系统在从消息中间件收到消息反馈,整个抓取完成。 多线程调度框架之前提到过,我们的抓取平台不可能在同一时刻只抓一个消息的任务;也不可能无限制抓取,这样资源会耗尽,导致恶性循环。这就需要使用多线程调度框架来调度多线程任务并行抓取,并且任务的数量,保证资源的消耗正常。

不管怎么模拟总还是会有异常的,这就需要有个异常处理模块,有些网站访问一段时间需要输入验证码,如果不处理后续永远返回不了正确数据。我们需要有机制能够处理像验证码这类异常,简单就是有验证码了人为去输入,高级一些可以破解验证码识别算法实现自动输入验证码的目的。

扩展一下 :所见即所得我们是不是真的做到?规则配置也是个重复的大任务?重复网页如何不抓取?

1、有些网站利用js生成网页内容,直接查看源代码是一堆js。 可以使用mozilla、webkit等可以解析浏览器的工具包解析js、ajax,不过速度会有点慢。
2、网页里有一些css隐藏的文字。使用工具包把css隐藏文字去掉。
3、图片flash信息。 如果是图片中文字识别,这个比较好处理,能够使用ocr识别文字就行,如果是flash目前只能存储整个url。
4、一个网页有多个网页结构。如果只有一套抓取规则肯定不行的,需要多个规则配合抓取。
5、html不完整,不完整就不能按照正常模式去扣取。这个时候用xpath肯定解析不了,我们可以先用htmlcleaner清洗网页后再解析。
6、 如果网站多起来,规则配置这个工作量也会非常大。如何帮助系统快速生成规则呢?首先可以配置规则可以通过可视化配置,比如用户在看到的网页想对它抓取数据,只需要拉开插件点击需要的地方,规则就自动生成好了。另在量比较大的时候可视化还是不够的,可以先将类型相同的网站归类,再通过抓取的一些内容聚类,可以统计学、可视化抓取把内容扣取出几个版本给用户去纠正,最后确认的规则就是新网站的规则。这些算法后续再讲。这块再补充一下(多谢zicjin建议):

背景:如果我们需要抓取的网站很多,那如果靠可视化配置需要耗费大量的人力,这是个成本。并且这个交给不懂html的业务去配置准确性值得考量,所以最后还是需要技术做很多事情。那我们能否通过技术手段可以帮助生成规则减少人力成本,或者帮助不懂技术的业务准确的把数据扣取下来并大量复制。

方案:先对网站分类,比如分为新闻、论坛、视频等,这一类网站的网页结构是类似的。在业务打开需要扣取的还没有录入我们规则库的网页时,他先设定这个页面的分类(当然这个也可以机器预先判断,他们来选择,这一步必须要人判断下),有了分类后,我们会通过“统计学、可视化判断”识别这一分类的字段规则,但是这个是机器识别的规则,可能不准确,机器识别完后,还需要人在判断一下。判断完成后,最后形成规则才是新网站的规则

7、 对付重复的网页,如果重复抓取会浪费资源,如果不抓需要一个海量的去重判断缓存。判断抓不抓,抓了后存不存,并且这个缓存需要快速读写。常见的做法有bloomfilter、相似度聚合、分类海明距离判断。

  • 监控管理指不管什么系统都可能出问题,如果对方服务器宕机、网页改版、更换地址等我们需要第一时间知道,这时监控系统就起到出现了问题及时发现并通知联系人。

目前这样的框架搭建起来基本可以解决大量的抓取需求了。通过界面可以管理资源、反监控规则、网页扣取规则、消息中间件状态、数据监控图表,并且可以通过后台调整资源分配并能动态更新保证抓取不断电。不过如果一个任务的处理特别大,可能需要抓取24个小时或者几天。比如我们要抓取一条微博的转发,这个转发是30w,那如果每页线性去抓取耗时肯定是非常慢了,如果能把这30w拆分很多小任务,那我们的并行计算能力就会提高很多。不得不提的就是把大型的抓取任务hadoop化,废话不说直接上图:

社会化海量数据抓取组件图

 



已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



基于WPAD的中间人攻击

$
0
0

0x00 前言


学习@Her0in《Windows名称解析机制探究及缺陷利用》很受启发,于是对其实际利用做了进一步研究,发现基于WPAD的中间人攻击很是有趣,现将收获分享给大家。

p1

0x01 简介


WPAD:

全称网络代理自动发现协议(Web Proxy Autodiscovery Protocol),通过让浏览器自动发现代理服务器,定位代理配置文件,下载编译并运行,最终自动使用代理访问网络。

PAC:

全称代理自动配置文件(Proxy Auto-Config),定义了浏览器和其他用户代理如何自动选择适当的代理服务器来访问一个URL。

要使用 PAC,我们应当在一个网页服务器上发布一个PAC文件,并且通过在浏览器的代理链接设置页面输入这个PAC文件的URL或者通过使用WPAD协议告知用户代理去使用这个文件。

WPAD标准使用 wpad.dat,PAC文件举例:

function FindProxyForURL(url, host) {
   if (url== 'http://www.baidu.com/') return 'DIRECT';
   if (host== 'twitter.com') return 'SOCKS 127.0.0.10:7070';
   if (dnsResolve(host) == '10.0.0.100') return 'PROXY 127.0.0.1:8086;DIRECT';
   return 'DIRECT';
}

0x02 WPAD原理


如图

这里写图片描述

用户在访问网页时,首先会查询PAC文件的位置,具体方式如下:

1、通过DHCP服务器

如图

这里写图片描述

web浏览器向DHCP服务器发送DHCP INFORM查询PAC文件位置

DHCP服务器返回DHCP ACK数据包,包含PAC文件位置

2、通过DNS查询

web浏览器向DNS服务器发起 WPAD+X 的查询

DNS服务器返回提供WPAD主机的IP地址

web浏览器通过该IP的80端口下载wpad.dat

3、通过NBNS查询

Tips:

Windows 2K , XP , 2K3 只支持 DNS 和 NetBIOS

Windows Vista 之后(包括 2K8 , Win7,Win8.x,Win 10)支持DNS、NBNS、LLMNR

如果DHCP和DNS服务器均没有响应,同时当前缓存没有所请求的主机名,就会发起如下名称解析:

如果当前系统支持LLMNR(Link-Local Multicast Name Resolution),先发起广播LLMNR查询,如果没有响应再发起广播NBNS查询

如果有主机回应PAC文件位置

web浏览器通过该IP的80端口下载wpad.dat

0x03 WPAD漏洞


对照WPAD的原理,不难发现其中存在的漏洞,如图

这里写图片描述

如果在被攻击用户发起NBNS查询时伪造NBNS响应,那么就能控制其通过伪造的代理服务器上网,达到会话劫持的目的。

0x04 WPAD漏洞测试


测试环境:

被攻击用户:
win7 x86
192.168.16.191

攻击用户:
kali linux
192.168.16.245

测试过程:

1、监听NBNS查询

use auxiliary/spoof/nbns/nbns_response
set regex WPAD
set spoofip 192.168.16.245
run

如图

这里写图片描述

2、设置WPAD服务器

use auxiliary/server/wpad
set proxy 192.168.16.245
run

如图

这里写图片描述

3、被攻击用户发起查询

构造广播NBNS查询

需要使当前dbcp和dns服务器均无法提供的PAC文件位置

4、响应被攻击机用户的广播NBNS查询

如图

这里写图片描述

攻击主机响应广播NBNS查询并指定PAC文件位置

被攻击主机访问指定的PAC位置请求下载

wireshark抓包如图

广播NBNS查询包,如图

这里写图片描述

NBNS查询响应包,如图

这里写图片描述

被攻击主机请求PAC文件位置,如图

这里写图片描述

攻击主机回复PAC文件信息,如图

这里写图片描述

Tips:

虚拟机环境下使用wireshark只抓本地数据包,需要取消混杂模式

如图

这里写图片描述

5、被攻击机用户使用伪造的代理配置上网

可在伪造的代理上面抓取被攻击用户的数据包,中间人攻击成功。

0x05 WPAD实际利用


基于WPAD的中间人攻击有多大威力,超级电脑病毒Flame给了我们很好的示范。

其工作模式如下:

1、SNACK: NBNS spoofing

监听当前网络,如果收到了NBNS查询包含WPAD字符,立即伪造NBNS响应

2、MUNCH: Spoofing proxy detection and Windows Update request

提供WPAD服务,用来更改被攻击主机的WPAD设置

当其成功作为被攻击主机的代理后,会劫持特定的Windows更新请求,提供带有后门的windows更新文件给用户下载

如图为测试环境下抓到的windows更新请求包

这里写图片描述

Burp suite抓到的数据包:

这里写图片描述

Flame最终成功实现了基于WPAD实施中间人攻击,篡改windows更新数据,最终感染了内网其他主机。

0x06 防护


可通过如下设置关闭WPAD应用来避免此种攻击:

Internet Explorer-Internet Options-Connections-LAN settings

取消选中Automatically detect settings

如图

这里写图片描述

这里写图片描述

如果已被NBNS中间人攻击,可通过查看netbios缓存检查

nbtstat -c

如图

这里写图片描述

0x07 补充


Responder:

Responder is a LLMNR, NBT-NS and MDNS poisoner, with built-in HTTP/SMB/MSSQL/FTP/LDAP rogue authentication server supporting NTLMv1/NTLMv2/LMv2, Extended Security NTLMSSP and Basic HTTP authentication.

Responder可以说是内网中间人攻击神器,很值得尝试

简单使用命令如下:

git clone https://github.com/SpiderLabs/Responder.git
cd Responder/

python Responder.py -I eth0 -i 192.168.16.245 -b

当被攻击主机访问主机共享时就能抓到其hash,如图

这里写图片描述

0x08 小结


虽然WPAD不是很新的技术,但是对其了解的都不太多,在内网渗透中应该被重视。

参考资料:

本文由三好学生原创并首发于乌云drops,转载请注明

使用 BaaS 工具 与 React Native 构建原生应用

$
0
0


"
React Native是 Facebook 在今年的 F8 大会上发布的移动应用开发方案。它基于JavaScript 和 React,可以让 Web 应用开发者在保持原有的开发体验和效率的同时,为 Web 应用带来原生应用的体验。
React Native 使用 JavaScript 作为开发语言,其内建的打包系统支持包括 CommonJS 在内的多种模块化标准,因此很多支持浏览器运行环境的 Node Package 也可以运行在 React Native 中。 LeanCloud JavaScript SDK便是其一。

本文将介绍:

  • 如何在 React Native 中使用 Flux 架构来组织应用的数据流,以配合我们的 JavaScript SDK 向 LeanCloud 云端保存数据,重用代码和实现平台差异化。

  • 异步获取当前用户对象

  • 文件上传

  • 向移动端推送消息

在介绍具体内容之前,读者需要先了解和熟悉 React 和 React Native 的基础概念,以及 LeanCloud JavaScript SDK 的基本用法,请参考以下资料:

安装

首先,按照 React Native 文档配置开发环境、生成项目脚手架。这一步完成时,你应该能运行文档中的「AwesomeProject」或者自己的项目。
然后,在项目的根目录下运行以下命令,安装 LeanCloud SDK:

npm install avoscloud-sdk@^1.0.0-rc5 --save

最后在 JavaScript 入口文件(一般是 index.(ios|android).js)中引入 SDK 并初始化:

var AV = require('avoscloud-sdk');  
AV.initialize(APP_ID, APP_KEY);

把 APP_ID 和 APP_KEY 替换成实际的应用数据,对应信息在 LeanCloud 的 控制台 / 设置 / 应用 Key里可以找到。再次运行或刷新应用,如果没有报错的话,说明 LeanCloud SDK 已经正确地加载并执行了。
我们的 实时通讯 SDK从 2.3.2 版本开始支持 React Native,你可以使用以下命令来安装最新版本的 SDK:

npm install leancloud-realtime --save

数据存储

在 React Native 中使用 LeanCloud SDK 对数据进行增删改查与在浏览器中使用没区别,具体用法请参考 SDK 文档的 「对象」章节以及 LeanCloudRocks Demo。接下来讨论的是 React 与 LeanCloud SDK 应该如何配合工作。
LeanCloudRocks 是一个基础 Demo,演示了如何在一个 React 组件中将用户输入的数据保存到云端。在这个 Demo 中,我们简单地将渲染视图、响应用户操作与操作数据封装在一个 React 组件中。随着应用变得复杂,组件之间开始出现需要共享的数据。我们试图将共享数据交给上级组件维护,却发现这会造成组件中充斥了数据与事件的代理逻辑,父子组件开始耦合。组件之间的耦合意味着 React 的优点之一的可预知性(predictablility)会大打折扣。解决这个问题的一个方案是 Flux架构。

Flux 解耦了视图(View)与数据(Store)。上图展示了 Flux 中的数据流向:视图层不再关心数据的变化,只负责将某一个数据快照渲染为页面,以及触发事件(Action);预先定义好的有限的事件通过 Dispatcher 派发给订阅了事件的数据;数据更新自己,然后通知视图重新渲染。在这个闭环中,React 很好地扮演了视图的角色,而 LeanCloud SDK 工作在数据层。Flux 架构很好地解耦了 React 与 LeanCloud SDK。

用户系统

在 React Native 中使用 SDK 需要特别注意的是,获取当前用户需要使用新增的异步 API: AV.User.currentAsync()

AV.User.currentAsync().then((currentUser)=>{  
 // do something with currentUser;  
});

由于 React Native 的 Native JavaScript Bridge 的异步通信机制,React Native 的本地存储相关 API 是异步的。为此,我们新增了该异步 API,并将 SDK 的运行环境分为两种:

  • 异步存储环境(React Native)

  • 同步存储环境(浏览器、Node)
    开发者可以通过 AV.localStorage.async来做判断。

如果在 异步存储环境中使用了同步 API 的 AV.User.current(),SDK 会抛异常。而在 同步存储环境中使用了同步 API 或异步 API 的 AV.User.currentAsync()则都不会有问题。换句话说,如果你不确定你的代码会在哪里运行, 使用异步 API 总是安全的
下面介绍一个稍复杂的 Demo: LeanTodo

LeanTodo 是一个使用 LeanCloud SDK 与 React Native 构建的 Todo 应用,支持 iOS 与 Android,通过这个 Demo 你可以学到:

  • 在 React Native 中使用我们的 JavaScript SDK 对数据进行增删改查。

  • 使用 Flux 来组织应用的数据流。

  • 使用我们 JavaScript SDK 中新的异步用户 API。

  • 重用代码,实现平台差异化。

图片上传

React Native 提供了 CameraRoll API 来访问用户的照片,与 JavaScript SDK 的文件上传功能配合使用非常简单,只需要将 CameraRoll.getPhotos()获取到的 image作为 blob参数传给 AV.File的构造函数即可:

CameraRoll.getPhotos({  
 first: 1  
}, (data) => {  
 var edge = data.edges[0];  
 var image = edge.node.image;  
 var file = new AV.File('image.jpg', {  
   blob: image  
 });  
 file.save()  
   .then(  
     () => console.log('图片上传成功'),  
     (err) => console.log('图片上传失败', err)  
   );  
}, console.log);

图片上传的完整 Demo: https://github.com/leancloud/react-native-image-upload-demo

需要注意的几点:

  • CameraRoll 模块使用前需要先 link,具体操作步骤参见 Linking Libraries

  • 图片的读取与发送是直接在 Native 中完成的,并不会出现大量数据在 Native > JS > Native 之间传递,不必担心此类性能问题。

  • CameraRoll 的 Android 实现还没有开源,在 Android 中调用会报错。

推送

推送是原生应用最常见的功能之一。对于客户端,主要需要实现「注册设备」与「响应推送」两个功能。React Native for iOS 也可以与 LeanCloud 的推送服务配合使用,并且几乎不需要写 Objective-C 或 Swift 代码。
使用推送服务的完整 Demo: https://github.com/leancloud/react-native-installation-demo。使用 JavaScript SDK 以及 installation 插件来注册设备、响应推送的详细步骤参见 其源码分析

大家还想用 React Native 与 LeanCloud SDK 实现哪些常用的功能呢?欢迎留言告诉我们。

从 Nginx 默认不压缩 HTTP/1.0 说起

$
0
0

临近年关,明显变忙,博客也更新得慢了,以后尽量保证周更吧。今天这篇文章属于计划之外的更新,源自于白天看到的《 一个基于 http 协议的优化》。在这篇文章中,作者描述了这样一个现象:

在移动的 http 请求量和联通不相上下的前提下,移动的 http response 带来的网络流量是联通的 2.5 倍。移动大概有 3 成的请求都没有做压缩,而联通几乎都是经过压缩的。那些没有经过压缩的 http 会话都是走了 1.0 的协议,相反经过压缩的 http 会话都是走了 http1.1 协议。

也就是说在相同的服务端配置下,移动运营商过来的流量中有 30% 走了 HTTP/1.0,而作者所使用的 HTTP Server,不对 HTTP/1.0 响应启用 GZip。

为什么在移动运营商网络下会有这么高比例的 HTTP/1.0 请求,本文按下不表,总之这一定是移动的原因。直接看另外一个问题,也就是本文标题所写:Nginx 为什么默认不压缩 HTTP/1.0?

那篇文章的作者并没有说明他用什么 HTTP Server,我这里直接当成 Nginx 好了。后面会发现这个问题跟 HTTP 协议有关,所有 HTTP Server 都会面临。

在 Nginx 的 官网文档中,有这样一个指令:

Syntax: gzip_http_version 1.0 | 1.1;
Default: gzip_http_version 1.1;
Context: http, server, location
Sets the minimum HTTP version of a request required to compress a response.

很明显,这个指令是用来设置 Nginx 启用 GZip 所需的 HTTP 最低版本,默认是 HTTP/1.1。也就是说 Nginx 默认不压缩 HTTP/1.0 是因为这个指令,将它的值改为 1.0就能解决问题。

对于文本文件,GZip 的效果非常明显,开启后传输所需流量大约会降至 1/4 ~ 1/3。这么好的事情,Nginx 改一下配置就可以支持,为什么它默认不开启?

Nginx 对于满足条件(请求头中有 Accept-Encoding: gzip,响应内容的 Content-Type 存在于 gzip_types 列表)的请求会采用即时压缩(On-The-Fly Compression),整个压缩过程在内存中完成,是流式的。也就是说,Nginx 不会等文件 GZip 完成再返回响应,而是边压缩边响应,这样可以显著提高 TTFB(Time To First Byte,首字节时间,WEB 性能优化重要指标)。这样唯一的问题是,Nginx 开始返回响应时,它无法知道将要传输的文件最终有多大,也就是无法给出 Content-Length这个响应头部。

我们还知道,HTTP/1.1 默认支持 TCP 持久连接(Persistent Connection),HTTP/1.0 也可以通过显式指定 Connection: keep-alive来启用持久连接。HTTP 运行在 TCP 连接之上,自然也有着跟 TCP 一样的三次握手、慢启动等特性,为了尽可能的提高 HTTP 性能,使用持久连接就显得尤为重要了。

明白以上两点,问题就水落石出了。对于 TCP 持久连接上的 HTTP 报文,客户端需要一种机制来准确判断结束位置,而在 HTTP/1.0 中,这种机制只有 Content-Length。于是,前面这种情况只能要么不压缩,要么不启用持久连接(对于非持久连接,TCP 断开就可以认为 HTTP 报文结束),而 Nginx 默认选择的是前者。

那么在 HTTP/1.1 中,这个问题解决了吗?当然!我在之前的文章中讲过,HTTP/1.1 新增的 Transfer-Encoding: chunked所对应的分块传输机制可以完美解决这类问题。有兴趣的同学可以查看我的这篇文章: HTTP 协议中的 Transfer-Encoding

理论知识先写到这里,最后用实践来验证一下:

首先,不启用 Nginx 的 HTTP/1.0 GZip 功能,使用 HTTP/1.0 请求报文测试:

http/1.0 without gzip

可以看到,尽管我的请求报文中指明了可以接受 GZip,但是返回的内容依然是未压缩的;同时服务端响应了 Content-LengthConnection: keep-alive,连接并没有断开。也就是说 Nginx 为了尽可能启用持久连接,放弃了 GZip,这是 Nginx 的默认策略。

然后,启用 Nginx 的 HTTP/1.0 GZip 功能,使用 HTTP/1.0 请求报文测试:

http/1.0 with gzip

可以看到,这次的请求报文与上次完全一样,但是结果截然不同:虽然返回的内容被压缩了,但是连接也被断开了,服务端返回了 Connection: close。原因就是之前说过的,动态压缩导致无法事先得知响应内容长度,在 HTTP/1.0 中只能依靠断开连接来让客户端知道响应结束了。

最后,使用 HTTP/1.1 请求报文测试:

http/1.1 with gzip

可以看到,由于请求报文是 HTTP/1.1 的,Nginx 能知道这个客户端可以支持 HTTP/1.1 的 Transfer-Encoding: chunked,于是通过分块传输解决了所有问题:既启用了压缩,也启用了持久连接。

那么,对于 HTTP/1.0 请求,我们是让 Nginx 放弃持久连接好,还是放弃 GZip 好呢?

实际上,由于 HTML 文档一般都是使用 PHP、Node.js 等动态语言输出,即使不压缩,Nginx 也无法事先得知它的 Content-Length,在 HTTP/1.0 中横竖都无法启用持久连接,这时还不如启用 GZip 省点流量。

对于 JS、CSS 等事先可以知道大小的静态文本文件,我的建议是,移动端首次访问把重要的 JS、CSS 都内联在 HTML 中,然后存在 localStorage 里,后续不输出;不重要的 JS、CSS 外链并启用 GZip,牺牲 keep-alive 来达到减少流量的目的。

本文先写到这里,欢迎来博客原文进行评论、交流。浏览器的 GZip 其实还有很多有趣的小故事,先卖个关子,以后有空再写。

本文链接: https://imququ.com/post/why-nginx-disable-gzip-in-http10.html参与讨论

推荐: 领略前端技术 阅读奇舞周刊

redis应用场景

$
0
0
Redis在很多方面与其他数据库解决方案不同:它使用内存提供主存储支持,而仅使用硬盘做持久性的存储;它的数据模型非常独特,用的是单线程。另一个大区别在于,你可以在开发环境中使用Redis的功能,但却不需要转到Redis。
转向Redis当然也是可取的,许多开发者从一开始就把Redis作为首选数据库;但设想如果你的开发环境已经搭建好, 应用已经在上面运行了,那么更换数据库框架显然不那么容易。另外在一些需要大容量数据集的应用,Redis也并不适合,因为它的数据集不会超过系统可用的 内存。所以如果你有大数据应用,而且主要是读取访问模式,那么Redis并不是正确的选择。

        然而我喜欢Redis的一点就是你可以把它融入到你的系统中来,这就能够解决很多问题,比如那些你现有的数据库处理起来感到缓慢的任务。这些你就可以通过 Redis来进行优化,或者为应用创建些新的功能。在本文中,我就想探讨一些怎样将Redis加入到现有的环境中,并利用它的原语命令等功能来解决 传统环境中碰到的一些常见问题。在这些例子中,Redis都不是作为首选数据库。

1、显示最新的项目列表

下面这个语句常用来显示最新项目,随着数据多了,查询毫无疑问会越来越慢。

SELECT * FROM foo WHERE ... ORDER BY time DESC LIMIT 10  
        在Web应用中,“列出最新的回复”之类的查询非常普遍,这通常会带来可扩展性问题。这令人沮丧,因为项目本来就是按这个顺序被创建的,但要输出这个顺序却不得不进行排序操作。

        类似的问题就可以用Redis来解决。比如说,我们的一个Web应用想要列出用户贴出的最新20条评论。在最新的评论边上我们有一个“显示全部”的链接,点击后就可以获得更多的评论。

        我们假设数据库中的每条评论都有一个唯一的递增的ID字段。

        我们可以使用分页来制作主页和评论页,使用Redis的模板,每次新评论发表时,我们会将它的ID添加到一个Redis列表:

LPUSH latest.comments <ID>  
       我们将列表裁剪为指定长度,因此Redis只需要保存最新的5000条评论:

       LTRIM latest.comments 0 5000

      每次我们需要获取最新评论的项目范围时,我们调用一个函数来完成(使用伪代码):

FUNCTION get_latest_comments(start, num_items): 
    id_list = redis.lrange("latest.comments",start,start+num_items - 1) 
    IF id_list.length < num_items 
        id_list = SQL_DB("SELECT ... ORDER BY time LIMIT ...") 
    END 
    RETURN id_list 
END 

      这里我们做的很简单。在Redis中我们的最新ID使用了常驻缓存,这是一直更新的。但是我们做了限制不能超过5000个ID,因此我们的获取ID函数会一直询问Redis。只有在start/count参数超出了这个范围的时候,才需要去访问数据库。

        我们的系统不会像传统方式那样“刷新”缓存,Redis实例中的信息永远是一致的。SQL数据库(或是硬盘上的其他类型数据库)只是在用户需要获取“很远”的数据时才会被触发,而主页或第一个评论页是不会麻烦到硬盘上的数据库了。

2、删除与过滤

      我们可以使用LREM来删除评论。如果删除操作非常少,另一个选择是直接跳过评论条目的入口,报告说该评论已经不存在。

       有些时候你想要给不同的列表附加上不同的过滤器。如果过滤器的数量受到限制,你可以简单的为每个不同的过滤器使用不同的Redis列表。毕竟每个列表只有5000条项目,但Redis却能够使用非常少的内存来处理几百万条项目。

3、排行榜相关

      另一个很普遍的需求是各种数据库的数据并非存储在内存中,因此在按得分排序以及实时更新这些几乎每秒钟都需要更新的功能上数据库的性能不够理想。

      典型的比如那些在线游戏的排行榜,比如一个Facebook的游戏,根据得分你通常想要:

         - 列出前100名高分选手

         - 列出某用户当前的全球排名

      这些操作对于Redis来说小菜一碟,即使你有几百万个用户,每分钟都会有几百万个新的得分。

      模式是这样的,每次获得新得分时,我们用这样的代码:

      ZADD leaderboard  <score>  <username>

     你可能用userID来取代username,这取决于你是怎么设计的。

      得到前100名高分用户很简单:ZREVRANGE leaderboard 0 99。

      用户的全球排名也相似,只需要:ZRANK leaderboard <username>。


4、按照用户投票和时间排序

      排行榜的一种常见变体模式就像Reddit或Hacker News用的那样,新闻按照类似下面的公式根据得分来排序:

       score = points / time^alpha

      因此用户的投票会相应的把新闻挖出来,但时间会按照一定的指数将新闻埋下去。下面是我们的模式,当然算法由你决定。

      模式是这样的,开始时先观察那些可能是最新的项目,例如首页上的1000条新闻都是候选者,因此我们先忽视掉其他的,这实现起来很简单。

      每次新的新闻贴上来后,我们将ID添加到列表中,使用LPUSH + LTRIM,确保只取出最新的1000条项目。

      有一项后台任务获取这个列表,并且持续的计算这1000条新闻中每条新闻的最终得分。计算结果由ZADD命令按照新的顺序填充生成列表,老新闻则被清除。这里的关键思路是排序工作是由后台任务来完成的。


5、处理过期项目

      另一种常用的项目排序是按照时间排序。我们使用unix时间作为得分即可。

      模式如下:

       - 每次有新项目添加到我们的非Redis数据库时,我们把它加入到排序集合中。这时我们用的是时间属性,current_time和time_to_live。

       - 另一项后台任务使用ZRANGE…SCORES查询排序集合,取出最新的10个项目。如果发现unix时间已经过期,则在数据库中删除条目。


6、计数

       Redis是一个很好的计数器,这要感谢INCRBY和其他相似命令。

       我相信你曾许多次想要给数据库加上新的计数器,用来获取统计或显示新信息,但是最后却由于写入敏感而不得不放弃它们。

       好了,现在使用Redis就不需要再担心了。有了原子递增(atomic increment),你可以放心的加上各种计数,用GETSET重置,或者是让它们过期。

       例如这样操作:

         INCR user:<id> EXPIRE

         user:<id> 60

       你可以计算出最近用户在页面间停顿不超过60秒的页面浏览量,当计数达到比如20时,就可以显示出某些条幅提示,或是其它你想显示的东西。

7、特定时间内的特定项目

        另一项对于其他数据库很难,但Redis做起来却轻而易举的事就是统计在某段特点时间里有多少特定用户访问了某个特定资源。比如我想要知道某些特定的注册用户或IP地址,他们到底有多少访问了某篇文章。

      每次我获得一次新的页面浏览时我只需要这样做:

       SADD page:day1:<page_id> <user_id>

      当然你可能想用unix时间替换day1,比如time()-(time()%3600*24)等等。

      想知道特定用户的数量吗?只需要使用SCARD page:day1:<page_id>。

       需要测试某个特定用户是否访问了这个页面?SISMEMBER page:day1:<page_id>。



8、实时分析正在发生的情况,用于数据统计与防止垃圾邮件等

        我们只做了几个例子,但如果你研究Redis的命令集,并且组合一下,就能获得大量的实时分析方法,有效而且非常省力。使用Redis原语命令,更容易实施垃圾邮件过滤系统或其他实时跟踪系统。


9、Pub/Sub

       Redis的Pub/Sub非常非常简单,运行稳定并且快速。支持模式匹配,能够实时订阅与取消频道。

10、队列

        你应该已经注意到像list push和list pop这样的Redis命令能够很方便的执行队列操作了,但能做的可不止这些:比如Redis还有list pop的变体命令,能够在列表为空时阻塞队列。

       现 代的互联网应用大量地使用了消息队列(Messaging)。消息队列不仅被用于系统内部组件之间的通信,同时也被用于系统跟其它服务之间的交互。消息队 列的使用可以增加系统的可扩展性、灵活性和用户体验。非基于消息队列的系统,其运行速度取决于系统中最慢的组件的速度(注:短板效应)。而基于消息队列可 以将系统中各组件解除耦合,这样系统就不再受最慢组件的束缚,各组件可以异步运行从而得以更快的速度完成各自的工作。

    此外,当服务器处在高并发操作的时候,比如频繁地写入日志文件。可以利用消息队列实现异步处理。从而实现高性能的并发操作。



11、缓存

        Redis的缓存部分值得写一篇新文章,我这里只是简单的说一下。Redis能够替代memcached,让你的缓存从只能存储数据变得能够更新数据,因此你不再需要每次都重新生成数据了。

已有 0人发表留言,猛击->> 这里<<-参与讨论


ITeye推荐



Viewing all 11805 articles
Browse latest View live