前情提要

之前我的项目里有一个合作编辑的功能,多个客户端的用户可以合作写一篇文章的不同部分,而且合作的任意作者互相还可以进行文字通讯。这种需求肯定是首选websocket了,因为服务器需要主动给客户端推送消息,维持一个长连接是最经济实惠的手段。如果一个客户端需要给另一个客户端做推送,那肯定是需要中间服务器作为中转。当两个客户端注册到同一个服务器的时候,发送方带上消息接受者的相关信息,服务器替发送方去对接受者做推送,这样实现了通讯的功能。

那么,这个设计有没有什么问题呢?

在微服务的时代,很多时候需要考虑的是部署多台机器的情况。这时候,上述的思路就不好用了。例如,当客户端A注册到了第一台机器,客户端B注册到了第二台机器,这时候如果客户端A还希望第一台机器为他完成转发的功能,显然是找不到客户端B的。这就是websocket的集群化问题。这问题也让我陷入了深深的思考,那就是产生了一个问题:微信是如何做出来的?

设计个微信?

当很久很久以前,微信还是一个小而美的通讯软件,只有通讯的功能和朋友圈功能。在这个时期,不妨思考思考微信会怎么做。消息通讯的核心仍然是websocket,只不过微信不可能只有一台服务器,微信肯定是一个集群才能正常运转,只有一台机器维持不了那么多的连接。这时候很容易想到微信应该是利用了消息中间件对消息进行了精准投递。对于不在线的用户,微信应该是将离线消息储存到了数据库,用户上线时进行查库,收到消息。

何为“精准投递”?

当客户端A想给客户端B投送消息时,服务器1需要找到客户端B注册到了哪台机器,这时可以考虑用数据库将客户端B建立连接的机器写到数据库里,通过这个数据去找到对应的机器。这时候,消息队列就产生了它应有的作用。核心在于Rabbit MQ的Topic交换机可以绑定多条队列,而且可以选择性推送到指定的队列。所以这时候就产生一个思考,每台机器进行一条队列的绑定,服务器和队列的对应关系可以使用服务器的ip地址,这样保证了唯一性,而且可以复用这个ip标志。

思路设计

当客户端A和服务器1建立连接时,需要往Redis里维持一份会话,这份会话信息保存了客户端A和服务器1建立了连接,也就是保存了服务器1的ip地址。客户端2也是如此。当客户端A想给客户端B发送消息时,先从redis里找到客户端B注册到机器的ip地址,进而确定消息投递的队列。当客户端B收到消息后,将消息推送到客户端B。

代码

那么看看代码吧:

 public static String WS_QUEUE = "wx_queue_";
public static final String WS_TOPIC_EXCHANGE = "wx_topic_exchange";
public static final String WS_BINDING_KEY = "wx_exchange_"; @Bean("WS_QUEUE")
public Queue wsQueue() {
WS_QUEUE += serverIpHost;
return new Queue(WS_QUEUE);
} //交换机
@Bean("WS_TOPIC_EXCHANGE")
public TopicExchange wsExchange() {
return new TopicExchange(WS_TOPIC_EXCHANGE);
} //绑定队列和ES交换机
@Bean
public Binding wsTopicBinding(@Qualifier("WS_QUEUE") Queue wsQueue, @Qualifier("WS_TOPIC_EXCHANGE") TopicExchange wsExchange) {
return BindingBuilder.bind(wsQueue).to(wsExchange).with(WS_BINDING_KEY + serverIpHost);
}

将本机ip写到配置文件里,在初始化队列的时候,将队列名字和该ip关联起来。不同机器这个地方是需要改配置的。也就是一台机器对应一个队列。

对于某一个通讯ws接口:

    @MessageMapping("/chat/{from}/{to}/{blogId}")
public void chat(String msg, @DestinationVariable String from, @DestinationVariable Long to, @DestinationVariable Long blogId) {
UserEntityVo userEntityVo = MyUtils.jsonToObj(redisTemplate.opsForHash().get(Const.CO_PREFIX + blogId, to.toString()), UserEntityVo.class);
if (userEntityVo != null) {
String toServerIpHost = userEntityVo.getServerIpHost();
String idStr = blogId.toString();
String toStr = to.toString();
ChatDto dto = MyUtils.transferToDto(Message.class, ChatDto.class, new Object[]{msg, from, toStr, idStr},
new Class[]{msg.getClass(), from.getClass(), toStr.getClass(), idStr.getClass()});
rabbitTemplate.convertAndSend(
RabbitConfig.WS_TOPIC_EXCHANGE,RabbitConfig.WS_BINDING_KEY + toServerIpHost,
dto);
}
}

即是从redis查出对方所在机器的ip地址,然后投递到那个队列里。


