Save&Load

Save The World, Load The Game

0%

最近在写OC的单元测试,虽然XCTest已经越来越好了,但是对mock的支持还是欠缺很多。所以上网查了下,发现多说OCMock不错,去官网看了看,第一眼看过去主页做的很好看,主页做的好的开源团队一般东西都不会差,赶紧用起来。

配置OCMock

这个官网上有详细的步骤教程,按着做就好了。贴个连接http://ocmock.org/ios/

开始mock

1. mock方法返回值

mock方法返回值这个应该是最常用的一种情况了,也是非常简单的一种情况。示例代码如下:

id mockClass = OCMClassMock([SomeClass class]);
//没有参数的方法
OCMStub([mockClass someMethod]).andReturn(anObject);
//有参数的方法
OCMStub([mockClass someMethod:[OCMArg any]]).andReturn(anObject);

这里需要注意的就是有参数的方法,参数是可以具体指定的,也就是说只有满足你指定的具体参数的调用才会被mock指定的返回值。例子中的[OCMArg any]是指任意参数。

2. 验证mock方法被调用

有些时候需要验证我们执行的代码流程是否调用了某个外部的方法,这个时候用OCMock就比较简单来实现。

id mockClass = OCMClassMock([SomeClass class]);
//...
//some code
//...
OCMVerify([mockClass someMethod]);

如果没有调用过这个方法的话,会立即抛一个异常出来。

3. 验证mock方法没有被调用

而有些时候呢,我们想要验证代码没有调用某个方法,这里因为OCMock对这种情况没有支持,我也查阅了很多资料,最后想出了一个比较取巧的办法,但还算有效。

static BOOL isCalled = false;
id mockClass = OCMClassMock([SomeClass class]);
OCMStub([mockClass someMethod]).andDo(^(NSInvocation *invocation){
isCalled = YES;
});
//...
//some code
//...
XCTAssertFalse(isCalled);

如果方法被调用了,就将isCalled设置为YES,这样最后assert的时候就会报错。

4. 验证mock方法传入的参数

还有的情况需要验证传递给外部调用的参数是否符合预期,示例代码:

