创建时间:2020年10月8日
作者:CondingGorit
项目版本:SpringBoot 2.3.4
参考文章:

一、什么是 WebSocket?

WebSocket 是一种在单个 TCP 连接上进行 全双工通信的协议, 使用 WebSocket 可以使 客户端 和 服务器 之间的数据交换变得更加简单,它允许服务端 向 客户端推送数据。游览器只用和服务器只需要 完成一次握手,两者之间就可以创建 持久性 的连接,并进行双向数据传输

二、WebSocket 的特性

  1. HTTP/1.1 的协议升级特性
  2. WebSocket 请求使用非正常的 HTTP 请求以特定的模式访问一个 URL,这个 URL 包含 ws 和 wss,对应 HTTP 协议中的 HTTP 和 HTTPS,

image.png

三、 WebSocket 的应用场景

  • 在线股票网站
  • 即时聊天
  • 多人在线游戏
  • 应用集群通信
  • 系统性能实时监控

四、SpringBoot 整合 WebSocket

项目结构:
image.png

4.1 导入相关依赖

搭建一个 Web 应用,导入 WebSocket 相关依赖

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-websocket</artifactId>
  4. </dependency>

4.2 编写 WebSocket 配置类

  1. package cn.gorit.config;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.socket.server.standard.ServerEndpointExporter;
  5. /**
  6. * @Classname WebSocketConfig
  7. * @Description TODO
  8. * @Date 2020/10/8 20:35
  9. * @Created by CodingGorit
  10. * @Version 1.0
  11. */
  12. @Configuration
  13. public class WebSocketConfig {
  14. @Bean
  15. public ServerEndpointExporter serverEndpointExporter() {
  16. return new ServerEndpointExporter();
  17. }
  18. }

