在开发过程中,时常遇到需要前后端实时通讯的需求。比如客户和客服的在线沟通。而通常会有使用轮询的方式,每隔几秒钟发送一次HTTP请求,来检查客户或客服有没有发送新的消息。这是因为HTTP请求的惰性特点,只能由客户端发起请求,而不能由服务端主动推送消息给客户端。由此,可以考虑使用WebSocket技术建立双端都可主动推送消息的长连接来解决类似场景的问题。

        本次讲述的是Spring+SpringMVC框架下开放WebSocket的方式。
        开放WebSocket接口可以采用以下步骤:

1、配置WebSocket接口信息。

        2、自定义WebSocket消息处理实现。

        3、自定义WebSocket消息拦截器(可选)。

        4、将WebSocket配置实例装入Spring容器。可选用扫描方式或bean配置方式。此示例中采用扫描方式。



     1、配置WebSocket接口信息。

        1)首先创建websocket专属的package(规范问题,实现某一功能的源码应放到统一路径下)。

        2)创建WebSocketConfig类,继承WebMvcConfigurationSupport类,实现WebSocketConfigurer接口并实现其定义的方法。添加类注解

        @Configuration
        @EnableWebMvc
        @EnableWebSocket

        3)实现的registerWebSocketHandlers方法中,使用参数WebSocketHandlerRegistry的对象开放WebSocket接口。

        registry.addHandler(websocket消息处理实现类实例,websocket地址).setAllowedOrigins("*").addInterceptors(拦截器实例);

        其中setAllowedOrigins为设置访问权限,通常不限制访问时使用“*”;addInterceptors为添加拦截器,如果不需要拦截器则可不调用此方法。

        通常开放WebSocket接口开放两个,因浏览器支持的问题,前端只能使用SockJS插件实现WebSocket连接,所以我们还要开放一个专为SockJS服务的WebSocket接口,只需调用withSockJS接口即可。

        registry.addHandler(websocket消息处理实现类实例,websocket地址).setAllowedOrigins("*").addInterceptors(拦截器实例).withSockJS();

        至此,第一步,配置WebSocket信息已完成。



    2、自定义WebSocket消息处理实现。

      1)创建WebSocket消息处理接口,继承WebSocketHandler接口(为开放操作接口给其他服务调用,一般与业务相关,服务端希望推送消息时调用此接口,同样关乎规范,亦可使用其他方式实现)。

      2)创建WebSocket消息处理实现类,实现第一步创建的接口。其中,业务接口请自行实现,从WebSocketHandler继承实现的接口有:

        afterConnectionEstablished:建立WebSocket连接成功时触发。

        handleMessage:收到客户端发送的消息时触发。

        handleTransportError:发生异常时触发。

        afterConnectionClosed:与客户端断开连接时触发。

        supportsPartialMessages:是否支持部分消息处理(一般返回false,此标志一般用来拆分大型或未知大小的WebSocket消息,当支持部分消息时,一个消息会拆分成多个消息调用handleMessage,可以利用WebSocketMessage的isLast方法判断是否最后一次消息)。



    3、自定义WebSocket消息拦截器(可选)

    1)创建WebSocket拦截器,实现HandshakeInterceptor接口。实现方法有:

    beforeHandshake:消息处理前。

    afterHandshake:消息处理后。



    4、将WebSocket配置实例装入Spring容器

    此处使用配置扫描包的形式,在配置文件中添加<context:component-scan base-package="com.???.???.???.websocket" />

    其中base-package可以根据自己项目的实际包路径配置。

    下面就是实例代码,细节处可根据实际项目修改:

  •     WebSocket配置信息
package com.???.???.???.websocket;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

/**
 * websocket配置类
 * 
 * @author Heller.Zhang
 * @since 2018年11月8日 上午10:23:04
 */
@Configuration
@EnableWebMvc
@EnableWebSocket
public class MyWebSocketConfig extends WebMvcConfigurationSupport implements WebSocketConfigurer {

    private Logger logger = LoggerFactory.getLogger(MyWebSocketConfig.class);

    /* (non-Javadoc)
     * @see org.springframework.web.socket.config.annotation.WebSocketConfigurer#registerWebSocketHandlers(org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry)
     */
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        logger.debug("----------开始注册MyWebSocketConfig----------");
        logger.debug("----------注册普通websocket----------");
        registry.addHandler(msgSocketHandle(), "/websocket/myWebSocketServer").setAllowedOrigins("*");

        logger.debug("----------注册sockjs/websocket----------");
        registry.addHandler(msgSocketHandle(), "/websocket/sockjs/myWebSocketServer").setAllowedOrigins("*").withSockJS();

    }

    @Bean(name = "myMsgScoketHandle", autowire = Autowire.BY_NAME)
    public MyMsgScoketHandle msgSocketHandle() {
        return new MyMsgScoketHandleImpl();
    }

}
  •     自定义WebSocket消息处理接口
package com.???.???.???.websocket;

import org.springframework.web.socket.WebSocketHandler;

/**
 * websocket消息处理器
 * 
 * @author Heller.Zhang
 * @since 2018年11月13日 下午5:13:20
 */
public interface MyMsgScoketHandle extends WebSocketHandler {

    public void sendMessage(Long userId, Integer num);
}
  • 自定义WebSocket消息处理接口实现
package com.???.???.???.websocket;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;

import com.hzsparrow.framework.common.exception.ServiceException;
import com.hzsparrow.framework.utils.JsonUtils;
import com.hzsparrow.framework.utils.NumberUtils;

/**
 * websocket消息处理类
 * 
 * @author Heller.Zhang
 * @since 2018年11月8日 上午11:15:22
 */
public class MyMsgScoketHandleImpl implements MyMsgScoketHandle {

