文档结构  
翻译进度:已翻译     翻译赏金:10 元 (?)    ¥ 我要打赏
参与翻译: mylxiaoyi (27), CY2 (1)

Copyright Red Hat 1998 - 2035

本文档以 "Creative Commons Attribution-ShareAlike (CC-BY-SA) 3.0" 许可发布。

关于教程

这是一份关于如何安装 JGroups 并编写简单应用的简短教程。本教程的目的是演示如何配置 JGroups 以及如何编写演示主要 API 主法的简单应用。

Bela Ban, Kreuzlingen, Switzerland, August 2016

1. 安装

1.1. 下载

JGroups 可以由 这里 下载。在本教程中,我使用 JGroups 4.0 的二进制版本,所以下载一个 jgroups-4.x.y.jar 文件 (例如 jgroups-4.0.0.Final.jar)。

第 1 段(可获 1.33 积分)

JAR 文件包含:

  • JGroups 核心,演示以及(选中的)测试类

  • 样例配置文件,例如 udp.xml 或 tcp.xml

NoteJGroups 4.x 需要 JDK 8

1.1.1. Maven

Maven / Gradle / Ivy 等可以用来包含 JGroups:

groupId: org.jgroups
artifactId: jgroups
version: 4.0.0.Final (for example)

1.2. 配置

将 jgroups-4.x.y.jar 添加到我们的类路径。如果你使用 log4j2 日志系统,你同时需要添加 log4j2.jar (如果你使用 JDK 日志系统,则该操作并不是必需的).

1.3. 测试你的配置

要测试你的系统是否能够找到 JGroups 类,执行下面的命令:

第 2 段(可获 1.2 积分)
java org.jgroups.Version

or

java -jar jgroups-4.x.y.jar

如果能够找到类,你应该看到下列输出(或多或少):

$  java org.jgroups.Version
   Version:      4.0.0.Final

1.4. 运行演示程序

要测试 JGroups 是否在你的机器上正常运行,运行下面的命令两次:

java -Djava.net.preferIPv4Stack=true org.jgroups.demos.Draw

应出现如下所示的两个白板窗口:

Draw

如果你同时启动他们,他们初始时在其标题栏中显示1的成员关系。 一段时间后,两个窗口应显示2。这意味着两个实例彼此发现对方并构成一个簇。

第 3 段(可获 1.01 积分)

当在一个窗口中绘制时,第二个实体也会被更新。因为默认组使用 IP 多制播进行传输,如果你在不同的子网内启动两个实体,确保 IP 多播被打开。否则,两个实体不会彼此发现,而该示例不会正常工作。

如果两个实体能够彼此发现且构成一个簇,你可以直接进入下一章 ("编写简单应用").

1.5. 无网络使用 JGroups

(如果在前一节中两个实体能够正确彼此发现,你可以略过本节).

第 4 段(可获 1.33 积分)

有时并没有网络连接 (e.g. DSL 调制解调器失效), 或是我们希望仅在本地机器上多播。为此,我们可以使用回环设备 (127.0.0.1):

java -Djgroups.bind_addr=127.0.0.1 -Djava.net.preferIPv4Stack=true org.jgroups.demos.Draw

我们应该再次看到构成一个簇的两个实体。如果不是这样的话,你也许需要向回环设备添加一个多播路由 (这需要超级用户或管理员权限):

route add -net 224.0.0.0 netmask 240.0.0.0 dev lo

这意味着所有指向 224.0.0.0 网络的流量将会被发送到回环接口,这意味着不再需要任何运行的网络。

第 5 段(可获 1.29 积分)

典型的家庭网络有一个2 NIC 的网关/炉火墙:第一个 (eth0) 与外部世界 (网络服务提供商)相连, 第二 (eth1) 个与内部网络相连,在内部与外部网络之间则是网关防火墙/伪装流量。如果没有添加用于多播的路由,则会使用默认网关,通常会将多播流量指向 ISP。 为避免这种情况 (例如, ISP 丢弃多播流量, 或是延迟过高), 我们推荐为多播流量添加一个指向内网的路由。

第 6 段(可获 1.25 积分)

1.6. 故障排除