4.3 配置 WebSocket 服务端类

  1. package cn.gorit.websocket;
  2. import org.springframework.stereotype.Component;
  3. import javax.websocket.*;
  4. import javax.websocket.server.PathParam;
  5. import javax.websocket.server.ServerEndpoint;
  6. import java.io.IOException;
  7. import java.util.concurrent.ConcurrentHashMap;
  8. /**
  9. * @Classname ProductWebSocket
  10. * @Description TODO
  11. * @Date 2020/10/8 20:36
  12. * @Created by CodingGorit
  13. * @Version 1.0
  14. */
  15. /**
  16. * @ServerEndpoint 注解是一个类层次的注解,它的功能主要是将目前的类定义成一个websocket服务器端,
  17. * 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端
  18. * @ServerEndpoint 可以把当前类变成 websocket服务类
  19. */
  20. @ServerEndpoint("/websocket/{userId}")
  21. @Component
  22. public class ProductWebSocket {
  23. //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
  24. private static int onlineCount = 0;
  25. //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。若要实现服务端与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户id
  26. private static ConcurrentHashMap<String, ProductWebSocket> webSocketSet = new ConcurrentHashMap<String, ProductWebSocket>();
  27. //与某个客户端的连接会话,需要通过它来给客户端发送数据
  28. private Session session;
  29. //当前发消息的人员编号
  30. private String userId = "";
  31. /**
  32. * 线程安全的统计在线人数
  33. *
  34. * @return
  35. */
  36. public static synchronized int getOnlineCount() {
  37. return onlineCount;
  38. }
  39. public static synchronized void addOnlineCount() {
  40. ProductWebSocket.onlineCount++;
  41. }
  42. public static synchronized void subOnlineCount() {
  43. ProductWebSocket.onlineCount--;
  44. }
  45. /**
  46. * 连接建立成功调用的方法
  47. *
  48. * @param param 用户唯一标示
  49. * @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
  50. */
  51. @OnOpen
  52. public void onOpen(@PathParam(value = "userId") String param, Session session) {
  53. userId = param;//接收到发送消息的人员编号
  54. this.session = session;
  55. webSocketSet.put(param, this);//加入线程安全map中
  56. addOnlineCount(); //在线数加1
  57. System.out.println("用户id:" + param + "加入连接!当前在线人数为" + getOnlineCount());
  58. }
  59. /**
  60. * 连接关闭调用的方法
  61. */
  62. @OnClose
  63. public void onClose() {
  64. if (!"".equals(userId)) {
  65. webSocketSet.remove(userId); //根据用户id从ma中删除
  66. subOnlineCount(); //在线数减1
  67. System.out.println("用户id:" + userId + "关闭连接!当前在线人数为" + getOnlineCount());
  68. }
  69. }
  70. /**
  71. * 收到客户端消息后调用的方法
  72. *
  73. * @param message 客户端发送过来的消息
  74. * @param session 可选的参数
  75. */
  76. @OnMessage
  77. public void onMessage(String message, Session session) {
  78. System.out.println("来自客户端的消息:" + message);
  79. //要发送人的用户uuid
  80. String sendUserId = message.split(",")[1];
  81. //发送的信息
  82. String sendMessage = message.split(",")[0];
  83. //给指定的人发消息
  84. sendToUser(sendUserId, sendMessage);
  85. }
  86. /**
  87. * 给指定的人发送消息
  88. *
  89. * @param message
  90. */
  91. public void sendToUser(String sendUserId, String message) {
  92. try {
  93. if (webSocketSet.get(sendUserId) != null) {
  94. webSocketSet.get(sendUserId).sendMessage(userId + "给我发来消息,消息内容为--->>" + message);
  95. } else {
  96. if (webSocketSet.get(userId) != null) {
  97. webSocketSet.get(userId).sendMessage("用户id:" + sendUserId + "以离线,未收到您的信息!");
  98. }
  99. System.out.println("消息接受人:" + sendUserId + "已经离线!");
  100. }
  101. } catch (IOException e) {
  102. e.printStackTrace();
  103. }
  104. }
  105. /**
  106. * 管理员发送消息
  107. *
  108. * @param message
  109. */
  110. public void systemSendToUser(String sendUserId, String message) {
  111. try {
  112. if (webSocketSet.get(sendUserId) != null) {
  113. webSocketSet.get(sendUserId).sendMessage("系统给我发来消息,消息内容为--->>" + message);
  114. } else {
  115. System.out.println("消息接受人:" + sendUserId + "已经离线!");
  116. }
  117. } catch (IOException e) {
  118. e.printStackTrace();
  119. }
  120. }
  121. /**
  122. * 给所有人发消息,按理说,这个是不允许对外开放的,只能对指定权限的用户(如管理员)开放
  123. *
  124. * @param message
  125. */
  126. public void sendAll(String message) {
  127. String sendMessage = message.split(",")[0];
  128. //遍历HashMap
  129. for (String key : webSocketSet.keySet()) {
  130. try {
  131. //判断接收用户是否是当前发消息的用户
  132. if (!userId.equals(key)) {
  133. webSocketSet.get(key).sendMessage("用户:" + userId + "发来消息:" + " <br/> " + sendMessage);
  134. System.out.println("key = " + key);
  135. }
  136. } catch (IOException e) {
  137. e.printStackTrace();
  138. }
  139. }
  140. }
  141. /**
  142. * 发生错误时调用
  143. *
  144. * @param session
  145. * @param error
  146. */
  147. @OnError
  148. public void onError(Session session, Throwable error) {
  149. System.out.println("发生错误");
  150. error.printStackTrace();
  151. }
  152. /**
  153. * 发送消息
  154. *
  155. * @param message
  156. * @throws IOException
  157. */
  158. public void sendMessage(String message) throws IOException {
  159. //同步发送
  160. this.session.getBasicRemote().sendText(message);
  161. //异步发送
  162. //this.session.getAsyncRemote().sendText(message);
  163. }
  164. }

