Fork me on GitHub

WebSocket

1、WebSocket的使用场景
社交聊天、弹幕、多玩家游戏、协同编辑、股票基金实时报价、体育实况更新、视频会议/聊天、基于位置的应用、在线教育、智能家居等需要高实时的场景
 
2、为什么要用WebSocket
WebSocket是HTML5开始提供的一种浏览器与服务器间进行全双工通讯的网络技术。依靠这种技术可以实现客户端和服务器端的长连接,双向实时通信。
特点:事件驱动、异步,使用ws或者wss协议的客户端socket,能够实现真正意义上的推送功能
缺点:少部分浏览器不支持,浏览器支持的程度与方式有区别。
 
判断浏览器是否支持webSocket:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>FORM Text</title>
        <script src="jquery-3.3.1.min.js"></script>
</head>
<body>
    <div id="test"></div>
    <script>
        $(function(){
            if(window.WebSocket){
                alert("Support");
            }else{
                alert("Unsupport");
            }
        });

    </script>
</body>
</html>
3、websocket与http

1、WebSocket是HTML5出的东西(协议),与Http协议没关系

2、WebSocket 支持全双工长连接,Http不支持长连接,一个request只能有一个response。而且这个response也是被动的,不能主动发起。

3、WebSocket 建立连接进行了一次Http握手。

Http响应模式:                                                                                  WebSocket响应模式:

   

4、轮询:

客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。客户端会轮询,有没有新消息。这种方式连接数会很多,一个接受,一个发送。而且每次发送请求都会有Http的Header,会很耗流量,也会消耗CPU的利用率。

HTTP的生命周期通过 Request 来界定,也就是一个 Request 一个 Response ,而且后台无法主动向前端发送数据,只能被动等前端http请求。

eg:放羊人,男孩,狼

放羊人:狼来了吗?

男孩:没有

放羊人:狼来了没?

男孩:没有

.....(此过程,狼已经来了)

放羊人:狼来了没?

男孩:来了

5、长轮询

长轮询是轮询的改进,也是采取轮询模式,不同的是采取阻塞模式(类似一直打电话,没收到就不挂电话) 也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

 放羊人1:狼来了吗?

男孩:....(等待,一直等狼来)

.....(此过程,狼已经来了)

放羊人2:狼来了吗?

男孩:....(等待,一直等狼来)

.....(此过程,狼已经来了)

男孩放羊1:来了

男孩放羊2:来了

6、WebSocket

放羊人1:狼来了吗?

男孩->放羊人1:没有,狼还在十里外

放羊人2:狼来了吗?

男孩->放羊人2:没有,狼还在八里外

男孩->放羊1:狼在五里外

男孩->放羊2:狼在五里外

.....(此过程,狼已经来了)

男孩->放羊1:来了

男孩->放羊2:来了

7、使用Spring实现webSocket

工程如下:

package com.example.demo.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Map;

@Controller
@RequestMapping(value = "/webSocket")
public class WebSocketController {
    private final Logger log = LoggerFactory.getLogger(getClass());


    @RequestMapping("/toLogin")
    private String toLoginPage(HttpServletRequest request, Map<String, Object> map) {
        return "login";
    }
    @RequestMapping("/login")
    private String loginPage(HttpServletRequest request, Map<String, Object> map) {
        String userName =  request.getParameter("userName");

        HttpSession session = null;
        if(!request.isRequestedSessionIdValid()){

            //会话失效重新登录
            session = request.getSession(true);
            //用户名放入Session
            session.setAttribute("SESSION_USERNAME",userName);
        }

        return "test_webSocket";
    }
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>WebSocket演示</title>
    <!-- js 引用 -->
    <script th:src="@{~/static/js/jquery_3.1.0_jquery.min.js}"></script>
    <script th:src="@{~/static/js/webSocket-1.0.js}"></script>

</head>
<body>

<form action="login" method="post">
    请输入:<input type="text" value="" name="userName" placeholder="请输入用户名"/>
            <input type="submit" value="登录">
</form>

</body>

<script>

</script>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>WebSocket演示</title>
    <!-- js 引用 -->
    <script th:src="@{~/static/js/jquery_3.1.0_jquery.min.js}"></script>
    <script th:src="@{~/static/js/webSocket-1.0.js}"></script>

</head>
<body>
请输入:<textarea rows="5" cols="10" id="inputMsg" name="inputMsg"></textarea>
<input type="button" id="send" value="发送"/>
<input type="button" id="close" value="关闭"/>
<!--<button id="s-close">关闭</button>-->

</body>

<script>

    var websocket = WebSkt.createNew("localhost:8080/websocket/socketServer.do");

