jgroups 例子

The JGroups tutorial

Bela Ban <[email protected]>

原文地址: http://jgroups.org/tutorial/index.html
本站备份: http://javaarm.com/file/opensource/jgroups/jgroups-tutorial_2014-10-15.zip

实例代码: http://javaarm.com/file/opensource/jgroups/jgroups-tutorial_2014-10-15_study/com.ybxiang.SimpleChat.java
        (YBXIANG稍加改写:每隔8秒自动发送一次消息,不需要手工输入消息了)
      链接 | 主题↑ | 回复↓
1. ybxiang 2014-10-15 发送消息

Table of Contents

    1. 安装
        1.1. 下载
        1.2. 配置
        1.3. 测试你的设置
        1.4. 运行一个演示程序
        1.5. 在没有网络的情况下使用JGroups
        1.6. 故障解答
    2. 编写一个简单的应用程序
        2.1. JGroups概述
        2.2. 创建一个信道并加入一个集群
        2.3. 主事件循环以及发送聊天信息
        2.4. 接收消息并查看变化通知
        2.5. 试试 SimpleChat 应用程序
        2.6. Extra credits: 维护共享的集群状态
        2.7. 总结

Copyright Red Hat 1998 - 2015

本文档的许可证为 "Creative Commons Attribution-ShareAlike (CC-BY-SA) 3.0" 许可证.

关于该教程

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

Bela Ban, Kreuzlingen, Switzerland, 2014年8月。

      链接 | 主题↑ | 回复↓
2. ybxiang 2014-10-15 发送消息

1. 安装

1.1. 下载

JGroups可以从 这里 下载。对于本教程,我使用的是JGroups 3.0的二进制版本,因此请下载一个 jgroups-3.x.y.jar 文件 (比如 jgroups-3.0.0.Final.jar)。

该JAR文件包含了:

  • JGroups内核、演示例子以及(筛选过的)测试类

  • INSTALL.html: 详细的配置指示加上故障解答

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

Note JGroups 3.0需要JDK 6或更高版本。

1.2. 配置

把jgroups-3.x.y.jar添加到你的classpath中。如果你用的是log4j日志系统,那么你还必须把log4j.jar添加进来 (如果你使用的是JDK日志系统,就不必了)。

1.3. 测试你的设置

要想查看你的系统是否能够找到JGroups的类,请执行下面的命令:

java org.jgroups.Version

或者

java -jar jgroups-3.x.y.jar

如果该类被找到的话,你应该能够看到下面的输出信息:

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

1.4. 运行一个演示程序

要想测试JGroups在你的机器上是否运行正常,请运行下面的程序2次:

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

应该会出现两个白板窗口,如下所示:

jgroups 例子_第1张图片

如果你同时启动它们,那么在刚开始,这些窗口的标题条上可能会显示只有1个成员。过了一定时间之后,两个窗口都应该会显示成员数为2。这意味着两个实例发现了对方并形成了一个集群。

当你在其中的一个窗口中绘制的适合,另外一个实例也会被更新。由于默认的分组传输使用的是IP多播,请确保:如果你想要在两个子网中启动这2个实例,那么这2个子网必须启用IP组播。如果情况不是这一,那么这2个实例就无法找到对方了,因此本例就无法工作了。

如果这2个实例找到了对方并形成了一个集群,那么你可以调到下一章节 ("编写一个简单的应用程序")。YBXIANG:不建议跳过下面的2个小节,因为我们未来可能会遇到这种情况。

1.5. 在没有网络的情况下使用JGroups

(如果在前面的章节中,2个实例找到了对方,你可以跳过本节)。

有适合,我们的机器并没有网络连接 (比如,DSL调制解调器宕机了),或者我们只想要在本机上进行多播。要做到这点,我们可以使用 loopback 设备(127.0.0.1):

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

你应该能够再次看到2个Draw实例,它们形成了一个集群。如果不是这样,你可能必须添加一条多播路由到 loopback 设备上 (这样需要超级用户 或 管理员权限):

route add -net 224.0.0.0 netmask 240.0.0.0 dev lo

这意味着,所有将要发送到224.0.0.0网络的数据流 都会被 发送到 loopback 接口,这意味着不需要任何网络。