4.4 控制层

  1. package cn.gorit.controller;
  2. import cn.gorit.websocket.ProductWebSocket;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.stereotype.Controller;
  5. import org.springframework.web.bind.annotation.*;
  6. /**
  7. * @Classname IndexController
  8. * @Description TODO
  9. * @Date 2020/10/8 20:40
  10. * @Created by CodingGorit
  11. * @Version 1.0
  12. */
  13. @CrossOrigin
  14. @Controller
  15. public class IndexController {
  16. @Autowired
  17. ProductWebSocket socket;
  18. @GetMapping(value = "/")
  19. @ResponseBody
  20. public Object index() {
  21. return "Hello,ALl。This is CodingGorit's webSocket demo!";
  22. }
  23. @ResponseBody
  24. @GetMapping("test")
  25. public String test(String userId, String message) throws Exception {
  26. if (userId == "" || userId == null) {
  27. return "发送用户id不能为空";
  28. }
  29. if (message == "" || message == null) {
  30. return "发送信息不能为空";
  31. }
  32. new ProductWebSocket().systemSendToUser(userId, message);
  33. return "发送成功!";
  34. }
  35. @RequestMapping(value = "/user")
  36. public String user() {
  37. return "user.html";
  38. }
  39. @RequestMapping(value = "/ws")
  40. public String ws() {
  41. return "ws.html";
  42. }
  43. @RequestMapping(value = "/ws2")
  44. public String ws1() {
  45. return "ws2.html";
  46. }
  47. /**
  48. * 管理员的页面,可以发送广播消息
  49. * @return
  50. */
  51. @RequestMapping(value = "/admin")
  52. public String admin() {
  53. return "gb.html";
  54. }
  55. @ResponseBody
  56. @RequestMapping(value = "/sendAll" ,method = RequestMethod.GET)
  57. public String sendAll (String msg) {
  58. socket.sendAll(msg);
  59. return "发送成功";
  60. }
  61. }

4.5 前端页面编写

我之前使用 内网穿透测试了,所以访问端口就是 80 端口

gb.html 编写 (管理员发送广播消息)

  1. <!DOCTYPE html>
  2. <html lang="en">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>Title</title>
  6. </head>
  7. <body>
  8. <h3>广播消息</h3>
  9. <input type="text" id="text">
  10. <button onclick="send()">发送消息</button>
  11. <br/>
  12. <button onclick="closeWebSocket()">关闭WebSocket连接</button>
  13. <br/>
  14. <div id="message"></div>
  15. <script type="text/javascript">
  16. var websocket = null;
  17. var userId = "admin"
  18. //判断当前浏览器是否支持WebSocket
  19. if ('WebSocket' in window) {
  20. websocket = new WebSocket("ws://127.0.0.1:80/websocket/" + userId);
  21. } else {
  22. alert('当前浏览器不支持websocket哦!')
  23. }
  24. //连接发生错误的回调方法
  25. websocket.onerror = function () {
  26. setMessageInnerHTML("WebSocket连接发生错误");
  27. };
  28. //连接成功建立的回调方法
  29. websocket.onopen = function () {
  30. setMessageInnerHTML("WebSocket连接成功");
  31. }
  32. //接收到消息的回调方法
  33. websocket.onmessage = function (event) {
  34. setMessageInnerHTML(event.data);
  35. }
  36. //连接关闭的回调方法
  37. websocket.onclose = function () {
  38. setMessageInnerHTML("WebSocket连接关闭");
  39. }
  40. //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
  41. window.onbeforeunload = function () {
  42. closeWebSocket();
  43. }
  44. //将消息显示在网页上
  45. function setMessageInnerHTML(sendMessage) {
  46. document.getElementById('message').innerHTML += sendMessage + '<br/>';
  47. }
  48. //关闭WebSocket连接
  49. function closeWebSocket() {
  50. websocket.close();
  51. }
  52. //发送消息
  53. function send() {
  54. var message = document.getElementById('text').value;//要发送的消息内容
  55. if (message === "") {
  56. alert("发送信息不能为空!")
  57. return;
  58. }
  59. // 给所有人发消息
  60. fetch('http://127.0.0.1:80/sendAll?msg='+message).then(res => {
  61. console.log(res)
  62. })
  63. //获取发送人用户id
  64. // var sendUserId = document.getElementById('sendUserId').value;
  65. // if (sendUserId === "") {
  66. // alert("发送人用户id不能为空!")
  67. // return;
  68. // }
  69. //
  70. // document.getElementById('message').innerHTML += (userId + "给" + sendUserId + "发送消息,消息内容为---->>" + message + '<br/>');
  71. // message = message + "," + sendUserId//将要发送的信息和内容拼起来,以便于服务端知道消息要发给谁
  72. // websocket.send(message);
  73. }
  74. </script>
  75. </body>
  76. </html>

