Spring boot WebSocket 学习总结

一、什么是WebSocket

      WebSocket是一种在单个TCP连接上进行全双工通信的协议。它最初于2008年被提出,后来由IETF标准化。WebSocket协议旨在解决HTTP协议的一些限制,例如HTTP请求只能由客户端发起,服务器不能主动向客户端发送数据等。

1.产生背景

       早期,很多网站为了实现推送技术,所用的技术都是轮询。轮询是指由浏览器每隔一段时间向服务器发出HTTP请求,然后服务器返回最新的数据给客户端。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求与回复可能会包含较长的头部,其中真正有效的数据可能只是很小的一部分,所以这样会消耗很多带宽资源。

       在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

2.WebSocket与HTTP轮询的异同

         

Spring boot WebSocket 学习总结_第1张图片

        如上图所示,从上图看出可以,WebSocket和HTTP轮询本质上都依赖于TCP的握手,他们二者都是应用层的协议,事实上,WebSocket和HTTP的工作端口都是80和443,WebSocket可以使用HTTP代理和中介,兼容HTTP协议。

        不同的是HTTP轮询获取信息每次都需要客户端向服务端发送请求建立连接,而WebScoket经过第一次建立连接后,连接就被持久化下来, 不需要重复建立连接,而且可以由服务端主动向客户端发送信息。

二、WebScoket的优缺点

WebSocket的优点包括:

  • 1. 实时性:WebSocket可以实现实时通信,服务器可以主动向客户端推送数据,而不需要客户端发起请求。
  • 2. 减少网络流量:WebSocket使用单个TCP连接,减少了网络流量和延迟。
  • 3. 更少的延迟:WebSocket使用二进制协议,减少了数据传输的开销,从而减少了延迟。
  • 4. 更好的跨域支持:WebSocket协议支持跨域通信,可以在不同的域名之间进行通信

WebSocket的缺点包括:

  • 1. 兼容性问题:WebSocket协议在一些旧的浏览器中不被支持,需要使用polyfill或者其他技术来解决兼容性问题。
  • 2. 安全问题:WebSocket协议需要使用SSL/TLS协议来保证通信的安全性,否则可能会被中间人攻击。
  • 3. 服务器资源占用:WebSocket协议需要服务器一直保持连接,会占用一定的服务器资源。

三、Spring boot集成WebSocket

    下面用一个简单的示例,展示Spring boot如何集成WebSocket,然后实现简单的建立连接后服务器向客户端发送信息并向其他在线用户广播。

步骤1.导入WebSocket依赖 

        新建Spring boot项目,在pom文件加入以下代码


    org.springframework.boot
    spring-boot-starter-web



    org.springframework.boot
    spring-boot-starter-websocket



    org.projectlombok
    lombok
true



    com.alibaba
    fastjson
    1.2.83

步骤2.编写拦截器、处理器、配置类

       创建如下文件夹:

Spring boot WebSocket 学习总结_第2张图片

       在interceptor文件家新建WebSocketInterceptor类文件,代码如下:

@Component
public class WebSocketInterceptor extends HttpSessionHandshakeInterceptor {

    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
         return super.beforeHandshake(request, response, wsHandler, attributes);
    }
}

        在handler文件夹中新建WebSocketHandler类文件

@Component
public class WebSocketHandler extends TextWebSocketHandler implements InitializingBean {


    @Autowired
    private ApplicationContext applicationContext;

    @Override
    public void afterPropertiesSet() {
       
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) {
        super.handleTextMessage(session,message);
    }

    @Override
    protected void handlePongMessage(WebSocketSession session, PongMessage message) {
        super.handlePongMessage(session, message);
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) {
        super.handleTransportError(session, exception);
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        super.afterConnectionClosed(session, status);
    }

    @Override
    public boolean supportsPartialMessages() {
        return super.supportsPartialMessages();
    }

}

        在config文件夹新建WebSocketConfig类文件

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private WebSocketHandler webSocketHandler;

    @Autowired
    private WebSocketInterceptor webSocketInterceptor;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        //配置拦截器、处理器,设置跨域
        registry.addHandler(webSocketHandler, "/")
                .addInterceptors(webSocketInterceptor)
                .setAllowedOrigins("*");
    }


}

        到这里,启动Spring boot项目,网站通过ws命令就可以连接到WebSocket。不过为了展示WebSocket的通信功能,我们要先实现WebSocket的信息处理相关机制。

步骤3.信息处理相关代码编写

        在message文件夹定义Message接口类:

