前情提要

之前我的项目里有一个合作编辑的功能,多个客户端的用户可以合作写一篇文章的不同部分,而且合作的任意作者互相还可以进行文字通讯。这种需求肯定是首选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. Golang(vs code) 调用其他自定义包解决方法

    初学Golang,昨天遇到一个问题:调用其他自定义包,会出现Gopath或者Goroot路径下找不到. 举个例子: 如图Gee文件夹下的三个.go 文件就是我main.go想调用的. 我有尝试过这种方 ...

  2. Nginx结合tomcat 负载均衡

    负载均衡是我们大流量网站要做的一个东西,下面我来给大家介绍在Nginx服务器上进行负载均衡配置方法,希望对有需要的同学有所帮助哦. 负载均衡 先来简单了解一下什么是负载均衡,单从字面上的意思来理解就可 ...

  3. DC约束笔记

    关于get_pins get_cells get_ports等的定义 关于建立保持与max/min的关系 2.setup time和hold time 建立时间(setup time)是指在触发器的时 ...

  4. DDL数据定义--Hive中数据可和表的基本操作(增删改查)

    数据库 创建数据库: CREATE DATABASE [ IF NOT EXISTS ]  database_name *****IF NOT EXISTS进行判断,避免创建的数据库已经存在的错误 创 ...

  5. Object.create() 方浅析

    Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__. Object.create(proto[, propertiesObject]) 参数 pro ...

  6. ctfshow web入门 命令执行 web29-36

    29-36 全是基于get传参执行 eval() 函数,均采用黑名单匹配,不同点在于黑名单的变化 web29 1 error_reporting(0); 2 if(isset($_GET['c'])) ...

  7. AUTOCAD——半径标注命令

    创建圆或圆弧的半径标注. 执行方式 命令行:DIMRADLUS 菜单栏:标注→半径标注 工具栏:半径标注图标 "半径标注操作命令位置"界面 执行以上命令后,命令行会显示以下信息: ...

  8. [Oracle19C 数据库管理] 加载和传输数据库

    移动数据的通用架构 数据泵data pump(impdp, expdp),借助DBMS_DATAPUMP存储过程,可以进行表的导出导入,行记录的导出导入,表空间的导出导入或者整个schema的导出导入 ...

  9. Virtualbox EFI 写入startup.nsh

    安装好的Debian系统,重启后会因为找不到入口而无法启动.定位grubx64.efi文件,可用ls直接查找或一步一步查找ls fs0:\EFI\debian\grubx64.efi edit fs0 ...

  10. JSONObject和JSONArray的区别

    JSONObject是用{}表示: JSONArray是用[]表示,相当于[{ }, { }],是由JSONObject的集合. 定义一个String类型 JSONObject jsonOject = ...