2013年6月13日星期四

RabbitMQ与PHP(二)


在上一节中,详细介绍了RabbitMQ的exchange/routingkey/queue等概念,以及示例了如何使用PHP发送和处理消息的代码。这一节,将介绍在项目中如何使用PHP多线程的进行消息实时处理,以及简要介绍一些RabbitMQ的安装相关。熟悉的可以将安装这部分跳过。

一, RabbitMQ的安装:
需要首先安装erlong 
1
2
3
4
5
6
7
wget http://www.erlang.org/download/otp_src_R16B.tar.gz
tar -xzvf otp_src_R16B.tar.gz
cd otp_src_R16B
./configure --help
./configure --prefix=/usr/local/erlong
make
make install
注意: 这里将erlong安装到了指定的目录: /usr/local/erlong,而不是使用默认的路径。这是一个好的习惯,对于版本控制等都会有好处。但是这会导致后面 rabbitMQ报错:找不到erl 执行文件,需要多做一些处理才行。

安装RabbitMQ
1
2
3
4
5
wget http://www.rabbitmq.com/releases/rabbitmq-server/v3.0.4/rabbitmq-server-generic-unix-3.0.4.tar.gz
tar -xzvf rabbitmq-server-generic-unix-3.0.4.tar.gz 
mv rabbitmq_server-3.0.4/ /usr/local/
cd /usr/local/rabbitmq_server-3.0.4/sbin
./rabbitmq-server
到这里会出现一个报错信息: ./rabbitmq-server: line 86: erl: command not found  这是因为erlong指定了安装路径,在系统的PATH中找不到。只要export PATH=$PATH:/usr/local/erlong/bin 就可以了。
如果为了rc.local启动方便,可以将 export PATH=$PATH:/usr/local/erlong/bin 这一行写入到 rabbitmq-server 文件中:
执行后,ps -aux 一下,看到进程中有/usr/local/erlong/lib/erlang/erts-5.10.1/bin/epmd -daemon 和 /usr/local/erlong/lib/erlang/erts-5.10.1/bin/beam.smp 就OK了。

sbin目录下还有一个脚本: rabbitmqctl 也很常用,与 rabbitmq-server 一样需要指明erlong的路径才能正确工作。
常用的方法:  
rabbitmqctl start-app 启动后,执行一下这个比较保险
rabbitmqctl list_exchanges 显示当前所有的交换机
rabbitmqctl list_queue 查看当前有效队列情况

二, PHP extension的安装:
PHP操作rabbitmq需要AMQP扩展的支持。下载扩展: http://pecl.php.net/package/amqp ,安装过程与一般扩展一样,/usr/local/php/bin/phpize; ./configure --with-php-config=/usr/local/php/bin/php-config; make && make install
然后编辑php.ini 插入: 
[amqp]
extension = amqp.so
重启apache或nginx,查看phpinfo其中有关于anqp的段落,就OK了。


三,如何使用PHP进行实时后端消息处理
首先,要保证PHP文件可以正确的以堵塞方式处理消息。代码可参见上一节RabbitMQ与PHP(一)
然后,我们来借助Python实现一个多线程的守护进程,由这个守护进程来调用PHP,把PHP作为工作线程。

启动脚本:start_jobs.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# _*_ coding:utf-8 _*_
'''
yoka at 实现多线程处理任务的守护进程
Created on 2012-4-7
@author: xwarrior
@update: jimmy.dong@gmail.com
'''
#在此引入项目需要的数据包
from MyJobs import MyJobs
from MyThread import MyThread
import logging
import time
     
def main():
    logger = logging.getLogger('main()')
    logger.info('server start!')
    worker_threads = 2 #定义需要启动的线程数量
    timeline = 2 #线程检查时间间隔,秒
    thread_pool = {}
         
    for in range(0, worker_threads ):
        param = 'some param'
        job = MyJobs( param )
        thread = MyThread( job, i )
        thread.setDaemon = True
        thread.start()
        logger.info('start thread %s' %( thread.getName() ))
        thread_pool[i] = thread
         
    #干完就结束模式
    #for eachKey in thread_pool.keys():
    #    thread_pool[eachKey].join()
         
    #保持线程数量模式
    while 1:
        time.sleep(timeline)
        #   检查每一个线程
        for eachKey in thread_pool.keys():
            if thread_pool[eachKey].isAlive():
                print 'thread alive:' + str(i)
            else:
                print 'thread down:' + str(i)
                thread_pool[eachKey].run()
             
    logger.info('main exist!')
    return
     
     
