冷月阁

知识库


  • 首页

  • 标签

  • 分类

  • 归档

  • 关于

  • 搜索

Java 编程思想第四版读书笔记

发表于 2018-09-13 | 分类于 java

java

一、对象

1、所有东西都是对象。可将对象想象成一种新型变量;它保存着数据,但可要求它对自身进行操作。理论上讲,可从要解决的问题身上提出所有概念性的组件,然后在程序中将其表达为一个对象。

2、程序是一大堆对象的组合;通过消息传递,各对象知道自己该做些什么。为了向对象发出请求,需向那个对象“发送一条消息”。更具体地讲,可将消息想象为一个调用请求,它调用的是从属于目标对象的一个子例程或函数。

3、每个对象都有自己的存储空间,可容纳其他对象。或者说,通过封装现有对象,可制作出新型对象。所 以,尽管对象的概念非常简单,但在程序中却可达到任意高的复杂程度。

4、每个对象都有一种类型。根据语法,每个对象都是某个“类”的一个“实例”。其中,“类”(Class)是“类型”(Type)的同义词。一个类最重要的特征就是“能将什么消息发给它?”。

5、同一类所有对象都能接收相同的消息。这实际是别有含义的一种说法,大家不久便能理解。由于类型为 “圆”(Circle)的一个对象也属于类型为“形状”(Shape)的一个对象,所以一个圆完全能接收形状消 息。这意味着可让程序代码统一指挥“形状”,令其自动控制所有符合“形状”描述的对象,其中自然包括 “圆”。这一特性称为对象的“可替换性”,是 OOP 最重要的概念之一。

二、数据保存

1、寄存器。这是最快的保存区域,因为它位于和其他所有保存方式不同的地方:处理器内部。然而,寄存器的数量十分有限,所以寄存器是根据需要由编译器分配。我们对此没有直接的控制权,也不可能在自己的程序里找到寄存器存在的任何踪迹。

2、堆栈。驻留于常规 RAM(随机访问存储器)区域,但可通过它的“堆栈指针”获得处理的直接支持。堆 栈指针若向下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存 方式,仅次于寄存器。创建程序时,Java编译器必须准确地知道堆栈内保存的所有数据的“长度”以及“存 在时间”。这是由于它必须生成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活 性,所以尽管有些Java数据要保存在堆栈里——特别是对象句柄,但Java对象并不放到其中。

3、堆。一种常规用途的内存池(也在RAM区域),其中保存了Java对象。和堆栈不同,“内存堆”或“堆”(Heap)最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new命令编制相关的代码即可。执行这些代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时会花掉更长的时间!

4、静态存储。这儿的“静态”(Static)是指“位于固定位置”(尽管也在RAM里)。程序运行期间,静态存储的数据将随时等候调用。可用static关键字指出一个对象的特定元素是静态的。但Java对象本身永远都不会置入静态存储空间。

5、常数存储。常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。有的常数需要严格地保护,所以可考虑将它们置入只读存储器(ROM)。

6、非RAM存储。若数据完全独立于一个程序之外,则程序不运行时仍可存在,并在程序的控制范围之外。其中两个最主要的例子便是“流式对象”和“固定对象”。对于流式对象,对象会变成字节流,通常会发给另一台机器。而对于固定对象,对象保存在磁盘中。即使程序中止运行,它们仍可保持自己的状态不变。对于这些类型的数据存储,一个特别有用的技巧就是它们能存在于其他媒体中。一旦需要,甚至能将它们恢复成普通的、基于RAM的对象。

三、初始化和清除

“初始化”和“清除”是这些安全问题的其中两个。许多C程序的错误都是由于程序员忘记初始化一个变量 造成的。对于现成的库,若用户不知道如何初始化库的一个组件,就往往会出现这一类的错误。清除是另一个特殊的问题,因为用完一个元素后,由于不再关心,所以很容易把它忘记。这样一来,那个元素占用的资源会一直保留下去,极易产生资源(主要是内存)用尽的后果。

1、用构建器自动初始化

对于方法的创建,可将其想象成为自己写的每个类都调用一次initialize()。这个名字提醒我们在使用对象之前,应首先进行这样的调用。但不幸的是,这也意味着用户必须记住调用方法。在Java中,由于提供了名为“构建器”的一种特殊方法,所以类的设计者可担保每个对象都会得到正确的初始化。若某个类有一个构建器,那么在创建对象时,Java会自动调用那个构建器——甚至在用户毫不知觉的情况下。所以说这是可以担保的!接着的一个问题是如何命名这个方法。存在两方面的问题。第一个是我们使用的任何名字都可能与打算为某个类成员使用的名字冲突。第二是由于编译器的责任是调用构建器,所以它必须知道要调用是哪个方法。C++采取的方案看来是最简单的,且更有逻辑性,所以也在Java里得到了应用:构建器的名字与类名相同。这样一来,可保证象这样的一个方法会在初始化期间自动调用。

下面是带有构建器的一个简单的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// SimpleConstructor.java
// Demonstration of a simple constructor;
class Rock {
Rock() { // This is the constructor
System.out.println("Creating Rock");
}
}

public class SimpleConstructor {
public static void main(String[] args) {
for(int i = 0; i < 10; i++){
new Rock();
}
}
}

2、清除:收尾和垃圾回收

程序员都知道“初始化”的重要性,但通常忘记清除的重要性。毕竟,谁需要来清除一个int呢?但是对于库来说,用完后简单地“释放”一个对象并非总是安全的。当然,Java可用垃圾收集器回收由不再使用的对象占据的内存。现在考虑一种非常特殊且不多见的情况。假定我们的对象分配了一个“特殊”内存区域,没有使用new。垃圾收集器只知道释放那些由new分配的内存,所以不知道如何释放对象的“特殊”内存。为解决这个问题,Java提供了一个名为finalize()的方法,可为我们的类定义它。在理想情况下,它的工作原理应该是这样的:一旦垃圾收集器准备好释放对象占用的存储空间,它首先调用finalize(),而且只有在下一次垃圾收集过程中,才会真正回收对象的内存。所以如果使用finalize(),就可以在垃圾收集期间进行一些重要的清除或清扫工作。

但也是一个潜在的编程陷阱,因为有些程序员(特别是在C++开发背景的)刚开始可能会错误认为它就是在C++中为“破坏器”(Destructor)使用的finalize()——破坏(清除)一个对象的时候,肯定会调用这个函数。但在这里有必要区分一下C++和Java的区别,因为C++的对象肯定会被清除(排开编程错误的因素),而Java对象并非肯定能作为垃圾被“收集”去。或者换句话说:

垃圾收集并不等于“破坏”!

若能时刻牢记这一点,踩到陷阱的可能性就会大大减少。它意味着在我们不再需要一个对象之前,有些行动是必须采取的,而且必须由自己来采取这些行动。Java并未提供“破坏器”或者类似的概念,所以必须创建一个原始的方法,用它来进行这种清除。例如,假设在对象创建过程中,它会将自己描绘到屏幕上。如果不从屏幕明确删除它的图像,那么它可能永远都不会被清除。若在finalize()里置入某种删除机制,那么假设对象被当作垃圾收掉了,图像首先会将自身从屏幕上移去。但若未被收掉,图像就会保留下来。所以要记住的第二个重点是:

我们的对象可能不会当作垃圾被收掉!

有时可能发现一个对象的存储空间永远都不会释放,因为自己的程序永远都接近于用光空间的临界点。若程序执行结束,而且垃圾收集器一直都没有释放我们创建的任何对象的存储空间,则随着程序的退出,那些资源会返回给操作系统。这是一件好事情,因为垃圾收集本身也要消耗一些开销。如永远都不用它,那么永远也不用支出这部分开销。

四、类再生

“Java 引人注目的一项特性是代码的重复使用或者再生。但最具革命意义的是,除代码的复制和修改以外,我们还能做多得多的其他事情。”

在象 C 那样的程序化语言里,代码的重复使用早已可行,但效果不是特别显著。与 Java 的其他地方一样,这个方案解决的也是与类有关的问题。我们通过创建新类来重复使用代码,但却用不着重新创建,可以直接使用别人已建好并调试好的现成类。

1、合成

在新类里简单地创建原有类的对象。我们把这种方法叫作“合成”,因为新类由现有类的对象合并而成。我们只是简单地重复利用代码的功能,而不是采用它的形式。

为进行合成,我们只需在新类里简单地置入对 象句柄即可。举个例子来说,假定需要在一个对象里容纳几个String对象、两种基本数据类型以及属于另一个类的一个对象。对于非基本类型的对象来说,只需将句柄置于新类即可;而对于基本数据类型来说,则需在自己的类中定义它们。如下所示:

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
// SprinklerSystem.java
// Composition for code reuse
class WaterSource {
private String s;
WaterSource() {
System.out.println("WaterSource()");
s = new String("Constructed");
}
public String toString() {
return s;
}
}
public class SprinklerSystem {
private String valve1, valve2, valve3, valve4;
WaterSource source;
int i;
float f;

void print() {
System.out.println("valve1 = " + valve1);
System.out.println("valve2 = " + valve2);
System.out.println("valve3 = " + valve3);
System.out.println("valve4 = " + valve4);
System.out.println("i = " + i);
System.out.println("f = " + f);
System.out.println("source = " + source);
}

public static void main(String[] args) {
SprinklerSystem x = new SprinklerSystem();
x.print();
}
}

2、继承

创建一个新类,将其作为现有类的一个“类型”。我们可以原样采取现有类的形式,并在其中加入新代码,同时不会对现有的类产生影响。这种魔术般的行为叫作“继承”(Inheritance),涉及的大多数工作都是由编译器完成的。对于面向对象的程序设计,“继承”是最重要的基础概念之一。

用于合成的语法是非常简单且直观的。但为了进行继承,必须采用一种全然不同的形式。需要继承的时候,我们会说:“这个新类和那个旧类差不多。”为了在代码里表面这一观念,需要给出类名。但在类主体的起始花括号之前,需要放置一个关键字extends,在后面跟随“基础类”的名字。若采取这种做法,就可自动获得基础类的所有数据成员以及方法。下面是一个例子:

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
// Detergent.java
// Inheritance syntax & properties
class Cleanser {
private String s = new String("Cleanser");
public void append(String a) {
s += a;
}
public void dilute() {
append(" dilute()");
}
public void apply() {
append(" apply()");
}
public void scrub() {
append(" scrub()");
}
public void print() {
System.out.println(s);
}
public static void main(String[] args) {
Cleanser x = new Cleanser();
x.dilute();
x.apply();
x.scrub();
x.print();
}
}
public class Detergent extends Cleanser { // Change a method:
public void scrub() {
append(" Detergent.scrub()");
super.scrub(); // Call base-class version
}
// Add methods to the interface:
public void foam() {
append(" foam()");
}
// Test the new class:
public static void main(String[] args) {
Detergent x = new Detergent();
x.dilute();
x.apply();
x.scrub();
x.foam();
x.print();
System.out.println("Testing base class:");
Cleanser.main(args);
}
}

五、多形性

“对于面向对象的程序设计语言,多型性是第三种最基本的特征(前两种是数据抽象和继承)。”

“多形性”(Polymorphism)从另一个角度将接口从具体的实施细节中分离出来,亦即实现了“是什么”与“怎样做”两个模块的分离。利用多形性的概念,代码的组织以及可读性均能获得改善。此外,还能创建“易于扩展”的程序。无论在项目的创建过程中,还是在需要加入新特性的时候,它们都可以方便地“成长”。

通过合并各种特征与行为,封装技术可创建出新的数据类型。通过对具体实施细节的隐藏,可将接口与实施细节分离,使所有细节成为“private”(私有)。这种组织方式使那些有程序化编程背景人感觉颇为舒适。但多形性却涉及对“类型”的分解。通过上一章的学习,大家已知道通过继承可将一个对象当作它自己的类型或者它自己的基础类型对待。这种能力是十分重要的,因为多个类型(从相同的基础类型中衍生出来)可被当作同一种类型对待。而且只需一段代码,即可对所有不同的类型进行同样的处理。利用具有多形性的方法调用,一种类型可将自己与另一种相似的类型区分开,只要它们都是从相同的基础类型中衍生出来的。这种区分是通过各种方法在行为上的差异实现的,可通过基础类实现对那些方法的调用。

六、传递和返回对象

实际传递的只是一个句柄。

在许多程序设计语言中,我们可用语言的“普通”方式到处传递对象,而且大多数时候都不会遇到问题。但 有些时候却不得不采取一些非常做法,使得情况突然变得稍微复杂起来(在 C++中则是变得非常复杂)。 Java 亦不例外,我们十分有必要准确认识在对象传递和赋值时所发生的一切。若读者是从某些特殊的程序设计环境中转移过来的,那么一般都会问到:“Java 有指针吗?”有些人认为指 针的操作很困难,而且十分危险,所以一厢情愿地认为它没有好处。同时由于Java 有如此好的口碑,所以应该很轻易地免除自己以前编程中的麻烦,其中不可能夹带有指针这样的“危险品”。然而准确地说,Java 是有指针的!事实上,Java 中每个对象(除基本数据类型以外)的标识符都属于指针的一种。但它们的使用受到了严格的限制和防范,不仅编译器对它们有“戒心”,运行期系统也不例外。或者换从另一个角度说, Java 有指针,但没有传统指针的麻烦。我曾一度将这种指针叫做“句柄”,但你可以把它想像成“安全指 针”。

1、传递句柄

将句柄传递进入一个方法时,指向的仍然是相同的对象。一个简单的实验可以证明这一点

1
2
3
4
5
6
7
8
9
10
11
12
// PassHandles.java
// Passing handles around
public class PassHandles {
static void f(PassHandles h) {
System.out.println("h inside f(): " + h);
}
public static void main(String[] args) {
PassHandles p = new PassHandles();
System.out.println("p inside main(): " + p);
f(p);
}
}

2、制作本地副本

稍微总结一下:Java 中的所有自变量或参数传递都是通过传递句柄进行的。也就是说,当我们传递“一个对 象”时,实际传递的只是指向位于方法外部的那个对象的“一个句柄”。所以一旦要对那个句柄进行任何修 改,便相当于修改外部对象。此外:

参数传递过程中会自动产生别名问题

不存在本地对象,只有本地句柄

句柄有自己的作用域,而对象没有

对象的“存在时间”在Java 里不是个问题

