组件整合之websocket

WebSocket,干什么用的?我们有了HTTP,为什么还要用WebSocket?很多同学都会有这样的疑问。我们先来看一个场景,大家的手机里都有微信,在微信中,只要有新的消息,这个联系人的前面就会有一个红点,这个需求要怎么实现呢?最简单,最笨的方法就是客户端轮询,在微信的客户端每隔一段时间(比如:1s或者2s),向服务端发送一个请求,查询是否有新的消息,如果有消息就显示红点。这种方法是不是太笨了呢?每次都要客户端去发起请求,难道就不能从服务端发起请求吗?这样客户端不就省事了吗。再看看股票软件,每个股票的当前价格都是实时的,这我们怎么做,每个一秒请求后台查询当前股票的价格吗?这样效率也太低了吧,而且时效性也很低。这就需要我们今天的主角WebSocket去实现了。

什么是WebSocket

WebSocket协议,它是通过一个TCP连接,在客户端与服务端之间建立的一个全双工、双向的通信渠道。它是一个不同于HTTP的TCP协议,但是它通过HTTP工作。它的默认端口也是80和443,和HTTP是一样的。

一个WebSocket的交互开始于一个HTTP请求,这是一个握手请求,这个请求中包含一个Upgrade请求头,具体如下:

GET /spring-websocket-portfolio/portfolio HTTP/1.1
Host: localhost:8080
Upgrade: websocket 
Connection: Upgrade 
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080

我们看到的第3行和第4行就是这个特殊的请求头,既然包含了这个特殊的请求头,那么请求就要升级,升级成WebSocket请求。这个握手请求的响应也比较特殊,它的成功状态码是101,而不是HTTP的200,如下:

HTTP/1.1 101 Switching Protocols 
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp

在这次成功的握手请求以后,在客户端和服务端之间的socket被打开,客户端和服务端可以进行消息的发送和接收。

快速入门

Spring在4.0后将websocket集成进去,要使用Spring的websocket的话,spring的版本要在4.0及以上。

1、pom配置

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-websocket</artifactId>
  <version>5.0.2.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-messaging</artifactId>
  <version>5.0.2.RELEASE</version>
</dependency>

2、spring对websocket的注解配置

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration
@EnableWebSocket
public class WebSocketServerConfig implements WebSocketConfigurer {

    @Autowired
    private SocketHandler socketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        //注册处理拦截器,拦截url为websocket的请求
        registry.addHandler(socketHandler, "/websocket").addInterceptors(new WebSocketInterceptor()).setAllowedOrigins("*");

        //注册SockJs的处理拦截器,拦截url为/sockjs/websocket的请求
        registry.addHandler(socketHandler, "/sockjs/websocket").addInterceptors(new WebSocketInterceptor()).setAllowedOrigins("*").withSockJS();
    }

}

3、握手拦截器,继承HttpSessionHandshakeInterceptor类,做一些连接握手或者握手后的一些处理

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

import javax.servlet.http.HttpSession;
import java.util.Map;
/**
 * @desp websocket拦截器
 *
 * HandshakeInterceptor
 * WebSocket握手请求的拦截器. 检查握手请求和响应, 对WebSocketHandler传递属性
 */
public class WebSocketInterceptor extends HttpSessionHandshakeInterceptor {

    // 握手前
    @Override
    public boolean beforeHandshake(ServerHttpRequest request,
                                   ServerHttpResponse response, WebSocketHandler wsHandler,
                                   Map<String, Object> attributes) throws Exception {

        System.out.println("Before Handshaske");
        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
            // getSession(boolean create)意思是返回当前request中的HttpSession ,
            // 如果当前request中的HttpSession为null,当create为true,就创建一个新的Session,否则返回null;
            HttpSession session = servletRequest.getServletRequest().getSession(false);
            if (session != null) {
                //使用userName区分WebSocketHandler,以便定向发送消息
                String userName = (String) session.getAttribute("SESSION_USERNAME");
                if (userName==null) {
                    userName="default-system";
                }
                attributes.put("WEBSOCKET_USERNAME",userName);
            }
        }
        return super.beforeHandshake(request, response, wsHandler, attributes);
    }

    // 握手后
    @Override
    public void afterHandshake(ServerHttpRequest request,
            ServerHttpResponse response, WebSocketHandler wsHandler,
            Exception ex) {

        System.out.println("++++++++++++++++ HandshakeInterceptor: afterHandshake  ++++++++++++++");

        super.afterHandshake(request, response, wsHandler, ex);
    }
}

4、创建SocketHandler类继承WebSocketHandler类(Spring提供的有AbstractWebSocketHandler类、TextWebSocketHandler类、BinaryWebSocketHandler类,看自己需要进行继承),该类主要是用来处理消息的接收和发送

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;

import java.io.IOException;
import java.util.ArrayList;

/**
 * @desp Socket处理类
 */
@Component
public class SocketHandler implements WebSocketHandler {

    //这个会出现性能问题,最好用Map来存储,key用userid
    private static final ArrayList<WebSocketSession> users = new ArrayList<WebSocketSession>();
    private static Logger logger = LoggerFactory.getLogger(SocketHandler.class);

    public SocketHandler() {
    }

