前情提要

之前我的项目里有一个合作编辑的功能,多个客户端的用户可以合作写一篇文章的不同部分,而且合作的任意作者互相还可以进行文字通讯。这种需求肯定是首选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. 路飞项目 day03 前端配置、后台主页、项目依赖问题

    一.路飞项目前端配置 1.先删除一些不要的 ​ 删除多余的组件,只要app和首页组件 ​ 然后改一下组件的内部代码 -App.vue中 ______________ <template> ...

  2. Delphi中Stringlist的自定义排序(将函数地址做为参数)

    近日,在编制一个程序过程,因为数据量较小,就使用了stringlist来暂存数据.在使用过程中,遇到了一个问题.Stringlist字符串列表的默认排序方法是按ASCII码的方式进行排序,如3,10, ...

  3. 记一次Mybatis-Plus动态分表DynamicTableNameInnerInterceptor里无法动态替换表名的坑

    首先上源码 protected String changeTable(String sql) { ExceptionUtils.throwMpe(null == tableNameHandler, & ...

  4. 数字成像系统—ISP

    ISP是什么Image Signal Processor,图像信号处理器.作用:处理Image Sensor(图像传感器)的输出数据.对前端图像传感器输出的信号做后期处理.如AWB等.DSP是什么Di ...

  5. 用FineBI实现hive图表的可视化

    图表的可视化,本来我以为很麻烦,因为看着图就感觉很难的样子,其实用FineBI来做很简单. 1.安装FineBI 2将下列jar包导入FineBI,webapps\webroot\WEB-INF\li ...

  6. const char* str和const char str[]的区别

    首先,字符串常量是存储在flash中的.假设字符串常量在flash中的地址是0x8003fb8. 第一种方式,str等价于str的内存单元的地址,str的内存单元存储着字符串常量的地址 第二种方式,s ...

  7. Goreplay流量回放-使用真实流量测试

    介绍: 1.goreplay是一个开源的网络监控工具,它可以记录你的实时流量,并用于跟踪.负载测试.监控和详细分析. 2.goreplay 是一款从生产环境 copy 流量到测试环境的工具,且不会影响 ...

  8. 读后笔记 -- Java核心技术(第11版 卷 II) Chapter3 XML

    3.1 XML Introduction An XML document is made up of elements. An element can have attributes (key/val ...

  9. twenty two

    打包处理less文件 1,运行npm i less-loader@10.0.1 less@4.1.1 -D 2, 在webpack.config.js的module->rules数组中 rule ...

  10. 記錄一下oracle数据库系统的comment數據字典(轉載)

    一.comment是oracle数据库系统的关键字,所以不能用来命名表明或字段名 二.comment关键字用于对表,字段添加注释,有利于用户对表结构以及数据库对象含义的理解. 三.用户可以通过数据字典 ...