if __name__ == '__main__':
    #init config format
    FORMAT = '%(asctime)-15s  %(name)s %(levelname)s file %(filename)s:lineno %(lineno)s - %(message)s'
    logging.basicConfig(format=FORMAT,level=logging.INFO)
     
    main()
    pass
线程脚本: MyThread.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# _*_ coding:utf-8 _*_
'''
Created on 2013-03-25
@author: jimmy.dong@gmail.com
'''
from threading import Thread
              
class MyThread(Thread):
    '''
    创建线程
    '''
              
    def __init__(self,job,thread_id):
        '''
        Constructor
        '''
        self.job = job
        Thread.__init__(self, name = 'my_thread_%s' %(thread_id))
                  
    def run(self):
        self.job.run()
                      
    def stop(self):
        self.job.exit()
任务脚本: MyJobs.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# _*_ coding:utf-8 _*_
'''
Created on 2013-03-25
@author: jimmy.dong@gmail.com
'''
import os
import urllib2
              
class MyJobs(object):
                  
    def __init__(self, param ):
        #do something
        self.param = param
                  
    def __del__(self):
        ''' destruct '''
        self.exit()
                      
    def exit(self):
        ''' 退出'''
        self.quit = True
              
    def run(self):
        ''' 开始处理 '''
        #使用shell模式
        #cmd = '/usr/bin/curl "http://at.yoka.com/try/amqp_consume.php?key=' + str(self.param) + '"'
        cmd = '/usr/local/php/bin/php -c /usr/local/php/lib/nginx.ini /home/jimmy/at/DocumentRoot/try/amqp_consume.php ' + str(self.param)
        re = os.system(cmd)
                      
        #使用web模式
        #req = urllib2.Request('http://at.yoka.com/try/amqp_consume.php?key=' + str(self.param))
        #response = urllib2.urlopen(req)
        #re = response.read()
        #print re
                     
在任务调度(start_jobs.py)中,设计了两种工作模式: 
一种工作模式是一共启动N个线程去干活,适合于尽快完成一个大任务;
另一种是保持进程数量,当发现某个进程完成后,再重新将进程启动起来。显然,用户守护处理消息适合这种模式。

具体工作在MyJob.py中,提供了系统Shell调用和采用URL调用两种方式。推荐使用shell直接调用php的方式,这样可以灵活控制Php.ini,比如增加auto_prepend_file、增长max_execution_time等。

实际项目中,假定有5种类型的消息,可以启动20个线程,将thread_id当作参数传递给PHP。PHP将thread_id%5当作待处理类型,就可以得到每种类型有4个线程工作的场景了。

考虑到PHP的执行时间限制及内存泄露问题,可以将consume.php脚本进行一下改进,让PHP脚本每次处理指定数量的消息后就退出,由Python多线程框架重新启动线程,以保证运行稳定可靠。另外,将应答改为手工应答,确保消息获得正确有效处理。
1
2
3
4
5
6
7
8
9
10
11
12
/$q->consume('processMessage');   //需手动应答
         
/**
 * 消费回调函数
 */
function processMessage($envelope$queue) { 
    global $counter;
    $msg $envelope->getBody(); 
    echo $msg."\n"//处理消息 
    $queue->ack($envelope->getDeliveryTag()); //手动发送ACK应答 
    if($counter++ > 5)return FALSE;  //处理5个消息后退出
}
用 两个线程,检查间隔2秒,SHELL模式 测试运行结果:

由上图可见,运行开始后,检查2个线程都处于活跃状态,并对消息进行了正确处理,当处理到一定数量后,PHP程序结束,父进程检查到有进程处于完成状态,重新将其启动(第二个绿色框)。完全与预期相符。


RabbitMQ与PHP(一)


