spring websocket开发实践

来源:互联网 发布:php命令行死循环 编辑:程序博客网 时间:2024/05/16 09:38
  1. Websocket、sockjs、stomp简介

    1. websocket

WebSocket protocol 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duple)。一开始的握手需要借助HTTP请求完成。

在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道,两者之间就直接可以数据互相传送。在此WebSocket 协议中,为我们实现即时服务带来了两大好处:

1. Header

互相沟通的Header是很小的,大概只有 2 Bytes

2. Server Push

服务器的推送,服务器不再被动的接收到浏览器的request之后才返回数据,而是在有新数据时就主动推送给浏览器。

  1. sockJs

SockJS 是一个浏览器上运行的 JavaScript 库,如果浏览器不支持 WebSocket,该库可以模拟对 WebSocket 的支持,实现浏览器和 Web 服务器之间低延迟、全双工、跨域的通讯通道。

  1. stomp

STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。

STOMP的客户端和服务器之间的通信是通过"帧"(Frame)实现的,每个帧由多"行"(Line)组成。
第一行包含了命令,然后紧跟键值对形式的Header内容。
第二行必须是空行。
第三行开始就是Body内容,末尾都以空字符结尾。

下面是一个STOMP消息帧样例:

  1. spring websocket

Spring4.0版本的发布,提供了对Websocket的支持,它不仅在基于JSR-356容器之上提供Websocket API,而且也为那些不支持或者不允许使用Websocket的浏览器和网络提供了一些候选项。更重要的是,它为在网络应用中构建Websocket形式的消息架构提供了基础。

spring websocket使用SockJS protocol作为候选项,解决了部分浏览器不兼容websocket的问题,能够提供最好和最广泛的数据传输方式,此外还提供了stomp协议的支持。spring支持使用Controller,同时处理Http请求以及websocket消息,而且非常容易地将消息广播给感兴趣的websocket客户端,或者特定的客户端。

  1. 开发实战

在使用spring websocket开发应用程序时,最简便的方法就是继承TextWebSocketHandler,但是需要自行维护WebSocketSession,并且需要对数据进行解析,此外,由于websocket发送的消息类型很多,因此还需要写很多的判断语句,不利用后续产品的更新迭代,下方是部分示例代码:

鉴于以上原因,在互动中心触屏版需求开发中,采用了Spring Websocket+SockJs+Stomp技术框架,支持发布/订阅、点对点模式,在服务器、访客端之间实现了双向通信,具有以下特点:

1)支持SpringMVC,和Controller开发类似,简单容易上手;

2)针对无法使用websocket的浏览器,支持自动切换至SSE或者轮询方式;

3)支持消息中间件;

4)在实际的业务场景中,可能还需要扩展Spring websocket的功能。

  1. websocket配置

websocket的配置,支持xml,同时也支持编码的方式,后者较为灵活方便,因此项目中采用了后者。在WebSocketBrokerConfig里面,主要是设置了消息代理、Stomp端点,以及Transport,详见下方文件:

  1. Controller开发

Spring message模块为我们提供了很多实用的注解,方便我们使用,请参照官方文档:

http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-handle-annotations

常用注解:

@Controller:标识这个类被spring管理,与SpringMVC相同;

@MessageMapping:标识websocket消息接收的映射方法;

@SendToUser:点对点模式,将消息发给指定的客户端;

@SubscribeMapping:订阅地址的映射方法

  1. 服务端主动推送消息

websocket是支持双向通讯的,在很多业务场景下,需要服务端主动地往触屏版客户端推送消息,spring websocket提供了发布订阅、点对点模式,实现数据交互。发布订阅模式,url地址以/topic开头,而点对点模式,url地址是以/queue开头。此外,spring还提供了@SendToUser注解,以及SimpMessagingTemplate,简化编码。

以下是触屏版访客端的订阅地址:

  1. /queue/chat/newMsg:订阅新消息
  2. /queue/chat/msgResponse:订阅消息响应
  3. /queue/system/close:订阅会话关闭通知
  4. /queue/system/newMsg:订阅系统新消息

 

  1. 推送订阅消息