public interface Message {

    String getType();
}

         为了实现建立连接后服务端向客户端发送消息,我们需要实现一个连接认证的请求消息,一个请求的回复消息。

@Data
public class AuthRequestMessage implements Message{

    public static final String TYPE = "AUTH_REQUEST";

    private String token;

    @Override
    public String getType() {
        return TYPE;
    }
}
@Data
public class AuthResponseMessage implements Message{

    public static final String TYPE = "AUTH_RESPONSE";

    private Integer code;

    private String data;

    @Override
    public String getType() {
        return TYPE;
    }
}

        创建以上消息后,在handler文件夹中创建MessageHandler接口类:

public interface MessageHandler {

    void execute(WebSocketSession webSocketSession, T message);

    String getType();
}

        然后在utils文件夹创建WebSocketUtils工具类用来管理WebSocket消息的发送。

@Component
public class WebSocketUtils {

    private static final Map USER_SESSION_MAP = new ConcurrentHashMap<>();

    private static final Map SESSION_USER_MAP = new ConcurrentHashMap<>();

    public static void addSession(WebSocketSession session, String user) {
        USER_SESSION_MAP.put(user, session);
        SESSION_USER_MAP.put(session, user);
    }

    public static void removeSession(WebSocketSession session) {
        String user = SESSION_USER_MAP.remove(session);
        if (StringUtils.isNotEmpty(user)) {
            USER_SESSION_MAP.remove(user);
        }
    }


    public static  void send(WebSocketSession session, String type, T message) throws JSONException, IOException {
        String messageText = buildTextMessage(type, message);
        sendTextMessage(session, messageText);
    }

    public static  boolean send(String user, String type, T message) throws JSONException, IOException {
        WebSocketSession session = USER_SESSION_MAP.get(user);
        if (session == null) {
            return false;
        }
        send(session, type, message);
        return true;
    }


    private static  String buildTextMessage(String type, T message) throws JSONException {
        JSONObject messageObject = new JSONObject();
        messageObject.put("type", type);
        messageObject.put("body", message);
        return messageObject.toString();
    }

    private static void sendTextMessage(WebSocketSession session, String messageText) throws IOException {
        if (session == null) {
            return;
        }
        session.sendMessage(new TextMessage(messageText));
    }

}

         在handler文件夹创建连接认证消息的处理类。

@Component
public class AuthRequestMessageHandler implements MessageHandler {

    @Override
    public void execute(WebSocketSession webSocketSession, AuthRequestMessage message) {
        try {
            if (StringUtils.isEmpty(message.getToken())) {
                AuthResponseMessage authResponseMessage = new AuthResponseMessage();
                authResponseMessage.setCode(1);
                authResponseMessage.setData("token未传入");
                WebSocketUtils.send(webSocketSession, AuthResponseMessage.TYPE, authResponseMessage);
                return;
            }
            WebSocketUtils.addSession(webSocketSession, message.getToken());
            AuthResponseMessage authResponseMessage = new AuthResponseMessage();
            authResponseMessage.setCode(0);
            authResponseMessage.setData("登录成功");
            WebSocketUtils.send(webSocketSession, AuthResponseMessage.TYPE, authResponseMessage);
        } catch (JSONException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String getType() {
        return AuthRequestMessage.TYPE;
    }
}

        从上面的代码可以看到有个token的概念,那么这个是从哪里获取的呢?答案是在前面的WebSocketInterceptor和WebSocketHandler中,修改如下:

WebSocketInterceptor:

    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map attributes) throws Exception {
        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest servletServerHttpRequest = (ServletServerHttpRequest) request;
            attributes.put("token", servletServerHttpRequest.getServletRequest().getParameter("token"));
        }
        return super.beforeHandshake(request, response, wsHandler, attributes);
    }