id mockClass = OCMClassMock([SomeClass class]);
OCMStub([mockClass someMethod:[OCMArg checkWithBlock:^BOOL(id obj) {
//...
//some code
//...
return YES;
]]);

5. mock单例

如果我们mock的类是个单例的话,那么使用之前的方法进行mock是不会生效的。不过OCMock提供了一个很简单的解决方法,那就是调用单例返回mock。

id mockClass = OCMClassMock([SomeClass class]);
OCMStub([mockClass instanceMethod]).andReturn(mockClass);

这样之后,我们就可以正常的进行mock了。

服务器性能测试备忘录(一)

最近又搞了一下性能测试,发现有一些命令或者是注意事项又忘记了。导致又重新上网查了一遍。觉得这样实在是很没有效率,所以还是整理一下自己写个备忘录,以后查起来也方便一些。

准备阶段

1. 查看服务器参数

在测试之前一般都是要看一下测试服务器的参数,这样能对服务器的性能有个大概的预估。一般比较关心的是CPU、内存、网卡、硬盘这几个参数。

a. 查看CPU信息

$ cat /proc/cpuinfo | grep name | cut -f2 -d: | uniq -c
    4  Intel(R) Xeon(R) CPU E5-2430 0 @ 2.20GHz

其实使用cat /proc/cpuinfo就可以看到CPU的详细信息,但是很多信息对我来说是无用的,我只想知道CPU的型号和核数就可以了。当然如果想要知道更详细的CPU信息,那么就可以用lscpu:

$ lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                8
On-line CPU(s) list:   0-7
Thread(s) per core:    1
Core(s) per socket:    1
CPU socket(s):         8
NUMA node(s):          1
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 45
Stepping:              7
CPU MHz:               2194.971
BogoMIPS:              4388.41
Hypervisor vendor:     Xen
Virtualization type:   full
L1d cache:             32K
L1i cache:             32K
L2 cache:              256K
L3 cache:              15360K
NUMA node0 CPU(s):     0-7

这里会看到CPU更详细的信息。

b. 查看内存信息

$ cat /proc/meminfo
MemTotal:        8191908 kB
MemFree:         1767552 kB
Buffers:          815540 kB
Cached:          1907692 kB
…
`</pre>

这个也是会显示出很多信息,比较常用的是前四项:MemTotal, MemFree, Buffers, Cached。当然如果你只关注这几项,还可以使用free命令来查看内存的使用情况。

<pre class="lang:sh decode:true ">`$ free
total     used       free     shared    buffers     cached
Mem: 8191908    6424488    1767420  0     815580    1908204
-/+ buffers/cache:    3700704    4491204
Swap:            0          0          0
`</pre>

c. 查看网卡信息

<pre class="lang:sh decode:true ">`/sbin/ifconfig -a
eth0   Link encap:Ethernet  HWaddr 00:16:3E:00:11:12
       inet addr:10.125.13.171  Bcast:10.125.15.255  Mask:255.255.240.0
       UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
       RX packets:584832095 errors:0 dropped:0 overruns:0 frame:0
       TX packets:372672049 errors:0 dropped:0 overruns:0 carrier:0
       collisions:0 txqueuelen:1000
       RX bytes:462552962703 (430.7 GiB)  TX bytes:440375675876 (410.1 GiB)
       Interrupt:18

lo     Link encap:Local Loopback
       inet addr:127.0.0.1  Mask:255.0.0.0
       UP LOOPBACK RUNNING  MTU:16436  Metric:1
       RX packets:208327178 errors:0 dropped:0 overruns:0 frame:0
       TX packets:208327178 errors:0 dropped:0 overruns:0 carrier:0
       collisions:0 txqueuelen:0
       RX bytes:387809519667 (361.1 GiB)  TX bytes:387809519667 (361.1 GiB)
`</pre>

看以看到有一个网卡eth0,如果想看网卡的速度,那么就可以用下面的命令:

<pre class="lang:sh decode:true ">`$ ethtool eth0
Settings for eth0:
    Supported ports: [ TP MII ]
    Supported link modes:   10baseT/Half 10baseT/Full 
                            100baseT/Half 100baseT/Full 
                            1000baseT/Half 1000baseT/Full #支持千兆半双工,全双工模式
    Supported pause frame use: No 
    Supports auto-negotiation: Yes #支持自适应模式,一般都支持
    Advertised link modes:  10baseT/Half 10baseT/Full 
                            100baseT/Half 100baseT/Full 
                            1000baseT/Half 1000baseT/Full
    Advertised pause frame use: Symmetric Receive-only
    Advertised auto-negotiation: Yes #默认使用自适应模式
    Link partner advertised link modes:  10baseT/Half 10baseT/Full 
                                         100baseT/Half 100baseT/Full 
    .....
    Speed: 100Mb/s #现在网卡的速度是100Mb,网卡使用自适应模式,所以推测路由是100Mb,导致网卡从支持千兆,变成要支持百兆
    Duplex: Full   #全双工
    .....
    Link detected: yes    #表示有网线连接,和路由是通的
`</pre>

如果ethool看不到的话,那么很有可能网卡是虚拟网卡。这种情况一般网卡带宽都是和其他虚拟机共享的,所以最好实际测试一下比较准确。

d. 查看硬盘信息

<pre class="lang:sh decode:true ">`$ df -h
Filesystem            Size  Used Avail Use% Mounted on
/dev/xvda1            243G   63G  168G  28% /
tmpfs                 4.0G  100K  4.0G   1% /dev/shm
`</pre>

硬盘信息就比较直观,这个一般在发生日志不可写或者其他类似问题的时候需要确认一下磁盘空间是否满了。

e. 查看机器信息

<pre class="lang:sh decode:true ">`$ sudo dmidecode -q
BIOS Information
Vendor: Xen
Version: 4.0.1
Release Date: 12/16/2014
Address: 0xE8000
Runtime Size: 96 kB
ROM Size: 64 kB
Characteristics:
PCI is supported
EDD is supported
Targeted content distribution is supported
BIOS Revision: 4.0

System Information
Manufacturer: Xen
Product Name: HVM domU
Version: 4.0.1
Serial Number: d008a82d-d1b9-7246-b613-57fc82a565c5
UUID: D008A82D-D1B9-7246-B613-57FC82A565C5
Wake-up Type: Power Switch
SKU Number: Not Specified
Family: Not Specified
...

dmidecode命令是以一种可读的方式dump出机器的DMI(Desktop Management Interface)信息。这些信息包括了BIOS、系统、硬件等信息。

OK,第一部分先写到这里。这一部分基本都是准备性能测试时会使用的命令,至于性能测试时监控和排查问题时使用的命令放到后面单独讲。

最近有时间准备恶补一下经典电影,今天看了当幸福来敲门(pursuit of happyness)[2006],题材是典型的励志电影。主人公先是各种遭遇不断,工作不景气、老婆跑了、孩子养不起、最后连住的地方都没有只能去教堂寻求救济。当然结局是好的,男主最终通过自己的不懈努力找到了一份好工作。

虽然情节很老套,但看完之后真的很振奋人心。就会让人觉得自己生活中遇到的苦难算得了什么呢,里面有两段对白印象比较深刻:

1. 你要尽全力保护你的梦想。那些嘲笑你梦想的人,他们必定会失败,他们想把你变成和他们一样的人。我坚信,只要我心中有梦想,我就会与众不同。你也是。

这是男主对他儿子说的话,感觉更像是他自己对自己说的话。在剧中男主的儿子一直是男主奋斗的动力,可能没有这个孩子男主就颓废下去了吧,所以说有个奋斗的目标和动力很重要。

2. 主,不要移开那座大山,请给我力量爬过他。请不要移开那些绊脚的石头,在任何时候指引我,主。我的负担很重,好像很难承受,但是我不会放弃。因为你向我许诺答应了。而且你已经满足了我在圣坛前的祷告。

这段是男主去教堂寻求帮助的时候,教堂里唱的一首歌,唱出了很多人的心声。宗教在人们最苦难的时候永远是最后一根救命的稻草。、

看完之后一直有种生命不息奋斗不止的感觉,正好比较适合这个阶段的自己。所以,继续fighting!!!

最近做的项目里面有这样一个需求,那就是使用Java实现POST不同大小的数据。数据通过读取本地文件得来,当然文件需要自己事先生成好,文件的大小从1KB到10MB,以2的倍数递增。

生成指定大小的文件

首先要做的就是生成指定大小的文件,这个上网一搜很多都是使用linux的dd命令来生成,但是这有一个不好的地方就是生成的文件是用空字符填充的,有些情况下读取一个文件的时候空字符会被忽略,这导致读一个文件最后的大小为0或者某些奇怪的数字。所以就想用实际的字符来生成文件。
既然是个很简单的类似原子操作的事情,果断祭出Python,几行代码搞定。

def create\_file(name, size):
file = open('./' + name, 'w')
for i in range(size):
file.write('t')
file.close()

create\_file(1KB, 1024)

读取文件

接下来就是要到了用Java来读取文件的时候了,感觉Java提供了非常丰富的方法来读取一个文件。但这次需要用到的是两种场景:一是读取整个文件到一个String对象中;二是从文件指定offset位置开始读取指定长度的数据。

第一种情况比较简单,google了一下之后发现一个非常简单的一句话搞定的方法:

String content = new Scanner(new File(filePath).useDelimiter("\\Z").next();

意思就是使用Scanner扫描一个文件直到文件的末尾为止,然后把扫描的内容保存到一个String对象中。

第二种情况就比较麻烦了,刚开始想的时候也比较麻烦,是想把整个文件都读到缓存里面,然后再根据需求读出要的数据。后来查来查去,发现有RandomAccessFile这么一个类,看了下功能简直就是为了这种场景量身定制的。

//打开要读取的文件
RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw");
//将指针直到要开始读取的offset
randomAccessFile.seek(offSet);
//读取指定长度的内容到自己创建的byte数组中
randomAccessFile.read(data, 0, readLength);

只需要三步就完活了,不得不感叹Java提供的工具类真的好强大,码代码之前还是需要多找找,不能一上来就蛮干,否则费力不讨好得不偿失。

昨天看到一个面试题,问的是Java的垃圾回收机制,当时想了想,发现自己对这块一知半解,很难组织系统的语言来描述。既然不是很清楚,当然要趁这个机会好好了解一下。这种东西自然是人家官网的文档最为准确,果断跑去Oracle网站上,找到了一篇讲Java垃圾回收的基础教学篇。看完之后顺便翻译一下,以便以后翻阅。

原文地址:http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

什么是自动垃圾回收?(What is Automatic Garbage Collection?)

自动垃圾回收是针对堆区内存中的对象,分辨哪些对象是正在使用中而哪些对象没有,然后删除那些没用的对象。一个正在使用中的对象或者是一个被引用的对象,意味着你的程序中的某部分仍然维护着指向这个对象的指针。一个无用的对象或者是一个无引用的对象,是说它已经不再被你的程序中任何一个模块所引用。所以被一个无用对象所占用的内存是可以被回收的。

在其他的编程语言,如C中,分配和回收内存是用户手动管理的。但在Java中,内存的回收是由垃圾回收器自动管理的。基本的处理过程如以下的描述。

步骤 1:标记(Marking)

处理过程的第一步叫做标记。这一步中垃圾回收器会分辨哪些内存是使用中,哪些没有。

QQ20140905-2@2x

被引用的对象用蓝色表示。没有被引用的对象用金黄色表示。在标记阶段会扫描所有的对象以判断。这会是一个非常耗时的处理过程因为系统中所有的对象都需要被扫描。

步骤 2:正常删除(Normal Deletion)

正常删除会删除那些没有被引用的对象,保留被引用的对象和指向可用空间的指针。

QQ20140905-3@2x

内存分配器会维护指向可用空间的指针,以便分配新的对象时使用。

步骤 2a:删除压缩(Deletion with Compacting)

为了提高性能,除了删除未被引用的对象之外,你还可以压缩保留的被引用对象。通过将被引用的对象合并到一起,这使得新内存的分配更加简单和快速。

QQ20140905-5@2x

为什么分代垃圾回收?(Why Generational Garbage Collection?)

上文说过,在一个JVM中标记和压缩所有的对象是很低效率的。随着越来越多的对象被分配,对象列表也变得越来越长而垃圾回收的时间就变得越来越长。然而,通过对应用程序的经验分析表明大多数对象的存活时间都很短。

这里是一个例子,Y轴表示已分配的字节数,X轴表示随着时间变化已分配的字节数的变化。

QQ20140905-6@2x

你可以看到,随着时间的增加越来越少的对象在占用内存。实际上,大多数的对象如图中左边所示只有很短的生命周期。

JVM分代(JVM Generations)

从对象分配的规律中学习到的信息,我们可以用以来改善JVM的性能。因此,堆区被分成更小的部分或者说代(generations)。主要有:年轻代(Young Generation)、年老代(Old or Tenured Generation)和永久代(Permanent Generation)。

QQ20140905-7@2x

年轻代,所有新分配和成长中的对象。当年轻代装满时,会引起一次小垃圾回收(minor garbage collection)。小回收可以通过提高对象的淘汰率来提高效率。一个充满死对象的年轻代可以被非常迅速的回收。一些幸存的对象会变老,最终会被移到年老代中。

让地球停止转到的事件(Stop the World Event)- 所有的小垃圾回收都是“让地球停止转动”的事件。这意味着,所有的应用线程都会被停止直到这次回收结束。小垃圾回收总是“让地球停止转动”的事件。

年老代,用来存储长久幸存下来的对象。一般地,对年轻代的对象设置一个阈值,当它的年龄到达了阈值时,这个对象就会被转移到年老代中。最终年老代也需要被回收。这个事件叫做大垃圾回收(major garbage collection)。

大垃圾回收也是一个“让地球停止转动”的事件。一个大垃圾回收经常会更加的缓慢,这是因为它包含了所有活着的对象。所以对于响应型应用程序(Responsive application)来说,大垃圾回收的次数要尽可能的少。同样需要注意,大垃圾回收占用的时间与使用的垃圾回收器的种类也有很大的关系。

永久代,包含JVM所需要的用于描述类和方法的元数据。永久代是在运行时由JVM根据应用程序中所使用的类来进行分配。另外,Java SE库的类和方法可能会存储在这里。

如果JVM发现程序不再需要一些类的时候,它会将这些类回收然后将空间分配给其他的类。在一次全垃圾回收(full garbage collection)中,永久代也会被回收。

分代垃圾回收过程(The Generational Garbage Collection Process)

现在你已经明白了为什么堆区会被分为不同的代,是时候来看一下这些代之间是怎样联系的。以下的图片讲述了JVM中对象的分配和年龄增长的过程。

1. 首先,任何新分配的对象会被分配到伊甸园(eden)中。两个幸存者(survivor)空间一开始都是空的。

QQ20140905-8@2x

2. 当伊甸园被装满时,触发一次小垃圾回收。

QQ20140905-9@2x

3. 被引用的对象会被转移到第一个幸存者空间中。未被引用的对象会被删除。

QQ20140905-10@2x

4. 在下一次小垃圾回收时,伊甸园中发生相同的事情。未被引用的对象被删除而被引用的对象被移到一个幸存者空间中。然后,这回它们被转移到第二个幸存者空间中(S1)。另外,在第一个幸存者空间(S0)中仍幸存的对象会年龄增加然后转移到S1中。一旦所有幸存的对象都被移动到S1中,S0和伊甸园就都清理完毕。注意现在我们现在在幸存者空间中有不同年龄的对象了。

QQ20140905-11@2x

5. 再下一次小垃圾回收,处理流程与上一次一样。不过这次幸存者空间调换了位置。被引用的对象都被转移到了S0.幸存的对比年龄增加。伊甸园和S1被清理完毕。

QQ20140905-12@2x

6. 这次有了晋级操作。在一次小垃圾回收后,当对象的年龄达到了阈值(在例子中为8),它们就从年轻代晋级到了年老代。

QQ20140905-13@2x

7. 由于小垃圾回收不断地被触发,就有对象不断的晋级到年老代。

QQ20140905-14@2x

  1. 到目前为止已经差不多覆盖了年轻代所有的处理过程。最终,在年老代会进行一次大垃圾回收来清理和压缩空间。

QQ20140905-15@2x

PS:看完整个垃圾回收的基础内容之后,很深的一个感受就是命名的时候真的是大有深意啊。有些时候看中文翻译过来的名词会感觉很莫名其妙,但是看了原文后就会恍然大悟。例如本文中涉及到的词:eden, survivor, yong generation, old generation。有兴趣的可以查一下eden的典故,相信很容易就可以把Java分代垃圾回收这一过程理解为一个讲述大自然优胜劣汰适者生存的故事。

最近参与了一个服务器异步化改造的项目,对异步化的理解又多了一些。正好这周还没有写东西,那就借这个话题谈谈同步异步和阻塞非阻塞。

刚开始接触这几个概念的时候,本人也是很容易把他们弄混。这主要是由于网上的资料很多都把这两组名词混在一起讲。其实硬要说这两组名词有什么联系的话,那就是阻塞和非阻塞都是同步,而异步就只是异步。

这里讨论的主要都是网络I/O环境,建议大家读一下Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models ”,书中对各种I/O进行了详细的讲解,读完之后对各种I/O就能有个比较完整的认识了。

首先要明确一下,一次I/O操作其实可以分为两个阶段:

1.数据准备阶段,等待需要的数据到位

2.数据拷贝阶段,将数据拷贝到需要的进程中

同步和异步

同步I/O就是在本次I/O操作完成之前,进程被阻塞在那里。注意这里并没有说是在哪个阶段被阻塞。

异步I/O就是进程不会被阻塞,也就是说进程可以同时进行多个I/O操作。

可以看到同步I/O相当于一心一意只做一件事情,直到这件事情做完了。而异步I/O就是“三心二意”,同时做好多件事。谁好谁坏真的不能一言以蔽之,要根据具体的使用场景来决定用哪种I/O。

阻塞和非阻塞

阻塞I/O就是在数据准备阶段就被阻塞了,直到这次I/O操作结束为止。

非阻塞I/O就比阻塞的要“聪明”一点,它在数据准备阶段会先询问数据是否准备好了,如果没有准备好系统会返回一个错误告诉它。这样它就不会等在那里,而是每隔一段时间就去询问一下直到数据准备就绪。不过数据拷贝阶段依然还是会被阻塞。但这样比阻塞I/O来说被阻塞的时间变短了。

阻塞和非阻塞的区别就是一个“守株待兔”,而另一个“投石问路”。不过既然阻塞和非阻塞I/O都会被阻塞,那也就是说它们都属于同步I/O的范畴内。

最后举一个例子来说明,这个脱胎于在知乎上看到的一个比喻,觉得很好:

有A,B,C三个人去书店买书,

A得知书没有了,所以他就在书店住下了,直到书到货了买到书为止(这只是个比喻。。。)

B得知书没有了,他就回家了。然后每天都来问一次,直到书到货了买到了书。

C得知书没有了,他给书店留了一个电话,这样等书到货了,书店打电话通知他,他再来买书就可以了。

A是阻塞,B是非阻塞,C是异步,而A和B都是同步。

最近在写代码的时候需要模拟多个用户并发请求的场景,这种时候借助于多线程来实现是个很好的办法。而利用线程池来管理和实现多线程是目前比较流行和高效的做法了。

什么是线程池?

我的理解是,线程池就像一个池子,里面有好多的线程。当我们有任务要执行的时候,就从池子里面拿出一个线程来执行,执行完毕之后就把线程放回到池子里。当然如果此时池子里没有线程,那就要等待了。

为什么要用线程池?

通过上面的描述,其实很容易得出使用线程池的好处,主要有三点:第一,降低系统消耗,线程可以重复使用,避免资源反复创建销毁。第二,减少任务等待,线程池中的线程是已经创建好的,任务创建后直接就可以支持,减少了等待线程创建的时间。第三,提高线程可管理性,因为线程都在一个池子里,可以统一的进行分配,监控,管理等。

怎么使用?

说了线程池的好,那就要说说具体该怎么使用了。Java提供了Executors类来创建线程池。能够创建线程池主要有以下几种:

  1. newFixedThreadPool,创建指定线程个数的线程池,也就是说如果任务数比线程数多了,那任务就只能在一个队列中排队等候了。
  2. newSingleThreadExecutor,顾名思义就是创建单个线程,所有的任务按照FIFO的顺序执行。
  3. newCachedThreadPool,这是创建一个可以动态变化大小的线程池,这个就比较智能的感觉,当线程池中线程不够用的时候可以主动创建线程,然后当线程空闲一段时间后,可以主动回收线程。
  4. newScheduledThreadPool,创建一个周期执行任务的线程池。
  5. newWorkStealingPool,JAVA 8中新加的一种,它创建和当前机器可用处理器个数等量的线程数。

实际编码中,大同小异这里以newFixedThreadPool为例,示例代码如下:

先创建一个Runable类说明线程执行的任务内容:

static class Work implements Runnable {
        private int workNumber;

        public Work (int n) {
            this.workNumber = n;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() +
            " Start, " + "Work Number: " + this.workNumber);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() +
                    " Done, " + "Work Number: " + this.workNumber);

        }
    }

 