项目中使用RabbitMQ作为队列处理用户消息通知,消息由前端PHP代码产生,处理消息使用Python,这就导致代码一致性问题,调整消息定义时需要PHP和Python都进行修改。这两天抽时间研究了下,如何将消息的产生与处理(消费)全部用PHP来做。查资料时发现,关于PHP处理消息队列的资料很少,有必要把一些初学者容易混淆的地方再讲一下。

拟分成两部分: 一,RabbitMQ的原理与操作示例;二,具体服务安装及如何用PHP作为守护模式处理消息。

RabbitMQ是流行的开源消息队列系统,用erlang语言开发,完整的实现了AMPQ(高级消息队列协议)。网站在: http://www.rabbitmq.com/  上面有教程和实例代码(Python和Java的)。




AMPQ协议为了能够满足各种消息队列需求,在概念上比较复杂。首先,rabbitMQ启动默认是没有任何配置的,需要客户端连接上去,设置交换机等才能工作。不把这些基础概念弄清楚,后面程序设计就容易产生问题。

1,vhosts : 虚拟主机。
一个RabbitMQ的实体上可以有多个vhosts,用户与权限设置就是依附于vhosts。对一般PHP应用,不需要用户权限设定,直接使用默认就存在的"/"就可以了,用户可以使用默认就存在的"guest"。一个简单的配置示例:
1
2
3
4
5
6
7
$conn_args = array(
    'host' => '127.0.0.1',
    'port' => '5672',
    'login' => 'guest',
    'password' => 'guest',
    'vhost'=>'/'
);
2,connection 与 channel :  连接与信道
connection是指物理的连接,一个client与一个server之间有一个连接;一个连接上可以建立多个channel,可以理解为逻辑上的连接。一般应用的情况下,有一个channel就够用了,不需要创建更多的channel。示例代码:
1
2
3
4
5
6
//创建连接和channel
$conn = new AMQPConnection($conn_args);  
if (!$conn->connect()) {  
    die("Cannot connect to the broker!\n");  
}  
$channel = new AMQPChannel($conn);
3,exchange 与  routingkey : 交换机 与 路由键
为了将不同类型的消息进行区分,设置了交换机与路由两个概念。比如,将A类型的消息发送到名为‘C1’的交换机,将类型为B的发送到'C2'的交换机。当客户端连接C1处理队列消息时,取到的就只是A类型消息。进一步的,如果A类型消息也非常多,需要进一步细化区分,比如某个客户端只处理A类型消息中针对K用户的消息,routingkey就是来做这个用途的。
1
2
3
4
5
6
7
8
9
10
11
$e_name = 'e_linvo'; //交换机名
$k_route = array(0=> 'key_1', 1=> 'key_2'); //路由key
//创建交换机  
$ex = new AMQPExchange($channel);  
$ex->setName($e_name);
$ex->setType(AMQP_EX_TYPE_DIRECT); //direct类型
$ex->setFlags(AMQP_DURABLE); //持久化
echo "Exchange Status:".$ex->declare()."\n";  
for($i=0; $i<5; ++$i){
    echo "Send Message:".$ex->publish($message . date('H:i:s'), $k_route[i%2])."\n";
}
由以上代码可以看到,发送消息时,只要有“交换机”就够了。至于交换机后面有没有对应的处理队列,发送方是不用管的。routingkey可以是空的字符串。在示例中,我使用了两个key交替发送消息,是为了下面更便于理解routingkey的作用。
对于交换机,有两个重要的概念:
A,类型。有三种类型: Fanout类型最简单,这种模型忽略routingkey;Direct类型是使用最多的,使用确定的routingkey。这种模型下,接收消息时绑定'key_1'则只接收key_1的消息;最后一种是Topic,这种模式与Direct类似,但是支持通配符进行匹配,比如: 'key_*',就会接受key_1和key_2。Topic貌似美好,但是有可能导致不严谨,所以还是推荐使用Direct。
B,持久化。指定了持久化的交换机,在重新启动时才能重建,否则需要客户端重新声明生成才行。
需要特别明确的概念:交换机的持久化,并不等于消息的持久化。只有在持久化队列中的消息,才能持久化;如果没有队列,消息是没有地方存储的;消息本身在投递时也有一个持久化标志的,PHP中默认投递到持久化交换机就是持久的消息,不用特别指定。