WebSocketHandler:

    private final Map HANDLERS = new HashMap<>();

    private final Map> MESSAGE_CLASS = new HashMap<>(); 

    @Override
    public void afterPropertiesSet() {
        applicationContext.getBeansOfType(MessageHandler.class).values()
                .forEach(messageHandler -> HANDLERS.put(messageHandler.getType(), messageHandler));
        applicationContext.getBeansOfType(Message.class).values()
                .forEach(message -> MESSAGE_CLASS.put(message.getType(), message.getClass()));
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        String token = (String) session.getAttributes().get("token");
        AuthRequestMessage authRequestMessage = new AuthRequestMessage();
        authRequestMessage.setToken(token);
        MessageHandler messageHandler = HANDLERS.get(AuthRequestMessage.TYPE);
        if (messageHandler == null) {
            return;
        }
        messageHandler.execute(session, authRequestMessage);
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        try {
            JSONObject jsonMessage = JSON.parseObject(message.getPayload());
            String messageType = jsonMessage.getString("type");
            MessageHandler messageHandler = HANDLERS.get(messageType);
            if (messageHandler == null) {
                return;
            }
            Class messageClass = MESSAGE_CLASS.get(messageType);
            if (messageClass == null) {
                return;
            }
            Message messageObj = JSON.parseObject(jsonMessage.getString("body"), messageClass);
            messageHandler.execute(session, messageObj);
        } catch (Exception ignored) {

        }
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        WebSocketUtils.removeSession(session);
    }

      这样建立连接服务端向客户端回复的机制就实现了,我们还要实现一个建立连接通知所有在线用户的功能,有了前面的铺垫,这个功能实现很简单。

     在message文件夹下创建UserLoginNoticeMessage:

@Data
public class UserLoginNoticeMessage implements Message{

    public static final String TYPE = "USER_LOGIN_NOTICE";

    private String nickname;

    @Override
    public String getType() {
        return TYPE;
    }
}

       修改WebSocketUtils,增加broadcast方法

    public static  void broadcast(String type, T message) throws IOException, JSONException {
        String messageText = buildTextMessage(type, message);
        for (WebSocketSession session : SESSION_USER_MAP.keySet()) {
            sendTextMessage(session, messageText);
        }
    }

       修改AuthRequestMessageHandler的excute方法:

    @Override
    public void execute(WebSocketSession webSocketSession, AuthRequestMessage message) {
        try {
            if (StringUtils.isEmpty(message.getToken())) {
                AuthResponseMessage authResponseMessage = new AuthResponseMessage();
                authResponseMessage.setCode(1);
                authResponseMessage.setData("token未传入");
                WebSocketUtils.send(webSocketSession, AuthResponseMessage.TYPE, authResponseMessage);
                return;
            }
            WebSocketUtils.addSession(webSocketSession, message.getToken());
            AuthResponseMessage authResponseMessage = new AuthResponseMessage();
            authResponseMessage.setCode(0);
            authResponseMessage.setData("登录成功");
            WebSocketUtils.send(webSocketSession, AuthResponseMessage.TYPE, authResponseMessage);
            UserLoginNoticeMessage userLoginNoticeMessage = new UserLoginNoticeMessage();
            userLoginNoticeMessage.setNickname(message.getToken());
            WebSocketUtils.broadcast(UserLoginNoticeMessage.TYPE, userLoginNoticeMessage);
        } catch (JSONException | IOException e) {
            throw new RuntimeException(e);
        }
    }

步骤4.测试 

     为了方便测试,我们直接选择用在线的WebSocket测试工具。

                                                 WebSocket在线测试工具

      启动服务,然后同时启动三个测试工具,用以下代码开启链接:

ws://127.0.0.1:8080/?token=1
ws://127.0.0.1:8080/?token=2
ws://127.0.0.1:8080/?token=3

 Spring boot WebSocket 学习总结_第3张图片

      开启链接,接收到信息如下:

Spring boot WebSocket 学习总结_第4张图片

Spring boot WebSocket 学习总结_第5张图片

Spring boot WebSocket 学习总结_第6张图片

     可以看到,每个WebSocket客户端链接成功都收到了服务端返回的信息,然后每次新客户端链接成功时,已存在的老客户端会收到新客户端登录的信息,说明我们实现了链接后返回信息并给其他在线用户广播的功能。

四、集成简化版

      首先定义一个websocketServer类

@Component
@ServerEndpoint("你自己想要定义的ws路径")
public class WebSocketServer {



}

      在此类中分别添加自定义方法然后分别打上@OnOpen、@OnClose、@OnMessage、@OnError注解

/**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session){

    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose(Session session){

    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     * @param session 可选的参数
     */
    @OnMessage
    public void onMessage(String message, Session session) {


    }

    /**
     * 发生错误时调用
     *
     * @param session 可选的参数
     * @param error   错误
     */
    @OnError
    public void onError(Session session, Throwable error) {

    }

        最后定义config类

@Configuration
@EnableWebSocket
public class WebSocketConfig {

    @Bean
    public ServerEndpointExporter serverEndpoint(){
        return new ServerEndpointExporter();
    }
}

你可能感兴趣的:(spring,boot,websocket)