再创建线程池来执行任务:

 public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(4);
        for (int i = 0; i < 10; i++) {
            executor.execute(new Work(i));
        }

        executor.shutdown();
        try {
            executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("All done.");
    }

 

需要注意的就是在提交完任务给线程池后,如果想要等所有线程都执行完就要先调用shutdown(),再调用awaitTermination()即可。

下面就是程序运行的结果:

java thread pool result

OK,对于线程池的简单使用到这里就可以了。当然根据需求的不同可以对线程池做各种定制修改,可以参考官方文档。这里就不多说了,看以后有没有机会碰到吧。

  • 物品要用才有价值 — 是为断
  • 物品在此时、当下,应出现在需要它的地方 — 是为舍
  • 物品处于恰当的位置,才能展现美感 — 是为离

第一次听到“断舍离”这个名词应该是从数字尾巴中的一篇文章里面看到的,文章里面讲的是刻有“断舍离”三字的刻字图章。当时只是觉得这个东西有一种简单的美感。后来又在“知日”上面看到了它,才知道这是一种生活态度。直到今年,由于年初制定目标每个月要读一本书,所以找着找着就看到这本书了,果断拿下开读。读完一遍之后,个人感觉有所收获但达不到受益匪浅的地步,可能跟本人原本的生活习惯有很多和书中相似的缘故吧。

