利用spring framework在本app内的订阅和发布十分简单。当我们系统越来越复杂的时候,我们需要向其他app发布消息。本学习将给出一个通过websocket来实现不同app之间消息的订购和发布。
在小例子中,我们在所有节点之间都建立webSocket连接来实现消息的发布和订阅。这种方式,节点既是publisher,又是subcriber,还是broker。我们利用spring app内可监听不同消息,而无区分地将所有消息直接广播出去。具体步骤如下:
很显然,这是个N*N的websocket连接。在小规模的情况下,可能满足我们的要求。我们也可以有专门的broker,而websocket是节点与该broker之间的连接,这种模式即是WebSocket Application Messaging Protocol,不过小例子不采用专门broker的方式。
小例子将Event对象直接在websocket中进行传递,采用java序列化的方式。这种方式简单,但也有限制,也就是所对方也必须在java程序。我们还可以选择JSON或者XML的格式进行传递。我们在之前专门学习了序列化的基本知识,现在可以直接使用。
public class ClusterEvent extends ApplicationEvent implements Serializable{
private static final long serialVersionUID = 1L;
private final Serializable serializableSource;
//在上一学习的基础上增加rebroadcasted,用于标识这是一个从外部接收的事件,不需要向集群广播。
private boolean rebroadcasted;
public ClusterEvent(Serializable source) {
super(source);
this.serializableSource = source;
}
public final boolean isRebroadcasted() {
return rebroadcasted;
}
public final void setRebroadcasted() {
this.rebroadcasted = true;
}
private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException{
in.defaultReadObject();
this.source = this.serializableSource;
}
}
我们设置相关的Event和listener
public abstract class AuthenticationEvent extends ClusterEvent{
private static final long serialVersionUID = 1L;
public AuthenticationEvent(Serializable source) {
super(source);
}
}
public class LoginEvent extends AuthenticationEvent{
private static final long serialVersionUID = 1L;
public LoginEvent(String username) {
super(username);
}
}
@Service
public class AuthenticationInterestedParty implements ApplicationListener{
private static final Logger log = LogManager.getLogger();
@Inject ServletContext servletContext;
@Override
public void onApplicationEvent(AuthenticationEvent event) {
log.info("Authentication event from context {} received in context {}.",
event.getSource(), this.servletContext.getContextPath());
}
}
@Component
public class LoginInterestedParty implements ApplicationListener{
private static final Logger log = LogManager.getLogger();
@Inject ServletContext servletContext;
@Override
public void onApplicationEvent(LoginEvent event) {
log.info("Login event for context {} received in context {}.",
event.getSource(), this.servletContext.getContextPath());
}
}
这作为一个Service纳入到spring framework中,我们的自定义ApplicationEventMulticaster为ClusterEventMulticaster,具体的websocket连接在ClusterMessagingEndpoint中实现。Spring在上下文初始化结束后,发布ContextRefreshedEvent事件,我们可以监听这个事件,就如同我们监听前面设置的LoginEvent那样。
@Service public class ClusterManager implements ApplicationListener{...}
上下文,包括root上下文,web上下文和Rest上下文,按bootstrap的顺序,先是root上下文完成初始化,但此时app尚未能正常启动。我们可以具体检查事件,是否是最后一个上下文启动完毕。小例子中,我们采用另外一个方式,Controller中提供了一个ping接口,如果app正常工作,这个ping接口就可以正常回复200 OK。
@Controller
public class HomeController {
@RequestMapping("/ping")
public ResponseEntity ping() {
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "text/plain;charset=UTF-8");
return new ResponseEntity<>("ok", headers, HttpStatus.OK);
}
}
@Service
public class ClusterManager implements ApplicationListener{
private static final Logger log = LogManager.getLogger();
//组播是UDP,本例子中组播地址为224.0.0.4,端口为6780
private static final InetAddress MULTICAST_GROUP;
private static final int MULTICAST_PORT = 6780;
static{
try {
MULTICAST_GROUP = InetAddress.getByName("224.0.0.4");
} catch (UnknownHostException e) {
throw new FatalBeanException("Could not initialize IP addresses.", e);
}
}
private boolean initialized,destroyed = false;
private MulticastSocket socket;
private String pingUrl, messagingUrl;
private Thread listenThread;
@Inject ServletContext servletContext; //获取servlet Context,即可以获得在web.xml中的配置
//multicaster是我们自定义的ApplicationEventMulticaster,我们将在那里维护和集群其他app的websocket连接。
@Inject ClusterEventMulticaster multicaster;
//【1】初始化:创建组播socket,并启动监听
@PostConstruct
public void listenForMulticastAnnouncements() throws NumberFormatException, IOException{
//1.1】初始化设置pingUrl和websocket的Url。这里的host没有自动获取,而是通过配置,主要是多网卡的情况下,例如开发机上同时安装了虚机,可能会指定到其他地址。在稍后的组播设置中,需要指定network interface。所以方便起见,小例子采用了配置的方式。web的端口port也一样采用配置方式。
String host = servletContext.getInitParameter("host");
if(host == null)
host = InetAddress.getLocalHost().getHostAddress();
String port = servletContext.getInitParameter("port");
if(port == null)
port = "8080";
this.pingUrl = "http://" + host + ":" + port + this.servletContext.getContextPath() + "/ping";
this.messagingUrl = "ws://" + host + ":" + port + this.servletContext.getContextPath() + "/services/Messaging/a83teo83hou9883hha9";
//1.2】这里组播socket的创建,并在线程中开启监听
this.socket = new MulticastSocket(MULTICAST_PORT);
this.socket.setInterface(InetAddress.getByName(host));//需要放在joinGroup()前,用于多网卡时确定使用哪个网卡,如单网卡,无需设置
this.socket.joinGroup(MULTICAST_GROUP);
this.listenThread = new Thread(this::listen, "cluster-listener"); //设置监听的线程
this.listenThread.start();
}
//【2】在app正常运行后,通过组播socket,将自己的websocket的URL广播出去
@Async //确保一定运行在线程中。
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
//2.1】initialized用于确保只执行一次,否则root context初始化完成执行一次,web context初始化完成又执行一次
if(initialized)
return;
initialized = true;
//【2.2】不断尝试访问自己的/ping接口,不成功,则休眠500ms,再次尝试,总的尝试次数限制为120次,即1分钟内,都尝试失败,就放弃
try {
URL url = new URL(this.pingUrl);
log.info("Attempting to connect to self at {}.", url);
int tries = 0;
while(true){
tries ++;
//(2.2.1)方位自己的/ping,看看是否正常回复。这里学习一下URLConnection的使用
URLConnection connection = url.openConnection();
connection.setConnectTimeout(100);
try(InputStream stream = connection.getInputStream()){
String response = StreamUtils.copyToString(stream,StandardCharsets.UTF_8);
if(response != null && response.equals("ok")){ //检查是否已经正常工作
//(2.2.2)app正常工作,此处将放置通过组播socket,将自己的websocket的url(messageUrl)广播出去的代码
DatagramPacket packet = new DatagramPacket(this.messagingUrl.getBytes(),this.messagingUrl.length(),
MULTICAST_GROUP, MULTICAST_PORT);
this.socket.send(packet);
return;
}else{
log.warn("Incorrect response: {}", response);
}
}catch(Exception e){
if(tries > 120) {
log.fatal("Could not connect to self within 60 seconds.",e);
return;
}
Thread.sleep(500L);
}
}
} catch (Exception e) {
log.fatal("Could not connect to self.", e);
}
}
//【3】组播socket监听,如果听到由websocket的URL,则连接该URL,建立起websocket的连接。由于是组播,因此也会收到自己广播初期的自己的websocket的URL,需要将此过滤掉
private void listen(){
byte[] buffer = new byte[2048];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
while(true){
try {
this.socket.receive(packet);
String url = new String(buffer, 0, packet.getLength()); //获取内容
if(url.length() == 0)
log.warn("Received blank multicast packet.");
else if(url.equals(this.messagingUrl)) //过滤掉自己的webSocket地址
log.info("Ignoring our own multicast packet from {}",packet.getAddress().getHostAddress());
else
//3.1】在自定义的ApplicationEventMulticaster(维护各websocket连接)中,根据url创建一个websocket链接
this.multicaster.registerNode(url);
} catch (IOException e) {
if(this.destroyed)
return;
log.error(e);
}
}
}
//【4】app关闭前,应关闭组播socket
@PreDestroy
public void shutDownMulticastConnection() throws IOException {
this.destroyed = true;
try{
this.listenThread.interrupt();
this.socket.leaveGroup(MULTICAST_GROUP);
}finally{
this.socket.close();
}
}
}
相关链接: 我的Professional Java for Web Applications相关文章