侧边栏壁纸
博主头像
叩钉壹刻博主等级

7分技术,3分管理,2分运气

  • 累计撰写 30 篇文章
  • 累计创建 13 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

WebSocket入门与实践

鹿心肺语
2023-11-22 / 0 评论 / 0 点赞 / 26 阅读 / 20340 字

整体介绍

WebSocket 是一种网络通信协议,它提供了一种在客户端和服务器之间进行的双向实时通信的技术。WebSocket 协议建立在 HTTP 协议之上,使用 ws://或 wss://作为 URI 方案,其中 wss 表示使用 SSL/TLS 加密的 WebSocket 链接。

本文中将通过以下几方面介绍:

  • WebSocket 概述:使用 WebSocket 的原因、通信过程、底层原理。
  • WebSocket 使用:Java 语言通过 SpringBoot 框架实现 WebSocket。
  • WebSocket 实战:实现简单多人聊天功能,了解开发思路。
  • WebSocket 场景:了解 WebSocket 实际开发中的使用场景。

WebSocket 概述

使用 WebSocket 的原因:WebSocket 提供了客户端和服务器之间进行双向实时通信。

不使用 WebSocket 技术,普通 B/S 架构的交互图如下:

image-20231020151911089

通过交互图可以发现,客户端和服务器之间通信方向固定,HTTP 请求只能由客户端发起,服务器无法直接向浏览器发送请求。

如果面对三方应用间结果的及时通知可以采取轮询方式,但是这样的轮询,会导致浪费带宽,实时性差,服务器压力大。

为了避免轮询所带来的系列问题,采取 WebSocket 协议进行通信。

1. 介绍

WebSocket 协议的介绍

  • 在 2008 年提出,2011 年正式称为国际标准(RFC 6455)。
  • 是一种单个 TCP 连接上进行全双工通信协议,解决 HTTP 协议的一些局限性。
  • 是作为 HTML5 标准的一部分。

2. 特点

WebSocket 协议的特点

  1. 双向通信:WebSocket 允许客户端和服务器之间进行双向实时通信,而不需要频繁发送 HTTP 请求。
  2. 实时性:WebSocket 协议支持实时数据传输,可以在短时间内发送和接收大量数据。
  3. 较少的资源消耗:与传统的 HTTP 请求相比,WebSocket 协议可以减少客户端和服务器之间的通信次数,从而降低网络延迟和资源消耗。

3. 通信过程

通信过程步骤简述

步骤 1:浏览器发起 HTTP 请求,请求建立 WebSocket 连接

步骤 2:服务器响应同意协议更改

步骤 3:浏览器和服务器之间相互发送消息

通信过程示意图:

image-20231020154847990

4. 底层原理

WebSocket 底层原理

  • WebSocket 协议在建立 TCP 协议基础上,所以服务器端也是容易实现,不同语言都有支持。
  • TCP 协议是全双工协议,HTTP 协议是基于它,设计成了单向的。
  • WebSocket 没有同源限制。同源是指 IP 和端口相同。

WebSocket 使用

通过 Spring 实现的 WebSocket 功能,进行实现业务功能。

1. 基于 Java 注解

WebSocket 服务端实现

  • 使用的注解有:@ServerEndpoint(监听连接)、@OnOpen(连接成功)、@OnClose(连接关闭)、@OnMessage(收到消息)。
  • 配置类: 需要将 Spring 中的 ServerEndpointExporter 对象注入。

步骤1:搭建一个 SpringBoot 工程,在 pom.xml 文件中引入 websocket 依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.17</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

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

    <!--lombok-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.24</version>
        <scope>provided</scope>
    </dependency>

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

步骤2:编写服务器端处理 WebSocket 请求类

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@ServerEndpoint("/myWs")
@Component
@Slf4j
public class WsServerEndport {
    private static Map<String, Session> sessionMap = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(Session session) {
        sessionMap.put(session.getId(), session);
        log.info("websocket is open: {}", session.getId());
    }

    @OnMessage
    public String onMessage(String text) {
        log.info("websocket is receive message:{}", text);
        return "receive message: " + text;
    }

    @OnClose
    public void onClose(Session session) {
        sessionMap.remove(session.getId());
        log.info("websocket is close");
    }

    //定时 服务器-> 客户端 发送消息
    @Scheduled(fixedRate = 2000)
    public void sendMsg() throws IOException {
        for (String sessionId : sessionMap.keySet()) {
            log.info("websocket is send message:{} ", sessionId);
            sessionMap.get(sessionId).getBasicRemote().sendText("心跳");
        }
    }
}