本书的作者是日本人山下英子,她通过修行瑜伽参透了修行哲学“断行,舍行,离行”,随后以杂物管理咨询师的身份在日本各地研究宣传“断舍离”的理念。所以本书讲的是如何通过“断舍离”来更好的管理我们日常生活中的各种物品,从而通过这种过程完成内心的修炼。下面就讲讲本人对“断舍离”的理解:

断,想清楚这个物品对自己到底有没有用。这是实行“断舍离”的第一步也是最重要的一步,这里最关键的一点在于如何定义“有用”这个概念。例如,厨房里许多无用的杯子,它们可能只在朋友或者亲戚来做客的时候用得上,那这些杯子算“有用”吗?书中说“物品要用才有价值”,而且为了更明确的做判断,把时间条件限定为“当下”,所以那些杯子对于当下来说是没有价值的,也就是应该进行“舍”的。这里面是要表达一个含义:不要做物品的奴隶,生活应该以人为中心,而不是以物品为中心。能做到这一点其实很难,至少我就看到我的长辈们家里存放了很多没用的被子、衣服、厨具、餐具等等的东西,而这些东西可能他们这辈子也不会有再用到的机会了。书中说这些陈旧的东西会影响房间的风水,当然这东西本人是不怎么信,但是这些无用的东西堆积在那里看着烦倒是真的。所以说,“断”需要决心。

