一个简易的消息中间件设计

简要介绍

本项目采用了观察者模式,实现了一个具有发布订阅的功能的消息中间件。

功能:不同的客户端用户能够订阅自己感兴趣的事件类型,在该类型的事件发生之后消息中心会自动地将事件发送给注册过此事件类型的用户,用户自动接收并消费该事件。

github项目代码地址

什么是消息中间件?

我们要设计一个消息中间件,首先要了解它的功能和定义。

根据百度百科释义,消息中间件是基于队列与消息传递技术,在网络环境中为应用系统提供同步或异步、可靠的消息传输的支撑性软件系统。

一般来说,一个进程要将消息发送给另外一个进程,这个过程中发送进程称为消息的生产者,接收消息的称为消费者,而消息中间件就是生产者和消费者之间的一个中介,它能够管理生产者发送消息,消费者接收消息的这样一个过程,它为二者之间的消息传递提供了可靠通信机制。

消息中间件的作用

虽然在尚未了解消息中间件具体设计的时候讨论其作用可能有点不切实际,但这仍然是很重要的,了解其作用可以帮助我们在设计的时候能够有更多的好的想法和思路。

消息中间件在进程消息传递过程中的角色定位,可以理解为OSI七层网络模型中的表示层,会话层,它直接地管理下层两进程间的TCP通信,并为上层的网络层提供基础的网络通信服务,在这个过程中,它要保证消息的准确稳定,保证消息的不同形式的传输(如数据的流,Java对象的表示形式)和转换的过程顺利完成。

同时,由于不同主机的程序运行的平台环境可能有所差异,消息中间件也要承担这种屏蔽不同平台间环境差异的任务,使得消息可以在不同的平台间相互传递。

另外,消息中间件还要保证消息传递过程的有序进行,由于客户端和服务器通信的情况可能很复杂,多个客户端的请求可能超过服务器的处理能力,如果没有一个可靠的管理机制,必然会造成大量的数据的丢失,例如淘宝电商服务,短时间内有天量的数据请求消息,服务器如何正确且快速地处理每个请求?这也离不开消息消息中间件的强大的消息管理能力。

消息队列

消息队列是消息中间件的核心之一。针对于短时间内高并发请求的情况,为了避免数据丢失和维护系统运行的稳定,服务器端我们可以将生产者产生的数据暂时地存放到一个队列中去,按照FIFO先入先出的顺序对消息队列中的事件进行处理,这样能够保证在一定程度上避免由于数据请求过多导致的丢失情况。

观察者模式

根据设计模式的原则,我们介绍以下观察者模式的四个要素:模式名称,问题,解决方案,效果。

  1. 模式名称:观察者模式。
  2. 问题:如何定义对象间的一对多依赖,即当一个对象的状态发生改变时,它的所有依赖者都会收到通知并更新
  3. 解决方案:
    定义被观察者Observable和观察者Observer
    对于系统中的每一个模块块都允许其他模块向⾃⼰所能发送的某些事件表明兴趣
    当某⼀模块发出某⼀事件时,它⾃动将这些事件发布给那些曾经向⾃⼰注册过此事件的模块
    在这里插入图片描述
    如图所示,对于被观察的主题,它保存了一个包含它的所有订阅者的一个列表。
    主题实现了增加和删除订阅者的方法以及一个通知所有订阅者的方法。

4.效果: 当事件发生时,notifyObservers会调用所有的订阅者的update方法使其消费自己的信息。

设计与实现

要求

设计之前,我们先简单回顾一下项目要求实现的功能:

  1. 消息不同进程间网络传递
  2. 消息队列功能
  3. 发布-订阅功能

模块设计

我将整个项目设计分为客户端和服务器端,客户端管理客户对于消息的发送,接收,消费和对于事件类型的订阅行为,服务器端作为消息中心,接收来自用户的消息,并进行消息的处理,分发,传送等一系列的管理行为。

对于消息和事件,以及其整套的管理工具,我将其定义为一个模块,包含Event事件,EventManager事件管理器,EventHandler事件处理器,EventSource事件发送源,EventType事件类型,Message消息,Topic话题(用于发布-订阅),后面会进行介绍。

事件模块介绍

具体实现参考项目代码,此处仅阐述一些基本的要点

事件类型是一个枚举类型的类,包含有若干种类型,可以自行定义,它作为一个事件的属性成员,事件类型默认为normal,即无类型。