在访客和客服建立好会话之后,就会创建websocket连接,并且订阅以上地址,同时访客会收到类似欢迎之类的消息。在这种场景下,需要在请求订阅地址的时候,返回该消息,编码如下。其中SubscribeMapping是订阅的映射注解,客户端发起订阅时会调用该方法,并返回消息实体。但是,值得注意的是,需要在MessageBrokerRegistry中将"/user"路径添加到应用访问路径前缀中,不然在Contronller中是接收不到对应的订阅请求的,因为客户端发送过来的地址是以/user开头的。

  1. 推送客服消息

下图是客服-访客的对话时序图,客服发送的消息到达dubbo之后,从当前的ChatSession中获取对应的websocket信息(包括tomcat节点),接着http异步通知对应的tomcat节点,然后调用Spring提供的消息模板SimpMessageTemplate的convertAndSend方法,使用点对点的模式,将消息推送给对应的websocket客户端,这样访客端便可以收到客服发送的消息,相比传统的ajax轮询模式,实时性得以大幅度提高。

  1. 推送确认消息

访客发送消息之后,需要经过Tomcat和Spring线程池才会被Controller接收处理,然后再返回一个WebSocketMessageResponse,告知客户端发送成功或者失败,代码如下:

其中,@MessageMapping是消息接收的映射地址,@SendToUser是将数据推送给该websocket。这一块的逻辑,都是在spring中处理的。

  1. Spring websocket扩展

    1. 自定义WebSocketHandler

先来看看WebscoketHandler的类图,由图中可知,这个接口提供了afterConnectionEstablished、afterConnectionClosed等API方法,虽然spring提供了SessionConnectedEvent、SessionDisconnectEvent事件,但是只能获取websocketId,如果我们想添加websocket连接的监听器,只需要扩展WebscoketHandler即可。

要扩展WebsocketHandler需要借助DecoratoryFactory,如下图所示:

WebSocketBrokerConfig.java

  1. 自定义拦截器

    1. HandshakeInterceptor

在实际开发过程中,往往需要关联http中的信息,或者是限制websocket的连接,这个时候就需要在websocket握手阶段进行处理,在Spring里面只需要实现HandshakeInterceptor接口,并且将其实现类注册到StompEndpointRegistry即可。如下图所示,HttpSessionHandshakeInterceptor是spring提供的实现类,它将HttpSession中的信息,保存至WebSocketSession中方便后续读取。ChatShakeHandlerInterceptor是项目中另行开发的,主要目的是限制websocket连接,不允许未登录、未建立会话的用户进行连接。

 

如下图所示,将ChatShakeHandlerInterceptor注册到StompEndpointRegistry中:

  1. ChannelInterceptor

使用ChannelInterceptor,可以对接收、发送的前后进行逻辑处理,在某些场景下,可以改变消息的内容,以及拦截消息,例如spring session中为了维护HttpSession的有效性便在preSend接口中更新了时间戳。

首先,实现ChannelInterceptor接口,或者继承ChannelInterceptorAdapter,然后将实现注册到ChannelRegistration即可,例如:channelRegistration.setInterceptors( channelInterceptor );

  1. 自定义事件

spring为我们提供了很多事件,比如SessionConnectEvent、SessionConnectedEvent、SessionDisconnectEvent、SessionSubscribeEvent、SessionUnsubscribeEvent,我们只需要实现ApplicationListener <E extends ApplicationEvent>接口,并且注册到spring容器中即可,如下图所示:

但是,在项目开发过程中,需要获取到WebSocketSession对象,而spring并没有定义这样的Event,只能自定义事件了。

首先,实现ApplicationEvent接口,如下所示:

然后,实现ApplicationListener<WebsocketConnectedEvent>接口,并且将其注册到spring容器中;

最后,在合适的地方触发这个事件即可,核心代码如下:

  1. 前端js开发

需要在页面上引用sockjs-1.1.1.js、stomp.js,另外,需要根据服务端提供的地址,进行连接、订阅即可。

  1. 项目实践问题

    1. 如何维护Spring WebsocketSession

