前言

入门 WebSocket
后端 Springboot
前端 HTML5 (原生方法)

简单介绍下WebSocket,是类似于HTTP的基于TCP连接协议,相比于HTTP是单次单向传输的短连接,WebSocket是多次双向传输的长连接,更适合用于游戏、聊天室等有实时监听的需求

本来是设想直接用H5,其中的一个房主作为主机,就可以省去服务端,但考虑到可能存在公网ip变动、端口不开放、防火墙等问题,决定放弃该方案

所以本文还是使用主流方案实现前后端websocket交互,即所有用户与服务器端建立连接,服务器端代理实现用户端所有请求和响应



折腾

Java SpringBoot端
新建一个空的SpringBoot项目
我这里选择 JDK8 + maven + war包 (可以根据需要自行选择)
(过程省略)



修改访问端口
application.yml

  1. server:
  2. port: 3000



增加maven依赖
(这里如果只提供socket接口,可以把不需要的去掉,例如web依赖可以不要,只保留websocket)
pom.xml

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



新建 websocket配置
WebSocketConfig.java

  1. package com.zzzmh.ws.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. * @author zzzmh
  7. * @date 2021/10/11
  8. */
  9. @Configuration
  10. public class WebSocketConfig {
  11. /**
  12. * 注入一个ServerEndpointExporter,该Bean会自动注册使用@ServerEndpoint注解申明的websocket endpoint
  13. */
  14. @Bean
  15. public ServerEndpointExporter serverEndpointExporter() {
  16. return new ServerEndpointExporter();
  17. }
  18. }



新建 websocket接口
TestSocket.java

  1. package com.zzzmh.ws.socket;
  2. import org.slf4j.Logger;
  3. import org.slf4j.LoggerFactory;
  4. import org.springframework.stereotype.Component;
  5. import org.springframework.web.bind.annotation.RequestParam;
  6. import javax.websocket.*;
  7. import javax.websocket.server.PathParam;
  8. import javax.websocket.server.ServerEndpoint;
  9. import java.util.HashMap;
  10. import java.util.Map;
  11. import java.util.concurrent.ConcurrentHashMap;
  12. import java.util.concurrent.atomic.AtomicInteger;
  13. /**
  14. * @author zzzmh
  15. * @date 2021/10/11
  16. */
  17. @Component
  18. @ServerEndpoint(value = "/test/{username}")
  19. public class TestSocket {
  20. private Logger log = LoggerFactory.getLogger(getClass());
  21. /**
  22. * 记录当前在线连接数 (线程安全)
  23. */
  24. private static AtomicInteger onlineCount = new AtomicInteger(0);
  25. /**
  26. * 存放所有在线的客户端 (线程安全)
  27. */
  28. private static Map<String, Session> clients = new ConcurrentHashMap<>();
  29. /**
  30. * 连接建立成功调用的方法
  31. */
  32. @OnOpen
  33. public void onOpen(Session session) {
  34. clients.put(session.getId(), session);
  35. // 在线人数加1
  36. onlineCount.incrementAndGet();
  37. log.info("有新连接加入:{},当前在线人数为:{}", session.getId(), onlineCount.get());
  38. }
  39. /**
  40. * 连接关闭调用的方法
  41. */
  42. @OnClose
  43. public void onClose(Session session) {
  44. onlineCount.decrementAndGet(); // 在线数减1
  45. clients.remove(session.getId());
  46. log.info("有一连接关闭:{},当前在线人数为:{}", session.getId(), onlineCount.get());
  47. }
  48. /**
  49. * 收到客户端消息后调用的方法
  50. *
  51. * @param message 客户端发送过来的消息
  52. */
  53. @OnMessage
  54. public void onMessage(String message, Session session) {
  55. log.info("服务端收到客户端[{}]的消息:{}", session.getId(), message);
  56. this.sendMessage(message, session);
  57. }
  58. @OnError
  59. public void onError(Session session, Throwable error) {
  60. log.error("发生错误");
  61. error.printStackTrace();
  62. }
  63. /**
  64. * 群发消息
  65. *
  66. * @param message 消息内容
  67. */
  68. private void sendMessage(String message, Session fromSession) {
  69. for (Map.Entry<String, Session> sessionEntry : clients.entrySet()) {
  70. Session toSession = sessionEntry.getValue();
  71. String username = toSession.getPathParameters().get("username");
  72. // 排除掉自己
  73. if (!fromSession.getId().equals(toSession.getId())) {
  74. String fromUsername = fromSession.getPathParameters().get("username");
  75. log.info("服务端给客户端[{}]发送消息{}", toSession.getId(), message);
  76. toSession.getAsyncRemote().sendText(message + "( 发送者: " + fromUsername + ", 接受者: " + username + ")");
  77. }
  78. }
  79. }
  80. }