@Bean("WSMessageListener")
//processMessage作为listener
MessageListenerAdapter wSMessageListener(WSMessageHandler wSMessageHandler) {
return new MessageListenerAdapter(wSMessageHandler, "processMessage");
} //在container内将queue和listener绑定
@Bean("WSMessageListenerContainer")
SimpleMessageListenerContainer wSMessageListenerContainer(ConnectionFactory connectionFactory,
@Qualifier("WSMessageListener") MessageListenerAdapter listenerAdapter,
@Qualifier("WS_QUEUE") Queue queue) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames(queue.getName());
container.setMessageListener(listenerAdapter);
return container;
} public void processMessage(MessageDto msg) { String methodName = msg.getMethodName(); switch (methodName) {
...
case "chat":
Container<Message> containerV4 = msg.getData();
Message message = containerV4.getData();
String id = message.getBlogId();
String to = message.getTo();
simpMessagingTemplate.convertAndSendToUser(to, "/" + id + "/queue/chat", message);
break;
... }

这里监听到方法的机器其实就是客户端B所在的机器,其他机器不监听这个队列,是不会做消息转发的尝试的。这里直接推送给客户端B就可以了。

方案的优点

这个方案的优点就是理论上集群可以无限扩张,每台机器的代码并不需要改,只是配置文件会有一些差异。当然也可以将ip地址换成uuid,只要有唯一性就可以了。

一个分布式websocket的实现的更多相关文章

  1. 分享在Linux下使用OSGi.NET插件框架快速实现一个分布式服务集群的方法

    在这篇文章我分享了如何使用分层与模块化的方法来设计一个分布式服务集群.这个分布式服务集群是基于DynamicProxy.WCF和OSGi.NET插件框架实现的.我将从设计思路.目标和实现三方面来描述. ...

  2. Elasticsearch是一个分布式可扩展的实时搜索和分析引擎,elasticsearch安装配置及中文分词

    http://fuxiaopang.gitbooks.io/learnelasticsearch/content/  (中文) 在Elasticsearch中,文档术语一种类型(type),各种各样的 ...

  3. Flink 另外一个分布式流式和批量数据处理的开源平台

    Apache Flink是一个分布式流式和批量数据处理的开源平台. Flink的核心是一个流式数据流动引擎,它为数据流上面的分布式计算提供数据分发.通讯.容错.Flink包括几个使用 Flink引擎创 ...

  4. Dubbo[一个分布式服务框架

    http://alibaba.github.io/dubbo-doc-static/User+Guide-zh.htm#UserGuide-zh-API%E9%85%8D%E7%BD%AE http: ...

  5. JStorm 是一个分布式实时计算引擎

    alibaba/jstorm JStorm 是一个分布式实时计算引擎. JStorm 是一个类似Hadoop MapReduce的系统, 用户按照指定的接口实现一个任务,然后将这个任务递交给JStor ...

  6. Cola:一个分布式爬虫框架 - 系统架构 - Python4cn(news, jobs)

    Cola:一个分布式爬虫框架 - 系统架构 - Python4cn(news, jobs) Cola:一个分布式爬虫框架 发布时间:2013-06-17 14:58:27, 关注:+2034, 赞美: ...

  7. 日志采集框架Flume以及Flume的安装部署(一个分布式、可靠、和高可用的海量日志采集、聚合和传输的系统)

    Flume支持众多的source和sink类型,详细手册可参考官方文档,更多source和sink组件 http://flume.apache.org/FlumeUserGuide.html Flum ...

  8. Sql Server 中如果使用TransactionScope开启一个分布式事务,使用该事务两个并发的连接会互相死锁吗

    提问: 如果使用TransactionScope开启一个分布式事务,使用该事务两个并发的连接会互相死锁吗? 如果在.Net中用TransactionScope开启一个事务. 然后在该事务范围内启动两个 ...

  9. Spring Cloud 5分钟搭建教程(附上一个分布式日志系统项目作为参考) - 推荐

    http://blog.csdn.net/lc0817/article/details/53266212/ https://github.com/leoChaoGlut/log-sys 上面是我基于S ...

  10. 设计一个分布式RPC框架

    0 前言 提前先祝大家春节快乐!好了,先简单聊聊. 我从事的是大数据开发相关的工作,主要负责的是大数据计算这块的内容.最近Hive集群跑任务总是会出现Thrift连接HS2相关问题,研究了解了下内部原 ...

随机推荐

  1. 13.java栈实现计算器

    更新了代码,能跑通了,昨天果然是太晚了脑子混了,今天一看其实就差一句,在最后while循环的时候忘记把拿到的oper从栈里pop出去了,导致oper栈一直不空就要一直从数据栈中取数据进行计算所以一直在 ...

  2. CamstarVP提交前验证

    方法一: public override FormsFramework.ValidationStatus ValidateInputData(Service serviceData) { Valida ...

  3. dart的基本使用

    1.windows上环境搭建 (1)  在dart官网上下载对应的sdk安装即可.归档 | Dart (2)  使用vscode开发,安装dart插件和Code Runner插件即可.  2.Dart ...

  4. C 标准 C89/C90/C99/C11/C17

    C89/C90 PDF:https://www.yodaiken.com/wp-content/uploads/2021/05/ansi-iso-9899-1990-1.pdf C99 PDF:htt ...

  5. c语言创建线程的函数封装

    头文件xxx.h #include <stdlib.h> #include <pthread.h> #include <string.h> typedef int ...

  6. JMeter创建上传文件脚本

    环境:Macbook10 ,apache-jmeter-4.0 1. 创建脚本:添加 -> Sampler -> HTTP请求 2.基本配置 3. 设置实现方式,这一步是关键的关键.... ...

  7. #0延时与for循环+fork join_none

    参考资料 (1) https://blog.csdn.net/moon9999/article/details/104207565 1.for循环+fork...join_none 注1: for循环 ...

  8. maven私服设置与使用详细

    1.私服搭建环境 在Linux系统中,我选择比较方便下载安装docker容器,具体安装步骤可以根据Docker菜鸟教程安装自己需要的镜像.在这里我们先选择 Docker 安装 Nginx.这里就不做具 ...

  9. 将含两列的csv文件生成二维矩阵

    gen_diea=pd.read_csv('../data/ddg_data/diea-gene.csv', header=None, names=['diease','gene']) #生成关联矩阵 ...

  10. Respecting causality is all you need for training physics-informed neural networks

    未发表 本篇工作时关于连续时间的PDE.也是从因果关系的角度入手,最近看过几篇该作者的工作.(简而言之就是从初始条件方向开始训练) 目前的PINN框架缺乏尊重物理系统演化所固有的时空因果结构.因此,作 ...