舍,just throw it。既然通过“断”确定物品有没有用了,那还留着做什么,果断扔掉吧。所以这一步就是通过清理物品来使房间变得整洁。好处就是我们基本上不怎么需要整理屋子了,为什么?因为屋子里无用的东西都被扔掉了,剩下的东西已经没有很多,而且大部分都在它们应该在的位置,自然不需要整理了。但把那些看着还能用甚至说还没用过的东西直接扔掉就好像来一场说走就走的旅行一样,所以说,“舍”需要勇气。

离,一种舒适的生活状态。“断”和“舍”是动禅,而“离”就是禅定的状态。每天生活在干净整洁的屋子里,自然会有一种舒适的感觉。这与人生气的时候或者郁闷的时候,收拾一下房间,心情就变好了是一个道理。当然要达到这种状态就需要不断的进行“断舍离”,习惯成自然,所以,“断舍离”需要坚持。

“断舍离”不仅可以应用在整理房间和处理物品上,而且还可以应用在做人做事上。例如,你有许多想要做的事情,可以通过“断舍离”快速过滤找到你当前最应该做的事情等等。这些就是本人读后的一点收获,好了写完这些,晚上要回去继续“断舍离”了。

最后引用书中的一段话作为结尾:

一切有形的东西都是虚幻的,我们的心也是不断变化的。尽情地享受与物品难能可贵的短暂相遇,这一定是我们所追求的幸福本身。当缘尽了,就潇洒地放手。不仅对物品,对一切的一切都能这样,这就是断舍离的愿望。