    $("#send").on("click",function(){
         if (websocket.readyState == websocket.OPEN) {
             var msg = document.getElementById("inputMsg").value;
             websocket.send(msg);
             alert("发送成功!");
         } else {
             alert("连接失败!");
         }
    });

    $("#close").on('click',function(){
        websocket.close();
    });

</script>
</html>

------

package com.example.demo.interceptor;

import java.util.Map;

import javax.servlet.http.HttpSession;

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;

/**
 * 继承HttpSessionHandshakeInterceptor对象。该对象作为页面连接websocket服务的拦截器
 *
 * @author Administrator
 **/
public class SpringWebSocketHandlerInterceptor extends HttpSessionHandshakeInterceptor {

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

        System.out.println("Before Handshake");
        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
            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) {
        super.afterHandshake(request, response, wsHandler, ex);
    }
}
package com.example.demo.handle;

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

import com.example.demo.config.SpringWebSocketConfig;
import com.example.demo.util.BloomFilter;
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.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

import javax.annotation.Resource;

/*
 * 继承WebSocketHandler对象 该对象提供了客户端连接,关闭,错误,发送等方法,重写这几个方法即可实现自定义业务逻辑
 *
 * @author Administrator
 */
public class SpringWebSocketHandler extends TextWebSocketHandler {

    //这个会出现性能问题,最好用Map来存储,key用userid
    private static final ArrayList<WebSocketSession> users;                                                        //

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

    /**
     * 利用布隆算法,判断Session用户是否在线
     */
    @Resource
    private BloomFilter bloomFilter;

    static {
        users = new ArrayList<WebSocketSession>();
    }

    public SpringWebSocketHandler() {
    }

    /**
     * 连接成功时候,会触发页面上onopen方法
     */
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {

        String sessionId = (String) session.getAttributes().get("HTTP.SESSION.ID");

        //这边也可以用Map<String,WebSocketSession> usersMap 的方式判断Session是否存在
        if(!bloomFilter.contains(sessionId)){
            //不在
            bloomFilter.add(sessionId);
            users.add(session);
        }

        System.out.println("connect to the websocket success......当前数量:" + users.size());
        // 这块会实现自己业务,比如,当用户登录后,会把离线消息推送给用户
        // TextMessage returnMessage = new TextMessage("你将收到的离线");
        // session.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());
    }

    /**
     * js调用websocket.send时候,会调用该方法
     */
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        super.handleTextMessage(session, message);
        super.handleTextMessage(session, message);
        System.out.println("收到消息:" + message.getPayload());

        //交互
        TextMessage returnMessage = new TextMessage("Server has recieved Your message and will be processed later... ");
        session.sendMessage(returnMessage);
    }

    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();
            }
        }
    }
}
package com.example.demo.config;

import com.example.demo.handle.SpringWebSocketHandler;
import com.example.demo.interceptor.SpringWebSocketHandlerInterceptor;
import com.example.demo.util.BloomFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.WebMvcConfigurerAdapter;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.handler.TextWebSocketHandler;


/**
 * 建立一个类实现WebSocketConfigurer接口
 *
 * @author Administrator
 *
 */

@Configuration
//@EnableWebMvc
@EnableWebSocket
public class SpringWebSocketConfig extends WebMvcConfigurerAdapter implements WebSocketConfigurer {

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

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

        logger.info("SpringWebSocketConfig registerWebSocketHandlers ................");

        registry.addHandler(webSocketHandler(), "/websocket/socketServer.do").addInterceptors(
                new SpringWebSocketHandlerInterceptor());

        //根据用途,添加其他WebSocket路径监听
        /*registry.addHandler(webSocketHandler(), "/sockjs/socketServer.do")
                .addInterceptors(new SpringWebSocketHandlerInterceptor()).withSockJS();*/
    }

    @Bean
    public TextWebSocketHandler webSocketHandler() {
        return new SpringWebSocketHandler();
    }

    @Bean
    public BloomFilter getBloomFilter(){
        return new BloomFilter();
    }
}

运行测试:---------------

 

相对于Http协议,多了Upgrade: websocket Connection: Upgrade,告诉Apache、Nginx等服务器:注意啦,窝发起的是Websocket协议,快点帮我找到对应的助理处理。

Sec-WebSocket-Key: FrZGy1EVeL//7yZDEYrM4g==
Sec-WebSocket-Version: 13
Sec-WebSocket-Key 是一个Base64 encode的值,这个是浏览器随机生成的,告诉服务器:我要验证尼是不是真的是Websocket助理。
Sec-WebSocket-Version 是告诉服务器所使用的Websocket Draft(协议版本)

 Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key。服务器:知道啦,给你看我的ID CARD来证明行了吧。。

 

参考:

https://www.zhihu.com/question/20215561

https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket

 

posted @ 2019-06-20 20:34  小传风  阅读(702)  评论(0编辑  收藏  举报