没有语言上的支持(如常量)可防止对象被修改(以避免别名的副作用)

若只是从对象中读取信息,而不修改它,传递句柄便是自变量传递中最有效的一种形式。这种做非常恰当;默认的方法一般也是最有效的方法。然而,有时仍需将对象当作“本地的”对待,使我们作出的改变只影响一个本地副本,不会对外面的对象造成影响。许多程序设计语言都支持在方法内自动生成外部对象的一个本地副本。尽管Java 不具备这种能力,但允许我们达到同样的效果。

3、克隆

若需修改一个对象,同时不想改变调用者的对象,就要制作该对象的一个本地副本。这也是本地副本最常见的一种用途。若决定制作一个本地副本,只需简单地使用 clone()方法即可。Clone 是“克隆”的意思,即制作完全一模一样的副本。这个方法在基础类Object 中定义成“protected”(受保护)模式。但在希望克隆的任何衍生类中,必须将其覆盖为“public”模式。

4、只读类

尽管在一些特定的场合,由 clone()产生的本地副本能够获得我们希望的结果,但程序员(方法的作者)不得不亲自禁止别名处理的副作用。假如想制作一个库,令其具有常规用途,但却不能担保它肯定能在正确的类中得以克隆,这时又该怎么办呢?更有可能的一种情况是,假如我们想让别名发挥积极的作用——禁止不必要的对象复制——但却不希望看到由此造成的副作用,那么又该如何处理呢?

一个办法是创建“不变对象”,令其从属于只读类。可定义一个特殊的类,使其中没有任何方法能造成对象 内部状态的改变。在这样的一个类中,别名处理是没有问题的。因为我们只能读取内部状态,所以当多处代 码都读取相同的对象时,不会出现任何副作用。

作为“不变对象”一个简单例子,Java 的标准库包含了“封装器”(wrapper)类,可用于所有基本数据类 型。大家可能已发现了这一点,如果想在一个象 Vector(只采用 Object 句柄)这样的集合里保存一个 int 数值,可以将这个int封装到标准库的Integer类内部。

七、IO系统

“对语言设计人员来说,创建好的输入/输出系统是一项特别困难的任务。”

由于存在大量不同的设计方案,所以该任务的困难性是很容易证明的。其中最大的挑战似乎是如何覆盖所有可能的因素。不仅有三种不同的种类的 IO 需要考虑(文件、控制台、网络连接),而且需要通过大量不同的方式与它们通信(顺序、随机访问、二进制、字符、按行、按字等等)。

Java 库的设计者通过创建大量类来攻克这个难题。事实上,Java 的 IO 系统采用了如此多的类,以致刚开始会产生不知从何处入手的感觉(具有讽刺意味的是,Java 的 IO 设计初衷实际要求避免过多的类)。从 Java 1.0 升级到 Java 1.1 后,IO 库的设计也发生了显著的变化。此时并非简单地用新库替换旧库,Sun 的设计人员对原来的库进行了大手笔的扩展,添加了大量新的内容。因此,我们有时不得不混合使用新库与旧库,产生令人无奈的复杂代码。

1、输入和输出

可将Java库的IO类分割为输入与输出两个部分,这一点在用Web浏览器阅读联机Java类文档时便可知道。通过继承,从 InputStream(输入流)衍生的所有类都拥有名为 read()的基本方法,用于读取单个字节或者字节数组。类似地,从 OutputStream 衍生的所有类都拥有基本方法 write(),用于写入单个字节或者字节数组。然而,我们通常不会用到这些方法;它们之所以存在,是因为更复杂的类可以利用它们,以便提供一个更有用的接口。因此,我们很少用单个类创建自己的系统对象。一般情况下,我们都是将多个对象重叠在一起,提供自己期望的功能。我们之所以感到Java 的流库(Stream Library)异常复杂,正是由于为了创建单独一个结果流,却需要创建多个对象的缘故。

2、增添属性和有用的接口

利用层次化对象动态和透明地添加单个对象的能力的做法叫作“装饰器”(Decorator)方案。装饰器方案规定封装于初始化对象中的所有对象都拥有相同的接口,以便 利用装饰器的“透明”性质——我们将相同的消息发给一个对象,无论它是否已被“装饰”。这正是在 Java IO 库里存在“过滤器”(Filter)类的原因:抽象的“过滤器”类是所有装饰器的基础类(装饰器必须拥有与它装饰的那个对象相同的接口,但装饰器亦可对接口作出扩展,这种情况见诸于几个特殊的“过滤器”类中)。

子类处理要求大量子类对每种可能的组合提供支持时,便经常会用到装饰器——由于组合形式太多,造成子 类处理变得不切实际。Java IO库要求许多不同的特性组合方案,这正是装饰器方案显得特别有用的原因。但是,装饰器方案也有自己的一个缺点。在我们写一个程序的时候,装饰器为我们提供了大得多的灵活性(因为可以方便地混合与匹配属性),但它们也使自己的代码变得更加复杂。原因在于Java IO 库操作不便,我们必须创建许多类——“核心”IO 类型加上所有装饰器——才能得到自己希望的单个 IO 对象。

3、本身的缺陷:RandomAccessFile

RandomAccessFile 用于包含了已知长度记录的文件,以便我们能用 seek()从一条记录移至另一条;然后读取或修改那些记录。各记录的长度并不一定相同;只要知道它们有多大以及置于文件何处即可。

首先,我们有点难以相信RandomAccessFile不属于InputStream或者OutputStream分层结构的一部分。除了恰巧实现了DataInput以及DataOutput(这两者亦由DataInputStream和DataOutputStream实现)接口之外,它们与那些分层结构并无什么关系。它甚至没有用到现有InputStream或OutputStream类的功能——采用的是一个完全不相干的类。该类属于全新的设计,含有自己的全部(大多数为固有)方法。之所以要这样做,是因为RandomAccessFile拥有与其他IO类型完全不同的行为,因为我们可在一个文件里向前或向后移动。不管在哪种情况下,它都是独立运作的,作为Object 的一个“直接继承人”使用。

从根本上说,RandomAccessFile 类似 DataInputStream 和 DataOutputStream 的联合使用。其中,getFilePointer()用于了解当前在文件的什么地方,seek()用于移至文件内的一个新地点,而 length()用于判断文件的最大长度。此外,构建器要求使用另一个自变量(与C 的fopen()完全一样),指出自己只是随机读(“r”),还是读写兼施(“rw”)。这里没有提供对“只写文件”的支持。也就是说,假如是从DataInputStream继承的,那么 RandomAccessFile 也有可能能很好地工作。

还有更难对付的。很容易想象我们有时要在其他类型的数据流中搜索,比如一个 ByteArrayInputStream,但搜索方法只有RandomAccessFile才会提供。而后者只能针对文件才能操作,不能针对数据流操作。此时,BufferedInputStream 确实允许我们标记一个位置(使用 mark(),它的值容纳于单个内部变量中),并用reset()重设那个位置。但这些做法都存在限制,并不是特别有用。

4、File 类

File 类有一个欺骗性的名字——通常会认为它对付的是一个文件,但实情并非如此。它既代表一个特定文件的名字,也代表目录内一系列文件的名字。若代表一个文件集,便可用list()方法查询这个集,返回的是一个字串数组。之所以要返回一个数组,而非某个灵活的集合类,是因为元素的数量是固定的。而且若想得到一个不同的目录列表,只需创建一个不同的File 对象即可。事实上,“FilePath”(文件路径)似乎是一个更好的名字。

5、Java 1.1的IO流

Java 1.1对IO流库进行了一些重大的改进。看到Reader 和Writer类时,大多数人的第一个印象(就象我一样)就是它们用来替换原来的InputStream和 OutputStream 类。但实情并非如此。尽管不建议使用原始数据流库的某些功能(如使用它们,会从编译器收 到一条警告消息),但原来的数据流依然得到了保留,以便维持向后兼容,而且:

(1) 在老式层次结构里加入了新类,所以Sun 公司明显不会放弃老式数据流。

(2) 在许多情况下,我们需要与新结构中的类联合使用老结构中的类。

为达到这个目的,需要使用一些“桥”类:

InputStreamReader 将一个 InputStream 转换成 Reader

OutputStreamWriter 将一个 OutputStream 转换成 Writer

所以与原来的IO流库相比,经常都要对新IO流进行层次更多的封装。同样地,这也属于装饰器方案的一个缺点——需要为额外的灵活性付出代价。

之所以在Java 1.1里添加了Reader和Writer层次,最重要的原因便是国际化的需求。老式IO流层次结构只支持8位字节流,不能很好地控制16位Unicode字符。由于Unicode主要面向的是国际化支持(Java内含的char是16位的Unicode),所以添加了Reader和Writer层次,以提供对所有IO操作中的Unicode的支持。除此之外,新库也对速度进行了优化,可比旧库更快地运行。

6、压缩

Java 1.1 也添加一个类,用以支持对压缩格式的数据流的读写。它们封装到现成的 IO 类中,以提供压缩功能。

此时Java 1.1的一个问题显得非常突出:它们不是从新的Reader和Writer类衍生出来的,而是属于 InputStream 和 OutputStream 层次结构的一部分。所以有时不得不混合使用两种类型的数据流(注意可用 InputStreamReader 和 OutputStreamWriter 在不同的类型间方便地进行转换)。

Java 1.1 压缩类功能:

1、CheckedInputStream GetCheckSum()为任何InputStream产生校验和(不仅是解压)

2、CheckedOutputStream GetCheckSum()为任何OutputStream产生校验和(不仅是解压)

3、DeflaterOutputStream 用于压缩类的基础类

4、ZipOutputStream 一个DeflaterOutputStream,将数据压缩成Zip文件格式

5、GZIPOutputStream 一个DeflaterOutputStream,将数据压缩成GZIP 文件格式

6、InflaterInputStream 用于解压类的基础类

7、ZipInputStream 一个DeflaterInputStream,解压用Zip文件格式保存的数据

8、GZIPInputStream 一个DeflaterInputStream,解压用GZIP 文件格式保存的数据

尽管存在许多种压缩算法,但是 Zip 和 GZIP 可能最常用的。所以能够很方便地用多种现成的工具来读写这些 格式的压缩数据。

7、对象序列化

Java 1.1 增添了一种有趣的特性,名为“对象序列化”(Object Serialization)。它面向那些实现了Serializable 接口的对象,可将它们转换成一系列字节,并可在以后完全恢复回原来的样子。这一过程亦可通过网络进行。这意味着序列化机制能自动补偿操作系统间的差异。换句话说,可以先在 Windows 机器上创建一个对象,对其序列化,然后通过网络发给一台 Unix 机器,然后在那里准确无误地重新“装配”。不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。

就其本身来说,对象的序列化是非常有趣的,因为利用它可以实现“有限持久化”。请记住“持久化”意味着对象的“生存时间”并不取决于程序是否正在执行——它存在或“生存”于程序的每一次调用之间。通过序列化一个对象,将其写入磁盘,以后在程序重新调用时重新恢复那个对象,就能圆满实现一种“持久”效果。之所以称其为“有限”,是因为不能用某种“persistent”(持久)关键字简单地地定义一个对象,并让系统自动照看其他所有细节问题(尽管将来可能成为现实)。相反,必须在自己的程序中明确地序列化和 组装对象。

语言里增加了对象序列化的概念后,可提供对两种主要特性的支持。Java 1.1 的“远程方法调用”(RMI)使本来存在于其他机器的对象可以表现出好象就在本地机器上的行为。将消息发给远程对象时,需要通过对 象序列化来传输参数和返回值。

对象的序列化也是Java Beans必需的,后者由Java 1.1引入。使用一个Bean 时,它的状态信息通常在设计期间配置好。程序启动以后,这种状态信息必须保存下来,以便程序启动以后恢复;具体工作由对象序列化完成。

对象的序列化处理非常简单,只需对象实现了 Serializable 接口即可(该接口仅是一个标记,没有方法)。在Java 1.1 中,许多标准库类都发生了改变,以便能够序列化——其中包括用于基本数据类型的全部封装器、所有集合类以及其他许多东西。甚至 Class 对象也可以序列化(第 11 章讲述了具体实现过程)。为序列化一个对象,首先要创建某些OutputStream对象,然后将其封装到ObjectOutputStream对象内。此 时,只需调用 writeObject() 即可完成对象的序列化,并将其发送给 OutputStream。相反的过程是将一个 InputStream 封装到ObjectInputStream 内,然后调用 readObject()。和往常一样,我们最后获得的是指向一个上溯造型Object 的句柄,所以必须下溯造型,以便能够直接设置。对象序列化特别“聪明”的一个地方是它不仅保存了对象的“全景图”,而且能追踪对象内包含的所有句柄并保存那些对象;接着又能对每个对象内包含的句柄进行追踪;以此类推。我们有时将这种情况称为“对象网”,单个对象可与之建立连接。而且它还包含了对象的句柄数组以及成员对象。若必须自行操纵一套对象序列化机制,那么在代码里追踪所有这些链接时可能会显得非常麻烦。在另一方面,由于Java 对象的序列化似乎找不出什么缺点,所以请尽量不要自己动手,让它用优化的算法自动维护整个对象网。

八、多线程

利用对象,可将一个程序分割成相互独立的区域。我们通常也需要将一个程序转换成多个独立运行的子任务。

象这样的每个子任务都叫作一个“线程”(Thread)。编写程序时,可将每个线程都想象成独立运行,而且都有自己的专用 CPU。一些基础机制实际会为我们自动分割 CPU 的时间。我们通常不必关心这些细节问题,所以多线程的代码编写是相当简便的。

这时理解一些定义对以后的学习狠有帮助。“进程”是指一种“自包容”的运行程序,有自己的地址空间。“多任务”操作系统能同时运行多个进程(程序)——但实际是由于CPU分时机制的作用,使每个进程都能循环获得自己的 CPU 时间片。但由于轮换速度非常快,使得所有程序好象是在“同时”运行一样。“线程”是进程内部单一的一个顺序控制流。因此,一个进程可能容纳了多个同时执行的线程。

多线程的应用范围很广。但在一般情况下,程序的一些部分同特定的事件或资源联系在一起,同时又不想为它而暂停程序其他部分的执行。这样一来,就可考虑创建一个线程,令其与那个事件或资源关联到一起,并让它独立于主程序运行。一个很好的例子便是“Quit”或“退出”按钮——我们并不希望在程序的每一部分代码中都轮询这个按钮,同时又希望该按钮能及时地作出响应(使程序看起来似乎经常都在轮询它)。事实上,多线程最主要的一个用途就是构建一个“反应灵敏”的用户界面。