最近准备玩耍一下Python,因为发现语言这个东西,总不用的话就会生疏了。所以打算用Python写点小东西,一方面练练手,一方面还可以深入了解Python的一些机制,何乐而不为呢。

PS:这里要推荐一个新手练习Python的好网站:http://www.checkio.org/,边玩游戏边编码,而且完成一个任务之后可以看其他人的解法,在这个网站上学到了很多有用的东西。目前追求就是用最少的代码完成任务,至少我目前level 9,做过的很多任务都是可以一句话搞定的 – 尽显Python的魅力。

这次做的是一个百度贴吧的自动签到工具,其实这个东西网上已经有了好多了。而且成品也不少。当然在看之前先自己想一下思路。
其实说白了这个工具就是要模拟用户的行为进行签到,那么第一步肯定是要模拟用户登录;用户登录之后呢,我们肯定要到要签到的贴吧进行签到,但是程序自己是不知道用户要签到的贴吧在哪里的,好在百度有个很贴心的贴吧关注功能,这样我们就可以把要签到的贴吧添加关注,这样我们就可以通过获取关注贴吧来知道用户要签到的贴吧,第二步就是获取“关注”贴吧第三步贴吧签到。到此整个流程就完成了。
下面具体讲每一步是如何实现的:

(1)模拟用户登录

这算是整个程序的难点了,因为登录涉及到的东西比较多,首先还是需要看看登录的POST请求到底长什么样子。因为目前来说,网页的登录都是需要POST一个表单给服务器端,服务器端再做鉴权返回结果。所以我们要先用工具来看一下我们登录的时候到底发生了什么。打开Chrome,输入baidu.com,然后打开Chrome的调试工具,登录一下之后我们就可以看到一个POST请求了。如下图:
QQ20140418-1@2x

点开查看细节,主要关注的是Form表单里面传了什么,我们可以看到表单里面传了很多的值,接下来我们就分析一下这些值都是怎么来的。

QQ20140418-2@2x

这些值里面有大部分一看就能够确定是传固定的值就可以了,不需要特别关注,等写的时候copy一份过来就可以。其中有几个字段是需要关注的:
token:看格式就像是一个MD5加密的32位字段,这个要伪造有点困难,要找一下有没有什么获得的方法。
username:用户名
password:密码
callback:看起来也不像是个固定的值,查一下是如何生成的

下面先看一下token,首先我们想这个值不可能是我们这边自己生成的,肯定是服务器端生成然后传给我们的。那既然是传给我们的,那肯定是在登录之前的某次请求中返回的。想清楚这个问题就好办了,在登录之前请求的url就那么几个,一个一个试过去,果然在请求
https://passport.baidu.com/v2/api/?getapi&tpl=mn&apiver=v3&tt=1397637562619&class=login&logintype=dialogLogin&callback=bd__cbs__pksb6x
这个url的时候,看到了我们要找的token。

QQ20140418-3@2x

最后经过尝试,发现只要请求https://passport.baidu.com/v2/api/?getapi&tpl=mn&apiver=v3就可以获得token了,不用后面那一串参数。

接下来是callback,之前在找token的时候请求的url里面有callback,但是我们把callback字段去掉之后请求依然能够返回结果,是不是说明callback也是个非必要的字段呢?我们试试就知道了。有了获取token的方法,我们就可以编写模拟登录的代码了。代码如下:

# -*- coding=utf-8 -*-