user.html 模拟任何用户(都可以登录的页面发送消息)

<!DOCTYPE html>
<html lang="zh">
<meta charset="UTF-8">
<head>
    <title>WebSocket SpringBootDemo</title>
</head>
<body>
<h3>
    用户id:xiaoyou001  用户id:xiaoyou002 用户id:admin 可以聊天
</h3>
<!--userId:发送消息人的编号-->
<div>
    用户名:<input type="text" id="userId" placeholder="请输入用户的 ID">
    <button type="button" onclick="connect()">建立连接</button>
</div>

<br/><input id="text" type="text"/>
<input id="sendUserId" placeholder="请输入接收人的用户id"></input>
<button onclick="send()">发送消息</button>
<br/>

<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<br/>
<div>公众号:猿码优创</div>
<br/>
<div id="message"></div>
</body>

<script type="text/javascript">
    var websocket = null;

    function connect() {
        var userId = document.getElementById("userId").value;

        if (userId === '') {
            return alert('userId 不能为空!!!')
        }
        //判断当前浏览器是否支持WebSocket
        if ('WebSocket' in window) {
            websocket = new WebSocket("ws://127.0.0.1:80/websocket/" + userId);
        } else {
            alert('当前浏览器不支持websocket哦!')
        }
    }


    //连接发生错误的回调方法
    websocket.onerror = function () {
        setMessageInnerHTML("WebSocket连接发生错误");
    }

    //连接成功建立的回调方法
    websocket.onopen = function () {
        setMessageInnerHTML("WebSocket连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function (event) {
        setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function () {
        setMessageInnerHTML("WebSocket连接关闭");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
        closeWebSocket();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(sendMessage) {
        document.getElementById('message').innerHTML += sendMessage + '<br/>';
    }

    //关闭WebSocket连接
    function closeWebSocket() {
        websocket.close();
    }

    //发送消息
    function send() {
        var message = document.getElementById('text').value;//要发送的消息内容

        if (message === "") {
            alert("发送信息不能为空!")
            return;
        } //获取发送人用户id
        var sendUserId = document.getElementById('sendUserId').value;
        if (sendUserId === "") {
            alert("发送人用户id不能为空!")
            return;
        }

        document.getElementById('message').innerHTML += (userId + "给" + sendUserId + "发送消息,消息内容为---->>" + message + '<br/>');
        message = message + "," + sendUserId//将要发送的信息和内容拼起来,以便于服务端知道消息要发给谁
        websocket.send(message);
    }
</script>
</html>

ws.html 测试账号

<!DOCTYPE html>
<html lang="zh">
<meta charset="UTF-8">
<head>
    <title>WebSocket SpringBootDemo</title>
</head>
<body>
<!--userId:发送消息人的编号-->
<div>默认用户id:xiaoyou001(后期可以根据业务逻辑替换)</div>

<br/><input id="text" type="text"/>
<input id="sendUserId" placeholder="请输入接收人的用户id"></input>
<button onclick="send()">发送消息</button>
<br/>

<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<br/>
<div>公众号:猿码优创</div>
<br/>
<div id="message"></div>
</body>

<script type="text/javascript">
    var websocket = null;

    var userId = "xiaoyou001"

    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://127.0.0.1:80/websocket/" + userId);
    } else {
        alert('当前浏览器不支持websocket哦!')
    }

    //连接发生错误的回调方法
    websocket.onerror = function () {
        setMessageInnerHTML("WebSocket连接发生错误");
    };

    //连接成功建立的回调方法
    websocket.onopen = function () {
        setMessageInnerHTML("WebSocket连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function (event) {
        setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function () {
        setMessageInnerHTML("WebSocket连接关闭");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
        closeWebSocket();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(sendMessage) {
        document.getElementById('message').innerHTML += sendMessage + '<br/>';
    }

    //关闭WebSocket连接
    function closeWebSocket() {
        websocket.close();
    }

    //发送消息
    function send() {
        var message = document.getElementById('text').value;//要发送的消息内容

        if (message === "") {
            alert("发送信息不能为空!")
            return;
        } //获取发送人用户id
        var sendUserId = document.getElementById('sendUserId').value;
        if (sendUserId === "") {
            alert("发送人用户id不能为空!")
            return;
        }

        document.getElementById('message').innerHTML += (userId + "给" + sendUserId + "发送消息,消息内容为---->>" + message + '<br/>');
        message = message + "," + sendUserId//将要发送的信息和内容拼起来,以便于服务端知道消息要发给谁
        websocket.send(message);
    }
</script>
</html>

ws2.html 测试账号

<!DOCTYPE html>
<html lang="zh">
<meta charset="UTF-8">
<head>
    <title>WebSocket SpringBootDemo</title>
</head>
<body>
<!--userId:发送消息人的编号-->
<div>默认用户id:xiaoyou002(后期可以根据业务逻辑替换)</div>

<br/><input id="text" placeholder="请输入要发送的信息" type="text"/>
<input id="sendUserId" placeholder="请输入接收人的用户id"/>
<button onclick="send()">发送消息</button>
<br/>

<button onclick="closeWebSocket()">关闭WebSocket连接</button>
<br/>
</br>
<div id="message"></div>
</body>

<script type="text/javascript">
    var websocket = null;

    var userId = "xiaoyou002"

    //判断当前浏览器是否支持WebSocket
    if ('WebSocket' in window) {
        websocket = new WebSocket("ws://127.0.0.1:80/websocket/" + userId);
    } else {
        alert('当前浏览器不支持websocket哦!')
    }

    //连接发生错误的回调方法
    websocket.onerror = function () {
        setMessageInnerHTML("WebSocket连接发生错误");
    };

    //连接成功建立的回调方法
    websocket.onopen = function () {
        setMessageInnerHTML("WebSocket连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function (event) {
        setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function () {
        setMessageInnerHTML("WebSocket连接关闭");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function () {
        closeWebSocket();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(sendMessage) {
        document.getElementById('message').innerHTML += sendMessage + '<br/>';
    }

    //关闭WebSocket连接
    function closeWebSocket() {
        websocket.close();
    }

    //发送消息
    function send() {
        var message = document.getElementById('text').value;//要发送的消息内容

        if (message == "") {
            alert("发送信息不能为空!")
            return;
        } //获取发送人用户id
        var sendUserId = document.getElementById('sendUserId').value;
        if (sendUserId == "") {
            alert("发送人用户id不能为空!")
            return;
        }

        document.getElementById('message').innerHTML += ("我给" + sendUserId + "发送消息,消息内容为---->>" + message + '<br/>');
        message = message + "," + sendUserId//将要发送的信息和内容拼起来,以便于服务端知道消息要发给谁
        websocket.send(message);
    }
</script>
</html>

五、项目运行截图

image.png

image.png

image.png
image.png