1、反应灵敏的用户界面

“线程模型”(以及Java 中的编程支持)是一种程序编写规范,可在单独一个程序里实现几个操作的同时进行。根据这一机制,CPU 可为每个线程都分配自己的一部分时间。每个线程都“感觉”自己好象拥有整个 CPU,但 CPU 的计算时间实际却是在所有线程间分摊的。线程机制多少降低了一些计算效率,但无论程序的设计,资源的均衡,还是用户操作的方便性,都从中获得了巨大的利益。综合考虑,这一机制是非常有价值的。当然,如果本来就安装了多块 CPU,那么操作系统能够自行决定为不同的 CPU 分配哪些线程,程序的总体运行速度也会变得更快(所有这些都要求操作系统以及应用程序的支持)。多线程和多任务是充分发挥多处理机系统能力的一种最有效的方式。

1、为创建一个线程,最简单的方法就是从 Thread 类继承。这个类包含了创建和运行线程所需的一切东西。 Thread 最重要的方法是 run()。但为了使用 run(),必须对其进行过载或者覆盖,使其能充分按自己的吩咐行事。因此,run()属于那些会与程序中的其他线程“并发”或“同时”执行的代码。

2、线程类(Thread)与程序的主类(Main)是分隔开的。这样做非常合理,而且易于理解。然而,还有另一种方式也是经常要用到的。尽管它不十分明确,但一般都要更简洁一些(这也解释了它为什么十分流行)。通过将主程序类变成一个线程,这种形式可将主程序类与线程类合并到一起。由于对一个GUI程序来说,主程序类必须从Frame或Applet继承,所以必须用一个接口加入额外的功能。这个接口叫作Runnable,其中包含了与Thread一致的基本方法。事实上,Thread也实现了Runnable,它只指出有一个 run()方法。对合并后的程序/线程来说,它的用法不是十分明确。当我们启动程序时,会创建一个 Runnable (可运行的)对象,但不会自行启动线程。线程的启动必须明确进行。

3、创建多个不同的线程的问题。我们不可用前面的例子来做到这一点,所以必须倒退回去,利用从Thread继承的多个独立类来封装run()。但这是一种更常规的方案,而且更易理解,所以尽管前例揭示了我们经常都能看到的编码样式,但并不推荐在大多数情况下都那样做,因为它只是稍微复杂一些,而且灵活性稍低一些。

4、“Daemon”线程的作用是在程序的运行期间于后台提供一种“常规”服务,但它并不属于程序的一个基本部分。因此,一旦所有非 Daemon 线程完成,程序也会中止运行。相反,假若有任何非 Daemon 线程仍在运行 (比如还有一个正在运行 main()的线程),则程序的运行不会中止。 通过调用isDaemon(),可调查一个线程是不是一个Daemon,而且能用setDaemon()打开或者关闭一个线程的 Daemon状态。如果是一个Daemon线程,那么它创建的任何线程也会自动具备Daemon属性。

2、共享有限的资源

可将单线程程序想象成一种孤立的实体,它能遍历我们的问题空间,而且一次只能做一件事情。由于只有一个实体,所以永远不必担心会有两个实体同时试图使用相同的资源,就象两个人同时都想停到一个车位,同时都想通过一扇门,甚至同时发话。

进入多线程环境后,它们则再也不是孤立的。可能会有两个甚至更多的线程试图同时同一个有限的资源。必须对这种潜在资源冲突进行预防,否则就可能发生两个线程同时访问一个银行帐号,打印到同一台计算机,以及对同一个值进行调整等等。

有的时候,我们并不介意一个资源在尝试使用它的时候是否正被访问。但为了让多线程机制能够正常运转,需要采取一些措施来防止两个线程访问相同的资源——至少在关键的时期。为防止出现这样的冲突,只需在线程使用一个资源时为其加锁即可。访问资源的第一个线程会其加上锁以后,其他线程便不能再使用那个资源,除非被解锁。如果车子的前座是有限的资源,高喊“这是我的!”的孩子会主张把它锁起来。

3、堵塞

一个线程可以有四种状态:

(1) 新(New):线程对象已经创建,但尚未启动,所以不可运行。

(2) 可运行(Runnable):意味着一旦时间分片机制有空闲的CPU周期提供给一个线程,那个线程便可立即开始运行。因此,线程可能在、也可能不在运行当中,但一旦条件许可,没有什么能阻止它的运行——它既没有“死”掉,也未被“堵塞”。

(3) 死(Dead):从自己的run()方法中返回后,一个线程便已“死”掉。亦可调用stop()令其死掉,但会产生一个违例——属于 Error 的一个子类(也就是说,我们通常不捕获它)。记住一个违例的“掷”出应当是一个特殊事件,而不是正常程序运行的一部分。所以不建议你使用 stop()(在 Java 1.2 则是坚决反对)。另外还有一个 destroy()方法(它永远不会实现),应该尽可能地避免调用它,因为它非常武断,根本不会解除对象的锁定。

(4) 堵塞(Blocked):线程可以运行,但有某种东西阻碍了它。若线程处于堵塞状态,调度机制可以简单地跳过它,不给它分配任何 CPU 时间。除非线程再次进入“可运行”状态,否则不会采取任何操作。

堵塞状态是前述四种状态中最有趣的,值得我们作进一步的探讨。线程被堵塞可能是由下述五方面的原因造成的:

(1) 调用sleep(毫秒数),使线程进入“睡眠”状态。在规定的时间内,这个线程是不会运行的。

(2) 用suspend()暂停了线程的执行。除非线程收到resume()消息,否则不会返回“可运行”状态。

(3) 用wait()暂停了线程的执行。除非线程收到nofify()或者notifyAll()消息,否则不会变成“可运行”(是的,这看起来同原因 2 非常相象,但有一个明显的区别是我们马上要揭示的)。

(4) 线程正在等候一些 IO(输入输出)操作完成。

(5) 线程试图调用另一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用。

由于线程可能进入堵塞状态,而且由于对象可能拥有“同步”方法——除非同步锁定被解除,否则线程不能 访问那个对象——所以一个线程完全可能等候另一个对象,而另一个对象又在等候下一个对象,以此类推。 这个“等候”链最可怕的情形就是进入封闭状态——最后那个对象等候的是第一个对象!此时,所有线程都 会陷入无休止的相互等待状态,大家都动弹不得。我们将这种情况称为“死锁”。尽管这种情况并非经常出现,但一旦碰到,程序的调试将变得异常艰难。

4、优先级

线程的优先级(Priority)告诉调试程序该线程的重要程度有多大。如果有大量线程都被堵塞,都在等候运行,调试程序会首先运行具有最高优先级的那个线程。然而,这并不表示优先级较低的线程不会运行(换言之,不会因为存在优先级而导致死锁)。若线程的优先级较低,只不过表示它被准许运行的机会小一些而已。

所有线程都隶属于一个线程组。那可以是一个默认线程组,亦可是一个创建线程时明确指定的组。在创建之初,线程被限制到一个组里,而且不能改变到一个不同的组。每个应用都至少有一个线程从属于系统线程组。若创建多个线程而不指定一个组,它们就会自动归属于系统线程组。 线程组也必须从属于其他线程组。必须在构建器里指定新线程组从属于哪个线程组。若在创建一个线程组的时候没有指定它的归属,则同样会自动成为系统线程组的一名属下。因此,一个应用程序中的所有线程组最终都会将系统线程组作为自己的“父”。

九、网络编程

历史上的网络编程都倾向于困难、复杂,而且极易出错。

程序员必须掌握与网络有关的大量细节,有时甚至要对硬件有深刻的认识。一般地,我们需要理解连网协议中不同的“层”(Layer)。而且对于每个连网库,一般都包含了数量众多的函数,分别涉及信息块的连接、 打包和拆包;这些块的来回运输;以及握手等等。这是一项令人痛苦的工作。

但是,连网本身的概念并不是很难。我们想获得位于其他地方某台机器上的信息,并把它们移到这儿;或者相反。这与读写文件非常相似,只是文件存在于远程机器上,而且远程机器有权决定如何处理我们请求或者发送的数据。

Java 最出色的一个地方就是它的“无痛苦连网”概念。有关连网的基层细节已被尽可能地提取出去,并隐藏在JVM 以及Java 的本机安装系统里进行控制。我们使用的编程模型是一个文件的模型;事实上,网络连接 (一个“套接字”)已被封装到系统对象里,所以可象对其他数据流那样采用同样的方法调用。除此以外,在我们处理另一个连网问题——同时控制多个网络连接——的时候,Java 内建的多线程机制也是十分方便的。

1、机器的标识

为了分辨来自别处的一台机器,以及为了保证自己连接的是希望的那台机器,必须有一种机制能独一无二地标识出网络内的每台机器。早期网络只解决了如何在本地网络环境中为机器提供唯一的名字。但 Java 面向的是整个因特网,这要求用一种机制对来自世界各地的机器进行标识。为达到这个目的,我们采用了 IP (互联网地址)的概念。IP 以两种形式存在着:

(1) 大家最熟悉的DNS(域名服务)形式。我自己的域名是bruceeckel.com。所以假定我在自己的域内有一 台名为 Opus 的计算机,它的域名就可以是 Opus.bruceeckel.com。这正是大家向其他人发送电子函件时采用的名字,而且通常集成到一个万维网(WWW)地址里。

(2) 此外,亦可采用“四点”格式,亦即由点号(.)分隔的四组数字,比如202.98.32.111。 不管哪种情况,IP地址在内部都表达成一个由32个二进制位(bit)构成的数字,所以IP地址的每一组数字都不能超过255。利用由java.net 提供的 static InetAddress.getByName(),我们可以让一个特定的 Java 对象表达上述任何一种形式的数字。结果是类型为 InetAddress 的一个对象,可用它构成一个“套接字”(Socket),大家在后面会见到这一点。

1.1、服务器和客户机

网络最基本的精神就是让两台机器连接到一起,并相互“交谈”或者“沟通”。一旦两台机器都发现了对方,就可以展开一次令人愉快的双向对话。但它们怎样才能“发现”对方呢?这就象在游乐园里那样:一台机器不得不停留在一个地方,侦听其他机器说:“嘿,你在哪里呢?”

“停留在一个地方”的机器叫作“服务器”(Server);到处“找人”的机器则叫作“客户机”(Client) 或者“客户”。它们之间的区别只有在客户机试图同服务器连接的时候才显得非常明显。一旦连通,就变成了一种双向通信,谁来扮演服务器或者客户机便显得不那么重要了。

所以服务器的主要任务是侦听建立连接的请求,这是由我们创建的特定服务器对象完成的。而客户机的任务是试着与一台服务器建立连接,这是由我们创建的特定客户机对象完成的。一旦连接建好,那么无论在服务器端还是客户机端,连接只是魔术般地变成了一个 IO 数据流对象。从这时开始,我们可以象读写一个普通的文件那样对待连接。所以一旦建好连接,使用自己熟悉的 IO 命令即可。这正是 Java 连网最方便的一个地方。

1.2、端口:机器内独一无二的场所

有些时候,一个 IP 地址并不足以完整标识一个服务器。这是由于在一台物理性的机器中,往往运行着多个服务器(程序)。由 IP 表达的每台机器也包含了“端口”(Port )。我们设置一个客户机或者服务器的时候, 必须选择一个无论客户机还是服务器都认可连接的端口。就象我们去拜会某人时,IP 地址是他居住的房子,而端口是他在的那个房间。

注意端口并不是机器上一个物理上存在的场所,而是一种软件抽象(主要是为了表述的方便)。客户程序知道如何通过机器的 IP 地址同它连接,但怎样才能同自己真正需要的那种服务连接呢(一般每个端口都运行着一种服务,一台机器可能提供了多种服务,比如 HTTP 和 FTP 等等)?端口编号在这里扮演了重要的角色,它是必需的一种二级定址措施。也就是说,我们请求一个特定的端口,便相当于请求与那个端口编号关联的服务。“报时”便是服务的一个典型例子。通常,每个服务都同一台特定服务器机器上的一个独一无二的端口编号关联在一起。客户程序必须事先知道自己要求的那项服务的运行端口号。

系统服务保留了使用端口 1 到端口 1024 的权力,所以不应让自己设计的服务占用这些以及其他任何已知正在使用的端口。

2、套接字

“套接字”或者“插座”(Socket)也是一种软件形式的抽象,用于表达两台机器间一个连接的“终端”。 针对一个特定的连接,每台机器上都有一个“套接字”,可以想象它们之间有一条虚拟的“线缆”。线缆的每一端都插入一个“套接字”或者“插座”里。当然,机器之间的物理性硬件以及电缆连接都是完全未知的。抽象的基本宗旨是让我们尽可能不必知道那些细节。

在 Java 中,我们创建一个套接字,用它建立与其他机器的连接。从套接字得到的结果是一个 InputStream 以及OutputStream(若使用恰当的转换器,则分别是Reader和Writer),以便将连接作为一个IO流对象对 待。有两个基于数据流的套接字类:ServerSocket,服务器用它“侦听”进入的连接;以及 Socket,客户用它初始一次连接。一旦客户(程序)申请建立一个套接字连接,ServerSocket 就会返回(通过accept()方法)一个对应的服务器端套接字,以便进行直接通信。从此时起,我们就得到了真正的“套接字-套接字” 连接,可以用同样的方式对待连接的两端,因为它们本来就是相同的!此时可以利用 getInputStream()以及 getOutputStream()从每个套接字产生对应的InputStream和OutputStream对象。这些数据流必须封装到缓冲区内。对类进行格式化,就象对待其他任何流对象那样。

对于Java 库的命名机制,ServerSocket(服务器套接字)的使用无疑是容易产生混淆的又一个例证。大家可能认为 ServerSocket 最好叫作“ServerConnector”(服务器连接器),或者其他什么名字,只是不要在其中安插一个“Socket”。也可能以为ServerSocket和Socket都应从一些通用的基础类继承。事实上,这两种类确实包含了几个通用的方法,但还不够资格把它们赋给一个通用的基础类。相反,ServerSocket 的主要任务是在那里耐心地等候其他机器同它连接,再返回一个实际的 Socket。这正是“ServerSo cket ”这个命名不恰当的地方,因为它的目标不是真的成为一个 Socket,而是在其他人同它连接的时候产生一个 Socket 对象。