    private Logger logger = LoggerFactory.getLogger(MyMsgScoketHandleImpl.class);

    private ConcurrentMap<String, List<WebSocketSession>> webSocketMap = new ConcurrentHashMap<>();

    public MyMsgScoketHandleImpl() {
        logger.debug("--------init MyMsgScoketHandle-----------");
    }

    /* (non-Javadoc)
     * @see org.springframework.web.socket.WebSocketHandler#afterConnectionEstablished(org.springframework.web.socket.WebSocketSession)
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        logger.debug("-----------建立websocket连接---------");
    }

    /* (non-Javadoc)
     * @see org.springframework.web.socket.WebSocketHandler#handleMessage(org.springframework.web.socket.WebSocketSession, org.springframework.web.socket.WebSocketMessage)
     */
    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        logger.debug("-----------处理消息---------");
        TextMessage msg = (TextMessage) message;

        //建立连接发送的用户id
        String msgStr= msg.getPayload();
        logger.debug("----------msg:" + msgStr);
        if (StringUtils.equals(msgStr, "heartbeat")) {
            // 心跳消息,不予处理
            return;
        }
        session.getAttributes().put("mykey", msgStr + "|" + UUID.randomUUID().toString());
        //通过用户id获取session集合
        List<WebSocketSession> list = webSocketMap.get(msgStr);
        //判断集合是否为空
        if (CollectionUtils.isNotEmpty(list)) {
            list.add(session);
        } else {
            list = new ArrayList<WebSocketSession>();
            list.add(session);
        }

        //将用户id和session集合绑定到map
        webSocketMap.put(msgStr, list);
        logger.debug("消息处理完毕");

    }

    /* (non-Javadoc)
     * @see org.springframework.web.socket.WebSocketHandler#handleTransportError(org.springframework.web.socket.WebSocketSession, java.lang.Throwable)
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        logger.debug("-----------处理异常---------");
    }

    /* (non-Javadoc)
     * @see org.springframework.web.socket.WebSocketHandler#afterConnectionClosed(org.springframework.web.socket.WebSocketSession, org.springframework.web.socket.CloseStatus)
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        logger.debug("-----------断开连接---------");
        String myKey = session.getAttributes().get("mykey").toString();
        String userId = myKey.split("|")[0];
        List<WebSocketSession> list = webSocketMap.get(userId);
        WebSocketSession delSession = null;
        if (CollectionUtils.isNotEmpty(list)) {
            for (WebSocketSession webSocketSession : list) {
                String delKey = webSocketSession.getAttributes().get("mykey").toString();
                if (StringUtils.equals(myKey, delKey)) {
                    delSession = webSocketSession;
                    break;
                }
            }
        }
        if (delSession != null) {
            list.remove(delSession);
        }

    }

    /* (non-Javadoc)
     * @see org.springframework.web.socket.WebSocketHandler#supportsPartialMessages()
     */
    @Override
    public boolean supportsPartialMessages() {
        return false;
    }

    /**
     * 给单个用户发送消息
     * 
     * @param session
     * @param type
     * @param data
     * @throws IOException
     * @author Heller.Zhang
     * @since 2018年11月8日 下午2:05:32
     */
    public void sendMessage(WebSocketSession session, Object data) throws IOException {
        if (session.isOpen()) {
            String json = JsonUtils.serialize(data);
            TextMessage outMsg = new TextMessage(json);
            session.sendMessage(outMsg);
        }
    }

    /**
     * 发送链接成功消息
     * 
     * @param session
     * @throws IOException
     * @author Heller.Zhang
     * @since 2018年11月10日 下午3:16:35
     */
    public void sendSuccessMsg(WebSocketSession session, Integer type) throws IOException {
        logger.debug("---------发送链接成功消息-----------");
        TextMessage outMsg = new TextMessage("");
        session.sendMessage(outMsg);
    }

    @Override
    public void sendMessage(Long userId, Integer num) {
        //获取用户下的session集合
        List<WebSocketSession> list = webSocketMap.get(NumberUtils.num2Str(userId));
        //向每一条session中发送数据
        if (CollectionUtils.isNotEmpty(list)) {
            for (WebSocketSession webSocketSession : list) {
                try {
                    sendMessage(webSocketSession, num);
                } catch (IOException e) {
                    throw new ServiceException("发送消息失败");
                }
            }
        }

    }

}
  • 拦截器
package com.???.???.???.websocket;

import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;

/**
 * websocket拦截器
 * 
 * @author Heller.Zhang
 * @since 2018年11月8日 上午10:40:51
 */
public class MyWebSocketHandshakeInterceptor implements HandshakeInterceptor {

    private Logger logger = LoggerFactory.getLogger(MyWebSocketHandshakeInterceptor.class);

    /* (non-Javadoc)
     * @see org.springframework.web.socket.server.HandshakeInterceptor#beforeHandshake(org.springframework.http.server.ServerHttpRequest, org.springframework.http.server.ServerHttpResponse, org.springframework.web.socket.WebSocketHandler, java.util.Map)
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
            Map<String, Object> attributes) throws Exception {
        logger.debug("---------websocket 拦截器  握手前------------");
        return true;
    }

    /* (non-Javadoc)
     * @see org.springframework.web.socket.server.HandshakeInterceptor#afterHandshake(org.springframework.http.server.ServerHttpRequest, org.springframework.http.server.ServerHttpResponse, org.springframework.web.socket.WebSocketHandler, java.lang.Exception)
     */
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
            Exception exception) {
        logger.debug("---------websocket 拦截器  握手后------------");
    }

}
  • 扫描包配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context" 
    xmlns:task="http://www.springframework.org/schema/task" 
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd">

    <context:component-scan base-package="com.???.???.???.websocket" />
    
</beans>