消息Message是对事件的封装,它实现了Serializable接口,可以将对象转换为流形式,是进行网络通信的唯一中介类,当事件发生器要传输一个事件时,它首先将事件封装成消息,再通过将此消息转换为流,将流数据传输到与服务器的socket的输出流中,这样就实现了对于对象的传输。

事件管理器EventManager是服务器的管理类,它采用了单例模式,包含有一个消息队列,当类初始化时创建了一个始终运行的线程(直到程序退出才结束),这个线程不断地扫描消息队列,判断消息队列是否为空,如果不为空,则取出队首的一个事件,交给事件管理器的receiveEvent方法处理。

话题Topic代表一个事件类型下的所有已发布的事件的集合,它包含一个订阅者列表,它是观察者模式中的被观察者,当有事件被加入到它的事件集合中时,它会向它的所有订阅者EventHandler通知此事件,调用它们的update方法。

EventHandler是事件处理器,它是观察者模式中的观察者,它观察topic,接收来自topic的事件,并消费此事件,它是服务器上的对象,所以要承担把待消费事件发送给相应用户的任务,因此它包含有一个套接字成员表示绑定唯一的用户。

服务器设计

首先,实现一个基本的阻塞式的服务器,即初始化服务器后阻塞地等待用户的连接(也可以使用非阻塞的或IO多路复用等等方法),一旦接受到来自用户的请求,就创建一个新的线程,把用户的套接字socket交给ServerHandleThread线程处理,后面与用户的所有数据通信都交给此线程,也就是将这个与客户的TCP连接交给ServerHandleThread线程管理,如果用户下线关闭socket,服务器端ServerHandleThread线程会抛出异常,表示客户端套接字已关闭,但并不会影响整个服务器的正常运行。

线程接收到关键字之后,解析来自客户的请求,判断请求属于哪个类型,如果是订阅类型,创建一个新的事件处理器EventHandler(注意事件处理器是在服务器上的),交给事件管理器EventManager的register方法进行处理,如果是事件类型,也就是说用户想发送一个事件,就把这个事件加入到事件管理器的消息队列中。

线程在未关闭套接字的时候等待用户的消息,如果用户的套接字中流数据不为空,则对其进行上述处理。

客户端设计

客户端Client包括三个基本构件,register,productor和consumer。

register是订阅模块,负责接收来自用户的输入,将其封装为订阅消息,发送给消息中心。

productor是事件发生器EventSource的子类,在其基础上增加了一个流程,用以接收用户的输入,封装为事件消息,发送给消息中心。

consumer是消费模块,它包含有一个独立的线程,这个线程非阻塞地接收来自服务器的消息,解析其事件,并输出相应的事件信息。

结果展示

客户端:
在这里插入图片描述
服务器端:
在这里插入图片描述

出现的问题

我实现了这个简易的消息中间件,简单测试后功能没有异常。

但是很快问题发生了,我的一个小伙伴运行了我的代码并提出程序没有正确运行,客户没有获取到自己订阅的事件消息,我感到十分诧异,然后同时在我和他的笔记本进行了同样的测试行为,但是结果却完全不同,服务器没有把消息发回给订阅的客户端!!

我们排除了ide环境因素导致的差异(我的环境是VScode,他使用的是idea),最终将问题锁定到了系统对于线程的调度机制上来。

经过多次的断点调试,我发现事件管理器中的检测消息队列的线程没有启动,如果我在线程中打一个断点,程序就会进入这个线程,最终能够正常运行,但是如果不对其进行断点处理,线程就不会启动。

问题在于,处理线程是在类的构造方法中启动的,事件管理器类被我实现为单例模式,在类加载的过程中就要创建一个唯一实例,启动处理线程,但是为什么实际运行过程中在个别环境下处理线程却没有正常工作呢。

我们推测是内核对于线程调度方式不同所导致的,我们知道,对于一个CPU,它在一个时间只能执行一个线程,对于多线程的并发,其实是通过分片机制实现的,也就是每个线程占一定的时间片,时间片结束了就讲这个线程暂时挂起,启动另外一个线程,由于线程间调度很频繁,所以给我们造成的感觉好像是处理器在同时执行多个任务,每个任务都能独占计算资源,但实际上并非如此。

要解决这个问题,就要使用Java的线程同步机制,具体的过程我会在之后另外发一篇有关学习Java多线程的笔记中阐述,这里就不再展开了。

结尾

对消息中间件感兴趣的读者可以进一步查阅相关资料,或了解一些开源的成熟消息中间件如kafka,会很有收获。

以上就是我的一个简易消息中间件的设计,虽然功能很简陋,但是却体现了一些基本的思想,希望能对读者有所帮助。

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值