问题
在微服务中用过WebSocket的有没有?来举个爪
虽说像Spring Cloud Gateway这类网关已经支持了WebSocket的转发
但是当我们在向客户端发送消息的时候仍会由于客户端的连接负载均衡到了其他的服务实例而发送不了消息
举个栗子:
假设有service-a和service-b两个服务实例
客户端client通过网关的负载均衡连接到了service-a上
现在我们调用接口触发了给client发送消息的业务
好死不死,这个接口调用被负载均衡到了service-b上
而service-b和client并没有建立连接以至于无法发送消息
一个注解就够了?
基于一些原因我实现了一个库来解决上述问题,只需一个启动注解
核心原理其实就是让service-b将消息转发给service-a,然后service-a再发给client
给大家简单演示一下(最简)用法
WebSocket
先在启动类上添加注解@EnableWebSocketLoadBalanceConcept启用功能
java
@EnableWebSocketLoadBalanceConcept
@SpringBootApplication
public class WsServiceApplication {
public static void main(String[] args) {
SpringApplication.run(WsServiceApplication.class, args);
}
}
接着我们在需要发送消息的地方注入WebSocketLoadBalanceConcept就可以愉快的跨实例发消息啦
java
@RestController
@RequestMapping("/ws")
public class WsController {
@Autowired
private WebSocketLoadBalanceConcept concept;
@RequestMapping("/send")
public void send(@RequestParam String msg) {
concept.send(msg);
}
}
Netty
先在启动类上添加注解@EnableNettyLoadBalanceConcept启用功能
java
@EnableNettyLoadBalanceConcept
@SpringBootApplication
public class NettyServiceApplication {
public static void main(String[] args) {
SpringApplication.run(NettyServiceApplication.class, args);
}
}
比WebSocket多一步,配置NettyLoadBalanceHandler
java
@Component
public class NettySampleServer {
@Autowired
private NettyLoadBalanceConcept concept;
public void start(int port) {
EventLoopGroup boss = new NioEventLoopGroup(1);
EventLoopGroup worker = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(boss, worker)
.channel(NioServerSocketChannel.class)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel channel) throws Exception {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(1024));
pipeline.addLast(new StringEncoder());
pipeline.addLast(new StringDecoder());
//将连接交由 NettyLoadBalanceHandler 管理
pipeline.addLast(new NettyLoadBalanceHandler(concept));
}
});
ChannelFuture future = bootstrap.bind(port).sync();
future.channel().closeFuture().sync();
} catch (Throwable e) {
e.printStackTrace();
} finally {
boss.shutdownGracefully();
worker.shutdownGracefully();
}
}
}
接着我们在需要发送消息的地方注入NettyLoadBalanceConcept就可以愉快的跨实例发消息啦
java
@RestController
@RequestMapping("/netty")
public class NettyController {
@Autowired
private NettyLoadBalanceConcept concept;
@RequestMapping("/send")
public void send(@RequestParam String msg) {
concept.send(msg);
}
}
是!不!是!非常简单!
是!不!是!非常方便!
是!不!是!非常心动!
是 2.0 版本啦
看到这里,可能有读者开始皱起了眉头,怎么有点眼熟呢,好像在哪里看过
(中二预警)
没错,一切都是石头门的选择,El Psy Congroo
(中二结束)
其实你应该是看过我的这篇文章 【Spring Cloud】一个配置注解实现 WebSocket 集群方案
对比之前的实现,新的版本带来了如下功能(主要)
| 功能 | 1.x.x | 2.x.x |
|---|---|---|
| 长连接类型 | WebSocket | 1. WebSocket 2. Netty |
| 订阅(转发)方式 | 服务间 ws(s) 双向连接 | 1. 服务间 ws(s) 双向连接 2. Redis(Redisson) 3. Kafka 4. RabbitMQ |
| 主从订阅(转发) | 不支持 | 1. 主订阅(转发)失败切换到从订阅(转发) 2. 主订阅(转发)恢复切回到主订阅(转发) |
在1.x.x版本中我只实现了WebSocket以及服务间ws(s)的双向连接转发,虽然我有规划其他的一些功能,但是基于工作量等因素就暂时实现了基本的核心功能,主要也不需要依赖其他的库,对于只有2-3个服务实例的场景还是比较适用的
然后过了一段时间我发现这个库的反响还不错,于是决定将之前规划的一些功能完善上去
核心设计
接下来给大家说明一下核心理念:
继续用service-a和service-b举例
service-a通过订阅的方式监听service-b中的消息发送
service-a监听到service-b发送的消息之后,将消息也发送给连接自身的客户端
反过来,service-b也用同样的方式监听service-a
如果有3个服务实例,service-a,service-b,service-c也是一样
service-a监听service-b和service-c
service-b监听service-a和service-c
service-c监听service-a和service-b
如果有4,5,6...n个服务实例,和上述的逻辑一样,以此类推
连接订阅
因为WebSocket本身就可以用来发送消息
所以我们可以通过WebSocket在两个服务实例间转发消息(作为订阅通道)
把连接进行一个分类
| 类型 | 说明 |
|---|---|
| Client | 普通客户端 |
| Subscriber | 订阅其他的服务消息的连接,该类型连接接收到的消息需要被转发 |
| Observable | 其他服务监听自身消息的连接,发送消息时需要转发消息到该类型的连接 |
还是以service-a和service-b为例
service-a作为客户端连接service-b,可以看作service-a订阅监听service-b的消息发送
service-a持有的连接为Subscriber,service-b持有的连接为Observable
service-b在发送消息给客户端的时候,同时通过Observable给service-a也发送消息
service-a通过Subscriber收到service-b的消息,可以看作监听到service-b的消息发送
service-a再把消息发送给自己的客户端
我将订阅逻辑抽象成了ConnectionSubscriber
java
public interface ConnectionSubscriber {
//订阅
void subscribe(Consumer<Connection> consumer);
}
非常简单,只有一个subscribe方法,对于调用该方法的组件来说,不用关心具体怎么订阅的,只需要知道会返回作为Subscriber的连接就行了
我们可以实现成WebSocket连接,也可以实现成订阅Redis或是监听RabbitMQ和Kafka
还可以实现多种方式,比如Kafka/RabbitMQ+Redis,也就是最新的主从订阅功能,默认使用Kafka/RabbitMQ可以避免消息丢失,当Kafka/RabbitMQ不可用时,切换到Redis转发,提高容错
简单的背后是更复杂的设计
虽然大家看我上面的使用实例可能会觉得很简单
但是整个框架其实还是蛮复杂的,源码量已经近w行了
因为很多逻辑我都通过接口抽象了出来方便扩展
ConnectionSubscriber连接订阅只是其中的一个组件,比如还有:
连接仓库
连接仓库ConnectionRepository用于缓存连接
方便自定义效率更高的算法来存取连接,当然默认就是用Map了
连接服务管理器
连接服务管理器ConnectionServerManager用于获得其他服务的信息
服务实例间的ws(s)就是根据这个信息来连接的
默认通过DiscoveryClient获取其他实例的信息
当然也可以自定义通过数据库或是配置文件来获取
连接工厂
连接工厂ConnectionFactory用于扩展不同的连接
如目前已经实现的WebSocketConnectionFactory和NettyConnectionFactory
之后如果有新的长连接可以直接扩展
连接选择器
连接选择器ConnectionSelector用于在发送消息的时候确定发送给哪些连接
能够实现精确的条件发送,比如根据WebSocket的路径Path,userId或是分组group来发送消息
消息工厂
消息工厂MessageFactory用于将消息内容统一成Message方便添加消息头等参数
消息编解码适配器
消息编解码适配器MessageCodecAdapter用于适配消息的编解码器MessageEncoder和MessageDecoder
如普通的客户端的消息要如何编码和解码,服务实例间转发的消息要如何编码和解码,都是可以自定义的
消息重试策略适配器
消息重试策略适配器MessageRetryStrategyAdapter用于指定消息重试策略MessageRetryStrategy
可以分别定义普通客户端消息发送的重试策略和服务实例间转发消息的重试策略
消息幂等校验器
当我们使用RabbitMQ或是Kafka来转发消息的时候,可能会存在重复消费的情况
可以自定义MessageIdempotentVerifier来实现消息重复的校验
事件监听
整个生命周期会触发大量的事件发布
| 事件 | 说明 |
|---|---|
ConnectionLoadBalanceConceptInitializeEvent |
Concept初始化 |
ConnectionLoadBalanceConceptDestroyEvent |
Concept销毁 |
ConnectionEstablishEvent |
连接建立 |
ConnectionCloseEvent |
连接关闭 |
ConnectionCloseErrorEvent |
连接关闭异常 |
ConnectionErrorEvent |
连接异常 |
ConnectionSubscribeErrorEvent |
连接订阅异常 |
MessagePrepareEvent |
消息准备 |
MessageSendEvent |
消息发送 |
MessageSendSuccessEvent |
消息发送成功 |
MessageSendErrorEvent |
消息发送异常 |
DeadMessageEvent |
当一个消息不会发送给任何一个连接 |
MessageDecodeErrorEvent |
消息解码异常 |
MessageForwardEvent |
消息转发 |
MessageForwardErrorEvent |
消息转发异常 |
MessageReceiveEvent |
消息接收 |
MessageDiscardEvent |
消息丢弃 |
MasterSlaveSwitchEvent |
主从切换 |
MasterSlaveSwitchErrorEvent |
主从切换异常 |
HeartbeatTimeoutEvent |
心跳超时 |
EventPublishErrorEvent |
事件发布异常 |
LoadBalanceMonitorEvent |
监控触发 |
UnknownCloseEvent |
未知的连接关闭 |
UnknownErrorEvent |
未知的连接异常 |
UnknownMessageEvent |
未知的消息 |
基于事件也能非常方便的实现一些自定义扩展
可以直接用Spring的@EventListener来监听
结束
其实写这个库的契机,之前有个前同事说他现在的公司想要把项目(设备和服务直连)做成微服务,然后遇到了类似的问题
分析了之后发现和公司之前遇到的微服务 + WebSocket很类似,抽象之后其实就是长连接 + 负载均衡的问题,WebSocket无非就是长连接的一种实现,于是就有了这个库