前端html代码 (需要放在容器里才能执行,本地执行无法请求接口,我这里用webstorm编辑和执行,也可以用vscode)

  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <title>websocket</title>
  6. </head>
  7. <body>
  8. <input id="text" type="text"/>
  9. <button onclick="send()">Send</button>
  10. <button onclick="closeWebSocket()">Close</button>
  11. <div id="message"></div>
  12. <script type="text/javascript">
  13. if (!'WebSocket' in window) {
  14. alert('当前浏览器不支持WebSocket 无法继续进行游戏,请更换浏览器或设备!')
  15. }
  16. let username;
  17. while (!username) {
  18. username = prompt("请输入你的游戏昵称:");
  19. }
  20. const websocket = new WebSocket("ws://localhost:3000/test/" + username);
  21. //连接发生错误的回调方法
  22. websocket.onerror = function () {
  23. setMessageInnerHTML("连接错误");
  24. };
  25. //连接成功建立的回调方法
  26. websocket.onopen = function (event) {
  27. setMessageInnerHTML("已连接");
  28. }
  29. //接收到消息的回调方法
  30. websocket.onmessage = function (event) {
  31. setMessageInnerHTML("已收到消息:" + event.data);
  32. }
  33. //连接关闭的回调方法
  34. websocket.onclose = function () {
  35. setMessageInnerHTML("已断开");
  36. }
  37. //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
  38. window.onbeforeunload = function () {
  39. websocket.close();
  40. }
  41. //将消息显示在网页上
  42. function setMessageInnerHTML(innerHTML) {
  43. document.getElementById('message').innerHTML += '<br>' + innerHTML;
  44. }
  45. //关闭连接
  46. function closeWebSocket() {
  47. websocket.close();
  48. }
  49. //发送消息
  50. function send() {
  51. const message = document.getElementById('text').value;
  52. setMessageInnerHTML("已发送消息:" + message)
  53. websocket.send(message);
  54. }
  55. </script>
  56. </body>
  57. </html>



服务端大致目录如下

  1. ws
  2. ├── src
  3. ├── main
  4. ├── java
  5. ├── com.zzzmh.ws
  6. ├── config
  7. ├── WebSocketConfig.java
  8. ├── socket
  9. ├── TestSocket.java
  10. ├── ServletInitializer.java
  11. ├── WsApplication.java
  12. ├── resources
  13. ├── application.yml
  14. ├── pom.xml



效果展示

首先我这里打开3个页面模拟3个用户,分别输入昵称 user1 user2 user3
分别都可以看到已连接 说明连接websocket正常
在user1 中输入666并点击发送
在user2 和 user3可以看到效果
截图如下

user1


user2


user3



END

后续打算写几个能联机的多人游戏玩一玩
当其中一个人进行操作,就可以用ws实时同步到其他玩家的界面
包括实时的多人在线聊天也可以用这个实现
用户本地缓存数据用 localStorage
还可以实现断线重连


最终代码
https://gitee.com/tczmh/ws-java
https://gitee.com/tczmh/websocket-html


参考
https://www.cnblogs.com/xuwenjin/p/12664650.html