通常情况下,家庭网络都会有一个带有两块网卡的网关/防火墙:第一块 (eth0) 连接到外部世界 (网络服务提供商),第二块 (eth1) 连接到内网,让网关隔离内网和外网之间的数据流。如果没有为多播数据流添加路由,那么默认情况下就会使用默认的网关,这么做的话 通常会导致 多播数据流被转发到网络服务提供商(ISP)。要防止这种情况发生的话(比如说,ISP会丢弃多播数据流 或者 等待时间太长),我们推荐为在内网(比如 eth1)流转的多播数据流添加一条路由。

1.6. 故障解答

如果2个Draw实例无法找到对方,那么请阅读 INSTALL.html,JGroups软件包带有,其中有更详细的故障解答信息。简而言之,有多种原因导致集群无法形成:

  • 防火墙抛弃了数据包。要想验证是否如此,请关闭防火墙。如果集群形成了,那么再把防火墙打开,然后选择性地为添加规则 让JGroups的数据流通过。

  • 使用了IPv6。JGroups的确支持IPv6,但是某些JDK实现依旧有和IPv6相关的问题,因此,你可以通过将系统属性 "-Djava.net.preferIPv4Stack=true"传入JVM 来禁止JVM用 IPv6。你可以通过设置系统属性-Djava.net.preferIPv6Addresses=true来强制使用IPv6。如果你使用 IPv6 地址,那么你就应该在你的配置中也定义 IPv6地址;比如如果你在UDP中设置 bind_addr="192.168.1.5",那么JGroups将试图使用IPv4地址,如果 IPv4协议栈可用 或者你使用了双协议栈。

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

java -Djgroups.bind_addr=192.168.5.2 java.org.jgroups.demos.Draw
  • 你选用的网卡没有多播路由。

      链接 | 主题↑ | 回复↓
3. ybxiang 2014-10-15 发送消息

2. 编写一个简单的应用程序

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

  • 所有的SimpleChat实例能够相互发现并形成一个集群。

  • 不需要运行一个 中心聊天服务器以便让这些SimpleChat实例连上。因此,不存在单点故障。

  • 一条聊天消息被发送到集群中所有的实例中。

  • 一个实例会在另外一个实例离开(或崩溃)集群 以及 其它实例加入集群时得到一个通知回调。

  • (可选),我们维护了一个常规的集群范围内的共享状态,比如聊天历史。新的实例可以从现有的实例中获取到该历史信息。

2.1. JGroups概述

JGroups使用JChannel作为其主要API,用于 连接到集群、发送/接收消息,以及注册监听器,当某些事情(比如成员加入)发生时就会调用这些监听器。

发送出去的是Message,它包含了一个byte buffer (负载),加上发送者和接收者的地址。地址是org.jgroups.Address的子类,通常包含一个IP地址加上一个端口。

在集群中的JGroups实例列表叫做一个视图(View),每个JGroups实例都包含完全相同的视图。可以通过调用View.getMembers()来得到所有JGroups实例的地址的一个列表。

JGroups实例只能在它们加入集群之后才能发送或接收消息。

当一个JGroups实例想要离开集群的时候,可以调用方法JChannel.disconnect()或者JChannel.close()。如果信道依旧连在集群上的话,在关闭该信道之前,后者实际上调用disconnect()。

2.2. 创建一个信道并加入一个集群

要想加入一个集群,我们将使用一个JChannel。我们可以通过一个定义了信道属性的配置(比如一个XML文件)来创建JChannel的实例。要想实际地连接到集群上,那么使用 connect(String clustername)方法。所有用相同参数调用connect()方法的 channel实例 都会加入相同的集群。因此,让我们实际地创建一个JChannel并连接到一个叫做"ChatCluster"的集群上:

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(); // 使用默认的配置, udp.xml【YBXIANG:】该文件位于jgroups-x.y.z.Final.jar中。 channel.connect("ChatCluster"); } public static void main(String[] args) throws Exception { new SimpleChat().start(); } }

首先,我们利用无参数的构造函数创建了一个信道。这将会用默认的属性来配置该信道。此外,我们也可以传递一个XML文件来配置该信道,比如new JChannel("/home/bela/udp.xml")。

connect() 方法会加入集群"ChatCluster"。请注意,我们不需要预先明确地创建一个集群;如果它是第一个实例的话,connect()方法就会创建该集群。所有加入相同集群的实例都位于相同的集群中,比如如果我们让

  • ch1 加入 "cluster-one"

  • ch2 加入 "cluster-two"

  • ch3 加入 "cluster-two"

  • ch4 加入 "cluster-one"

  • ch5 加入 "cluster-three"