然而,ServerSocket 确实会在主机上创建一个物理性的“服务器”或者侦听用的套接字。这个套接字会侦听进入的连接,然后利用 accept()方法返回一个“已建立”套接字(本地和远程端点均已定义)。容易混淆的地方是这两个套接字(侦听和已建立)都与相同的服务器套接字关联在一起。侦听套接字只能接收新的连接请求,不能接收实际的数据包。所以尽管 ServerSocket 对于编程并无太大的意义,但它确实是“物理性”的。

创建一个 ServerSocket 时,只需为其赋予一个端口编号。不必把一个 IP 地址分配它,因为它已经在自己代表的那台机器上了。但在创建一个 Socket 时,却必须同时赋予 IP 地址以及要连接的端口编号(另一方面, 从 ServerSocket.accept()返回的 Socket 已经包含了所有这些信息)。

3、服务多个客户

在典型的服务器中,我们希望同时能处理多个客户的请求。解决这个问题的关键就是多线程处理机制。而对于那些本身不支持多线程的语言,达到 这个要求无疑是异常困难的。大家已经知道Java 已对多线程的处理进行了尽可能的简化。由于Java 的线程处理方式非常直接,所以让服务器控制多名客户并不是件难事。 最基本的方法是在服务器(程序)里创建单个 ServerSocket,并调用accept()来等候一个新连接。一旦 accept()返回,我们就取得结果获得的 Socket,并用它新建一个线程,令其只为那个特定的客户服务。然后再调用 accept() ,等候下一次新的连接请求。

4、数据报

大家迄今看到的例子使用的都是“传输控制协议”(TCP),亦称作“基于数据流的套接字”。根据该协议的设计宗旨,它具有高度的可靠性,而且能保证数据顺利抵达目的地。换言之,它允许重传那些由于各种原因半路“走失”的数据。而且收到字节的顺序与它们发出来时是一样的。当然,这种控制与可靠性需要我们付出一些代价:TCP 具有非常高的开销。

还有另一种协议,名为“用户数据报协议”(UDP),它并不刻意追求数据包会完全发送出去,也不能担保它们抵达的顺序与它们发出时一样。我们认为这是一种“不可靠协议”(TCP 当然是“可靠协议”)。听起来似乎很糟,但由于它的速度快得多,所以经常还是有用武之地的。对某些应用来说,比如声音信号的传输,如果少量数据包在半路上丢失了,那么用不着太在意,因为传输的速度显得更重要一些。大多数互联网游戏,如 Diablo,采用的也是 UDP 协议通信,因为网络通信的快慢是游戏是否流畅的决定性因素。也可以想想一台报时服务器,如果某条消息丢失了,那么也真的不必过份紧张。另外,有些应用也许能向服务器传回一条UDP消息,以便以后能够恢复。如果在适当的时间里没有响应,消息就会丢失。

Java 对数据报的支持与它对 TCP 套接字的支持大致相同,但也存在一个明显的区别。对数据报来说,我们在客户和服务器程序都可以放置一个DatagramSocket(数据报套接字),但与ServerSocket不同,前者不会干巴巴地等待建立一个连接的请求。这是由于不再存在“连接”,取而代之的是一个数据报陈列出来。另一项本质的区别的是对 TCP 套接字来说,一旦我们建好了连接,便不再需要关心谁向谁“说话”——只需通过 会话流来回传送数据即可。但对数据报来说,它的数据包必须知道自己来自何处,以及打算去哪里。这意味着我们必须知道每个数据报包的这些信息,否则信息就不能正常地传递。

DatagramSocket用于收发数据包,而DatagramPacket包含了具体的信息。准备接收一个数据报时,只需提供一个缓冲区,以便安置接收到的数据。数据包抵达时,通过 DatagramSocket,作为信息起源地的因特网地址以及端口编号会自动得到初化。所以一个用于接收数据报的 DatagramPacket 构建器是:

1
DatagramPacket(buf, buf.length)

其中,buf 是一个字节数组。既然 buf 是个数组,大家可能会奇怪为什么构建器自己不能调查出数组的长度呢?实际上我也有同感,唯一能猜到的原因就是 C 风格的编程使然,那里的数组不能自己告诉我们它有多大。

可以重复使用数据报的接收代码,不必每次都建一个新的。每次用它的时候(再生),缓冲区内的数据都会被覆盖。

缓冲区的最大容量仅受限于允许的数据报包大小,这个限制位于比64KB稍小的地方。但在许多应用程序中,我们都宁愿它变得还要小一些,特别是在发送数据的时候。具体选择的数据包大小取决于应用程序的特定要求。

发出一个数据报时,DatagramPacket 不仅需要包含正式的数据,也要包含因特网地址以及端口号,以决定它的目的地。所以用于输出DatagramPacket 的构建器是:

1
DatagramPacket(buf, length, inetAddress, port)

这一次,buf(一个字节数组)已经包含了我们想发出的数据。 length 可以是 buf 的长度,但也可以更短一些,意味着我们只想发出那么多的字节。另两个参数分别代表数据包要到达的因特网地址以及目标机器的一个目标端口。

5、用 JDBC 连接数据库

据估算,将近一半的软件开发都要涉及客户(机)/服务器方面的操作。Java 为自己保证的一项出色能力就是构建与平台无关的客户机/服务器数据库应用。在Java 1.1 中,这一保证通过 Java 数据库连接(JDBC)实现了。

数据库最主要的一个问题就是各家公司之间的规格大战。确实存在一种“标准”数据库语言,即“结构查询语言”(SQL-92),但通常都必须确切知道自己要和哪家数据库公司打交道,否则极易出问题,尽管存在所谓的“标准”。JDBC 是面向“与平台无关”设计的,所以在编程的时候不必关心自己要使用的是什么数据库产品。然而,从JDBC里仍有可能发出对某些数据库公司专用功能的调用,所以仍然不可任性妄为。

和Java中的许多API一样,JDBC也做到了尽量的简化。我们发出的方法调用对应于从数据库收集数据时想当然的做法:同数据库连接,创建一个语句并执行查询,然后处理结果集。

为实现这一“与平台无关”的特点,JDBC 为我们提供了一个“驱动程序管理器”,它能动态维护数据库查询所需的所有驱动程序对象。所以假如要连接由三家公司开发的不同种类的数据库,就需要三个单独的驱动程序对象。驱动程序对象会在装载时由“驱动程序管理器”自动注册,并可用 Class.forName()强行装载。 为打开一个数据库,必须创建一个“数据库 URL”,它要指定下述三方面的内容:

(1) 用“jdbc”指出要使用JDBC。

(2) “子协议”:驱动程序的名字或者一种数据库连接机制的名称。由于 JDBC 的设计从 ODBC 吸收了许多灵 感,所以可以选用的第一种子协议就是“jdbc-odbc 桥”,它用“odbc”关键字即可指定。

(3) 数据库标识符:随使用的数据库驱动程序的不同而变化,但一般都提供了一个比较符合逻辑的名称,由数据库管理软件映射(对应)到保存了数据表的一个物理目录。为使自己的数据库标识符具有任何含义,必须用自己的数据库管理软件为自己喜欢的名字注册(注册的具体过程又随运行平台的不同而变化)。所有这些信息都统一编译到一个字串里,即“数据库URL”。举个例子来说,若想通过ODBC子协议同一个标识为“people”的数据库连接,相应的数据库URL可设为:

1
2
>String dbUrl = "jdbc:odbc:people"
>

如果通过一个网络连接,数据库 URL 也需要包含对远程机器进行标识的信息。

准备好同数据库连接后,可调用静态方法DriverManager.getConnection(),将数据库的URL以及进入那个数据库所需的用户名密码传递给它。得到的返回结果是一个Connection 对象,利用它即可查询和操纵数据库。

6、远程方法

为通过网络执行其他机器上的代码,传统的方法不仅难以学习和掌握,也极易出错。思考这个问题最佳的方式是:某些对象正好位于另一台机器,我们可向它们发送一条消息,并获得返回结果,就象那些对象位于自己的本地机器一样。Java 1.1 的“远程方法调用”(RMI)采用的正是这种抽象。

十、设计范式

在向面向对象程序设计的演化过程中,或许最重要的一步就是“设计范式”(Design Pattern)的问世。它在由Gamma,Helm和Johnson编著的《Design Patterns》一书中被定义成一个“里程碑”(该书由Addison-Wesley于1995年出版)。那本书列出了解决这个问题的23种不同的方法。在本章中,我们准备伴随几个例子揭示出设计范式的基本概念。这或许能激起您阅读《Design Pattern》一书的欲望。事实上,那本书现在已成为几乎所有 OOP 程序员都必备的参考书。

1、范式的概念

在最开始,可将范式想象成一种特别聪明、能够自我适应的手法,它可以解决特定类型的问题。也就是说,它类似一些需要全面认识某个问题的人。在了解了问题的方方面面以后,最后提出一套最通用、最灵活的解决方案。具体问题或许是以前见到并解决过的。然而,从前的方案也许并不是最完善的,大家会看到它如何在一个范式里具体表达出来。

尽管我们称之为“设计范式”,但它们实际上并不局限于设计领域。思考“范式”时,应脱离传统意义上分析、设计以及实施的思考方式。相反,“范式”是在一个程序里具体表达一套完整的思想,所以它有时可能出现在分析阶段或者高级设计阶段。这一点是非常有趣的,因为范式具有以代码形式直接实现的形式,所以可能不希望它在低级设计或者具体实施以前显露出来(而且事实上,除非真正进入那些阶段,否则一般意识不到自己需要一个范式来解决问题)。

范式的基本概念亦可看成是程序设计的基本概念:添加一层新的抽象!只要我们抽象了某些东西,就相当于隔离了特定的细节。而且这后面最引人注目的动机就是“将保持不变的东西身上发生的变化孤立出来”。这样做的另一个原因是一旦发现程序的某部分由于这样或那样的原因可能发生变化,我们一般都想防止那些改变在代码内部繁衍出其他变化。这样做不仅可以降低代码的维护代价,也更便于我们理解(结果同样是降低开销)。

为设计出功能强大且易于维护的应用项目,通常最困难的部分就是找出我称之为“领头变化”的东西。这意味着需要找出造成系统改变的最重要的东西,或者换一个角度,找出付出代价最高、开销最大的那一部分。一旦发现了“领头变化”,就可以为自己定下一个焦点,围绕它展开自己的设计。

所以设计范式的最终目标就是将代码中变化的内容隔离开。如果从这个角度观察,就会发现本书实际已采用了一些设计范式。举个例子来说,继承可以想象成一种设计范式(类似一个由编译器实现的)。在都拥有同样接口(即保持不变的东西)的对象内部,它允许我们表达行为上的差异(即发生变化的东西)。合成亦可想象成一种范式,因为它允许我们修改——动态或静态——用于实现类的对象,所以也能修改类的运作方 式。

在《Design Patterns》一书中,大家还能看到另一种范式:“继承器”(即Iterator,Java 1.0和1.1不负责任地把它叫作 Enumeration,即“枚举”;Java1.2 的集合则改回了“继承器”的称呼)。当我们在集合里遍历,逐个选择不同的元素时,继承器可将集合的实施细节有效地隐藏起来。利用继承器,可以编写出通用的代码,以便对一个序列里的所有元素采取某种操作,同时不必关心这个序列是如何构建的。这样一来,我们的通用代码即可伴随任何能产生继承器的集合使用。

2、观察器范式

观察器(Observer)范式解决的是一个相当普通的问题:由于某些对象的状态发生了改变,所以一组对象都需要更新,那么该如何解决?在Smalltalk 的MVC(模型-视图-控制器)的“模型-视图”部分中,或在几乎等价的“文档-视图结构”中,大家可以看到这个问题。现在我们有一些数据(“文档”)以及多个视图,假定为一张图(Plot)和一个文本视图。若改变了数据,两个视图必须知道对自己进行更新,而那正是“观察器”要负责的工作。这是一种十分常见的问题,它的解决方案已包括进标准的 java.util 库中。

在 Java 中,有两种类型的对象用来实现观察器范式。其中,Observable 类用于跟踪那些当发生一个改变时希望收到通知的所有个体——无论“状态”是否改变。如果有人说“好了,所有人都要检查自己,并可能要进行更新”,那么 Observable 类会执行这个任务——为列表中的每个“人”都调用notifyObservers()方法。notifyObservers()方法属于基础类Observable的一部分。

在观察器范式中,实际有两个方面可能发生变化:观察对象的数量以及更新的方式。也就是说,观察器范式允许我们同时修改这两个方面,不会干扰围绕在它周围的其他代码。

3、模拟垃圾回收站

这个问题的本质是若将垃圾丢进单个垃圾筒,事实上是未经分类的。但在以后,某些特殊的信息必须恢复,以便对垃圾正确地归类。在最开始的解决方案中,RTTI 扮演了关键的角色。 这并不是一种普通的设计,因为它增加了一个新的限制。正是这个限制使问题变得非常有趣——它更象我们在工作中碰到的那些非常麻烦的问题。这个额外的限制是:垃圾抵达垃圾回收站时,它们全都是混合在一起的。程序必须为那些垃圾的分类定出一个模型。这正是RTTI发挥作用的地方:我们有大量不知名的垃圾,程序将正确判断出它们所属的类型。

4、改进设计

《Design Patterns》书内所有方案的组织都围绕“程序进化时会发生什么变化”这个问题展开。对于任何设计来说,这都可能是最重要的一个问题。若根据对这个问题的回答来构造自己的系统,就可以得到两个方面的结果:系统不仅更易维护(而且更廉价),而且能产生一些能够重复使用的对象,进而使其他相关系统的构造也变得更廉价。这正是面向对象程序设计的优势所在,但这一优势并不是自动体现出来的。它要求对我们对需要解决的问题有全面而且深入的理解。

就目前这个回收系统来说,对“什么会变化”这个问题的回答是非常普通的:更多的类型会加入系统。因此,设计的目标就是尽可能简化这种类型的添加。在回收程序中,我们准备把涉及特定类型信息的所有地方都封装起来。这样一来(如果没有别的原因),所有变化对那些封装来说都是在本地进行的。这种处理方式也使代码剩余的部分显得特别清爽。

5、抽象的应用