    /**
     * 连接成功时候,会触发页面上onopen方法
     */
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        System.out.println("connect to the websocket success......当前数量:" + users.size());
        users.add(session);
        //这块会实现自己业务,比如,当用户登录后,会把离线消息推送给用户
        //TextMessage returnMessage = new TextMessage("你将收到的离线");
        //session.sendMessage(returnMessage);
    }

    /**
     * js调用websocket.send时候,会调用该方法
     */
    @Override
    public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
        System.err.println(webSocketSession + "---->" + webSocketMessage + ":" + webSocketMessage.getPayload().toString());
        String userName = (String) webSocketSession.getAttributes().get("WEBSOCKET_USERNAME");
        TextMessage returnMessage = new TextMessage(userName + ": 【" + webSocketMessage.getPayload().toString()+"】");
        webSocketSession.sendMessage(returnMessage);
    }

    /**
     * 关闭连接时触发
     */
    public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
        logger.debug("websocket connection closed......");
        String username = (String) session.getAttributes().get("WEBSOCKET_USERNAME");
        System.out.println("用户" + username + "已退出!");
        users.remove(session);
        System.out.println("剩余在线用户" + users.size());
    }

    /**
     * 异常时触发
     *
     * @param session
     * @param exception
     * @throws Exception
     */
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        if (session.isOpen()) {
            session.close();
        }
        logger.debug("websocket connection closed......");
        users.remove(session);
    }

    public boolean supportsPartialMessages() {
        return false;
    }

    /**
     * 给某个用户发送消息
     *
     * @param userName
     * @param message
     */
    public void sendMessageToUser(String userName, TextMessage message) {
        for (WebSocketSession user : users) {
            if (user.getAttributes().get("WEBSOCKET_USERNAME").equals(userName)) {
                try {
                    if (user.isOpen()) {
                        user.sendMessage(message);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                break;
            }
        }
    }

    /**
     * 给所有在线用户发送消息
     *
     * @param message
     */
    public void sendMessageToUsers(TextMessage message) {
        for (WebSocketSession user : users) {
            try {
                if (user.isOpen()) {
                    user.sendMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

要进行发送消息的操作,自己可以写方法,利用保存的已经完成连接的WebSocketSession,通过调用sendMessage(WebScoketMessage<?> message)方法进行消息的发送,参数message是发送的消息内容,Spring提供的类型有TextMessage、BinaryMessage、PingMessage、PongMessage。

5、login.jsp

<%@ page language="java" contentType="text/html; charset=utf-8"
         pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<body>
<h2>Hello World!</h2>
<body>
<form action="/websocket/login">
    登录名:<input type="text" name="username"/>
    <input type="submit" value="登录"/>
</form>
</body>
</body>
</html>

6、websocket.jsp

<%@ page language="java" contentType="text/html; charset=utf-8"
         pageEncoding="utf-8" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Insert title here</title>
</head>
<body>
请输入:<textarea rows="5" cols="10" id="inputMsg" name="inputMsg"></textarea>
<button onclick="doSend();">发送</button>
<br/>
<hr/>
<div id="div">

</div>
<script type="text/javascript" src="http://cdn.bootcss.com/jquery/3.1.0/jquery.min.js"></script>
<script type="text/javascript" src="http://cdn.bootcss.com/sockjs-client/1.1.1/sockjs.js"></script>
<script type="text/javascript">
    var receiveDiv = document.getElementById('div');
    var websocket = null;
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://localhost:8888/websocket");
    } else {
        websocket = new SockJS("http://localhost:8888/quicksand/sockjs/websocket");
    }
    websocket.onopen = function (event) {
        console.log(event);
        websocket.send('websocket client connect test');
    };
    websocket.onmessage = function (event) {
        console.log(event);
        receiveDiv.innerHTML += "<br/>";
        receiveDiv.innerHTML += (' @_@ ' + event.data + ' ~_~ ');
    };
    websocket.onerror = function (event) {
        console.log(event);
    };
    websocket.onclose = function (event) {
        console.log(event);
    };

    function doSend() {
        if (websocket.readyState == websocket.OPEN) {
            var msg = document.getElementById("inputMsg").value;
            websocket.send(msg);//调用后台handleTextMessage方法
            alert("发送成功!");
        } else {
            alert("连接失败!");
        }
    }

    window.close = function () {
        websocket.onclose();
    }
</script>
</body>
</html>

7、写一个websocket控制器

import com.spring.ws.SocketHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.socket.TextMessage;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

/**
 * @desp Socket控制器
 *
 */
@Controller
public class SocketController{
    
    private static final Logger logger = LoggerFactory.getLogger(SocketController.class);
    
    @Autowired
    private SocketHandler socketHandler;

    @RequestMapping("/websocket/login")
    public ModelAndView login(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String username = request.getParameter("username");
        System.out.println(username+"登录");
        HttpSession session = request.getSession(true);
        session.setAttribute("SESSION_USERNAME", username);
        //response.sendRedirect("/quicksand/jsp/websocket.jsp");
        return new ModelAndView("websocket");
    }

    @RequestMapping("/websocket/send")
    @ResponseBody
    public String send(HttpServletRequest request) {
        String username = request.getParameter("username");
        socketHandler.sendMessageToUser(username, new TextMessage("你好,测试!!!!"));
        return "发送成功";
    }
}

8、启动项目,访问http://localhost:8888/websocket/login?username=harvey

常见错误

前台403,后台未报错

这种情况需要配置:setAllowedOrigins(*)

//注册处理拦截器,拦截url为websocket的请求
registry.addHandler(socketHandler, "/websocket").addInterceptors(new WebSocketInterceptor()).setAllowedOrigins("*");

//注册SockJs的处理拦截器,拦截url为/sockjs/websocket的请求
registry.addHandler(socketHandler, "/sockjs/websocket").addInterceptors(new WebSocketInterceptor()).setAllowedOrigins("*").withSockJS();

 

posted @ 2022-01-03 21:02  残城碎梦  阅读(126)  评论(0编辑  收藏  举报