• WebSocket学习笔记


    1 概述

    1.1 WebSocket简介

    WebSocket是一种协议,用于在Web应用程序和服务器之间建立实时、双向的通信连接。它通过一个单一的TCP连接提供了持久化连接,这使得Web应用程序可以更加实时地传递数据。WebSocket协议最初由W3C开发,并于2011年成为标准。

    • WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。

    • 在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

    1.2 WebSocket优劣势

    优势:

    • 实时性: 由于WebSocket的持久化连接,它可以实现实时的数据传输,避免了Web应用程序需要不断地发送请求以获取最新数据的情况。

    • 双向通信(全双工): WebSocket协议支持双向通信,这意味着服务器可以主动向客户端发送数据,而不需要客户端发送请求。

    • 减少网络负载: 由于WebSocket的持久化连接,它可以减少HTTP请求的数量,从而减少了网络负载。

    劣势:

    • 需要浏览器和服务器都支持: WebSocket是一种相对新的技术,需要浏览器和服务器都支持。一些旧的浏览器和服务器可能不支持WebSocket。

    • 需要额外的开销: WebSocket需要在服务器上维护长时间的连接,这需要额外的开销,包括内存和CPU。

    • 安全问题: 由于WebSocket允许服务器主动向客户端发送数据,可能会存在安全问题。服务器必须保证只向合法的客户端发送数据。

    1.3 与HTTP协议的区别

    HTTP协议有一个缺陷:通信只能由客户端发起。因为一般的请求都是HTTP请求(单向通信),HTTP是一个短连接(非持久化),且通信只能由客户端发起,HTTP协议做不到服务器主动向客户端推送消息。而WebSocket的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

    2 基本概念

    2.1 协议

    WebSocket 协议是一种基于TCP的协议,用于在客户端和服务器之间建立持久连接,并且可以在这个连接上实时地交换数据。WebSocket协议有自己的握手协议,用于建立连接,也有自己的数据传输格式。

    WebSocket连接在端口80 (ws)或者443 (wss)上创建,与HTTP使用的端口相同,这样,基本上所有的防火墙都不会阻止WebSocket连接。此外WebSocket支持跨域,可以避免Ajax的限制。

    总之,WebSocket协议是一种可靠的、高效的、双向的、持久的通信协议,它适用于需要实时通信的Web应用程序,如在线游戏、实时聊天等。

    2.2 生命周期

    WebSocket 生命周期描述了 WebSocket 连接从创建到关闭的过程。一个 WebSocket 连接包含以下四个主要阶段:

    • 连接建立阶段(Connection Establishment): 在这个阶段,客户端和服务器之间的 WebSocket 连接被建立。客户端发送一个 WebSocket 握手请求,服务器响应一个握手响应,然后连接就被建立了。

    • 连接开放阶段(Connection Open): 在这个阶段,WebSocket 连接已经建立并开放,客户端和服务器可以在连接上互相发送数据。

    • 连接关闭阶段(Connection Closing): 在这个阶段,一个 WebSocket 连接即将被关闭。它可以被客户端或服务器发起,通过发送一个关闭帧来关闭连接。

    • 连接关闭完成阶段(Connection Closed): 在这个阶段,WebSocket 连接已经完全关闭。客户端和服务器之间的任何交互都将无效。

    1. sequenceDiagram
    2. Client ->>Server: 1.WebSocket握手请求
    3. Server ->>Client: 2.WebSocket握手响应
    4. Client ->> Server: 3.WebSocket连接开放
    5. Server ->> Client: 3.WebSocket连接开放
    6. Server ->> Client: 4.WebSocket连接关闭

    2.3 消息格式

    WebSocket 消息格式由两个部分组成:消息头和消息体。

    消息头包含以下信息:

    • FIN: 表示这是一条完整的消息,一般情况下都是1。

    • RSV1、RSV2、RSV3: 暂时没有使用,一般都是0。

    • Opcode: 表示消息的类型,包括文本消息、二进制消息等。

    • Mask: 表示消息是否加密。

    • Payload length: 表示消息体的长度。

    • Masking key: 仅在消息需要加密时出现,用于对消息进行解密。

    消息体就是实际传输的数据,可以是文本或二进制数据。

    2.4 API

    WebSocket API 是用于在 Web 应用程序中创建和管理 WebSocket 连接的接口集合。WebSocket API 由浏览器原生支持,无需使用额外的 JavaScript 库或框架,可以直接在 JavaScript 中使用。

    let ws = new WebSocket('ws://example.com/ws');

    WebSocket.send() 方法: WebSocket.send() 方法用于向服务器发送数据。它接受一个参数,表示要发送的数据。数据可以是字符串、Blob 对象或 ArrayBuffer 对象。例如:

    ws.send('Hello, server!');

    WebSocket.onopen 事件: WebSocket.onopen 事件在 WebSocket 连接成功建立时触发。例如:

    1. ws.onopen = function() {
    2. console.log('WebSocket 连接已经建立。');
    3. };

    WebSocket.onmessage 事件: WebSocket.onmessage 事件在接收到服务器发送的消息时触发。它的 event 对象包含一个 data 属性,表示接收到的数据。例如:

    1. ws.onmessage = function(event) {
    2. console.log('收到服务器消息:', event.data);
    3. };

    WebSocket.onerror 事件: WebSocket.onerror 事件在 WebSocket 连接出现错误时触发。例如:

    1. ws.onerror = function(event) {
    2. console.error('WebSocket 连接出现错误:', event);
    3. };

    WebSocket.onclose 事件: WebSocket.onclose 事件在 WebSocket 连接被关闭时触发。例如:

    1. ws.onclose = function() {
    2. console.log('WebSocket 连接已经关闭。');
    3. };

    3 应用示例

    需求:多个前端用户通过WebSocket连接到后端的某个业务接口上,后端定时或实时将数据发送到前端。

    3.1 后端

    整体架构

    • 依赖

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

    固定写法:

    1. /** * websocket * 的配置信息 */
    2. @Configuration
    3. public class WebSocketConfig {
    4. @Bean
    5. public ServerEndpointExporter serverEndpointExporter() {
    6. return new ServerEndpointExporter();
    7. }
    8. }
    • WebSocket的服务类

    相当于三层架构中的Controller

    1. @ServerEndpoint("/api/pushMessage/{userId}") 前端通过此 URI 和后端交互,建立连接

    2. @OnOpen websocket 建立连接的注解,前端触发上面 URI 时会进入此注解标注的方法

    3. @OnMessage 收到前端传来的消息后执行的方法

    4. @OnClose 顾名思义关闭连接,销毁 session

    5. 因为WebSocket是类似客户端服务端的形式(采用ws协议),那么这里的WebSocketServer其实就相当于一个ws协议的Controller

    6. 新建一个ConcurrentHashMap webSocketMap 用于接收当前userId的WebSocket,方便IM之间对userId进行推送消息

    注意这里存储的是sessionId,而不是userId

    1. @Getter
    2. @Component
    3. @Slf4j
    4. @ServerEndpoint("/api/websocket/service1")
    5. public class WebSocketServerService1 {
    6. /* 最大在线人数 */
    7. public static int socketMaxOnlineCount = 100;
    8. private static final Semaphore socketSemaphore = new Semaphore(socketMaxOnlineCount);
    9. /* concurrent包的线程安全Set,用来存放每个客户端对应的Session对象 */
    10. /* key: sessionId value: WebSocketServer */
    11. private static final Map<String, WebSocketServerService1> webSocketMap = new ConcurrentHashMap<>();
    12. /* 当前的Session */
    13. private Session session;
    14. /* 连接建立成功调用的方法 */ @OnOpen
    15. public void onOpen(Session session) {
    16. this.session = session;
    17. String sessionId = session.getId();
    18. // 尝试获取信号量
    19. boolean semaphoreFlag = SemaphoreUtils.tryAcquire(socketSemaphore);
    20. if (!semaphoreFlag) {
    21. // 未获取到信号量
    22. log.error("当前在线人数超过限制数 - {}", socketMaxOnlineCount);
    23. this.sendMessage(session, "当前在线人数超过限制数:" + socketMaxOnlineCount);
    24. try {
    25. session.close();
    26. } catch (IOException e) {
    27. throw new RuntimeException(e);
    28. }
    29. } else {
    30. webSocketMap.put(sessionId, this);
    31. log.info("用户连接: {} - {},当前在线人数为: {}", sessionId, session, webSocketMap.size());
    32. this.sendMessage(session, "连接成功");
    33. }
    34. }
    35. /* 连接关闭调用的方法 */ @OnClose
    36. public void onClose(Session session) {
    37. String sessionId = session.getId();
    38. webSocketMap.remove(sessionId);
    39. // 获取到信号量则需释放
    40. SemaphoreUtils.release(socketSemaphore);
    41. log.info("用户退出: {} - {},当前在线人数为: {}", sessionId, session, webSocketMap.size());
    42. }
    43. /* 收到客户端消息后调用的方法 */
    44. @OnMessage
    45. public void onMessage(String message, Session session) {
    46. String sessionId = session.getId();
    47. log.info("收到用户消息: {} - {},报文: {}", sessionId, session, message);
    48. // 可以群发消息
    49. // 消息保存到数据库、redis
    50. if(StringUtils.isNotBlank(message)){
    51. try {
    52. // 解析发送的报文
    53. JSONObject jsonObject = JSON.parseObject(message);
    54. // 追加发送人(防止篡改)
    55. jsonObject.put("fromSessionId", sessionId);
    56. String toSessionId = jsonObject.getString("toSessionId");
    57. // 传送给对应toUserId用户的websocket
    58. if (StringUtils.isNotBlank(toSessionId) && webSocketMap.containsKey(toSessionId)) {
    59. webSocketMap.get(toSessionId).getSession().getBasicRemote().sendText(message);
    60. } else {
    61. // 否则不在这个服务器上,发送到mysql或者redis
    62. log.error("请求的sessionId:{} 不在该服务器上", toSessionId);
    63. }
    64. }catch (Exception e){
    65. e.printStackTrace();
    66. }
    67. }
    68. }
    69. /* 连接报错时的处理方法 */
    70. @OnError
    71. public void onError(Session session, Throwable error) {
    72. String sessionId = session.getId();
    73. if (session.isOpen()) {
    74. // 关闭连接
    75. try {
    76. session.close();
    77. } catch (IOException e) {
    78. throw new RuntimeException(e);
    79. }
    80. }
    81. // 移出用户
    82. webSocketMap.remove(sessionId);
    83. // 获取到信号量则需释放
    84. SemaphoreUtils.release(socketSemaphore);
    85. log.error("用户错误: {} - {},原因:{}", sessionId, session, error.getMessage());
    86. }
    87. /* 实现服务器主动推送 */
    88. public void sendMessage(Session session, String message) {
    89. try {
    90. session.getBasicRemote().sendText(message);
    91. } catch (IOException e) {
    92. log.error("推送报错:{}", e.toString());
    93. }
    94. }
    95. /* 群发:发送自定义消息 */
    96. public static void sendInfo(String message) {
    97. // 发送到每一个已连接的session中
    98. for (WebSocketServerService1 service : webSocketMap.values()) {
    99. try {
    100. Session session = service.getSession();
    101. session.getBasicRemote().sendText(message);
    102. log.info("发送消息报文至:{} - {}: {}", session.getId(), session, message);
    103. } catch (IOException e) {
    104. throw new RuntimeException(e);
    105. }
    106. }
    107. }
    108. }
    • 业务层和控制器

    1. @Service
    2. public class HelloServiceImpl implements HelloService {
    3. // 每3s推送至前端
    4. @Scheduled(fixedRate = 3000)
    5. @Override
    6. public void printTimeToWeb() {
    7. SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
    8. String date = dateFormat.format(new Date());
    9. WebSocketServerService1.sendInfo(date);
    10. }
    11. }
    1. @RestController
    2. @RequestMapping("/api/hello")
    3. public class HelloController {
    4. @Resource
    5. public HelloService helloService;
    6. @PostMapping("/pushToWeb")
    7. public void pushToWeb(){
    8. helloService.printTimeToWeb();
    9. }
    10. }

    3.2 前端

    1. <!DOCTYPE html>
    2. <html>
    3. <head>
    4. <meta charset="utf-8">
    5. <title>websocket通讯</title>
    6. </head>
    7. <script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js"></script>
    8. <script>
    9. let socket;
    10. function openSocket() {
    11. const socketUrl = "ws://localhost:8080/api/websocket/service1";
    12. console.log(socketUrl);
    13. if (socket != null) {
    14. socket.close(); socket = null;
    15. }
    16. socket = new WebSocket(socketUrl);
    17. //打开事件
    18. socket.onopen = function() {
    19. console.log("websocket已打开");
    20. };
    21. //获得消息事件
    22. socket.onmessage = function(msg) {
    23. console.log(msg.data);
    24. // 发现消息进入,开始处理前端触发逻辑
    25. };
    26. //关闭事件
    27. socket.onclose = function() {
    28. console.log("websocket已关闭");
    29. };
    30. //发生了错误事件
    31. socket.onerror = function() {
    32. console.log("websocket发生了错误");
    33. }
    34. }
    35. function closeSocket() {
    36. socket.close();
    37. }
    38. function sendMessage() {
    39. socket.send('{"toSessionId":"'+$("#toSessionId").val()+'","contentText":"'+$("#contentText").val()+'"}');
    40. }
    41. </script>
    42. <body>
    43. <p>【socket开启者的ID信息】:<div><input id="userId" name="userId" type="text" value="10"></div>
    44. <p>【客户端向服务器发送的内容】:<div><input id="toSessionId" name="toSessionId" type="text" value="20">
    45. <input id="contentText" name="contentText" type="text" value="hello websocket"></div>
    46. <p>【操作】:<div><a onclick="openSocket()">开启socket</a></div>
    47. <p>【操作】:<div><a onclick="closeSocket()">关闭socket</a></div>
    48. <p>【操作】:<div><a onclick="sendMessage()">发送消息</a></div>
    49. </body>
    50. </html>

    测试

    连接成功时:

     

     

    数据推送:

    关闭连接:

  • 相关阅读:
    spring:简介
    JSP response对象:响应客户端的请求并向客户端输出信息
    【排故】线上排故,如何快速定位线上系统的故障
    图书管理系统(https://github.com/plusmultiply0/bookmanagesystem)
    从外卖点单浅谈伪需求
    javaScript的底层(详解)
    设备监理师证书含金量怎样?值得考吗?
    道路千万条,为什么这家创新存储公司会选这条?
    解决方案:用决策树算法如何生成决策树图及生成SQL规则
    数据可视化项目1
  • 原文地址:https://blog.csdn.net/xinnian_yyds/article/details/143272936