,那么,我们就有3个集群:"cluster-one"具有实例 ch1和ch4,"cluster-two"具有ch2和ch3,"cluster-three"只有ch5。

      链接 | 主题↑ | 回复↓
4. ybxiang 2014-10-15 发送消息

2.3. 主事件循环以及发送聊天信息

我们现在运行一个事件循环,该循环会从stdin读取输入 (一条消息),然后将输入信息发送给当前处于集群中的所有实例。当输入"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, null, line);
			channel.send(msg);
		}
		catch(Exception e) {
		}
	}
}

我们将 eventLoop() 以及信道关闭语句 添加到 start() 方法中,这样我们就提供了 eventLoop 的一个实现。

这个事件循环会阻塞着,直到输入了新的一行数据 (来自标准输入),然后发送一条消息到该集群。发送消息是通过创建一条新的消息然后以该消息作为参数调用Channel.send()方法来完成的。

Message的构造函数的第一个参数是发送目标的地址。空的目标地址将会导致 消息被发送到集群中的每个节点 (某个JGroups实例的非空地址 会导致 该消息只发送给该JGroups实例)。

第二个参数是我们自己的地址。这里也是空的,因为协议栈总是会插入正确的地址。

第三个参数是我们从stdin读取的数据行,Message会使用Java serialization来创建一个 byte[] buffer,然后将其设置为消息的负载。 请注意,我们也可以自己序列化被发送数据对象 (实际上,这是推荐的方式!) 并使用第三个参数是一个byte[] buffer的Message构造函数。

现在该应用程序功能完备了,除了我们还不能接收消息或查看通知之外。我们将在下一节完成。

2.4. 接收消息并查看变化通知

现在,让我们注册一个 Receiver 来接收消息并查看变化。为了达到该目的,我们本例可以实现 org.jgroups.Receiver,然而,我选择了扩展 ReceiverAdapter,它带有默认的实现,我们只需要覆写我们感兴趣的回调方法(receive()和viewChange())既可。现在, 我们需要扩展ReceiverAdapter:

public class SimpleChat extends ReceiverAdapter {

,然后在start()方法中设置该接收器:

private void start() throws Exception { channel=new JChannel();  channel.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()); }

当有任何一个新的JGroups实例加入该集群或者一个现有的JGroups实例离开(包括崩溃)该集群时,这个viewAccepted()回调 方法就会被调用。View的toString()方法会打印出 view ID (一个递增的ID)以及在该集群中的JGroups实例的一个列表。

在 receive() 回调方法中,我们得到一个作为参数传入的Message。我们只是将其buffer作为一个对象 (再一次,使用了Java serialization),然后将其打印到 stdout。我们也打印出了发送者的地址(Message.getSrc())。

注意,我们也可以通过调用 Message.getBuffer() 来得到该 byte[] buffer (负载),然后我们自己反序列化,比如 String line=new String(msg.getBuffer())。


YBXIANG: 关于序列化和反序列化
(a)请参见下面的文章:
  • 序列化和反序列化介绍(java.io.Serializable,java.io.Externalizable)
  • java-custom-serialization-example
  • Discover the secrets of the Java Serialization API
(b) 也可以参见 org.jgroups.util.Util 中的相关处理。
      链接 | 主题↑ | 回复↓
5. ybxiang 2014-10-15 发送消息

2.5. 试试 SimpleChat 应用程序

Now that the demo chat application is fully functional, let’s try it out. Start an instance of 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]
>

The name of this instance is linux-48776 and the physical address is 192.168.1.5:42442 (IP address:port). A name is generated by JGroups (using the hostname and a random short) if the user doesn’t set it. The name stays with an instance for its lifetime, and maps to an underlying UUID. The UUID then maps to a physical address.

We started the first instance, let’s start the second instance:

[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]
>

The cluster list is now [linux-48776, linux-37238], showing the first and second instance that joined the cluster. Note that the first instance (linux-48776) also received the same view, so both instances have the exact same view with the same ordering of its instances in the list. The instances are listed in order of joining the cluster, with the oldest instance as first element.