如果2个绘制实体没有发现彼此,请阅读 INSTALL.html, 该文档由 JGroups 附带,具有更为详细的故障排除的信息。简而言之, 没有构成簇有多种可能的原因:

  • 防火墙丢包。要验证这种可能,关闭防火墙。如果集群形成,那么打开防火墙,并且选择性地添加规则以放行 JGroups 流量。

  • IPv6 的使用. JGroups 可以与 IPv6 一起工作,但是某些 JDK 实现依然存在问题,所以你可以通过向 JVM 传递 "-Djava.net.preferIPv4Stack=true" 系统属性来关闭 IPv6。你可以通过设置系统属性 -Djava.net.preferIPv6Addresses=true 来强制使用 IPv6地址。如果你使用 IPv6 地址,你同时应该在你的配置中定义 IPv6 地址;例如,如果你在 UDP 中设置 bind_addr="192.168.1.5" ,如果 IPv4 栈可用或是你正在运行双栈,则 JGroups 将会尝试选择 IPv4 地址。

  • 你没有使用正确的网络接口 (NIC): 使用  -Djgroups.bind_addr 系统属性定义 NIC:

第 7 段(可获 2.38 积分)
java -Djgroups.bind_addr=192.168.5.2 java.org.jgroups.demos.Draw
  • 对于选定的 NIC 没有多播路由。

2. 编写简单应用

本章的目的是编写一个简单的基于文本的聊天程序 (SimpleChat), 具有下列特性:

  • 所有的 SimpleChat 实体彼此发现并形成集群。

  • 不需要运行实体必须连接的中心聊天服务器。所以不存在单点失败。

  • 聊天信息被发送给集群的所有实体。

  • 当其他实体离开(或崩溃)以及其他实体加入时,实体会得到通知回调。

  • (可选) 我们维护一个通用的集群级共享状态,例如,聊天历史。新的实体由已存在的实体请求该历史。

第 8 段(可获 1.45 积分)

2.1. JGroups 概述

JGroups 使用 JChannel 作为连接集群,发送与接收消息,以及注册当事件(例如成员加入)发生时的调用的监听器的主要 API。

发送的内容是 Messages, 其包含一个字节缓冲区(负载),以及发送者与接收者的地址。地址是 org.jgroups.Address 的子类,通常包含一个 IP 地址与一个端口。

集群中的实体列表被称为 View, 且每一个实体包含相同的视图。所有实体的地址列表可以通过调用 View.getMembers() 来获得.

第 9 段(可获 1.25 积分)

实体只有在他们加入集群之后才能发送或接收消息。

当一个实体要离开集群时,可以调用 JChannel.disconnect()JChannel.close() 方法. 如果在关闭通道之前通道依然处于连接状态,则后者实际调用 disconnect() 方法。

2.2. 创建通道并加入集群

要加入集群,我们将会使用 JChannel. 使用定义了通道属性的配置(例如,XML文件)创建 JChannel 实例。要实际连接集群,使用 connect(String clustername) 方法。使用相同的参数调用 connect() 的所有通道实例将会加入相同的集群。所以让我们实际创建一个 JChannel 并连接到一个名 "ChatCluster":

第 10 段(可获 1.48 积分)
import org.jgroups.JChannel;

public class SimpleChat {
    JChannel channel;
    String user_name=System.getProperty("user.name", "n/a");

    private void start() throws Exception {
        channel=new JChannel(); // use the default config, udp.xml
        channel.connect("ChatCluster");
    }

    public static void main(String[] args) throws Exception {
        new SimpleChat().start();
    }
}

首先我们使用一个空构造函数创建一个通道。这会使用默认属性配置通道。相对应地,我们可以传递一个 XML 文件来配置通道,例如 new JChannel("/home/bela/udp.xml").

第 11 段(可获 0.38 积分)

connect() 方法加入集群 "ChatCluster". 注意我们必不需要预先显示创建一个集群;如果他是第一个实体,connect() 会创建集群。加入相同集群的所有实体将存在于相同的集群内,例如,如果我们有

  • ch1 加入 "cluster-one"

  • ch2 加入 "cluster-two"

  • ch3 加入 "cluster-two"

  • ch4 加入 "cluster-one"

  • ch5 加入 "cluster-three"

, 那么我们将会有三个集群: "cluster-one" 拥有实体 ch1 与 ch4, "cluster-two" 拥有实体 ch2 与 ch3, 而 "cluster-three" 仅拥有实体 ch5.

2.3. 主事件循环与发送聊天消息

第 12 段(可获 1.15 积分)

我们现在运行事件循环,该循环会由标准输入读取输入(消息)并发送给当前集群中的所有实体。当输入 "exit" 或 "quit" 时,我们退出循环并关闭通道。

private void start() throws Exception {
    channel=new JChannel();
    channel.connect("ChatCluster");
    eventLoop();
    channel.close();
}

private void eventLoop() {
    BufferedReader in=new BufferedReader(new InputStreamReader(System.in));
    while(true) {
        try {
            System.out.print("> "); System.out.flush();
            String line=in.readLine().toLowerCase();
            if(line.startsWith("quit") || line.startsWith("exit"))
                break;
            line="[" + user_name + "] " + line;
            Message msg=new Message(null, line);
            channel.send(msg);
        }
        catch(Exception e) {
        }
    }
}
第 13 段(可获 0.49 积分)