4,queue: 队列
讲了这么多,才讲到队列呀。事实上,队列仅是针对接收方(consumer)的,由接收方根据需求创建的。只有队列创建了,交换机才会将新接受到的消息送到队列中,交换机是不会在队列创建之前的消息放进来的。换句话说,在建立队列之前,发出的所有消息都被丢弃了。下面这个图比RabbitMQ官方的图更清楚——Queue是属于ReceiveMessage的一部分。

接下来看一下创建队列及接收消息的示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
$e_name = 'e_linvo'; //交换机名
$q_name = 'q_linvo'; //队列名
$k_route = ''; //路由key
                   
//创建连接和channel
$conn = new AMQPConnection($conn_args);  
if (!$conn->connect()) {  
    die("Cannot connect to the broker!\n");  
}  
$channel = new AMQPChannel($conn);  
                   
//创建交换机  
$ex = new AMQPExchange($channel);  
$ex->setName($e_name);
$ex->setType(AMQP_EX_TYPE_DIRECT); //direct类型
$ex->setFlags(AMQP_DURABLE); //持久化
echo "Exchange Status:".$ex->declare()."\n";  
                     
//创建队列  
$q = new AMQPQueue($channel);
$q->setName($q_name);  
$q->setFlags(AMQP_DURABLE); //持久化
                   
//绑定交换机与队列,并指定路由键
echo 'Queue Bind: '.$q->bind($e_name, $k_route)."\n";
                   
//阻塞模式接收消息
echo "Message:\n";  
$q->consume('processMessage', AMQP_AUTOACK); //自动ACK应答
   
$conn->disconnect();  
                   
/**
 * 消费回调函数
 * 处理消息
 */
function processMessage($envelope, $queue) {
    var_dump($envelope->getRoutingKey);
    $msg = $envelope->getBody();
    echo $msg."\n"; //处理消息
}
从上述示例中可以看到,交换机既可以由消息发送端创建,也可以由消息消费者创建。
创建一个队列(line:20)后,需要将队列绑定到交换机上(line:25)队列才能工作,routingkey也是在这里指定的。有的资料上写成bindingkey,其实一回事儿,弄两个名词反倒容易混淆。
消息的处理,是有两种方式:
A,一次性。用 $q->get([...]),不管取到取不到消息都会立即返回,一般情况下使用轮询处理消息队列就要用这种方式;
B,阻塞。用 $q->consum( callback, [...] ) 程序会进入持续侦听状态,每收到一个消息就会调用callback指定的函数一次,直到某个callback函数返回FALSE才结束。
关于callback,这里多说几句: PHP的call_back是支持使用数组的,比如: $c = new MyClass(); $c->counter = 100; $q->consume( array($c,'myfunc') ) 这样就可以调用自己写的处理类。MyClass中myfunc的参数定义,与上例中processMessage一样就行。

在上述示例中,使用的$routingkey = '', 意味着接收全部的消息。我们可以将其改为 $routingkey = 'key_1',可以看到结果中仅有设置routingkey为key_1的内容了。

注意: routingkey = 'key_1'  与 routingkey = 'key_2' 是两个不同的队列。假设: client1 与 client2 都连接到 key_1 的队列上,一个消息被client1处理之后,就不会被client2处理。而 routingkey = '' 是另类,client_all绑定到 '' 上,将消息全都处理后,client1和client2上也就没有消息了。

在程序设计上,需要规划好exchange的名称,以及如何使用key区分开不同类型的标记,在消息产生的地方插入发送消息代码。后端处理,可以针对每一个key启动一个或多个client,以提高消息处理的实时性。如何使用PHP进行多线程的消息处理,将在下一节中讲述。

更多消息模型,可以参考: http://www.rabbitmq.com/tutorials/tutorial-two-python.html


中国“软件盗版大王”在美获刑12年