Sending messages is now as simple as typing a message after the prompt and pressing return. The message will be sent to the cluster and therefore it will be received by both instances, including the sender.

When "exit" or "quit" is entered, then the instance will leave the cluster. This means, a new view will be installed immediately.

To simulate a crash, simply kill an instance (e.g. via CTRL-C, or from the process manager). The other surviving instance will receive a new view, with only 1 instance (itself) and excluding the crashed instance.

      链接 | 主题↑ | 回复↓
6. ybxiang 2014-10-15 发送消息

2.6. Extra credits: 维护共享的集群状态

JGroups的用法之一是维护状态,该状态在集群中进行复制。比如,状态可以是 web server中的HTTP sessions。如果在集群中复制这些 sessions,那么客户端就能够访问该集群中的任何一台服务器,在为该客户端服务的 session崩溃之后,该user sessions依旧可用(位于集群中的其它节点上)。

任何对一个session的更新都会被复制到整个集群中,比如通过序列化被修改的属性,然后通过JChannel.send()把该修改信息发送到该集群中的每台服务器上。这需要所有的服务器具有相同的状态。

然而,当一台新的服务器启动时,会发生什么事情呢?该服务器必须通过某种方法从该集群中的某台已经存在的服务器上复制状态(比如,所有的HTTP sessions)。这叫做 状态转移。

在JGroups中,状态转移是通过实现2个回调方法(getState()和setState()) 以及 调用 JChannel.getState() 方法来做到的。请注意,为了能够在一个应用程序中使用状态转移功能,协议栈必须包含一个状态转移协议 (该demo应用程序所使用的默认协议栈就是这样)。

现在修改start()方法,让它包含对JChannel.getState()的调用:

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

这个getState()方法的第一个参数是目标JGroups实例,这里的null意思是从第一个JGroups(协调者)实例获取状态。第二个参数是超时时间;这里,我们准备等待10秒以完成状态转移。如果状态转移没有在这个时间之内完成,那么就会抛出一个异常。0意味着永远等待。

ReceiverAdapter定义了一个回调方法 getState(),通过对一个已存在的JGroups实例(通常是协调者) 调用该方法 来获取集群的状态。在我们的实例应用程序中,我们将状态定义为 聊天会话。这是一个简单的列表,我们每次收到消息都会添加到它的末尾。(请注意,这可能不是最好的关于状态转移的例子,因为状态数据总是在增加的。作为一 个 workaround,我们本可以使用一个带有边界的 list,尽管我们在这里没有这么干)。

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

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

当然,现在我们需要修改 receive() 方法来添加每条接收到的消息到我们的状态变量中:

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()方法在 state provider(也就是一个已经存在的JGroups实例) 中被调用,用以返回共享的集群状态。该方法被传入一个 output stream,以便将state数据写入。请注意,在状态被写入该output stream之后,JGroups会自动关闭该它,即便出现异常也会关闭,因此在这个回调方法中不必关闭该output stream。

由于对 state 的访问可能是并发的,因此我们同步调用它。然后我们调用Util.objectToStream(),它是 JGroups 的辅助方法,用于将对象写入一个output stream。

setState()方法在 state requester(也就是调用JChannel.getState()的JGroups实例)端 被调用。它的任务是 从input stream中读取状态,然后做相应的设置:

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):"); for(String str: list) { System.out.println(str); } }

我们再次调用了JGroups的辅助方法 (Util.objectFromStream()),来从一个input stream中(读取并)创建一个对象。

然后我们对 state 进行同步,将其内容设置为接收到的状态。

我们将 接收到的聊天历史中的消息的数量 打印到stdout。请注意,对于一个大型的聊天历史而言,这是行不通的,但是,再一次地,我们本来是可以使用带有边界的聊天历史列表的。



YBXIANG实践:

运行 http://javaarm.com/file/opensource/jgroups/jgroups-tutorial_2014-10-15_study/com.ybxiang.SimpleChat.java 步骤:
1) 运行com.ybxiang.SimpleChat.java
2) 等待半分钟以上
3) 再次运行com.ybxiang.SimpleChat.java,在1)中运行的SimpleChat不要关闭!
4) 等待大约15秒左右,关闭3)中的SimpleChat
5) 等待大约15秒左右,关闭1)中的SimpleChat