import urllib
import urllib.request
import json
import http
import http.cookiejar
import re
import os
import zlib 
import time
from urllib.parse import urlencode

TOKEN_URL = "https://passport.baidu.com/v2/api/?getapi&tpl=mn&apiver=v3"
INDEX_URL = "http://www.baidu.com/"
LOGIN_URL = "https://passport.baidu.com/v2/api/?login"

reg_token = re.compile("\"token\"\s+:\s+\"(\w+)\"")

bdHeaders = {
                "Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
                "Accept-Encoding":"gzip,deflate,sdch",
                "Accept-Language":"en-US,en;q=0.8,zh;q=0.6",
                "Host":"passport.baidu.com",
                "Origin":"http://www.baidu.com",
                "Referer":"http://www.baidu.com/",
                "User-Agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36",
             }

bdData = {
            "staticpage":"https://passport.baidu.com/static/passpc-account/html/V3Jump.html",
            "token":"",
            "tpl":"mn",                               #重要,需要跟TOKEN_URL中的相同
            "username":"",
            "password":"",
          }

class bdLogin:
    def __init__(self):
        self._cookie = http.cookiejar.LWPCookieJar()
        self._opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(self._cookie))

    def login(self, user = "", psw = ""):
        print ("User:" + user)

        self._initial()                         
        self._getToken()                         #取得token,必要

        bdData["username"] = user
        bdData["password"] = psw
        bdData["token"] = self._token
        # print ("Token:" + self._token)

        request = urllib.request.Request(LOGIN_URL, headers = bdHeaders)
        result = self._opener.open(request, urlencode(bdData).encode("utf-8"))   #登录
        # decompressed_data=zlib.decompress(result.read(), 16+zlib.MAX_WBITS)
        # print (decompressed_data) 

        result = json.loads(self._opener.open("http://tieba.baidu.com/f/user/json_userinfo").read().decode("utf-8"))
        # print (self._opener.open("http://tieba.baidu.com/f/user/json_userinfo").read().decode("utf-8"))
        if(result["no"] == 0): 
            print ("OK, login succes!")                                 #判断是否登录成功
            return self._opener
        else:
            print("WTF, there is something wrong...")
            return None

    def _getToken(self):
        self._token = reg_token.findall(str(self._opener.open(TOKEN_URL).read()))[0]

    def _initial(self):
        self._opener.open(INDEX_URL)

def main():
    robot = bdLogin()
    #传入用户名和密码
    for line in open("user.conf"):
        user, password = str(line).strip('\n').split(",")
        robot.login(user, password)

if __name__ == "__main__":
    main()

运行之后,可以从log中看到已经登录成功了,而且通过不断的精简参数,发现提交的form表单里面,只需要staticpage,token,tpl,username,password这几项就可以了。

QQ20140418-4@2x

(2)获取“关注”贴吧

OK,登录已经搞定了。下面就是要获取到用户关注了哪些贴吧。先正常操作,可以看到当我们访问“我的i贴吧”的时候会显示我关注的贴吧。但是看url是http://tieba.baidu.com/i/179045774/forum,明显中间那串数字是类似用户的id的东东,应该是可以通过某种方式获得,但是有点显得麻烦了。那怎么办呢,再次用调试工具看一看请求的资源,发现请求这个页面的时候并没有返回我关注的贴吧的具体内容,那说明是在其他的请求中请求的这个内容,在里面找一下就发现有这样一个请求http://tieba.baidu.com/f/like/mylike,从url的格式上看就比较靠谱,发到浏览器里试一下果然是

QQ20140418-5@2x

到此,我们已经完成了大部分的工作了。

(3)贴吧签到

好,下面还是正常的用浏览器到贴吧里面签到一下,记得打开调试工具关注发送的请求。然后我们在签到的时候就又发现了一个POST请求http://tieba.baidu.com/sign/add。打开看一下详情。

QQ20140418-6@2x

这回的form表单里面只有三项:
ie:编码格式
kw:贴吧名称(好吧,我随便找的“头像吧”= =)
tbs:这个猜测应该是贴吧的编号之类的,查了下可以从当前贴吧首页的源码PageData.tbs字段中拿到
这时候可能有人会问,这样请求服务器怎么知道是谁签到呢?这就要用到cookie了。因为之前登陆成功之后会生成一个cookie,在签到的时候只要把这个cookie带上就可以了。

OK,下面的工作和登陆一样了,码代码了:

# -*- coding=utf-8 -*-

import baidu_autologin
import re
import urllib
import urllib.request
import multiprocessing
import json
import pickle
import os
import time
from urllib.parse import urlencode

TIEBA_URL = "http://tieba.baidu.com"
GETLIKE_URL = "http://tieba.baidu.com/f/like/mylike"
SIGN_URL = "http://tieba.baidu.com/sign/add"

reg_likeUrl = re.compile("<a href=\"([^\"]+)\" title=\"([^\"]+)\">")
reg_getTbs = re.compile("PageData.tbs = \"(\w+)\"")

def getTbTbs(opener, url):
    return reg_getTbs.findall(opener.open(TIEBA_URL + url).read().decode("gbk"))[0]

#获取喜欢的贴吧列表    
def getList(opener):
    return reg_likeUrl.findall(opener.open(GETLIKE_URL).read().decode("gbk"))