新京报讯  据国外媒体报道,美国法院当地时间本周二对中国人李翔(音)进行宣判,称其盗版多种软件价值约1亿美元,李翔被判刑12年。在美国服刑结束后,李翔将遣返回中国。此前2011年6月,李翔被美国政府雇佣人员引至美属塞班岛进行盗版软件交易,当场被捕。
美诱捕中国“盗版大王”
李翔今年36岁,四川成都人,被称为中国“盗版大王”。2008年4月至2011年6月间,李翔通过网站和电子邮件向美国等61个国家和地区的300多名买家,销售了500多件受版权保护作品的盗版制品,总价值达一亿美元。买主甚至包括美国国家航天局(NASA)的前雇员以及一些科学工作者。
此前披露的信息显示,2010年初,美国国土安全部雇佣的代理人向李翔购买了5个软件产品,这其中包括Satellite Tool Kit(航天卫星工具套件),总共价值15万美元,但李翔只收取数千美元。
随后,上述代理人约李翔见面谈生意,并将其吸引至美属塞班岛。2011年6月,在塞班岛的酒店当中,李翔随身携带了一个容量为20GB非法从美国软件公司获得的未授权文件,于是当场被捕。据报道,李翔已于今年1月在美国特拉华州地区法院认罪。
涉及近200家公司
根据诉讼文件,李翔盗用的软件售价则不足原价的5%,一般是20美元到1200美元不等。被盗版的软件产品其中涉及微软、Oracle、SAP、罗克韦尔、安捷伦、西门子等近200家公司。
据媒体报道,业内人士称,在中国企业级软件的盗版现象并不严重,最常出现的情况是企业用户会增多许可证的数量,比如,有些企业用户只购买了20个License(许可证),却使用了50个。此外,无论是微软的个人电脑操作系统,还是企业级软件,都存在漏洞,从技术上是可以破解的。
目前,微软等在中国不断加大反盗版力度,其近期目标主要针对那些销售安装盗版操作系统电脑的销售商。
■ 疑问
李翔是否应回国受审?
昨日,有网友讨论“李翔是否应该回国受审”。
对此,中国政法大学知识产权中心特约研究员赵占领昨日对新京报记者表示,售卖盗版软件属一般刑事犯罪。对于该类行为,被告人住所地、犯罪行为发生地、犯罪行为结果地都可以对其进行审判。
因此,李翔的售卖盗版行为可以属美国管辖范围,并不需要回国受审。
美方是否涉及钓鱼执法?
对于此案,有质疑声音认为,此案中美国国土安全部雇佣代理人,对李翔进行“诱捕”的行为已涉及“钓鱼执法”。
对此,北京市盛峰律师事务所主任律师于国富昨日对新京报记者表示,钓鱼执法本身并不是法律概念。法律上,只有合法执法和违法执法。有些犯罪行为是警方平时无法碰到的,这时,警方只能设置条件进行逮捕。例如,在美国某些州,卖淫嫖娼属违法行为,女警也会经常扮作诱饵对违法分子进行逮捕。
此外,于国富称,也要看犯罪者是否有“犯罪故意”,当其发生犯罪行为后,警方通常会对其进行诱捕。

延安被城管踩头商户:本人没写道歉信

