Java for Web学习笔记(九十):消息和集群(5)利用websocket实现订阅和发布(上)

集群中的订阅和发布

利用spring framework在本app内的订阅和发布十分简单。当我们系统越来越复杂的时候,我们需要向其他app发布消息。本学习将给出一个通过websocket来实现不同app之间消息的订购和发布。

在小例子中,我们在所有节点之间都建立webSocket连接来实现消息的发布和订阅。这种方式,节点既是publisher,又是subcriber,还是broker。我们利用spring app内可监听不同消息,而无区分地将所有消息直接广播出去。具体步骤如下:

  1. 启动后,开启一个组播socket(224.0.0.4:6780),监听该组播,同时广播其websocket的地址
    • 使用@Service ClusterManager来实现
    • 需要在app启动完毕,可以正常工作时,才广播自己的websocket地址,例子讲通过一个ping的url来测试是否得到正确的回应来判断是否已经正常工作
  2. 集群中的其他app,监听到该广播后,获得新app的websocket地址,与之建立websocket连接
    • 由于是在组播地址中进行广播,所有自己也会收到,需要过滤掉自己发送的自己websocket地址
    • 维护与自己相连的websocket连接,无论自己作为server还是client,通过@Bean ClusterEventMulticaster来实现
    • websocket不属于spring framework,因此需要将其纳入spring框架,才能支持spring的自动注入
  3. 发布消息,如果需要发布到集群,则向所有建立的websocket连接发布
    • 有些消息需要发布到集群,有些消息可能只需要在本app内发布,用ClusterEvent来表示需要发布到集群的消息。
    • 发布消息由@Bean ClusterEventMulticaster来实现,向所有连接发布
    • 接收消息由websocket endpoint来实现
  4. app关闭时,关闭相关连接
    • 关闭组播socket
    • 关闭websocket的连接。

很显然,这是个N*N的websocket连接。在小规模的情况下,可能满足我们的要求。我们也可以有专门的broker,而websocket是节点与该broker之间的连接,这种模式即是WebSocket Application Messaging Protocol,不过小例子不采用专门broker的方式。

ClusterEvent

小例子将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<AuthenticationEvent>{
    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<LoginEvent>{
    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());
    }
}

ClusterManager:通过组播发布自己的位置

这作为一个Service纳入到spring framework中,我们的自定义ApplicationEventMulticaster为ClusterEventMulticaster,具体的websocket连接在ClusterMessagingEndpoint中实现。Spring在上下文初始化结束后,发布ContextRefreshedEvent事件,我们可以监听这个事件,就如同我们监听前面设置的LoginEvent那样。

@Service public class ClusterManager implements ApplicationListener<ContextRefreshedEvent>{...}

上下文,包括root上下文,web上下文和Rest上下文,按bootstrap的顺序,先是root上下文完成初始化,但此时app尚未能正常启动。我们可以具体检查事件,是否是最后一个上下文启动完毕。小例子中,我们采用另外一个方式,Controller中提供了一个ping接口,如果app正常工作,这个ping接口就可以正常回复200 OK。

用于检测是否正常工作的ping接口

@Controller
public class HomeController {
    @RequestMapping("/ping")
    public ResponseEntity<String> ping()  {
        HttpHeaders headers = new HttpHeaders();
        headers.add("Content-Type", "text/plain;charset=UTF-8");
        return new ResponseEntity<>("ok", headers, HttpStatus.OK);
    }
}

ClusterManager的代码

@Service
public class ClusterManager implements ApplicationListener<ContextRefreshedEvent>{
    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相关文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值