Springboot integration WebSocket, push information to achieve the specified page

Technology Selection

Springboot + WebSocket + Mybatis + Enjoy (similar Jsper, freemarker template engine) + FastJson + SpringBoot default connection pool Hikari

As the lazy writing style, and I do not want to use JQuery, directly Vue plus ElementUI used as the page display.

Code section

First on the code

· EvaluationServer · class information as a server class memory Session

@ServerEndpoint("/im/{winNum}")
@Component
@Slf4j
public class EvaluationServer {

    /**
     *  静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
     * @date 2019/7/3 9:25
    */
    private static int onlineCount = 0;
    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     * @date 2019/7/3 9:26
    */
    private Session session;
    /**
     * 使用map对象,便于根据winNum来获取对应的WebSocket
     * @date 2019/7/3 9:26
    */
    private static ConcurrentHashMap<String,EvaluationServer> websocketList = new ConcurrentHashMap<>();
    /**
     *  接收winNum
     * @date 2019/7/3 9:27
    */
    private String winNum="";
    /**
     * 连接建立成功调用的方法*/
    @OnOpen
    public void onOpen(Session session,@PathParam("winNum") String fromWinNum) throws IOException {
        this.session = session;
        if(StringUtils.isEmpty(fromWinNum)){
            log.error("请输入窗口号!!!!!!!!!!!!!!!!");
            return;
        }else{
            try {
                if(websocketList.get(fromWinNum) == null){
                    this.winNum = fromWinNum;
                    websocketList.put(fromWinNum,this);
                    addOnlineCount();           //在线数加1
                    log.info("有新窗口开始监听:{},当前窗口数为{}",fromWinNum,getOnlineCount());
                }else{
                    session.getBasicRemote().sendText("已有相同窗口,请重新输入不同窗口号");
                    CloseReason closeReason = new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE,"相同窗口");
                    session.close(closeReason);
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
        if(session.isOpen()){
            String jo = JSON.toJSONString(ApiReturnUtil.success());
            session.getBasicRemote().sendText(jo);
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        if(websocketList.get(this.winNum)!=null){
            websocketList.remove(this.winNum);
            subOnlineCount();           //在线数减1
            log.info("有一连接关闭!当前在线窗口为:{}",getOnlineCount());
        }
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息*/
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("收到来自窗口{}的信息:{},会话ID:",winNum,message,session.getId());
        if(StringUtils.isNotBlank(message)){
            //解析发送的报文
            Map<String,Object> map = JSON.parseObject(message, Map.class);
        }
    }

    @OnError
    public void onError(Session session, Throwable error) {
        log.error("发生错误");
        error.printStackTrace();
    }

    /**
     * 服务器指定推送至某个客户端
     * @param message
     * @author 杨逸林
     * @date 2019/7/3 10:02
     * @return void
    */
    private void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }


    /**
     * 发送给指定 浏览器
     * @ param message
     * @param winNum
     * @return void
    */
    public static void sendInfo(String message,@PathParam("winNum") String winNum) throws IOException {
        if(websocketList.get(winNum) == null){
            log.error("没有窗口号!!!!!!!!!");
            return;
        }
        websocketList.forEach((k,v)->{
            try {
                //这里可以设定只推送给这个winNum的,为null则全部推送
                if(winNum==null) {
                    v.sendMessage(message);
                }else if(k.equals(winNum)){
                    log.info("推送消息到窗口:{},推送内容: {}",winNum,message);
                    v.sendMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
                log.info("找不到指定的 WebSocket 客户端:{}",winNum);
            }
        });
    }

    private synchronized int getOnlineCount() {
        return onlineCount;
    }

    private synchronized void addOnlineCount() {
        onlineCount++;
    }

    private synchronized void subOnlineCount() {
        onlineCount--;
    }

    public static synchronized ConcurrentHashMap<String,EvaluationServer> getWebSocketList(){
        return websocketList;
    }
}

IndexController For redirecting to page

@Controller
public class IndexController {

    @RequestMapping("/d")
    public ModelAndView index(String u){
        ModelAndView modelAndView = new ModelAndView();
        if(StringUtils.isBlank(u)){
            modelAndView.setViewName("error");
            return modelAndView;
        }
        modelAndView.addObject("winNum",u);
        modelAndView.setViewName("index");
        return modelAndView;
    }
}

GlobalConfig Springboot configuration class

@Configuration
public class GlobalConfig {

    @Value("${server.port}")
    private String port;

    /**
     * 添加Enjoy模版引擎
     * @date 2019-07-10 8:43
     * @return com.jfinal.template.ext.spring.JFinalViewResolver
    */
    @Bean(name = "jfinalViewResolver")
    public JFinalViewResolver getJFinalViewResolver() throws UnknownHostException {
        //获取本地ip,和端口,并将信息拼接设置成context
        String ip = InetAddress.getLocalHost().getHostAddress();
        String localIp = ip+":"+port;
        JFinalViewResolver jfr = new JFinalViewResolver();
        // setDevMode 配置放在最前面
        jfr.setDevMode(true);
        // 使用 ClassPathSourceFactory 从 class path 与 jar 包中加载模板文件
        jfr.setSourceFactory(new ClassPathSourceFactory());
        // 在使用 ClassPathSourceFactory 时要使用 setBaseTemplatePath
        JFinalViewResolver.engine.setBaseTemplatePath("/templates/");
        JFinalViewResolver.engine.addSharedObject("context",localIp);
        jfr.setSuffix(".html");
        jfr.setContentType("text/html;charset=UTF-8");
        jfr.setOrder(0);
        return jfr;
    }

    /**
     * 添加 WebSocket 支持
     * @date 2019/7/3 9:20
     * @return org.springframework.web.socket.server.standard.ServerEndpointExporter
    */
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

    /**
     * 添加 FastJson 支持
     * @date 2019/7/3 11:16
     * @return org.springframework.boot.autoconfigure.http.HttpMessageConverters
    */
    @Bean
    public HttpMessageConverters fastJsonHttpMessageConverters(){
        //1. 需要定义一个converter转换消息的对象
        FastJsonHttpMessageConverter fasHttpMessageConverter = new FastJsonHttpMessageConverter();

        //2. 添加fastjson的配置信息,比如:是否需要格式化返回的json的数据
        FastJsonConfig fastJsonConfig = new FastJsonConfig();
        fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);

        //3. 在converter中添加配置信息
        fasHttpMessageConverter.setFastJsonConfig(fastJsonConfig);
        HttpMessageConverter<?> converter = fasHttpMessageConverter;
        return new HttpMessageConverters(converter);
    }
}

CallEvaluationController Call interface class

/**
 *  用于 API 调用
 * 调用评价器的 api 接口
 * @version 1.0
 * @date 2019/7/3 9:34
 **/
@RestController
@RequestMapping("/api")
@Slf4j
public class CallEvaluationController {

    @Autowired
    private UserService userService;

    /**
     * 开始评价接口
     * @param winNum
     * @param userId
     * @return cn.luckyray.evaluation.entity.ApiReturnObject
    */
    @RequestMapping("/startEvaluate")
    public String startEvaluate(String winNum){
        // 验证窗口是否为空
        ConcurrentHashMap<String, EvaluationServer> map = EvaluationServer.getWebSocketList();
        if(map.get(winNum) == null){ return "窗口不存在"}
        String message = "message";
        try {
            EvaluationServer.sendInfo(message,winNum);
        } catch (IOException e) {
            e.printStackTrace();
            log.error("{}窗口不存在,或者客户端已断开",winNum);
            return "窗口不存在或者已经断开连接";
        }
        return "success";
    }
}

MavenConfiguration

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.luckyray</groupId>
    <artifactId>evaluation</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>evaluation</name>
    <description>评价功能模块</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <!-- 添加阿里 FastJson 依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.41</version>
        </dependency>
        <!-- enjoy模板引擎 begin -->
        <dependency>
            <groupId>com.jfinal</groupId>
            <artifactId>enjoy</artifactId>
            <version>3.3</version>
        </dependency>
        <!-- enjoy模板引擎 end -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- spring-boot-devtools热启动依赖包 start-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- spring-boot-devtools热启动依赖包 end-->

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>cn.luckyray.evaluation.EvaluationApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

index.html page, this uses the WebSocket can reconnect, to prevent the client halfway off the net result in the need to refresh the page in order to reconnect. (Where # () the contents inside to Enjoy template engine to render content)

<!DOCTYPE html>
<html>

<head>
    <meta charset='utf-8'>
    <meta http-equiv='X-UA-Compatible' content='IE=edge'>
    <title>评价页面</title>
    <meta name='viewport' content='width=device-width, initial-scale=1'>
    <!-- element-ui.css -->
    <link rel="stylesheet" href="../css/index.css">

</head>

<body>
    <div id="app">
        <el-row>
            <el-button v-on:click="click(1)" type="success" style="font-size:50px;font-family:微软雅黑;height: 570px;width: 410px" disabled>满意</el-button>
            <el-button v-on:click="click(2)" type="primary" style="font-size:50px;font-family:微软雅黑;height: 570px;width: 410px" disabled>一般</el-button>
            <el-button v-on:click="click(3)" type="danger" style="font-size:50px;font-family:微软雅黑;height: 570px;width: 410px" disabled>不满意</el-button>
        </el-row>
    </div>
</body>

<script src="../js/reconnecting-websocket.min.js"></script>
<script src="../js/vue.js"></script>
<!-- element-ui.js -->
<script src="../js/index.js"></script>
<script>
    var socket;
    if (typeof(WebSocket) == "undefined") {
        console.log("您的浏览器不支持WebSocket");
    } else {
        //实现化WebSocket对象,指定要连接的服务器地址与端口  建立连接
        let socketUrl = "ws://#(context)/im/#(winNum)";
        socket = new ReconnectingWebSocket(socketUrl, null, {
            debug: false,
            reconnectInterval: 3000
        });
        console.log("创建websocket");
        //打开事件
        socket.onopen = function() {
            console.log("websocket客户端已打开");
        };
        //获得消息事件
        socket.onmessage = function(msg) {
            if(msg.data != undefined && msg.data.indexOf("已有相同窗口") != -1){
                alert("已有相同窗口,请重新输入正确窗口号");
                socket.close();
                window.history.back(-1);
                return;
            }
            try{
                let data = JSON.parse(msg.data);
                console.log(data);
                if (data.code == "0" && data.data != undefined && data.data.active == "startEvaluate") {
                    userId = data.data.userId;
                    serialNum = data.data.serialNum;
                    speak();
                    app.allowClick();
                    setTimeout(app.allDisabled,10000);
                }
            }catch (e) {
                console.log(e);
            }

            //发现消息进入开始处理前端触发逻辑
        };
        //关闭事件
        socket.onclose = function() {
            //console.log("websocket已关闭,正在尝试重新连接");
        };
        //发生了错误事件
        socket.onerror = function() {
            //console.log("websocket已关闭,正在尝试重新连接");
        }
        //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
        window.onbeforeunload = function() {
            socket.close();
        }
    }
    //fullScreen()和exitScreen()有多种实现方式,此处只使用了其中一种
    //全屏
    function fullScreen() {
        var docElm = document.documentElement;
        docElm.webkitRequestFullScreen( Element.ALLOW_KEYBOARD_INPUT );
    }
    var app = new Vue({
        el: '#app',
        data: function() {

        },
        methods: {
            click: function(evaluation) {
                console.log(evaluation);
                let data = {
                    evaluation : evaluation,
                }
                let jsonData = JSON.stringify(data);
                console.log(jsonData);
                socket.send(jsonData);
                let childrens = app.$children[0].$children;
                for (let children of childrens) {
                    children.disabled = true;
                }
            },
            allowClick: function() {
                let childrens = app.$children[0].$children;
                for (let children of childrens) {
                    children.disabled = false;
                }
            },
            allDisabled:function () {
                let childrens = app.$children[0].$children;
                for (let children of childrens) {
                    children.disabled = true;
                }
            }
        },
    });
</script>
</html>

Specific code

The main thing is that these, in particular, on the content of index.html. "Netty real" only to say how to set up the server and no instructions on how to build a client.
The code below is the key, WebSocket protocol using ws, in fact, was the first to send http request for the http request header Connection:Upgrade, Upgrade:websocket the notification server upgrade http request is ws / wss agreement. The following may also be changed socket = new WebSocket (url, protocols ). Wherein url Mandatory, Optional Protocols parameter, string is the parameter | string [], where the protocol string may be used, including SMPP, SOAP or custom protocol.

Ws related to wss actually http and https relations with similar, but in the TCP protocol, ws coat layer protocol TLS protocol, the encryption process.

let socketUrl = "ws://#(context)/im/#(winNum)";
socket = new ReconnectingWebSocket(socketUrl, null, {
            debug: false,
            reconnectInterval: 3000
        });

WebSocket of four events, two methods, two properties

Four events

open, message, error, close
below the corresponding ts files
you can see there are four ways we need to achieve, corresponding to the four events. The following details
OnClose
onerror
onmessage
the OnOpen

interface WebSocket extends EventTarget {
    binaryType: BinaryType;
    readonly bufferedAmount: number;
    readonly extensions: string;
    onclose: ((this: WebSocket, ev: CloseEvent) => any) | null;
    onerror: ((this: WebSocket, ev: Event) => any) | null;
    onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null;
    onopen: ((this: WebSocket, ev: Event) => any) | null;
    readonly protocol: string;
    readonly readyState: number;
    readonly url: string;
    close(code?: number, reason?: string): void;
    send(data: string | ArrayBuffer | Blob | ArrayBufferView): void;
    readonly CLOSED: number;
    readonly CLOSING: number;
    readonly CONNECTING: number;
    readonly OPEN: number;
    addEventListener<K extends keyof WebSocketEventMap>(type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | AddEventListenerOptions): void;
    addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
    removeEventListener<K extends keyof WebSocketEventMap>(type: K, listener: (this: WebSocket, ev: WebSocketEventMap[K]) => any, options?: boolean | EventListenerOptions): void;
    removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
}

open

Once the server responds to the WebSocket connection request, open event triggers and establish a connection. open event corresponding callback function called onopen

message

message event trigger when a message is received, corresponding to the event callback function onmessage. In addition to text, a WebSocket also binary data, or such data as Blob message ArrayBuffer message processing. Before reading the data to decide the type of client binary input data. Wherein E is returned, the message is returned to the server e.data, the remaining properties of the incidental information websocket returned.

ws.binaryType="Blob";
ws.onmessage = function(e){
    if(e.data instanceof Blob){
        var blob = new Blob(e.data);
    }
}

error

When an unexpected failure of response is triggered, most errors will lead to WebSocket closed, usually accompanied by a close event. The error event handler is invoked server reconnect logic, and the best place to handle exceptions from the WebSocket object.

close

close event is triggered when the WebSocket connection is closed. Once the connection is closed, are two-terminal can not communicate.

Two properties

readyState

ws.readyState === 0; Ready
ws.readyState === 1; connected
ws.readyState === 2; is closing
ws.readyState === 3; Closed

bufferAmount

The reason is because when the attribute information is transmitted to the server WebSocket, there is a queue buffer, this parameter can limit the rate of data sent by the client to the server, thereby avoiding network saturation. Specific code as follows

// 10k max buffer size.
const THRESHOLD = 10240;

// Create a New WebSocket connection
let ws = new WebSocket("ws://w3mentor.com");

// Listen for the opening event
ws.onopen = function () {
   // Attempt to send update every second.
   setInterval( function() {
      // Send only if the buffer is not full
      if (ws.bufferedAmount < THRESHOLD) {
         ws.send(getApplicationState());
      }
   }, 1000);
};

Two methods

send

It must be able to send a message after the open event is triggered. In addition to text messages, but also allows the transmission of binary data. code show as below.
text

let data = "data";
if(ws.readyState == WebSocket.OPEN){
    ws.send(data);
}

Binary data

let blob = new Blob("blob");
ws.send(blob);
let a = new Unit8Array([1,2,3,4,5,6]);
ws.send(a.buffer);

close

Close connection, two parameters can be added  close(code,reason), corresponding to the client, the status code is code 1000 which, for the reason string“关闭连接原因”

 

Published 54 original articles · won praise 69 · Views 250,000 +

Guess you like

Origin blog.csdn.net/seanxwq/article/details/103799137