跳到主要内容

09、SpringBoot 实战 - 集成 WebSocket

1. 简介

WebSocket:是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。

  • WebSocket 使得客户端和服务端之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。
  • 在 WebSocket API 中,浏览器和服务器只需一次握手,就可以创建持久性的连接,并进行双向数据传输。

2. 项目结构

 

3. 项目地址

github:https://github.com/ACGkaka/SpringBootExamples/tree/main/springboot-websocket

本地测试: http:\\localhost:8080/websocket/socketTest

4. 配置

4.1 Maven

<!-- Websocket -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

<!-- thymeleaf -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

4.2 WebSocketConfig

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * <p> @Title WebSocketConfig
 * <p> @Description Websocket 配置类
 *
 * @author DDKK.COM 弟弟快看,程序员编程资料站
 */
@Configuration
public class WebSocketConfig {
   
     

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

5. Java 代码

5.1 WebSocketController.java

import com.demo.websocket.WebSocketServer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * <p> @Title WebSocketController
 * <p> @Description websocket controller
 *
 * @author DDKK.COM 弟弟快看,程序员编程资料站
 * @date 2022/1/10 10:13
 */
@Slf4j
@Controller
@RequestMapping("/websocket")
public class WebSocketController {
   
     

    /**
     * 本地socket测试
     */
    @GetMapping("/socketTest")
    public String socketTest() {
   
     
        return "socketTest";
    }

    /**
     * 页面请求
     */
    @GetMapping("/view/{socketId}")
    public String socketView(@PathVariable String socketId, Model model) {
   
     
        if (socketId == null || socketId.trim().length() == 0) {
   
     
            return "socketFail";
        }
        model.addAttribute("socketId", socketId);
        return "socketView";
    }

    /**
     * 推送数据接口
     */
    @GetMapping("/push/{socketId}")
    public Map<String, Object> pushToMap(@PathVariable String socketId, String message) {
   
     
        Map<String, Object> result = new HashMap<>();
        try {
   
     
            // 推送数据
            WebSocketServer.sendInfo(message, socketId);
            result.put("code", 200);
            result.put("msg", "success");
        } catch (IOException e) {
   
     
            // 抛出异常
            log.error(e.getMessage(), e);
            result.put("code", 500);
            result.put("msg", "推送数据失败:" + e.getMessage());
        }
        return result;
    }
}

5.2 WebSocketServer.java

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * <p> @Title WebSocketServer
 * <p> @Description Websocket 服务器
 *
 * @author DDKK.COM 弟弟快看,程序员编程资料站
 * @date 2022/1/10 10:02
 */
@Slf4j
@Component
@ServerEndpoint("/websocket/{sid}")
public class WebSocketServer {
   
     

    /**
     * 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
     */
    private static int onlineCount = 0;
    /**
     * concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
     */
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();

    /**
     * 与某个客户端的连接会话,需要通过它来给客户端发送数据
     */
    private Session session;