走到这一步,接下来该考虑一下设计方案剩下的部分了——在哪里使用类?既然归类到垃圾箱的办法非常不雅且过于暴露,为什么不隔离那个过程,把它隐藏到一个类里呢?这就是著名的“如果必须做不雅的事情,至少应将其本地化到一个类里”规则。

6、多重派遣

上述设计方案肯定是令人满意的。系统内新类型的加入涉及添加或修改不同的类,但没有必要在系统内对代码作大范围的改动。除此以外,RTTI 并不象它在 RecycleA.java 里那样被不当地使用。然而,我们仍然有可能更深入一步,以最“纯”的角度来看待 RTTI,考虑如何在垃圾分类系统中将它完全消灭。

为达到这个目标,首先必须认识到:对所有与不同类型有特殊关联的活动来说——比如侦测一种垃圾的具体类型,并把它置入适当的垃圾筒里——这些活动都应当通过多形性以及动态绑定加以控制。

以前的例子都是先按类型排序,再对属于某种特殊类型的一系列元素进行操作。现在一旦需要操作特定的类型,就请先停下来想一想。事实上,多形性(动态绑定的方法调用)整个的宗旨就是帮我们管理与不同类型有特殊关联的信息。既然如此,为什么还要自己去检查类型呢?

答案在于大家或许不以为然的一个道理:Java 只执行单一派遣。也就是说,假如对多个类型未知的对象执行某项操作,Java 只会为那些类型中的一种调用动态绑定机制。这当然不能解决问题,所以最后不得不人工判断某些类型,才能有效地产生自己的动态绑定行为。

为解决这个缺陷,我们需要用到“多重派遣”机制,这意味着需要建立一个配置,使单一方法调用能产生多个动态方法调用,从而在一次处理过程中正确判断出多种类型。为达到这个要求,需要对多个类型结构进行操作:每一次派遣都需要一个类型结构。下面的例子将对两个结构进行操作:现有的 Trash 系列以及由垃圾筒(Trash Bin)的类型构成的一个系列——不同的垃圾或废品将置入这些筒内。第二个分级结构并非绝对显然的。在这种情况下,我们需要人为地创建它,以执行多重派遣。

7、访问器范式

接下来,让我们思考如何将具有完全不同目标的一个设计范式应用到垃圾归类系统。

对这个范式,我们不再关心在系统中加入新型 Trash 时的优化。事实上,这个范式使新型 Trash 的添加显得更加复杂。假定我们有一个基本类结构,它是固定不变的;它或许来自另一个开发者或公司,我们无权对那个结构进行任何修改。然而,我们又希望在那个结构里加入新的多形性方法。这意味着我们一般必须在基础类的接口里添加某些东西。因此,我们目前面临的困境是一方面需要向基础类添加方法,另一方面又不能改动基础类。怎样解决这个问题呢?

“访问器”(Visitor)范式使我们能扩展基本类型的接口,方法是创建类型为 Visitor 的一个独立的类结构,对以后需对基本类型采取的操作进行虚拟。基本类型的任务就是简单地“接收”访问器,然后调用访问器的动态绑定方法。

8、RTTI 真的有害吗

各种设计方案都在努力避免使用RTTI,这或许会给大家留下“RTTI 有害”的印象(还记得可怜的 goto 吗,由于给人印象不佳,根本就没有放到 Java 里来)。但实际情况并非绝对如此。正确地说,应该是 RTTI 使用不当才“有害”。我们之所以想避免 RTTI 的使用,是由于它的错误运用会造成扩展性受到损害。而我们事前提出的目标就是能向系统自由加入新类型,同时保证对周围的代码造成尽可能小的影响。由于 RTTI 常被滥用(让它查找系统中的每一种类型),会造成代码的扩展能力大打折扣——添加一种新类型时,必须找出使用了RTTI 的所有代码。即使仅遗漏了其中的一个,也不能从编译器那里得到任何帮助。 然而,RTTI 本身并不会自动产生非扩展性的代码。

Android Kotlin介绍

发表于 2018-08-30 | 分类于 android

Java的辉煌与阴影

说到Android,就不能不谈谈Android的开发语言java。

Java的辉煌

1995年,当年如日中天的Sun公司发布了Java语言,引起了巨大的轰动,与当时主流的C语言和Basic语言比起来,Java语言简单、面向对象、稳定、与平台无关、解释型、多线程、动态等特点,就像是打开了一个新的世界,一时间风靡全球,云集者众,微软为了模仿Java搞出C#语言,Netscape为了赶时髦硬塞出一个JavaScript语言,IBM则捏着鼻子做了Java IDE Eclipse(日蚀,呵呵)。直到现在,Java在编程世界里还占据着举足轻重的地位,Andy Rubin在开发Android系统时,也很自然地采用了Java和C++(C++负责NDK开发)作为开发语言。

Java的阴影

Java毕竟是20多年前的语言了,虽然有不断扩展更新,但是底层设计思想是很难改动的,这就导致它很难实现一些新的语言特性,例如函数式编程、流式API、高阶函数、空指针安全等,这些新的语言特性大受好评,可以说解放了编程的生产力,这其实也说明了一个事实:开发效率/时间是软件公司真正的瓶颈,任何能压缩代码量,提高开发效率的举措,都应该受到重视。

而且,Android还存在Java版权危机的问题,收购了Sun公司的Oracle曾向Google索要巨额的Java版权费,这可能也加快了Google寻找Android开发替代语言的动作。

苹果公司已经在用Swift语言替代Object-C语言,Google也找到了替代Java的语言,也就是JetBrains公司(Android Studio也是用该公司的Intelli J改的)主推的Kotlin,Kotlin 有着“Android世界的Swift”的称号。

Kotlin简介


Kotlin是一门编程语言,由JetBrains公司开发的,早在2010年就推出Kotlin了。JetBrains就是那个开发了无数个牛逼IDE的公司,Android Studio就是建立在他家的Intellij之上的。而在2017年5月18日,谷歌在今日举行的I/O开发者大会上宣布,将Kotlin语言作为安卓开发的一级编程语言。

Kotlin是基于JVM的,可于Java进行无缝混编,所以开发者可以十分方便地用它来进行Android开发。下图为Android开发使用Kotlin的占比:

Kotlin的特性

空指针安全

空指针异常的确是困扰Java程序员很多年的问题,Swift语言巧妙地解决了这个问题,Kotlin采用了一样的解决方案,只是语法形式不太一样。

1
2
val room: Room? = ...
room?.window?.open()

函数方法

在Kotlin语言中,类终于不再是一等公民。Kotlin语言开始支持面向过程编程,Kotlin语言中可以声明全局函数,内联函数等,还支持函数嵌套,使用函数作为方法参数等操作。对于一些简单的操作,新建一个类去处理,的确有时候是一个让人头疼的问题,Kotlin语言终于让我们摆脱了这一尴尬的现状。

函数的写法有较大的不同,Kotlin语法类似Swift语言的写法,每个函数都必须使用fun关键字声明,参数类型在后,参数名称在前,对于Java语言开发的同学可以需要一个短暂的适应过程。

1
2
3
fun String sayHello(name: String?): String {
return "Hello, $name"
}

类扩展

Kotlin语言支持对现有的类进行扩展。Java程序员应该会对这个特性比较陌生,这也是Swift语言的一个特性之一。所谓扩展,就是在不使用继承的情况下,对现有的类新增方法,属性等操作,扩展不会破坏现有的类方法,仅仅在使用的时候进行动态添加。应该记住一个原则: 扩展优于继承。而这个特性Java语言并不支持。

1
2
3
4
5
fun String.format(): String {
return this.replace(' ', '_')
}

val formatted = str.format()

数据类

在开发过程中,我们常常要不断写一些Model类,不断地使用开发工具生成set/get方法。Data Class就是为简化这个操作而生的,数据类会自动生成set/get方法,而不用显式生成set/get方法。

1
2
3
4
5
data class Person(val name: String,
var email: String,
var age: Int)

val john = Person("John", "john@gmail.com", 112)

高阶函数

所谓的高阶函数就是

  • 可以接受函数作为参数
  • 也可以返回函数作为结果。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    fun <T> lock(lock: Lock, body: () -> T): T {
    lock.lock()
    try {
    return body()
    }
    finally {
    lock.unlock()
    }
    }

与java的交互性好

Kotlin和Java都属于基于JVM的编程语言。Kotlin和Java的交互性很好,可以说是无缝连接。这表现在

  • Kotlin可以自由的引用Java的代码,反之亦然。
  • Kotlin可以使用现有的全部的Java框架和库
  • Java文件可以很轻松的借助IntelliJ的插件转成kotlin

Kotlin的现状

随着技术的逐渐深入,不少开发者也发出从 Java 到 Kotlin,再到 Java 的无奈感叹,那么 Kotlin 现状究竟如何?

对此,国外一家名为 Pusher 的公司在今年 1 月至 3 月期间基于 Kotlin 对世界范围内的 2744 名 IT 人员进行了深入地调查,并于昨日正式发布了《The State of Kotlin 2018》报告,分享了如今 Kotlin 的使用情况,以及生态系统的发展趋势。

年轻的开发者更钟爱 Kotlin

超过一半的受访者表示其作为开发人员工作的时间不到 5 年,2-5 年工作经验的占 33.9%,1-2 年的占 15.2%。本报告受访者的工作经验比参加 StackOverflow 调查的开发者要少一些。然而,“开发者数量每 5 年翻一番”的传统理念依旧存在,未来 Kotlin 的开发者只会越来越多。

Kotlin 使用率迅猛增长,但不是开发者的首选语言