signHeaders = {
                "User-Agent":"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.66 Safari/537.36 LBBROWSER",
                "Host":"tieba.baidu.com",
                "Origin":"http://tieba.baidu.com",
                "Referer":"http://tieba.baidu.com",
              }

#要post的表格              
signData = {
            "ie":"utf-8",
            "kw":"",
            "tbs":"",
           }

class autoSign:
    def __init__(self, user = "", psw = ""):
        login = baidu_autologin.bdLogin()
        self._opener = login.login(user, psw)
        self.user = user

    def getList(self):           
        self._likeList = getList(self._opener)

    def sign(self):
        self.getList()
        list = []
        for url in self._likeList:                  
            list.append(self._signProcess(url))
            time.sleep(2)

        for ret in list:                            #取回结果
            print (ret)

    def _signProcess(self, url):
        signData["kw"] = url[1]
        signData["tbs"] = getTbTbs(self._opener, url[0])   #获取tbs
        signHeaders["Referer"] = signHeaders["Origin"] + url[0]
        request = urllib.request.Request(SIGN_URL, headers = signHeaders)
        result = json.loads(self._opener.open(request, urlencode(signData).encode("utf-8")).read().decode("utf-8"))
        if(result["no"] == 0):           #签到成功
            return "{0}吧签到成功,今天是第{1}个签到!".format(url[1], result["data"]["uinfo"]["user_sign_rank"])
        elif(result["no"] == 1101):      #已签过
            return "{0}吧之前已经签到过了哦!".format(url[1])
        else:                            #出错
            return "未知错误!" + "\n" + url + "\n" + result

def main():
    ntime = time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time()))
    print (ntime)
    for line in open("user.conf"):
        user, password = str(line).strip('\n').split(",")
        asRobot = autoSign(user, password)          #传入用户名和密码
        asRobot.sign()

if(__name__ == "__main__"):
    main()

码完代码之后测试一下:

QQ20140418-7@2x

OK,上部分就说到这里了,下半部分重点说一下这次玩耍的时候遇到的各种问题。我觉得遇到问题的时候才是最能学到东西的时候。

之前的文章里有说,要在Mac下面测试一个C++的项目,结果安装好gtest之后,把开发的工程拿过来编译了一下,结果就报错了,报错部分内容如下:

ld: warning: ignoring file ../libs/libeasy.a, file was built for archive which is not the architecture being linked (x86_64): ../libs/libeasy.a
Undefined symbols for architecture x86_64:

看到这个错误,首先想到的是难道是因为我是64位的机器而lib是32位的?然后google了一下,貌似确实有这种可能,那既然不支持就在编译的时候加上-arch i386,咱在32位编译不就好了。结果问题依旧。。。没办法问了一下开发,开发说引用的lib就是64位的,好吧,看来是方向找错了。
这时候先用objdump确认一下文件的格式,

>gobjdump -f libeasy.a
In archive libeasy.a:

easy_buf.o:     file format elf64-x86-64-nacl
architecture: i386:x86-64:nacl, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x0000000000000000

可以看到文件格式是ELF64-x86-64-nacl,看来确实是64位的没跑了,那为什么会报这样的错呢?继续google之,翻了几页之后,看到有一种就是文件的格式问题。好了,继续验证一下是不是这个问题,手动在Mac下编译出一个lib看看:

>g++ -c main.cpp
>ar rvs main.a main.o
>gobjdump -f main.a
In archive main.a:

main.o:     file format mach-o-x86-64
architecture: i386:x86-64, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x0000000000000000

可以看到文件格式是Mach-O-x86-64,查了一下这个是mac自家使用的文件格式,也就是说和ELF是不兼容的。进一步查了一下,在Mac OS X for Unix Geeks中有这么一段话:

The Executable and Linking Format (ELF), developed by the Unix System Laboratories, is common in the Unix world. On ELF systems, there is no distinction between shared libraries and loadable modules; shared code can be used as a library for dynamic loading. ELF is the default binary format on Linux, Solaris 2.x, and SVR4. Since these systems cover a large share of the Unix base, most Unix developers have experience on ELF systems. Thus, it may come as a surprise to experienced Unix developers that shared libraries and loadable modules are not the same on Mac OS X. This is because the binary format used in Mac OS X is Mach-O, which is different from ELF.
意思就是Mach-O和ELF这俩东西不是一回事,然后又查了下如果想要在Mac下使用ELF的lib的话,几乎是不可能的。为啥说几乎不可能呢,因为还是有几种办法的:
1.使用xBinary,它可以扩展Mac支持的文件格式,但是这个工具是2009年开发的,后续维护也不是很多仅供科研使用,而且用起来也很麻烦。
2.使用objconv,这是个文件格式转换工具,不过转换出来的稳不稳定这个就不好说了。
3.修改Mac内核,添加支持,这个难度太大了。。。
所以说几乎不可能了。最好的解决办法是拿到lib的源代码,然后在本机编译一下,但是有些lib文件拿不到源代码,这时候只能用不是办法的办法了,使用虚拟机安装一个Ubuntu或者SSH登录到远程linux服务器上写代码。总之,这是个不大不小的坑吧,也算是对Mac系统了解了一下。