我们添加了 eventLoop() 调用同时将关闭通道添加到 start() 方法中,并提供了eventLoop 的实现。

事件循环是一直阻塞直到(由标准输入)读到新行,然后将消息发送给集群。这是通过创建一个新 Message 并将其用作参数来调用 Channel.send() 实现的。

Message 构造函数的第一个参数是目标地址。空目标地址会将消息发送给集群中的所有成员(非空地址的实体仅将消息发送给一个实体)。

第二个参数是我们由标准输入读取的行,这会使用 Java 序列化来创建一个 byte[] 缓冲区并设置消息的负载。注意我们同时序列化对象自身(实际推荐)并且将 byte[] 缓冲区作为 Message 构造函数的第二个参数。

第 14 段(可获 1.85 积分)

现在应用已具有全部功能,除了我们并没有接收消息或视图通知。这会在下一节中实现。

2.4. 接收消息与视图变化通知

现在让我们注册一个 Receiver 来接收消息与视图变化。为此,我们实现 org.jgroups.Receiver, 然而我选择扩展具有默认实现的 ReceiverAdapter 并仅重写我们感兴趣的回调 (receive()viewChange()) 。现在我们需要扩展 ReceiverAdapter:

public class SimpleChat extends ReceiverAdapter {
第 15 段(可获 0.94 积分)

, 在 start() 中设置接收器:

private void start() throws Exception {
    channel=new JChannel().setReceiver(this);
    channel.connect("ChatCluster");
    eventLoop();
    channel.close();
}

, 并实现 receive()viewAccepted():

public void viewAccepted(View new_view) {
    System.out.println("** view: " + new_view);
}

public void receive(Message msg) {
    System.out.println(msg.getSrc() + ": " + msg.getObject());
}

viewAccepted() 回调会在一个实体加入集群时,或是已存在的实体离开集群(包含崩溃)时调用。其 toString() 方法会输出视图 ID (一个增加的 ID) 以及集群中当前实体的列表。

第 16 段(可获 0.59 积分)

在 receive() 中,我们获得作为参数的 Message 。我们简单地获取其缓冲区作为对象(再次使用 Java 序列化)并将其输出到标准输出。我们同时输出发送者的地址  (Message.getSrc()).

注意我们同时可以通过调用 Message.getBuffer() 来获得 byte[] 缓冲区 (负载)并进反序列化,例如 String line=new String(msg.getBuffer()).

2.5. 试验 SimpleChat 应用

现在演示聊天程序已实现全部功能,让我们来试一下吧。启动 SimpleChat 的一个实体:

[linux]/home/bela$ java SimpleChat

-------------------------------------------------------------------
GMS: address=linux-48776, cluster=ChatCluster, physical address=192.168.1.5:42442
-------------------------------------------------------------------
** view: [linux-48776|0] [linux-48776]
>
第 17 段(可获 1.1 积分)

该实体的名字为 linux-48776 ,而物理地址为 192.168.1.5:42442 (IP地址:端口). 如果用户没有设置名字,则名字由 JGroups 生成(使用主机名与一个随机短整数)。名字在实体的整个生命周期内保持不变,且映射为一个底层 UUID。然后 UUID 映射为一个物理地址。

我们已启动第一个实体,让我们启动第二个实体:

[linux]/home/bela$ java SimpleChat

-------------------------------------------------------------------
GMS: address=linux-37238, cluster=ChatCluster, physical address=192.168.1.5:40710
-------------------------------------------------------------------
** view: [linux-48776|1] [linux-48776, linux-37238]
>
第 18 段(可获 0.85 积分)

现在集群列表为 [linux-48776, linux-37238], 显示了加入集群的第一个与第二个实体。注意第一个实体 (linux-48776) 同时接收到相同的视图,所以两个实体具有完全相同的视图,列表中的实体具有相同的顺序。实体是以加入集群的顺序排列的,最老的实体作为第一个参数。

现在发送消息仅是简单地在提示符后输入消息并按下回车。消息会被发送给集群,因而他将会被所有实体接收,包括发送者。

第 19 段(可获 1.2 积分)

当 "exit" or "quit" 被输入时,实体会离开集群。这意味着一个新的视图会被立即安装。

为模拟崩溃,简单地杀掉一个实体T (例如,通过 CTRL-C, 或进程管理器). 其他存活的实体将会接收到一个新的视图,仅包含1个实体(自身)并排除崩溃的实体。

2.6. 额外的学分: 维护共享集群状态

JGroups 的一个用例是维护在集群之间复制的状态。例如,状态可以是 web 服务器中的所有 HTTP 会话。如果这些会话在集群之间被复制,客户端可以访问集群中的任意服务器,在持有客户端会话的服务器崩溃之后,用户会话依然可用。

第 20 段(可获 1.64 积分)

对会话的任何更新会在集群之间复制,例如,通过序列化被修改的属性并通过 JChannel.send() 将修改发送给集群中的所有服务器。这是需要的,从而所有的服务器具有相同的状态。

然而,当一个新服务器被启动时会发生什么呢?该服务器必须由集群中已存在的服务器获取状态(例如,所有的HTTP 会话)。这被称为状态传输。

JGroups 中的状态传输是通过实现两个回调 (getState() 与 setState()) 并调用 JChannel.getState() 方法来完成的。注意,为了在应用中能够使用状态传输,协议栈必须拥有状态传输协议(演示所用的默认栈具有该协议)。

第 21 段(可获 1.59 积分)

现在 start() 方法被修改为包含 JChannel.getState()调用:

private void start() throws Exception {
    channel=new JChannel().setReceiver(this);
    channel.connect("ChatCluster");
    channel.getState(null, 10000);
    eventLoop();
    channel.close();
}

getState() 方法的第一个参数是目标实体,而 null 意味着由第一个实体(协调员)获取状态。第二个参数是超时时间;在这时我们希望等待 10 秒来传输状态。如果在这段时间状态不能被传输,则会抛出异常。0意味着永远等待。

第 22 段(可获 0.93 积分)

ReceiverAdapter 定义了一个回调 getState() ,该回调在存在的实体上调用(通常为协调者)来获取集群状态。在我们的演示程序中,我们将状态定义为聊天会话。这是一个简单列表,我们在其尾部添加我们收到的所有消息。(注意,这可能并不是状态的最好示例,因为状态总是在增长。作为解决方案,我们有一个带有边界限制的列表,尽管在这里并没有实现)。

列表被定义为一个实体变量:

final List<String> state=new LinkedList<String>();

当然,现在我们需要修改 receive() 将每一个接收到的消息添加到我们的状态中:

第 23 段(可获 1.3 积分)
public void receive(Message msg) {
    String line=msg.getSrc() + ": " + msg.getObject();
    System.out.println(line);
    synchronized(state) {
        state.add(line);
    }
}

getState() 回调实现为

public void getState(OutputStream output) throws Exception {
    synchronized(state) {
        Util.objectToStream(state, new DataOutputStream(output));
    }
}

getState() 在状态提供者内调用,例如一个存在的实体,来返回共享集群状态。他被传递给一个状态需要被写入的输出流。 注意 JGroups 会在状态写入后自动关闭流,甚至是异常状况中也是如此,所以流不需要被关闭。

第 24 段(可获 0.84 积分)

因为对 state 的访问也许是并发的,我们对其进行同步。然后我们调用 Util.objectToStream() ,这是一个将对象写入输出流的 JGroups 实用方法。

setState() 方法在状态请求者上调用,例如调用 JChannel.getState() 的实体。其任务是由输入流读取状态并进行相应的设置:

public void setState(InputStream input) throws Exception {
    List<String> list;
    list=(List<String>)Util.objectFromStream(new DataInputStream(input));
    synchronized(state) {
        state.clear();
        state.addAll(list);
    }
    System.out.println(list.size() + " messages in chat history):");
    list.forEach(System.out::println);
}
第 25 段(可获 0.73 积分)

我们再次调用 JGroups 实用方法 (Util.objectFromStream()) 来由输入流创建对象。

然后我们同步 state, 并由接收的状态设置其内容。

我们同时将接收历史中的消息数量输出到标准输出。注意,对于较大的聊天历史,这是不可行的,但是,再一次,我们有一个有限的聊天历史列表。

2.7. 结论

在本教程中,我们演示了如何创建通道,加入与离开集群,发送与接收消息,获取视图变化通知,并实现状态传输。这是 JGroups 通过 JChannelReceiver API 提供的核心功能。

第 26 段(可获 1.36 积分)

JGroups 还有两个我们没有讨论的方面:构建块与协议栈。

构建块是位于 JChannle 之上提供高级抽象级别的类,例如,请求-响应相关器,集群级方法调用,复制的哈希图,等。

协议栈允许 JGroups 的完全自定义:协议可以被配置,移除,替换,加强,或者可以编写新的协议并加入栈中。

SimpleChat 的代码在 ./code/SimpleChat.java[here].

下面是关于 JGroups 更多信息的链接:

第 27 段(可获 1.54 积分)

​​​​​​

第 28 段(可获 2 积分)

文章评论