Kotlin 的增长率每年都在翻倍,到了 2015 年,其使用量首次出现大幅飙升,从上一年的 1.4% 激增到 7.7%。对于增长的原因,想必和同年间 Android 大神 Jake Wharton 发布的文档(https://docs.google.com/document/d/1ReS3ep-hjxWA8kZi0YqDbEhCqTt29hG8P44aA9W0DM8)有关,该文档旨在主张使用 Kotlin 进行开发,并创建了一些流行的 Android 开源库。这就导致了很多的 Android 开发者效仿,并使用了 Kotlin 开始创建项目。

而 2017 年 5 月,Google 宣布 Kotlin 正式支持 Android,由此看出,大量 Android 开发者开始使用 Kotlin,占比达到了 46.8%。这其中不乏有因 Google 品牌吸引而投身于 Kotlin 开发的学生,据调查显示,早期,Kotlin 主要由经验丰富的专业开发者使用,如今大批年轻的开发者及学生开始涌入。

不过,目前 Java 仍占主要地位,同时也有很多开发者使用其他的编程语言,诸如 JavaScript、Python、Swift、C# 等等,因此大多数的开发者并没有将 Kotlin 作为其第一编程语言。

Kotlin 更多的是用于个人项目,而非企业项目

受访者中,有超过 60% 的开发者正在工作项目中使用 Kotlin。相比之下,有三分之一的受访学生将其用于工作和辅助项目。 也就是说,他们中有近一半的人表示打算在未来开始使用它。

Kotlin 更多的是被用于私人项目,而非工作项目中。

最爱的特性

对于每位 Android 开发者来说,只要使用过 Java 的 NullPointerException 的特性都会喜欢上 Kotlin 的安全性能,其次分别是扩展函数、Java 的互操作性。有 77% 的受访者表示,Kotlin 扩展功能往往使代码更具可读性,尤其是在函数编程环境中或创建 DSL 时。

跨平台 Kotlin 正在复苏,但速度较为缓慢。只有约四分之一的受访者提到他们运用了一些跨平台支持,大多数选择 Kotlin / Native,其次是 KotlinJS。

Kotlin的未来

Kotlin 整体的发展速度还是让不少开发者为之兴奋。同时鉴于 JetBrains 和Google 的大力支持,以及开发者社区的热捧,我们也相信它会越来越流行。但如今主要的问题是,它在未来是否会成功地入主 Android 以外的应用?

如今 JetBrains 正在大肆推动 Kotlin 的多平台应用,它是否会得到大家的认可也仍未可知。但是想要它成为 Web、iOS 以及后端开发者新的标准,想必也还需要数年的时间来扩展。根据调查报告显示,越多越多的开发者迈出了 Kotlin 编程的第一步,也可以在面向对象、函数、脚本之间穿梭。这就足以说明,Kotlin 正在渗入到整个开发的生态系统中。

商业云服务平台正在成为黑客对用户下手的渠道

发表于 2018-08-28 | 分类于 产品

前言

来自国外的Threat Stack网络安全团队的研究人员表示黑客利用正在利用商业云服务平台的特性对目标用户发起恶意活动,并隐藏自己的行踪。

越来越多的证据表明,黑客正在将目光对准云服务用户,利用公共云平台常见的功能来隐藏活动以长驻目标网络进行恶意活动。

来自Threat Stack网络安全团队数年来一直在跟踪和观看黑客利用云服务的模式。一个明显的分水岭是2016年,他们注意到利用亚马逊网络服务(AWS)进行攻击的复杂性陡然上升。在2017年这种趋势更加明显。该团队指出,问题不在于AWS服务和软件存在漏洞,而在于黑客能够以巧妙的方式利用它的特性。

举个简单的例子:

黑客通常可以通过窃取AWS密钥来获得存储在开放S3容器中的资源路径,或者启动新的Amazon Elastic Compute Cloud(EC2)来挖矿。这种情况已经很常见了,在过去的几年里配置错误的S3容器好几次都登上了头条新闻。亚马逊强调默认情况下S3容器是安全的;,还推出了Macie以保护AWS S3数据,并通过Trusted Advisor提供免费的容器检查。

针对亚马逊推出的一系列安全服务,黑客这边也没有闲着。利用AWS进行恶意活动变得越来越复杂,针对性越来越强,可与基于网络的入侵攻击相结合。

工作原理

大多数这些攻击都始于凭证被窃取,黑客通过网络钓鱼窃取访问密钥或凭据,部署恶意软件以获取用户名和密码,或者是其他感兴趣的信息。

在获取凭证之后,下一步是确定可以获得的权限级别。如果没有黑客想要的东西,他可能会尝试在AWS中创建其他账户或凭据,然后在目标环境中启动新的EC2实例。

此时,黑客可在网络中调用EC2实例来扫描主机。登陆新主机后,黑客会检查其AWS权限。如果只是在寻找少量数据,那么在受感染的终端或主机上绕过DLP工具即可。具体怎么做取决于黑客的动机。

行为模式

这种情况通常出现在针对性的持续攻击中,黑客试图获得对特定数据的访问权限,包括制造业、金融业和高科技行业等都是他们的热门目标。

如何获取数据和数据量多大还是取决于目的。举个例子,如果公司存储医疗保健信息或选民记录,则黑客可能会批量查找数据。而黑客瞄准媒体公司的话,可能只想要知道即将发布的产品信息或更具体的内容,这样只要复制和粘贴或截取屏幕截图即可,这种方式更难察觉到。

总结

在AWS场景中,横向攻击难以被检测到的一个原因是,大多数安全监控技术都假设攻击者潜入主机并升级权限。而在这种情况下,黑客会尽量离开主机层返回到AWS控制平面,大多数安全人员可能都不会注意到这种操作。

来自FreeBuf.COM

看Hidden Bee如何利用新型漏洞进行传播

发表于 2018-08-20 | 分类于 技术

写在前面的话

最近我们发现了一个试图利用CVE-2018-4878(Flash Player中的漏洞)漏洞的攻击,其序列与我们当前发现的任何漏洞利用工具都不一样。经过调查,我们发现这是中国安全公司奇虎360在2017年年底所引用的现有开发框架的一部分。但当时payload似乎是一个推广广告软件的木马。而这次使用的payload它不是一个标准的PE文件。相反,它更像是一种多阶段可执行格式,并且它还充当一个下载加载程序,用于检索隐藏的Bee miner僵尸网络使用的LUA脚本。这可能是第一个用来挖掘加密货币的bootkit案例。

广告概述

攻击者利用成人网站的诱惑性广告将受害者吸引到钓鱼页面。我们认为此系列广告主要针对亚洲国家地区用户,根据所投放的广告和我们已知的数据。这个声称是可以在线约会服务的服务器包含一个恶意的iframe,其主要负责开发和感染用户。

IE exploit

在这里,恶意代码从具有嵌入式加密块的网页开始执行。并采用Base64编码,然后使用RC4或Rabbit两种算法之一进行加密:

在解密之后,该块将被执行。您可以在这里找到正在运行的Java Script的解码版本。我们可以在脚本中看到,它会生成随机会话密钥,然后使用攻击者的公共RSA密钥对其进行加密:

加密的密钥将传递到下一个函数并转换为JSON格式,对硬编码的URL执行POST请求:

如果我们查看客户端和服务器之间的流量(客户端发送加密的“key”,服务器响应“value”),我们更明显发现这一点:

服务器端

1.攻击者的使用私有RSA密钥加密,服务器传递解密会话的密钥。

2.选择对称算法来(Rabbit或RC4)加密漏洞payload。

3.将加密的内容返回给客户端。由于客户端在内存中仍然有密钥的未加密版本,所以它能够解密并执行该漏洞。然而,只从通信流量不能检索原始会话密钥,也不可能重现漏洞。但幸运的是,我们在动态分析中成功捕获了漏洞。并且我们发现攻击者利用的漏洞是CVE-2018-8174。

Flash漏洞利用

这是一个较新的Flash漏洞(CVE-2018-4878)利用程序,在奇虎360发布文档时并不是其exploit kits的一部分,可能是为了增强其性能后来添加的。该漏洞中嵌入的shell代码仅仅是下一阶段的下载程序。成功利用后,它将在以下URL检索其payload:

这个扩展名为.wasm文件,伪造成一个Web Assembler模块。但事实上,它是完全不同的东西。

正如你所看到的,它加载了用于解压缩cabinet文件的Cabinet.dll模块。在后面的部分中,我们看到了用于通过HTTP协议进行通信的API和字符串。我们还发现了对“dllhost.exe”和“bin/i386/core.sdb”的引用。

我们很容易猜到这个模块将下载并利用dllhost.exe来运行。而另一个字符串Base64编码的内容为:

将其解码后的内容展现了更多的网址:

http://103.35.72.223/git/wiki.asp?id=530475f52527a9ae1813d529653e9501

http://103.35.72.223/git/glfw.wasm


http://103.35.72.223/rt/lsv3i06rrmcu491c3tv82uf228.wasm

看看Fiddler捕获的流量,我们发现其模块确实在查询这些URL:

请求来自dllhost.exe,这可能意味着上面的可执行文件已经被注入恶意代码。文件glfw.wasm与Web Assembly之间没有任何共同之处。事实上,它是一个Cabinet文件,包含内部路径下的打包内容:bin/i386/core.sdb。从内部看,我们发现了相同的自定义可执行格式,比如DLL名称:

然后另一个问题是参与者可能试图通过假装使用SLTP协议来检索实际payload来隐藏流量,这可以在从核心内部的Cabinet文件中提取的字符串core.sdb中发现这一点:

INSTALL_SOURCE
&sid=%u
INSTALL_SID
INSTALL_CID
sltp://setup.gohub[.]online:1108/setup.bin?id=128
ntdll.dll
ZwQueryInformationProcess
VolumeNumber
SCSIDISK
os=%d&ar=%d
kernel32.dll
IsWow64Process
RtlGetNtVersionNumbers
x
&sz=
sltp

该主机名解析为67.198.208[.]110:

Pinging setup.gohub.online [67.198.208.110] with 32 bytes of data:
Reply from 67.198.208.110: bytes=32 time=76ms TTL=51

来自沙盒计算机的加密TCP网络流量显示了如何检索二进制的payload:

这个矿机的独特之处在于,它通过使用bootkit实现持久性,如本文所述。受感染的主机将修改其主引导记录,以便在每次操作系统启动时启动矿机。

简单payload的复杂攻击

这种攻击在许多方面上都很有意思,因为它在漏洞利用交付部分中使用了不同的技术以及不同的打包payload技术。因此我们认为它集中在少数几个亚洲国家,不仅如此,它还表明威胁行动者并没有完全放弃exploit kits,尽管在过去几年有明显的下降趋势。

IOC
受污染的交友网站

144.202.87[.]106

exploit kits

103.35.72[.]223

52he3kf2g2rr6l5s1as2u0198k.wasm

087FD1F1932CDC1949B6BBBD56C7689636DD47043C2F0B6002C9AFB979D0C1DD

glfw.wasm

CCD77AC6FE0C49B4F71552274764CCDDCBA9994DF33CC1240174BCAB11B52313

Payload URL 和 IP

setup.gohub[.]online:1108/setup.bin?id=128
67.198.208[.]110

Miner Proxy

133.130.101[.]254

转载自FreeBuf

Ridrelay:一款用于在内网中快速查找域用户名的工具

发表于 2018-08-20 | 分类于 工具

今天给大家介绍一款名叫Ridrelay的工具,研究人员可以在只有低等级权限的情况下,利用该工具枚举出内部网络中的域用户名信息。

RidRelay运行机制

RidRelay整合了SMB Relay攻击技术,常见的基于lsarpc查询和RID循环来获取域用户名列表,其工作步骤如下:

  1. 启动一台SMB服务器并等待传入的SMB连接;
  2. 传入的凭证数据会被中继传输到指定的目标,并创建中继用户的上下文连接;
  3. 查询请求会传输到SMB连接的lasrpc管道,并获取域用户名信息(通过50000次RID循环实现);

依赖组件

Python2.7
Impacketv0.9.17(或更新版本)

工具安装

1
2
3
4
5
6
7
8
pipenv install --two
pipenv shell

#Optional: Run if installing impacket
git submodule update --init --recursive
cd submodules/impacket
python setup.py install
cd ../..

工具使用

首先,找到需要建立中继连接的目标主机,目标主机必须是域成员,而且必须支持SMB,这一步操作可以使用CrackMapExec来实现。

运行RidRelay,并指向目标主机:

1
python ridrelay.py -t 10.0.0.50

或者直接将用户名信息输出到文件中:

1
python ridrelay.py -t 10.0.0.50 -o path_to_output.txt

强烈建议:使用Responder来欺骗目标用户连接RidRelay。

新版本将添加的功能

  1. 添加密码策略枚举功能;
  2. 寻找拥有管理员权限的凭证,实现动态中继;
  3. 获取活跃会话连接;
  4. 整合BloodHound(一款强大的内网域渗透提权分析工具)

来自FreeBuf.COM

安卓与“Proguard”——安卓的代码混淆

发表于 2018-08-19 | 分类于 android

什么是代码混淆

混淆就是对发布出去的程序进行重新组织和处理,使得处理后的代码与处理前代码完成相同的功能,而混淆后的代码很难被反编译,即使反编译成功也很难得出程序的真正语义。被混淆过的程序代码,仍然遵照原来的档案格式和指令集,执行结果也与混淆前一样,只是混淆器将代码中的所有变量、函数、类的名称变为简短的英文字母代号,在缺乏相应的函数名和程序注释的况下,即使被反编译,也将难以阅读。同时混淆是不可逆的,在混淆的过程中一些不影响正常运行的信息将永久丢失,这些信息的丢失使程序变得更加难以理解。

为什么要进行代码混淆

Java 是一种跨平台的、解释型语言,Java 源代码编译成中间”字节码”存储于 class 文件中。由于跨平台的需要,Java 字节码中包括了很多源代码信息,如变量名、方法名,并且通过这些名称来访问变量和方法,这些符号带有许多语义信息,很容易被反编译成 Java 源代码。为了防止这种现象,我们可以使用 Java 混淆器对 Java 字节码进行混淆。

混淆器的作用不仅仅是保护代码,它也有精简编译后程序大小的作用。以“ProGuard”为例,“ProGuard”的主要作用就是混淆,同时它还能对字节码进行缩减体积、优化等。由于对变量名和方法名进行缩减,以及前面所说过的部分不影响正常运行的信息会被丢失,使得编译后的jar文件的体积减少,文件精简。

通过什么方式进行代码混淆

目前,主流的代码混淆的方式,就是使用“Proguard”,同时,你也可以在网上搜代码混淆,代码加固等等服务,可以轻易的搜索到诸如“360加固”、“爱加密”之类第三方的服务。那么他们的区别在哪里:
1>使用工具,就是在你自己的电脑上,通过配置“Proguard”之类的工具,自己进行代码混淆,自己编译,自己调试。而使用第三方服务,就是将你自己的APK,上传到他们的网站,他们帮你进行混淆,混淆/加固完成,再将混淆后的APK发回来给你。
2>使用工具,APK自始至终都在你自己手里,如果使用第三方的服务,原始APK就需要传给别人,这样增加了不安全性,当然你有可能会说,别人那么大间公司怎么觊觎你的APK?对于这个问题的看法就因人而异,像我实习那时处理我公司的那个项目,我的“leader”明确要求自己使用工具手动混淆,不能使用第三方的服务,不能将APK传给别人。

什么是“ProGuard”

“ProGuard”是一个混淆代码的开源项目。它的主要作用就是混淆,当然它还能对字节码进行缩减体积、优化等,但是对于我们来说,体积压缩以及优化功能,还不是最重要的。我们真正在乎的,就是他的混淆功能。这是对“ProGuard”简介,如果你想看详细的,这里附上官网地址:http://proguard.sourceforge.net/

“ProGuard”可以进行哪些优化?

以下资料,来自网络:除了在压缩操作删除的无用类,字段和方法外,“ProGuard”也能在字节码级提供性能优化,内部方法有:

常量表达式求值。
删除不必要的字段存取。
删除不必要的方法调用。
删除不必要的分支。
删除不必要的比较和instanceof验证。
删除未使用的代码。
删除只写字段。
删除未使用的方法参数。
像push/pop简化一样的各种各样的peephole优化。
在可能的情况下为类添加static和final修饰符。
在可能的情况下为方法添加private, static和final修饰符。
在可能的情况下使get/set方法成为内联的。
当接口只有一个实现类的时候,就取代它。
选择性的删除日志代码。实际的优化效果是依赖于你的代码和执行代码的虚拟机的。简单的虚拟机比有复杂JIT编译器的高级虚拟机更有效。无论如何,你的字节码会变得更小。

仍有一些明显需要优化的技术不被支持:

使非final的常量字段成为内联。
像get/set方法一样使其他方法成为内联。
将常量表达式移到循环之外。

如何使用“ProGuard”

前面说了混淆代码的起因和意义,也介绍了“ProGuard”各种好处,现在说说怎么使用这个工具。首先,以下说明全部基于”Eclipse”开发环境,Android2.3以后版本。在Android 2.3以前,混淆Android代码只能手动添加proguard来实现代码混淆,非常不方便。而2.3以后,Google已经将这个工具加入到了SDK的工具集里。该工具的具体路径:SDK\tools\proguard。当创建一个新的Android工程时,在工程目录的根路径下,会出现一个proguard的配置文件proguard.cfg。也就是说,我们可以通过简单的配置,在我们的elipse工程中直接使用ProGuard混淆Android工程。

如何启动“ProGuard”

在工程的根路径下,找到”project-properties.txt”文件,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# This file is automaticallygenerated by Android Tools.
# Do not modify this file -- YOURCHANGES WILL BE ERASED!
#
# This file must be checked inVersion Control Systems.
#
# To customize properties used bythe Ant build system edit
# "ant.properties", andoverride values to adapt the script to your
# project structure.
#
# To enable ProGuard to shrink andobfuscate your code,uncomment this (available properties: sdk.dir,user.home):
# proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt
# Project target.
target=android-22

你只要将倒数第三行那段代码,前面那个“#”号去掉,就能启动“ProGuard”工具。现在进行导包操作,在导包的过程当中,“Eclipse”会自动使用“ProGuard”工具。另外需要说明的一点是,你直接“run”到设备的应用是没有经过混淆的,即便你已经启动了“ProGuard”工具,只有手动导包之后所获得的APK,才是经过混淆工具处理过的。

如何配置“ProGuard”

混淆文件前面说了如何启动“ProGuard”工具,对于一些极简单的工程(没有引用第三方库,没有自定义View,没有调用“native”层的方法),比如你的一个测试“demo”,可能不用配置什么,只要启动工具就好,混淆工作就这样完成了。但是对于较复杂的工程,我们需要手动配置一些东西才能保证混淆过程正常完成,以及混淆之后的APK正常运行。那么为什么复杂的工程就一定要手动配置?究其根本原因就是:不是什么东西都能被混淆的,有些代码混淆之后,它所对应的功能就没法使用。这主要体现在两个地方:
1.所有第三方引用库不能混淆,只要混淆,基本都是要出错的。
2.诸如自定义“View”,“native”层的方法,等等都不能被混淆,混淆之后其对应的功能都要出错,这类代码比较多,我在这里只列举了两个,后面我会详细记录一些。配置混淆文件就是为了告诉系统,某些东西不能混淆,以免我的APP会出错,现在详细介绍一
下如何配置“ProGuard”混淆文件。

两个与混淆相关的配置文件

1.默认配置:工程刚创建的时候,开发环境其实已经默认配置好了混淆设置,你可以在“Eclipse路径\sdk\tools\proguard\”路径下面,找到一个“proguard-android.txt”文件,该文件是开发环境自动配置,混淆设置,这也是为什么对于极简单的项目而言,自己不用另外配置,系统默认配置就能满足。对于默认配置这里只做路径介绍,接下来是重点问题,如何根据项目具体状况,自行配置混淆文件。

2.自定义配置:在项目的根路径下,有个文件:proguard-project.txt。该文件就是手动进行配置的地方。我们需要按照一定语法规则,根据项目实际状况,编写该文件,这样才能正常通过混淆过程。

自定义配置的具体规则

在“proguard-project.txt”文件里面编写混淆规则,其实只有一个目的:如果某个类混淆后,将会导致应用无法正常运行(或者部分功能无法正常运行)那么就要在配置文件里,声明不要混淆这些类。我不关心为什么这些类混淆之后会出错,只关心哪些类在混淆后出错,然后在文件里面声明不要去混淆这些类。当然如果你非得要刨根问底,你也可以在网上找更详细地资料。

通用规则

以下,是通用的,几乎所有工程都要避开的类(所谓避开,就是声明这些类不要被混淆):

四大组件以及系统基本的API不要混淆。
语法规则:-keep public class * extends xxxx
代码示例:

1
2
3
4
5
6
7
8
9
#所有“Activity”及其子类不要混淆,同理,所有“Service”、“BroadcastReceiver”等等系统级别的类,不要混淆。  
-keep public class * extendsandroid.app.Activity
-keep public class * extendsandroid.app.Application
-keep public class * extendsandroid.app.Service
-keep public class * extendsandroid.content.BroadcastReceiver
-keep public class * extendsandroid.content.ContentProvider
-keep public class * extendsandroid.app.backup.BackupAgentHelper
-keep public class * extendsandroid.preference.Preference
-keep public classcom.android.vending.licensing.ILicensingService

保持”native”层的方法不要混淆。

1
2
3
-keepclasseswithmembernamesclass * {       
native<methods>;
}

保持自定义控件,以及指定格式构造方法不要混淆。

1
2
3
4
5
6
-keepclasseswithmembers class * {  
public <init>(android.content.Context, android.util.AttributeSet); #保持自定义控件类不被混淆,指定格式的构造方法不去混淆
}
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet, int);
}