首先,抽象WebSocketSessionManager接口,然后实现该接口,内部实现使用ConcurrentHashMap保存WebsocketSession,同时为了避免异常关闭WebsocketSession而SessionManager无法感知的情况,在getSession的时候返回了WebsocketSession的装饰类,具体如附件所示:

然后,在合适的地方,将WebsocketSession添加到这个容器中管理即可,项目中是这样做的:继承WebSocketHandlerDecoratorFactory,并重写decorate方法,返回自定义的ChatWebSocketHandler,这样一来,可以在合适的时机将WebsocketSession从容器中添加/移除即可。

  1. WebsocketSession监听器

在实际应用场景中,往往需要对Websocket的连接、断开事件进行监听,同样还是在ChatWebSocketHandler里面进行操作,在其afterConnectionEstablished、afterConnectionClosed方法中发布对应的事件即可。下方是核心代码:

  1. 维持HttpSession有效性

在websocket对话过程中,可能长时间无HTTP请求,这样一来就会导致HttpSession失效,websocket会话也会断开。所以,在websocket对话中需要更新HttpSession时间戳,避免会话失效。要解决这个问题,要用到spring提供的拦截器ChannelInterceptor

其实,spring session提供了解决方案,一方面需要在项目中使用spring session,另外一方面,需要在配置websocket的时候,继承AbstractSessionWebSocketMessageBrokerConfigurer<ExpiringSession>即可。如果项目中未使用spring session,只能另行编码,使用ChannelInterceptor拦截器,在接收到websocket消息之后,更新对应的HttpSession的时间戳。

AbstractSessionWebSocketMessageBrokerConfigurer.java

SessionRepositoryMessageInterceptor.java

  1. 访问不到Controller

在TransportHandlingSockJsService的构造方法里面有对messageCodec初始化的代码,如果没有jackson的jar包,则很有可能导致messageCodec未被初始化,使用stomp协议发送的消息则无法被正确解析,因此不会被Controller接收处理。

TransportHandlingSockJsService.java

  1. 分布式支持,nginx、openrestly

nginx对websocket的支持比较简单,只需要在nginx.conf中配置下即可,但是openrestly还需要lua脚本的支持。另外,websocket是TCP层协议,如果项目中用到了硬负载,可能还需要额外设置。

  1. 性能调优

    1. spring websocket线程池

适当提高InboundChannel、OutboundChannel的线程池大小,对于提升spring websocket的消息处理能力有非常大的帮助,默认的线程池大小是cpu核数*2。下列表格是压测中的一组数据,通过响应时间的对比,便知线程池的大小的优化效果。

测试业务

线程池

并发数

TPS域值

平均异步响应时间ms

发送消息

100/100

1000

1500

3869

300/200

1000

1500

87

下图是设置线程池大小的代码:

  1. 接入消息中间件

简单消息代理流程图:

使用消息中间件的流程图:

在SimpleBrokerMessageHandler中有一个致命的弱点,就是使用了DefaultSubscriptionRegistry,而在这个里面又使用了DestinationCache,这是一个基于LinkedHashMap实现的LRU缓存策略,如果缓存大小超过了cacheLimit值,则会去将原有旧的数据清理掉,并且再次读取的时候仍然会去读取所有的订阅地址直到找到符合条件的,涉及到写的部分,都加了synchronized重量级锁,所以在压测的过程中这一块的线程BLOCK也是相对较频繁的,程序的并发性能下降。目前,项目中还未使用消息中间件,后续如果接入消息中间件,对应用系统的性能将会有很大提高。

优化思路有以下3种:

1)、增大cacheLimit值,减少频繁遍历所有的订阅地址

2)、使用spring的BeanPostProcessor,改变SimpleBrokerMessageHandler的subscriptionRegistry实现类;

3)、使用消息中间件,spring会将订阅操作交给消息中间件处理,这样便可以很好的解决这个问题

 

  1. 参考资料:

1、http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html

2、http://blog.csdn.net/icode0410/article/details/39496823

原创粉丝点击