我们用下面的颜色标识这2个SimpleChat发出的消息:
  • 黑色:第1个SimpleChat
  • 绿色:第2个SimpleChat

第1个SimpleChat【黑色】的日志如下:
-------------------------------------------------------------------
GMS: address=WIN7U-20140428K-51001, cluster=ChatCluster, physical address=2001:0:5ef5:79fb:70:28a2:7804:e013:63812
-------------------------------------------------------------------
** view: [WIN7U-20140428K-51001|0] (1) [WIN7U-20140428K-51001]
Message from WIN7U-20140428K-51001: [User_1413446904321] hello, message 1
Message from WIN7U-20140428K-51001: [User_1413446904321] hello, message 2
Message from WIN7U-20140428K-51001: [User_1413446904321] hello, message 3
Message from WIN7U-20140428K-51001: [User_1413446904321] hello, message 4
Message from WIN7U-20140428K-51001: [User_1413446904321] hello, message 5
Message from WIN7U-20140428K-51001: [User_1413446904321] hello, message 6
** view: [WIN7U-20140428K-51001|1] (2) [WIN7U-20140428K-51001, WIN7U-20140428K-35044]
Message from WIN7U-20140428K-35044: [User_1413446950141] hello, message 1
Message from WIN7U-20140428K-51001: [User_1413446904321] hello, message 7
Message from WIN7U-20140428K-35044: [User_1413446950141] hello, message 2
Message from WIN7U-20140428K-51001: [User_1413446904321] hello, message 8
** suspect(WIN7U-20140428K-35044) is called!
** view: [WIN7U-20140428K-51001|2] (1) [WIN7U-20140428K-51001]

Message from WIN7U-20140428K-51001: [User_1413446904321] hello, message 9
Message from WIN7U-20140428K-51001: [User_1413446904321] hello, message 10
第2个SimpleChat【绿色】的日志如下:
-------------------------------------------------------------------
GMS: address=WIN7U-20140428K-35044, cluster=ChatCluster, physical address=2001:0:5ef5:79fb:70:28a2:7804:e013:63813
-------------------------------------------------------------------
** view: [WIN7U-20140428K-51001|1] (2) [WIN7U-20140428K-51001, WIN7U-20140428K-35044]
6 messages in chat history):

WIN7U-20140428K-51001: [User_1413446904321] hello, message 1
WIN7U-20140428K-51001: [User_1413446904321] hello, message 2
WIN7U-20140428K-51001: [User_1413446904321] hello, message 3
WIN7U-20140428K-51001: [User_1413446904321] hello, message 4
WIN7U-20140428K-51001: [User_1413446904321] hello, message 5
WIN7U-20140428K-51001: [User_1413446904321] hello, message 6
Message from WIN7U-20140428K-35044: [User_1413446950141] hello, message 1
Message from WIN7U-20140428K-51001: [User_1413446904321] hello, message 7
Message from WIN7U-20140428K-35044: [User_1413446950141] hello, message 2
Message from WIN7U-20140428K-51001: [User_1413446904321] hello, message 8
      链接 | 主题↑ | 回复↓
7. ybxiang 2014-10-15 发送消息

2.7. 总结

In this tutorial, we showed how to create a channel, join and leave a cluster, send and receive messages, get notified of view changes and implement state transfer. This is the core functionality provided by JGroups through the JChannel andReceiver APIs.

JGroups has two more areas that weren’t covered: building blocks and the protocol stack.

Building blocks are classes residing on top of a JChannel that provide a higher abstraction level, e.g. request-response correlators, cluster-wide method calls, replicated hashmaps and so forth.

The protocol stack allows for complete customization of JGroups: protocols can be configured, removed, replaced, enhanced, or new protocols can be written and added to the stack.

The code for SimpleChat can be found ./code/SimpleChat.java[here].

Here are some links for further information about JGroups:

  • SimpleChat code: file:./code/SimpleChat.java[SimpleChat.java]

  • JGroups web site: http://www.jgroups.org

  • Downloads: here

  • JIRA bug tracking: http://jira.jboss.com/jira/browse/JGRP

  • Mailing lists: http://sourceforge.net/mail/?group_id=6081


YBXIANG:
JBoss AS的集群是基于JGroups的,JBoss AS集群是有Master节点的,我相信JBoss AS的Master节点就是JGroups中的协调者节点(参见

你可能感兴趣的:(jgroups 例子)