原标题:延安被踩头商户:本人没写道歉信
称信中内容不是自己心里话,城管局有专人陪护,不方便多说;道歉信被指系当地政府危机公关
就在端午假期到来,延安城管临时工跳踩商户头部事件逐渐告一段落之际,一篇名为“致广大关心‘延安5·31事件’网友的一封信”的网帖,再度引发激烈争议。作者以被踩头者刘国峰的口吻,称“我也有一定责任”,希望“不要因为这样一个特殊事件否定延安革命圣地的形象”。
对此,6月12日,延安被踩头商户刘国峰接受《城市信报》记者采访时称:“信不是我写的,不代表我的意思。”城市信报记者了解到,目前刘国锋仍在医院住院,并称身旁有延安市城管局安排的专人陪护,“不方便多说”。
公开信:不要因此事否定延安形象
6月7日23时10分,百度贴吧“延安”吧出现一篇名为“致广大关心‘延安5·31事件’网友的一封信”的网帖。刘国峰表示,其希望网友“不要因为这样一个特殊事件否定延安革命圣地的形象”。
记者注意到,发帖者“美利达店员01”称,“受当事人刘国峰本人委托,美利达现将他致各网友的一封信发布于此”。刘国峰是前段时间引起广泛关注的延安城管暴力执法事件中的受害人,在这一事件中,美利达自行车店店主刘国峰倒地后,被一男城管双脚起跳爆踩头部,导致受伤住院。
这封公开信称,“在这个事件中,打人者固然不对,但客观地说,我也有一定责任,我想,当时我如果能冷静处理,不要太过冲动,也许可以避免肢体上的冲突。在此向在这件事中受到伤害的人们道歉。”
公开信中还说,他和家人对处理结果非常满意,而事件也引发了网上“部分不切实际的传言,对一些人造成了不公正的伤害,也引起一些社会不安定的因素,同时也对延安的城市形象带来了负面影响”。
曾证实委托他人发布道歉信
该公开信甫一面世,就引发了网友的广泛关注,甚至被指“官腔”。南都记者发现,其中不少网友怀疑,这封公开信并非出于刘国峰本人的真实想法和意愿,而是当地政府事后危机公关的举措之一。
主持人孟非就在其认证微博上点评道,“信的内容归纳起来大约是三点:1 .城管打我是有一定道理的;2.城管局的善后我是满意的;3.延安的形象才是最重要的”,并发问“信是谁帮你写的?”
据西部网报道,刘国峰本人证实,该公开信确实是受他委托向社会发布的。
在接受西部网记者采访时,刘国峰表示,对于事情的处理结果,他感觉比较满意,和自己预期差不多。他认为政府和管理部门在处理这件事情时“是比较积极的”。综合《城市信报》、《南方都市报》等媒体
公开信内容:
大家好,我叫刘国锋,是延安美利达自行车店的店主,在5月31日延安城管事件中受伤住院,截止目前,本人病情已基本康复,准备近期出院。5月31日的事情发生后,事件经过被网友传到了网上,引发了广泛的关注,受伤住院的这几天,不断有骑友前来医院看我,全国也有许多网友关心我的伤情,希望我早日康复。我深深为广大骑友和网友们的关心所感动,在此我谨代表自己和家人向所有关心我、帮助我的朋友们说一声谢谢!
回顾5月31日的事件,延安市城管支队凤凰大队执法队员因我车行在门口占道作业而对现场自行车进行收缴,但部分车辆为骑友个人车辆,因此与城管队员发生了争执,后引起了肢体冲突。在这个事件中,打人者固然不对,但客观的说,我也有一定责任,我想,当时我如果能冷静处理,不要太过冲动,也许可以避免肢体上的冲突。在此向在这件事中受到伤害的人们道歉。
事情发生后,引起了广大网友的广泛关注,同时也引起了政府的高度重视。延安市政府部门迅速成立了调查组,介入到整个事件的调查中来。这几天不断有来自政府部门的人跟我联系,调查事情发生的经过,商议处理事宜。在我住院期间,延安市城管局局长、书记多次看望、慰问我,并就此事向我道歉,还派出了专人在医院陪护我。经过与城管陪护人员的交流,我也能理解城管执法的困难。现在,整个事件已经被媒体进行了客观的报道,相关人员也得到了政府部门公正的处理。政府部门在处理事情时快速的反应和积极的态度给了我和家人许多希望,也使我们的权益得到了保障。我的住院治疗费用已由政府部门支付,被收缴车辆经过法定程序,也已经归还给我以及其他车主,我和家人对处理结果是非常满意的。
这件事情发生后引起的广泛关注超出了我的想象,我一方面因为广大网友的关注和关心而感动,但另一方面也深感不安,网络上存在部分不切实际的传言,对一些人造成了不公正的伤害,也引起一些社会不安定的因素,同时也对延安的城市形象带来了负面影响,这不是我希望看到的。作为一个延安人,我对这个城市和这里的人有着很深的感情,不希望他们因为这样一个特殊事件而蒙黑。也希望广大网友不要听信谣言,客观公正地看待这个事件,更不要因为这样一个特殊事件否定延安革命圣地的形象。
最后,再次深深感谢关注此事的媒体和网友,感谢所有关心和帮助我的人,感谢政府迅速积极处理此事,保障了我们应得的权益
文处萄 本文来源:新京报责任编辑:NN131