步骤3: 由于测试中使用到了定时器注解@Scheduled,所以在程序启动类上声明开启定时任务

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling // 开启定时任务
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

步骤4: 注入 ServerEndpointExporter 对象

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

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

WebSocket 客户端实现

步骤1: 创建 HTML 页面,编写 js 方法

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ws client</title>
</head>

<body>

    <div id="wslog"></div>

    <script>
        let wslog = document.getElementById("wslog");
        let ws = new WebSocket("ws://localhost:8080/myWs")

        ws.onopen = function () {
            ws.send("client");
        }

        ws.onmessage = function (message) {
            wslog.append(message.data);
        }

        ws.onclose = function () {
            ws.close();
        }

        ws.onerror = function (e) {
            console.log(e)
        }
    </script>
</body>

</html>

2. 基于 Spring 框架实现

Spring 针对 WebScoket 定义了一些接口和抽象类:

  • HttpSessionHandshakeInterceptor(抽象类):握手拦截器,在握手前后添加操作。
  • AbstractWebSocketHandler(抽象类):websocket处理程序,监听连接前,连接中和连接后。
  • WebSocketConfigurer(接口):配置程序,配置监听端口信息、握手拦截器、处理程序。

WebSocket 服务端实现

步骤1: 搭建一个 SpringBoot 工程,在 pom.xml 文件中引入 websocket 依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.17</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

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

    <!--lombok-->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.24</version>
        <scope>provided</scope>
    </dependency>

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

步骤2: 编写服务器端 WebSocket 建立握手类

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;

import java.util.Map;

@Component
@Slf4j
public class MyWsInterceptor extends HttpSessionHandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
        // 拦截握手前:
        log.info(request.getRemoteAddress().toString() + "开始握手");
        return super.beforeHandshake(request, response, wsHandler, attributes);
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
        // 拦截握手后:
        log.info(request.getRemoteAddress().toString() + "握手完成");
        super.afterHandshake(request, response, wsHandler, ex);
    }
}

步骤3: 除WebSocket本身的Sesssion外,实际场景中需要额外信息,所以封装Session

import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.web.socket.WebSocketSession;

@AllArgsConstructor
@Data
public class SessionBean {
    private WebSocketSession webSocketSession;
    private Integer clientId;
}

步骤4: 编写服务器端 WebSocket 处理方法类

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@Component
@Slf4j
public class MyWsHandler extends AbstractWebSocketHandler {

    private static Map<String, SessionBean> sessionBeanMap;
    private static AtomicInteger clientIdMaker;

    static {
        sessionBeanMap = new ConcurrentHashMap<>();
        clientIdMaker = new AtomicInteger(0);
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);
        // 建立连接:
        SessionBean sessionBean = new SessionBean(session, clientIdMaker.getAndIncrement());
        sessionBeanMap.put(session.getId(), sessionBean);
        log.info(sessionBeanMap.get(session.getId()).getClientId() + " : 打开连接");
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        super.handleTextMessage(session, message);
        log.info(sessionBeanMap.get(session.getId()).getClientId() + " : " + message.getPayload());
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        super.handleTransportError(session, exception);
        // 连接异常
        if (session.isOpen()) {
            session.close();
        }
        sessionBeanMap.remove(session.getId());
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);
        // 连接关闭
        log.info(sessionBeanMap.get(session.getId()).getClientId() + " : 关闭连接");
        if (session.isOpen()) {
            session.close();
        }
        sessionBeanMap.remove(session.getId());
    }

    //定时发送任务
    @Scheduled(fixedDelay = 2000)
    public void sendMsg() throws IOException{
        for (String key : sessionBeanMap.keySet()) {
            sessionBeanMap.get(key).getWebSocketSession().sendMessage(new TextMessage("心跳"));
        }
    }

}

步骤5: 由于测试中使用到了定时器注解@Scheduled,所以在程序启动类上声明开启定时任务

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling // 开启定时任务
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

步骤6: 编写服务器端 WebSocket 配置类

import org.springframework.stereotype.Component;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

import javax.annotation.Resource;

@Component
@EnableWebSocket
public class MyWsConfig implements WebSocketConfigurer {
    @Resource
    private MyWsHandler myWsHandler;

    @Resource
    private MyWsInterceptor myWsInterceptor;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myWsHandler, "/myWs1").addInterceptors(myWsInterceptor).setAllowedOrigins("*");
    }
}

WebSocket 客户端实现

步骤1: 创建 HTML 页面,编写 js 方法

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ws client</title>
</head>