保持指定规则的方法不被混淆(Android layout 布局文件中为控件配置的onClick方法不能混淆)

1
2
3
-keepclassmembersclass * extends android.app.Activity {  
public void *(android.view.View);
}

保持自定义控件指定规则的方法不被混淆

1
2
3
4
5
6
-keeppublic class * extends android.view.View {   
public<init>(android.content.Context);
public<init>(android.content.Context, android.util.AttributeSet);
public<init>(android.content.Context, android.util.AttributeSet, int);
public void set*(...);
}

所有枚举类型不要混淆

1
2
3
4
-keepclassmembers enum * {   
public static ** values();
public static ** valueOf(java.lang.String);
}

需要序列化和反序列化的类不能被混淆(注:Java反射用到的类也不能被混淆)

1
2
3
4
5
6
7
8
9
10
11
12
#保持实现"Serializable"接口的类不被混淆  
-keepnamesclass * implements java.io.Serializable
#保护实现接口Serializable的类中,指定规则的类成员不被混淆
-keepclassmembersclass * implements java.io.Serializable {
static final long serialVersionUID;
private static finaljava.io.ObjectStreamField serialPersistentFields;
!static !transient <fields>;
private voidwriteObject(java.io.ObjectOutputStream);
private voidreadObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}

保持实现”Parcelable”接口的类不被混淆

1
2
3
-keepclass * implements android.os.Parcelable {   
public static finalandroid.os.Parcelable$Creator *;
}

所有泛型不能混淆

1
-keepattributes Signature

假如项目中有用到注解,应加入这行配置

1
-keepattributes *Annotation*

保持R文件不被混淆,否则,你的反射是获取不到资源id的

1
-keep class **.R$*{*;}

保护WebView对HTML页面的API不被混淆

1
-keep class **.Webview2JsInterface {*; }

如果你的项目中用到了webview的复杂操作 ,最好加入

1
2
3
4
5
6
7
-keepclassmembers class * extends android.webkit.WebViewClient {    
public void *(android.webkit.WebView,java.lang.String,android.graphics.Bitmap);
public boolean *(android.webkit.WebView,java.lang.String);
}
-keepclassmembers class * extends android.webkit.WebChromeClient {
public void *(android.webkit.WebView,java.lang.String);
}

至此,主要通用规则已经介绍完毕,你可以直接拷贝到“ProGuard”配置文件里面去,这些规则都通用的。

保持第三方引用库(第三方Jar包)不被混淆

前文已经讨论混淆通用规则,如果一个项目里面,没有引用第三方库,基本上你只要将前面的规则根据情况给抄上去,混淆基本都不会有什么问题。不过现在绝大多数项目(尤其是公司的项目)都会或多或少引入第三方库,前文也已经有提到,所有第三方引用库都不能够混淆,所以接下来我们就聊聊,如何保持第三方库不被混淆。其实保持第三方库不被混淆并不复杂,最关键的就是要细心+耐心。为什么呢?保持第三方库不被混淆,是要将你所引用的所有第三方库,按照一定语法格式,写在混淆配置文件里面,简单地说,就是将你”Eclipse”工程里的”Android Private Libraries”目录下面所有的第三方的引用包,按照给定语法格式,全部(注意是全部)写到你的”proguard.project.txt”文件里面。来让我们看图说话。

一图胜千言,然后我们总结一下,对于每一个第三方的导入包,我们只要:

1
2
3
-libraryjars libs/xxxx.jar
-dontwarn 包名.**
-keep class 包名.** { *;}

大部分的第三方包都能按照这个规则配置,有些第三方引用包,在其官方网站上面会有混淆代码配置说明,比如高德地图就有,这个时候你抄上去就可以了。
好了现在让我们把所有(对是所有)第三方包全都写上,写到手软~

运行程序,查漏补缺

一般来说,按照前面我们说讨论的规则配置之后,混淆过程都能正常通过(不会报错)。不过非常遗憾的是,混淆通过并不代表你APP就能运行,我们之前有提到过,有些东西原来不能混淆,当你混淆之后他的功能就会出错。当你完成了混淆后,只要运行你APP,每个功能都按一按,多玩一下,就有可能发生一些奇怪的事。所以我们才需要做查漏补缺。
首先执行混淆之后,我们能够在路径”proguard”下面发现新出现了四个文件:

mapping.txt:表示混淆前后代码的对照表,这个文件非常重要。如果你的代码混淆后会产生bug的话,log提示中是混淆后的代码,希望定位到源代码的话就可以根据mapping.txt反推。每次发布都要保留它方便该版本出现问题时调出日志进行排查,它可以根据版本号或是发布时间命名来保存或是放进代码版本控制中。
dump.txt:描述apk内所有class文件的内部结构。
seeds.txt:列出了没有被混淆的类和成员。
usage.txt:列出了源代码中被删除在apk中不存在的代码。在我自己这个项目完成混淆代码之后,发生两件奇怪的事,现在说说怎么利用这些文件进行解决。
混淆过后的APP,所有列表(ListView)里的数据,都不显示,在确认了数据确实已经收到,就是没有显示之后,在usage.txt文件里面,发现了所有”ListView”的适配器,也就是说,混淆过后的Apk,代码里面已经没有适配器了,所以造成显示失败,而我当时所做的事,就是在混淆配置文件(proguard.project)文件里面,添加了如下代码:

1
2
#保持所有适配器类不被混淆,本应用中,不加这个将会导致适配器类加载失败,所有列表项没办法显示  
-keep public class * extends android.widget.BaseAdapter

不过这个不算是混淆的配置规则,因为我的另外一个同时,跟我类似项目结构,但是他没有加这句,他的列表显示正常。这是使用”usage.txt”文件进行查漏补缺的例子了。

混淆过后的APP,越用越卡(其实就是内存泄露),用着用着手机就会莫名其妙的死机了,不单单是应用卡死,整台手机都不动了。不论在APP里面进行什么操作,都会导致APP将手机给弄死了。这个异常最终不是通过前面四个文件来解决的,而是通过对APP功能进行考虑。基于所发生的现象,可以看出,一定有什么全局性东西,混淆之后发生错误,导致这个全局功能没法运行,但是却又不断请求,最终耗尽系统资源。最终确定的原因是,我们APP的推送以及IM功能,混淆之后没法工作,最终耗尽系统资源。那么解决的办法是,声明他不要被混淆。

总结

我们聊了那么多的东西,先介绍了什么叫做代码混淆,然后介绍混淆方式选择,接着介绍如何启动以及配置混淆,最重要的当然就是配置混淆。其中包括通用规则,第三方包,以及根据混淆后的文件进行查漏补缺。至此,APP的代码混淆基础,介绍完毕。

全球人口不足一部剧点击量,刷量黑色产业链了解一下

发表于 2018-08-19 | 分类于 产品

最近正火的剧算是《延禧攻略》中皇后娘娘可谓是吸粉无数,截至目前总播放量达56亿,均集播放量超1.3亿。回看之前的《孤芳不自赏》播放量突破160亿……如此看来“全球人口都不足一部剧的播放量”并非玩笑话,这些播放量究竟从何而来呢?

全球人口不足一部剧的点击量?

最近在微博上关于“电视剧播放量”的话题引起了大家的关注,目前的电视剧和网剧不断涌现,播放量也是屡创新高,但是面对这动辄几百亿的数字,好像让全球活着的生物一起参与其中才能够实现……

查看这个评分,再看这160亿的播放量,emmmmmm

这不禁让我们增添一丝疑问……

这几百亿的播放量究竟是如何计算出来的?

目前根据国内大部分视频网站计算方式来看,官方统计的视频播放量并不完全是正片的播放量,很多是加入花絮、预告片、剧照、影视原声、主角cut等相关视频的播放量一起统计而成。打个比方,你观看了20个预告片就贡献了20个播放量,同时自动跳转下一集就贡献2个播放量,异常暂停也就贡献了2个播放量。

这样一来,很多上百亿的播放量似乎可以理解了一点,但是背后的庞大的数字真的毫无掺水效果吗?

条条大路通黑产

抱着怀疑的心态我们随手搜索了一下各个渠道,结果一看吓一跳,可谓是每条路都是通向(黑产)发家致富的路啊……

一、QQ群

混进群内就发现市场很透明嘛,价格也是“童叟无欺”了,并且业务范围覆盖了市面上主要的视频网站。

但是不同平台仍然存在部分差距,这是因为不同平台对于的检测机制不同,攻破的难度自然也不同,刷播放量行业在初期经过打击后现在技术逐渐趋向成熟。

有些平台是根据IP来判断,这时需换一下IP,再进入其网站进行点击即可;有些平台是根据缓存来判断,只需清除下浏览器缓存即可;有些平台是根据当前不同用户的登录来判断,这就需要我们重复进入该网站,多次注册不同的马甲用户,再来操作;有些是根据当前浏览行为判断,即你只要打开目标网址就算一次点击,这样我们只需重复打开关闭该网址即可。

另外,除了原理的不同,各视频平台对点击量、播放量的处理方法也是不同,所以需要根据各个平台制定不同的脚本软件,脚本会随着平台的统计更新而升级。

顺着我也勾搭了一下群管理员,了解了一下目前的行情,平均价格在1,000播放量/8元左右,量大还可往下谈。

二、电商平台

在某宝上搜索“刷播放量”,此类产品也是很多了。

三、自建平台

除了上述的常见渠道,黑产这次还“自建平台”……

联系了一下该账号,发现他是前面qq群的管理员(黑产业务初成规模了)……经过一番交流,了解到目前主要的视频网站的对于异常播放量会有一定的监管机制。

在平台的检测下,常见的机刷播放量可能暂时使播放量冲上去,但是面对视频网站的“二次监控”,必定会出现“掉量”的情况。也就是如上图聊天记录中问到卖家的“是否会掉”,对此卖家暂时也无法给出保证。

四、软件刷播放量

在查找相关信息的时候,发现市面上还有一些“刷量软件”。

这些“刷量软件”大多出现在非官方应用商店内,来源不明确。知乎上也有人对这种软件提出质疑,有人回应被黑产骗了,看来这黑产真的使套路连套路,防不胜防……

除了上述渠道,在微博、贴吧、豆瓣等社交平台上,此项黑产“事业”也是渗透其中……

暴利前行、饭圈助攻

当我们聊到电视剧、网剧刷播放量的时候,就不得不提及“饭圈”这一神秘组织。为了爱豆,追星女孩男孩们可以彻夜打榜,也可以不远万里追寻光芒:

1.2014年10月10日,鹿晗发布微博:“我回家了。” 149分钟后,回帖数突破了一百万。鹿晗的一条微博曾经打破了吉尼斯世界纪录,回复数是 100,252,605 条(无敌是多么~多么寂寞 无敌是多么~多么空虚)。

2.9月21号是TFBOYS成员王俊凯的18岁生日,王俊凯粉丝再次给了他海陆空集体应援。 太空环游纪念应援(将王俊凯的照片看板从美国内华达州发射上太空);为王俊凯承包核心商圈LED屏;承包了全国500家影院的5,000块LED屏幕;无数流动的车身广告……

这样庞大又坚实的力量本是一腔热血为了爱豆成绩,但是一旦为黑产所用,变成其暴利四驱车上重要的车轮。

简单计算一下,如果一部剧有200亿播放量是刷的,借用10,000次/8元的市场价格,仅一部剧黑产的收入就高达千万元!而仅2017年仅网剧就超过600部,背后巨大暴利我们无法估量……

对于这些“刷播放量”的行为,主要伤害的还是影视业本身,部分剧组为了收视效果也会选择刷量,成本增加,也坏了整个行业的风气,用圈内人的话说“别人都刷你不刷不就有点傻了嘛”。