    /**
     * 接收sid
     */
    private String sid = "";

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
   
     
        this.session = session;
        webSocketSet.add(this);     //加入set中
        addOnlineCount();           //在线数加1
        log.info("有新窗口开始监听:" + sid + ",当前在线人数为" + getOnlineCount());
        this.sid = sid;
        try {
   
     
            sendMessage("连接成功");
        } catch (IOException e) {
   
     
            log.error("websocket IO异常");
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
   
     
        // 从set中删除
        webSocketSet.remove(this);
        // 在线数减1
        subOnlineCount();
        log.info("有一连接关闭!当前在线人数为" + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
   
     
        log.info("收到来自窗口" + sid + "的信息:" + message);
        //群发消息
        for (WebSocketServer item : webSocketSet) {
   
     
            try {
   
     
                item.sendMessage(message);
            } catch (IOException e) {
   
     
                e.printStackTrace();
            }
        }
    }

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

    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
   
     
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 群发自定义消息
     */
    public static void sendInfo(String message, @PathParam("sid") String sid) throws IOException {
   
     
        log.info("推送消息到窗口" + sid + ",推送内容:" + message);
        for (WebSocketServer item : webSocketSet) {
   
     
            try {
   
     
                //这里可以设定只推送给这个sid的,为null则全部推送
                if (sid == null) {
   
     
                    item.sendMessage(message);
                } else if (item.sid.equals(sid)) {
   
     
                    item.sendMessage(message);
                }
            } catch (IOException e) {
   
     
                continue;
            }
        }
    }

    public static synchronized int getOnlineCount() {
   
     
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
   
     
        WebSocketServer.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
   
     
        WebSocketServer.onlineCount--;
    }

    public static CopyOnWriteArraySet<WebSocketServer> getWebSocketSet() {
   
     
        return webSocketSet;
    }
}

6. 本地socket测试

 

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>本地websocket测试</title>
    <meta name="robots" content="all"/>
    <meta name="keywords" content="本地,websocket,测试工具"/>
    <meta name="description" content="本地,websocket,测试工具"/>
    <style>
        .btn-group {
     
       
            display: inline-block;
        }
    </style>
</head>
<body>
<input type='text' value='ws://localhost:8080/websocket/1' class="form-control" style='width:390px;display:inline'
       id='wsaddr'/>
<div class="btn-group">
    <button type="button" class="btn btn-default" onclick='addsocket();'>连接</button>
    <button type="button" class="btn btn-default" onclick='closesocket();'>断开</button>
    <button type="button" class="btn btn-default" onclick='$("#wsaddr").val("")'>清空</button>
</div>
<div class="row">
    <div id="output" style="border:1px solidccc;height:365px;overflow: auto;margin: 20px 0;"></div>
    <input type="text" id='message' class="form-control" style='width:810px' placeholder="待发信息" onkeydown="en(event);">
    <span class="input-group-btn">
				<button class="btn btn-default" type="button" onclick="doSend();">发送</button>
			</span>
</div>
</div>
</body>

<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script language="javascript" type="text/javascript">
    function formatDate(now) {
     
       
        var year = now.getFullYear();
        var month = now.getMonth() + 1;
        var date = now.getDate();
        var hour = now.getHours();
        var minute = now.getMinutes();
        var second = now.getSeconds();
        return year + "-" + (month = month < 10 ? ("0" + month) : month) + "-" + (date = date < 10 ? ("0" + date) : date) +
            " " + (hour = hour < 10 ? ("0" + hour) : hour) + ":" + (minute = minute < 10 ? ("0" + minute) : minute) + ":" + (
                second = second < 10 ? ("0" + second) : second);
    }

    var output;
    var websocket;

    function init() {
     
       
        output = document.getElementById("output");
        testWebSocket();
    }

    function addsocket() {
     
       
        var wsaddr = $("#wsaddr").val();
        if (wsaddr == '') {
     
       
            alert("请填写websocket的地址");
            return false;
        }
        StartWebSocket(wsaddr);
    }

    function closesocket() {
     
       
        websocket.close();
    }

    function StartWebSocket(wsUri) {
     
       
        websocket = new WebSocket(wsUri);
        websocket.onopen = function (evt) {
     
       
            onOpen(evt)
        };
        websocket.onclose = function (evt) {
     
       
            onClose(evt)
        };
        websocket.onmessage = function (evt) {
     
       
            onMessage(evt)
        };
        websocket.onerror = function (evt) {
     
       
            onError(evt)
        };
    }

    function onOpen(evt) {
     
       
        writeToScreen("<span style='color:red'>连接成功,现在你可以发送信息啦!!!</span>");
    }

    function onClose(evt) {
     
       
        writeToScreen("<span style='color:red'>websocket连接已断开!!!</span>");
        websocket.close();
    }

    function onMessage(evt) {
     
       
        writeToScreen('<span style="color:blue">服务端回应 ' + formatDate(new Date()) + '</span><br/><span class="bubble">' +
            evt.data + '</span>');
    }

    function onError(evt) {
     
       
        writeToScreen('<span style="color: red;">发生错误:</span> ' + evt.data);
    }

    function doSend() {
     
       
        var message = $("#message").val();
        if (message == '') {
     
       
            alert("请先填写发送信息");
            $("#message").focus();
            return false;
        }
        if (typeof websocket === "undefined") {
     
       
            alert("websocket还没有连接,或者连接失败,请检测");
            return false;
        }
        if (websocket.readyState == 3) {
     
       
            alert("websocket已经关闭,请重新连接");
            return false;
        }
        console.log(websocket);
        $("#message").val('');
        writeToScreen('<span style="color:green">你发送的信息 ' + formatDate(new Date()) + '</span><br/>' + message);
        websocket.send(message);
    }

    function writeToScreen(message) {
     
       
        var div = "<div class='newmessage'>" + message + "</div>";
        var d = $("#output");
        var d = d[0];
        var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
        $("#output").append(div);
        if (doScroll) {
     
       
            d.scrollTop = d.scrollHeight - d.clientHeight;
        }
    }
    function en(event) {
     
       
        var evt = evt ? evt : (window.event ? window.event : null);
        if (evt.keyCode == 13) {
     
       
            doSend()
        }
    }
</script>

</html>

整理完毕,完结撒花~ 🌻