<body>

    <div id="wslog"></div>

    <script>
        let wslog = document.getElementById("wslog");
        let ws = new WebSocket("ws://localhost:8080/myWs1")

        ws.onopen = function () {
            ws.send("client");
        }

        ws.onmessage = function (message) {
            wslog.append(message.data);
        }

        ws.onclose = function () {
            ws.close();
        }

        ws.onerror = function (e) {
            console.log(e)
        }
    </script>
</body>

</html>

WebSocket 实战

实现多人群聊功能,基于 Spring 框架实现的代码进行改造

WebSocket 服务端改造

步骤1: 改造服务器端 WebSocket 处理方法类

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

@Component
@Slf4j
public class MyWsHandler extends AbstractWebSocketHandler {

    private static Map<String, SessionBean> sessionBeanMap;
    private static AtomicInteger clientIdMaker;

    private static StringBuffer stringBuffer;

    static {
        sessionBeanMap = new ConcurrentHashMap<>();
        clientIdMaker = new AtomicInteger(0);
        stringBuffer = new StringBuffer();
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        super.afterConnectionEstablished(session);
        // 建立连接:
        SessionBean sessionBean = new SessionBean(session, clientIdMaker.getAndIncrement());
        sessionBeanMap.put(session.getId(), sessionBean);
        log.info(sessionBeanMap.get(session.getId()).getClientId() + " : 打开连接");

        stringBuffer.append(sessionBeanMap.get(session.getId()).getClientId()).append("进入群聊<br/>");
        sendAllMessage(sessionBeanMap);
    }

    private static void sendAllMessage(Map<String, SessionBean> sessionBeanMap) {
        for (String key : sessionBeanMap.keySet()) {
            try {
                sessionBeanMap.get(key).getWebSocketSession().sendMessage(new TextMessage(stringBuffer.toString()));
            } catch (IOException e) {
                e.printStackTrace();
                log.error(e.getMessage());
            }
        }
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        super.handleTextMessage(session, message);
        log.info(sessionBeanMap.get(session.getId()).getClientId() + " : " + message.getPayload());
        stringBuffer.append(sessionBeanMap.get(session.getId()).getClientId()).append(message.getPayload()).append("<br/>");
        sendAllMessage(sessionBeanMap);
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
        super.handleTransportError(session, exception);
        // 连接异常
        if (session.isOpen()) {
            session.close();
        }
        sessionBeanMap.remove(session.getId());
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        super.afterConnectionClosed(session, status);
        int clientId = sessionBeanMap.get(session.getId()).getClientId();
        // 连接关闭
        log.info(sessionBeanMap.get(session.getId()).getClientId() + " : 关闭连接");
        if (session.isOpen()) {
            session.close();
        }
        sessionBeanMap.remove(session.getId());
        stringBuffer.append(clientId).append("退出群聊<br/>");
        sendAllMessage(sessionBeanMap);
    }

//    //定时发送任务
//    @Scheduled(fixedDelay = 2000)
//    public void sendMsg() throws IOException{
//        for (String key : sessionBeanMap.keySet()) {
//            sessionBeanMap.get(key).getWebSocketSession().sendMessage(new TextMessage("心跳"));
//        }
//    }

}

WebSocket 客户端改造

步骤1: 改造 HTML 页面,编写 js 方法

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ws client</title>
</head>

<body>

    <!-- <div id="wslog"></div> -->

    <p style="border: 1px solid black; width: 600px; height: 400px;" id="talkMsg"></p>
    <input type="text" id="message" /><button id="sendBtn" onclick="sendMsg()">发送</button>

    <script>
        let wslog = document.getElementById("wslog");
        let ws = new WebSocket("ws://localhost:8080/myWs1")

        ws.onopen = function () {
            //ws.send("client");
        }

        ws.onmessage = function (message) {
            // wslog.append(message.data);
            document.getElementById("talkMsg").innerHTML = message.data;
        }

        function sendMsg() {
            ws.send(document.getElementById("message").value);
            document.getElementById("message").value = "";
        }

        ws.onclose = function () {
            ws.close();
        }

        ws.onerror = function (e) {
            console.log(e)
        }
    </script>
</body>

</html>

WebSocket 场景

应用多数是消息订阅

应用场景:

  1. 商城类的销售客服
  2. 管理类的人员沟通
  3. 视频的弹幕
  4. 股票基金类的数据
  5. 网页版的在线游戏
  6. 网站的总数统计

场景特点:

  1. 实时性要求高。
  2. 数据会收集到服务器端,从服务器端往客户端去发送。
0

评论区