其次也会影响观众的审美。现在影视作品层出不穷,我们的选择性也就更多了,原本整体的观众审美和品味在逐步的培养起来,但是过度的将“烂片”营造成好片必定会形成“柠檬市场”效应,资金少的好片子反而活不下去了,一切都被所谓的“流量”替代。

当然在这种情况下,我们的视频网站也在尝试做出更多努力,去年爱奇艺以杭州某信息科技公司的“刷量”行为侵犯了其合法权益、构成不正当竞争为由向上海市徐汇区人民法院提起诉讼,请求法院判令被告立即停止侵权行为,并赔偿其经济损失500万元。

相信未来在监管上,各个平台也会做出更多努力。作为“看客”的我们从自身做起,别给黑产可乘之机。电影电视原本是为平凡生活织造的一个个梦,在梦里黑产是否应当给我们留一丝宁静呢。

来自FreeBuf.COM

无线干扰及检测技术

发表于 2018-08-10 | 分类于 工具

前言

都听过一句话,没有网络安全就没有国家安全,网络安全一直是这几年的热议话题,未来战争是一个多元化的战争,网络战电子战成为主流必然是趋势所向。而信号干扰是这种战争的主流攻击手段,通过干扰手机、GPS、wifi,甚至卫星链路等电磁波的正常传输,可以影响到整个战争的走向。这里我们讨论的是基本的无线干扰,通过控制未加密的管理帧实现。

802.11标准将所有的数据包分为3种:数据、管理、控制。

数据帧:

携带更高层次的数据,也是唯一可以从无线网络转发到有线网络的数据包

控制帧:

通常与数据帧搭配使用,负责区域的清空、信道的取得以及载波监听的维护,并于收到数据时予以正面的应答,借此促进工作站间数据传输的可靠性

管理帧:

信标帧(Beacons): 在无线设备中,定时依次按指定间隔发送的有规律的无线信号(类似心跳包),主要用于定位和同步使用;

解除认证(Deauthentication)帧:解除身份认证,用于结束一段认证关系;

Probe(request and response)帧:探测区域内有哪些无线信号;

Authenticate(request and response)帧:身份验证;

Associate(request and response)帧:关联请求、响应;

Reassociate(request and response)帧:位于相同扩展服务区域,但在不同基本服务区域间游走的移动式工作站,再次使用分布式系统时,必须与网络重新关联;

Dissassociate(notify) 帧:取消关联。

攻击原理

仔细观察管理帧的类型就会发现,如果攻击者伪造MAC地址,对受害者ap或设备发送指定帧,就会造成目标设备或ap的巨大影响。具体技术手段如下:

第一种:验证洪水攻击,俗称Authdos,通过随机生成大量mac地址,伪装设备向ap发送大量身份验证Authenticate请求帧,使请求数量超出ap承载能力,从而造成拒绝服务攻击,使正常用户无法连接ap。

图示:

第二种:取消验证洪水攻击,俗称deauth攻击,通过伪造mac地址,伪装成目标ap已连接的设备,向ap发送Deauthentication解除认证帧,造成设备掉线,从而达到拒绝服务的目的。

图示:

破解无线密码时经常用到这种攻击,利用deauth帧解除设备的连接,从而嗅探到设备再次连接时的握手包,通过字典中的大量密码匹配,爆破穷举出无线密码。

第三种:关联洪水攻击,俗称asso攻击,主要针对空密码或已破解密码的无线信号,伪造大量设备,淹没ap的关联表,使ap无法给正常用户建立关联

图示:

第四种:射频干扰攻击 RF jammingAttack,这种不再针对管理帧的漏洞,而是上升到物理干扰的层次,用噪声信号淹没射频信号导致系统失效。 这种干扰不分敌我,会影响到一片区域指定频带范围的信号,高考等重要国家考试屏蔽信号用的就是这个方法。

攻击工具

纸上得来终觉浅,下面进行工具实操的演示:

第一个工具:aireplay-ng,这款工具利用的是上述中的deauth攻击。

首先:

1
2
3
airmon-ngstart wlan0       将网卡置为监听模式

airodump-ngwlan0mon –bssid 目标ap的ssid 开始侦听目标网络


1
aireplay-ng-0 0 -a ap的ssid-c 设备的ssid wlan0mon 开始攻击

可以看到攻击起了作用,我的windows主机已经ping不通路由器了。

第二个工具:mdk3

这是mdk3的帮助文档:

可以看到,a参数是第一种攻击——authdos攻击,d参数是第二种攻击——deauth攻击,b参数是信标泛洪攻击,伪造大量虚假ap,只能起到混淆的作用。

首先演示a参数,将网卡置成监听模式后,查找目标ap的mac地址:

1
airodump-ngwlan0mon

找到后,使用mdk3 wlan0mon a -a mac地址发起攻击:

发起攻击后,我的windows主机死活连不上ap,还会在连接过程中重启适配器:

再来试一下d参数,先来看一下d参数的帮助文档:

直接用d参数看起来杀伤范围比较大,还是用一下黑名单功能,只干扰自己的设备吧。

先把目标ap的设备mac地址加入blacklists.txt黑名单:

1
vimblack.txt

开始攻击:

1
mdk3wlan0mon –c 目标ap的信道 –b~/blacklists.txt

干扰成功。

第三个工具:esp8266

这是一款低功耗、高度集成的wifi芯片,将程序烧录进去后,用充电宝供电,不需要kali和无线适配器,也能进行无线干扰攻击,非常便携。

程序烧录后,手机连接控制板子的wifi,进入管理页面:

选择好目标ap后,发起deauth攻击:

被断开连接了。

第四个工具:airgeddon

安装:

1
2
3
git clone https://github.com/v1s1t0r1sh3r3/airgeddon.git
cd airgeddon
sudo bash airgeddon.sh

选择无线网卡:

进入监听模式,并开始dos攻击:


这款工具其实就是很多工具的集合,最后调用的还是mdk3和aireplay,只是设计的更新手化一些。

检测及防御手段

以上工具演示完可以发现,想要进行无线干扰攻击非常简单,不需要任何基础就可以对无线网络造成巨大影响。虽然脚本小子很容易就能破坏网络,但是他们也很容易被发现。

对于检测攻击来说,kismet和wireshark等嗅探工具都是不错的选择。如果想对网络发起监控,对可疑行为报警,可以选择kismet。如果想获得更多的packet信息,可以选择wireshark、omnipeek、commview等抓包工具,具体取决于哪款支持你的无线网卡。因为commview支持我的笔记本集成网卡,下面就用它来做一个抓包演示:

选择只抓管理帧的包并忽略beacons帧.

aireplay-ng的取消验证洪水攻击:

可以看到大量的deauth解除认证帧。

mdk3的身份验证洪水攻击:

可以看到大量设备的身份验证请求。

配合esp8266和kali来演示一下kismet的用法:

输入 sudo kismet –c wlan0mon 就进入了kistmet的主界面:

windows-alerts选项,可以在检测到异常情况时报警。

用esp8266发起deauth攻击:

检测到了攻击。

可以发现,虽然干扰攻击很容易被检测到,但是很难取证,因为源头的mac地址都是伪造的,所以提前做好防御才是解决的方法。

为防止这类攻击,最佳解决防范是尽量使用以太网,避免无线网络。如果必须使用,可以采取降低无线功率的手段降低被攻击概率,但大多数物联网设备都不具备这个功能。其实也不必担心,WPA3已经慢慢出现在公众的视线中,wpa3和wpa2核心差异之一是不允许伪造身份验证或解除关联数据帧,如果你的设备支持wpa3,可以升级以杜绝无线干扰攻击。

来自FreeBuf.COM

Android 脱壳工具

发表于 2018-08-04 | 分类于 android , 工具

drizzleDumper介绍

drizzleDumper是一款基于内存搜索的Android脱壳工具。是根据strazzere大神的android-unpacker优化改造而成,这是一款ndk写的动态Android脱壳的工具,原理简单来说就是ptrace,然后在内存中匹配特征码和dex的magic,最后dump到文件。

apk文件结构

dex文件头结构

drizzleDumper修改优化的地方:

1.android-unpacker基本上就是匹配odex magic的函数时(下图),而drizzleDumper不管odex了,专心匹配dex的magic。

2.android-unpacker用pread(下图),而drizzleDumper换了read和lseek,具体就不说为啥了,这是非常重要的一点。

3.直接抛弃了android-unpacker中的壳的特征匹配这一整块儿内容。

4.android-unpacker只匹配和dump一次,而drizzleDumper引入了双循环机制,这点对脱壳成功也非常重要。

5.android-unpacker在peek_memory(下图)中进行magic匹配,drizzleDumper改了逻辑,换了一种匹配方式。

6.另外还增加了一匹配种方法,来增强匹配的成功率。

7.引入了wait_times机制(很无奈,等待真正的程序dex加载到内存)

8.其他..

drizzleDumper使用

1.获取root权限
执行adb root命令 或者 adb shell su

2.将drizzleDumper文件adb push到/data/local/tmp路径下

3.安装apk
adb install apk

4.运行apk

5.执行脱壳工具
adb shell ./data/local/tmp drizzleDumper apk包名 2

6.dump下来的dex文件会存到/data/local/tmp路径下,将文件下载下来

7.剩下就可以通过dex2jar等工具,解析源码了。

drizzleDumper实战测试

Linux 常用命令

发表于 2018-07-16 | 分类于 linux

Linux路径

linux绝对路径以”/“开头

显示当前文件列表

ll或ls

复制文件

如果dir2目录不存在,则可以直接使用
cp -r dir1 dir2 即可。
如果dir2目录已存在,则需要使用
cp -r dir1/. dir2

统计某文件夹下文件的个数

ls -l |grep “^-“|wc -l

统计某文件夹下目录的个数

ls -l |grep “^d”|wc -l

统计文件夹下文件的个数,包括子文件夹里的

ls -lR|grep “^-“|wc -l

查看文件夹下文件大小

du -sh *

文件重命名

mv abc.txt 1234.txt

文件删除

find lib -type f -name “*.0” -exec rm -f {} \; 查找末尾.0的文件删除
rm -f 强制删除文件
rm -rf 向下递归删除文件夹

打开文件

cat [-n] 文件名 -n代表显示行号

设置权限

chmod -R 755 文件名

运行jar和停止jar

java -jar jar包 运行jar包 当前ssh窗口被锁定,可按CTRL + C打断程序运行,或直接关闭窗口,程序 退出
java -jar jar包 & &代表在后台运行。
特定:当前ssh窗口不被锁定,但是当窗口关闭时,程序中止运行。
nohup java -jar jar包 & nohup 意思是不挂断运行命令,当账户退出或终端关闭时,程序仍然运行
当用 nohup 命令执行作业时,缺省情况下该作业的所有输出被重定向到nohup.out的文件中,除非另外指定了输出文件。
nohup java -jar jar包 >temp.txt & command >out.file是将command的输出重定向到out.file文件,即输出内容不打印到屏幕上,而是输出到out.file文件中。
jobs 查看后台运行任务
fg 任务号 调到前台
ps aux|grep jar包 查看jar包进程
kill -9 查到的进程 停止jar运行

查看系统资源占用

free -b -s5 每5秒刷新 查看内存
top -c 每5秒刷新
df 查看磁盘空间
glances 查看系统工具

查看日志

linux查看日志文件内容命令tail、cat、tac、head、echo
tail -f test.log

你会看到屏幕不断有内容被打印出来. 这时候中断第一个进程Ctrl-C,

linux 如何显示一个文件的某几行(中间几行)
从第3000行开始,显示1000行。即显示3000~3999行
cat filename | tail -n +3000 | head -n 1000
显示1000行到3000行
cat filename| head -n 3000 | tail -n +1000
*注意两种方法的顺序
分解:
tail -n 1000:显示最后1000行
tail -n +1000:从1000行开始显示,显示1000行以后的
head -n 1000:显示前面1000行
用sed命令
sed -n ‘5,10p’ filename 这样你就可以只查看文件的第5行到第10行。

例:cat mylog.log | tail -n 1000 #输出mylog.log 文件最后一千行

cat主要有三大功能:
1.一次显示整个文件。$ cat filename
2.从键盘创建一个文件。$ cat > filename
只能创建新文件,不能编辑已有文件.
3.将几个文件合并为一个文件: $cat file1 file2 > file
参数:
-n 或 –number 由 1 开始对所有输出的行数编号
-b 或 –number-nonblank 和 -n 相似,只不过对于空白行不编号
-s 或 –squeeze-blank 当遇到有连续两行以上的空白行,就代换为一行的空白行
-v 或 –show-nonprinting
例:
把 textfile1 的档案内容加上行号后输入 textfile2 这个档案里
cat -n textfile1 > textfile2
把 textfile1 和 textfile2 的档案内容加上行号(空白行不加)之后将内容附加到 textfile3 里。
cat -b textfile1 textfile2 >> textfile3

把test.txt文件扔进垃圾箱,赋空值test.txt
cat /dev/null > /etc/test.txt

注意:>意思是创建,>>是追加。千万不要弄混了。

tac (反向列示)
tac 是将 cat 反写过来,所以他的功能就跟 cat 相反, cat 是由第一行到最后一行连续显示在萤幕上,

而 tac 则是由最后一行到第一行反向在萤幕上显示出来!

在Linux中echo命令用来在标准输出上显示一段字符,比如:
echo “the echo command test!”
这个就会输出“the echo command test!”这一行文字!
echo “the echo command test!”>a.sh
这个就会在a.sh文件中输出“the echo command test!”这一行文字!
该命令的一般格式为: echo [ -n ] 字符串其中选项n表示输出文字后不换行;字符串能加引号,也能不加引号。
用echo命令输出加引号的字符串时,将字符串原样输出;
用echo命令输出不加引号的字符串时,将字符串中的各个单词作为字符串输出,各字符串之间用一个空格分割。

退出命令

q
ctrl c

XShell5工具

上传文件到linux
-b 以二进制方式,默认为文本方式。
-e 对所有控制字符转义。
如果要保证上传的文件内容在服务器端保存之后与原始文件一致,最好同时设置这两个标志,如下所示方式使用:
rz –bey

下载一个文件
sz filename
下载多个文件
sz filename1 filename2
下载dir目录下的文件,不包括文件夹
sz dir/*

12

冷月灬雪魂

知识海洋,畅想遨游,点点灵光,挥洒满天

13 日志
7 分类
16 标签
RSS
GitHub E-Mail Weibo FaceBook
© 2018 冷月灬雪魂
总访问量次 | 总访客人